feature: add the ability to configure the disk widget's table columns (#1625)

* a bit of refactoring here...

* some refactoring, add columns

* cleanup

* add disk column feature

* update changelog
This commit is contained in:
Clement Tsang 2024-11-17 21:28:20 -05:00 committed by GitHub
parent c8cba49463
commit 196d6d18c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 413 additions and 154 deletions

View file

@ -22,6 +22,10 @@ That said, these are more guidelines rather than hardset rules, though the proje
## [0.11.0] - Unreleased ## [0.11.0] - Unreleased
### Features
- [#1625](https://github.com/ClementTsang/bottom/pull/1625): Add the ability to configure the disk widget's table columns.
### Bug Fixes ### Bug Fixes
- [#1551](https://github.com/ClementTsang/bottom/pull/1551): Fix missing parent section names in default config. - [#1551](https://github.com/ClementTsang/bottom/pull/1551): Fix missing parent section names in default config.

View file

@ -8,24 +8,34 @@
[flags] [flags]
# Whether to hide the average cpu entry. # Whether to hide the average cpu entry.
#hide_avg_cpu = false #hide_avg_cpu = false
# Whether to use dot markers rather than braille. # Whether to use dot markers rather than braille.
#dot_marker = false #dot_marker = false
# The update rate of the application. # The update rate of the application.
#rate = "1s" #rate = "1s"
# Whether to put the CPU legend to the left. # Whether to put the CPU legend to the left.
#cpu_left_legend = false #cpu_left_legend = false
# Whether to set CPU% on a process to be based on the total CPU or just current usage. # Whether to set CPU% on a process to be based on the total CPU or just current usage.
#current_usage = false #current_usage = false
# Whether to set CPU% on a process to be based on the total CPU or per-core CPU% (not divided by the number of cpus). # Whether to set CPU% on a process to be based on the total CPU or per-core CPU% (not divided by the number of cpus).
#unnormalized_cpu = false #unnormalized_cpu = false
# Whether to group processes with the same name together by default. # Whether to group processes with the same name together by default.
#group_processes = false #group_processes = false
# Whether to make process searching case sensitive by default. # Whether to make process searching case sensitive by default.
#case_sensitive = false #case_sensitive = false
# Whether to make process searching look for matching the entire word by default. # Whether to make process searching look for matching the entire word by default.
#whole_word = false #whole_word = false
# Whether to make process searching use regex by default. # Whether to make process searching use regex by default.
#regex = false #regex = false
# The temperature unit. One of the following, defaults to "c" for Celsius: # The temperature unit. One of the following, defaults to "c" for Celsius:
#temperature_type = "c" #temperature_type = "c"
##temperature_type = "k" ##temperature_type = "k"
@ -33,79 +43,112 @@
##temperature_type = "kelvin" ##temperature_type = "kelvin"
##temperature_type = "fahrenheit" ##temperature_type = "fahrenheit"
##temperature_type = "celsius" ##temperature_type = "celsius"
# The default time interval (in milliseconds). # The default time interval (in milliseconds).
#default_time_value = "60s" #default_time_value = "60s"
# The time delta on each zoom in/out action (in milliseconds). # The time delta on each zoom in/out action (in milliseconds).
#time_delta = 15000 #time_delta = 15000
# Hides the time scale. # Hides the time scale.
#hide_time = false #hide_time = false
# Override layout default widget # Override layout default widget
#default_widget_type = "proc" #default_widget_type = "proc"
#default_widget_count = 1 #default_widget_count = 1
# Expand selected widget upon starting the app # Expand selected widget upon starting the app
#expanded = true #expanded = true
# Use basic mode # Use basic mode
#basic = false #basic = false
# Use the old network legend style # Use the old network legend style
#use_old_network_legend = false #use_old_network_legend = false
# Remove space in tables # Remove space in tables
#hide_table_gap = false #hide_table_gap = false
# Show the battery widgets # Show the battery widgets
#battery = false #battery = false
# Disable mouse clicks # Disable mouse clicks
#disable_click = false #disable_click = false
# Show memory values in the processes widget as values by default # Show memory values in the processes widget as values by default
#process_memory_as_value = false #process_memory_as_value = false
# Show tree mode by default in the processes widget. # Show tree mode by default in the processes widget.
#tree = false #tree = false
# Shows an indicator in table widgets tracking where in the list you are. # Shows an indicator in table widgets tracking where in the list you are.
#show_table_scroll_position = false #show_table_scroll_position = false
# Show processes as their commands by default in the process widget. # Show processes as their commands by default in the process widget.
#process_command = false #process_command = false
# Displays the network widget with binary prefixes. # Displays the network widget with binary prefixes.
#network_use_binary_prefix = false #network_use_binary_prefix = false
# Displays the network widget using bytes. # Displays the network widget using bytes.
#network_use_bytes = false #network_use_bytes = false
# Displays the network widget with a log scale. # Displays the network widget with a log scale.
#network_use_log = false #network_use_log = false
# Hides advanced options to stop a process on Unix-like systems. # Hides advanced options to stop a process on Unix-like systems.
#disable_advanced_kill = false #disable_advanced_kill = false
# Hide GPU(s) information # Hide GPU(s) information
#disable_gpu = false #disable_gpu = false
# Shows cache and buffer memory # Shows cache and buffer memory
#enable_cache_memory = false #enable_cache_memory = false
# How much data is stored at once in terms of time. # How much data is stored at once in terms of time.
#retention = "10m" #retention = "10m"
# Where to place the legend for the memory widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right". # Where to place the legend for the memory widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
#memory_legend = "top-right" #memory_legend = "top-right"
# Where to place the legend for the network widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right". # Where to place the legend for the network widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
#network_legend = "top-right" #network_legend = "top-right"
# Processes widget configuration # Processes widget configuration
#[processes] #[processes]
# The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built): # The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built):
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU% # PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"] #columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"]
# CPU widget configuration # CPU widget configuration
#[cpu] #[cpu]
# One of "all" (default), "average"/"avg" # One of "all" (default), "average"/"avg"
# default = "average" #default = "average"
# Disk widget configuration # Disk widget configuration
#[disk] #[disk]
# The columns shown by the process widget. The following columns are supported:
# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s
#columns = ["Disk", "Mount", "Used", "Free", "Total", "Used%", "R/s", "W/s"]
# By default, there are no disk name filters enabled. These can be turned on to filter out specific data entries if you # By default, there are no disk name filters enabled. These can be turned on to filter out specific data entries if you
# don't want to see them. An example use case is provided below. # don't want to see them. An example use case is provided below.
#[disk.name_filter] #[disk.name_filter]
# Whether to ignore any matches. Defaults to true. # Whether to ignore any matches. Defaults to true.
#is_list_ignored = true #is_list_ignored = true
# A list of filters to try and match. # A list of filters to try and match.
#list = ["/dev/sda\\d+", "/dev/nvme0n1p2"] #list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
# Whether to use regex. Defaults to false. # Whether to use regex. Defaults to false.
#regex = true #regex = true
# Whether to be case-sensitive. Defaults to false. # Whether to be case-sensitive. Defaults to false.
#case_sensitive = false #case_sensitive = false
# Whether to be require matching the whole word. Defaults to false. # Whether to be require matching the whole word. Defaults to false.
#whole_word = false #whole_word = false
@ -113,51 +156,63 @@
#[disk.mount_filter] #[disk.mount_filter]
# Whether to ignore any matches. Defaults to true. # Whether to ignore any matches. Defaults to true.
#is_list_ignored = true #is_list_ignored = true
# A list of filters to try and match. # A list of filters to try and match.
#list = ["/mnt/.*", "/boot"] #list = ["/mnt/.*", "/boot"]
# Whether to use regex. Defaults to false. # Whether to use regex. Defaults to false.
#regex = true #regex = true
# Whether to be case-sensitive. Defaults to false. # Whether to be case-sensitive. Defaults to false.
#case_sensitive = false #case_sensitive = false
# Whether to be require matching the whole word. Defaults to false. # Whether to be require matching the whole word. Defaults to false.
#whole_word = false #whole_word = false
# Temperature widget configuration # Temperature widget configuration
#[temperature] #[temperature]
# By default, there are no temperature sensor filters enabled. An example use case is provided below. # By default, there are no temperature sensor filters enabled. An example use case is provided below.
#[temperature.sensor_filter] #[temperature.sensor_filter]
# Whether to ignore any matches. Defaults to true. # Whether to ignore any matches. Defaults to true.
#is_list_ignored = true #is_list_ignored = true
# A list of filters to try and match. # A list of filters to try and match.
#list = ["cpu", "wifi"] #list = ["cpu", "wifi"]
# Whether to use regex. Defaults to false. # Whether to use regex. Defaults to false.
#regex = false #regex = false
# Whether to be case-sensitive. Defaults to false. # Whether to be case-sensitive. Defaults to false.
#case_sensitive = false #case_sensitive = false
# Whether to be require matching the whole word. Defaults to false. # Whether to be require matching the whole word. Defaults to false.
#whole_word = false #whole_word = false
# Network widget configuration # Network widget configuration
#[network] #[network]
# By default, there are no network interface filters enabled. An example use case is provided below. # By default, there are no network interface filters enabled. An example use case is provided below.
#[network.interface_filter] #[network.interface_filter]
# Whether to ignore any matches. Defaults to true. # Whether to ignore any matches. Defaults to true.
#is_list_ignored = true #is_list_ignored = true
# A list of filters to try and match. # A list of filters to try and match.
#list = ["virbr0.*"] #list = ["virbr0.*"]
# Whether to use regex. Defaults to false. # Whether to use regex. Defaults to false.
#regex = true #regex = true
# Whether to be case-sensitive. Defaults to false. # Whether to be case-sensitive. Defaults to false.
#case_sensitive = false #case_sensitive = false
# Whether to be require matching the whole word. Defaults to false. # Whether to be require matching the whole word. Defaults to false.
#whole_word = false #whole_word = false
# These are all the components that support custom theming. Note that colour support # These are all the components that support custom theming. Note that colour support
# will depend on terminal support. # will depend on terminal support.
#[styles] # Uncomment if you want to use custom styling #[styles] # Uncomment if you want to use custom styling
# Built-in themes. Valid values are: # Built-in themes. Valid values are:
# - "default" # - "default"
# - "default-light" # - "default-light"

View file

@ -183,10 +183,35 @@
} }
} }
}, },
"DiskColumn": {
"type": "string",
"enum": [
"Disk",
"Free",
"Free%",
"Mount",
"R/s",
"Read",
"Rps",
"Total",
"Used",
"Used%",
"W/s",
"Wps",
"Write"
]
},
"DiskConfig": { "DiskConfig": {
"description": "Disk configuration.", "description": "Disk configuration.",
"type": "object", "type": "object",
"properties": { "properties": {
"columns": {
"description": "A list of disk widget columns.",
"type": "array",
"items": {
"$ref": "#/definitions/DiskColumn"
}
},
"mount_filter": { "mount_filter": {
"description": "A filter over the mount names.", "description": "A filter over the mount names.",
"anyOf": [ "anyOf": [

View file

@ -51,7 +51,7 @@ def main():
read_file = re.sub( read_file = re.sub(
r"^#(\s\s+)([a-zA-Z\[])", r"\2", read_file, flags=re.MULTILINE r"^#(\s\s+)([a-zA-Z\[])", r"\2", read_file, flags=re.MULTILINE
) )
print(f"uncommented file: \n{read_file}") print(f"uncommented file: \n{read_file}\n=====\n")
toml_str = tomllib.loads(read_file) toml_str = tomllib.loads(read_file)
else: else:

View file

@ -25,7 +25,7 @@ use crate::{
processes::{Pid, ProcessHarvest}, processes::{Pid, ProcessHarvest},
temperature, Data, temperature, Data,
}, },
utils::data_prefixes::*, dec_bytes_per_second_string,
}; };
pub type TimeOffset = f64; pub type TimeOffset = f64;
@ -423,20 +423,11 @@ impl DataCollection {
*io_curr = (r_rate, w_rate); *io_curr = (r_rate, w_rate);
*io_prev = (io_r_pt, io_w_pt); *io_prev = (io_r_pt, io_w_pt);
// TODO: idk why I'm generating this here tbh
if let Some(io_labels) = self.io_labels.get_mut(itx) { if let Some(io_labels) = self.io_labels.get_mut(itx) {
let converted_read = get_decimal_bytes(r_rate);
let converted_write = get_decimal_bytes(w_rate);
*io_labels = ( *io_labels = (
if r_rate >= GIGA_LIMIT { dec_bytes_per_second_string(r_rate),
format!("{:.*}{}/s", 1, converted_read.0, converted_read.1) dec_bytes_per_second_string(w_rate),
} else {
format!("{:.*}{}/s", 0, converted_read.0, converted_read.1)
},
if w_rate >= GIGA_LIMIT {
format!("{:.*}{}/s", 1, converted_write.0, converted_write.1)
} else {
format!("{:.*}{}/s", 0, converted_write.0, converted_write.1)
},
); );
} }
} }

View file

@ -25,11 +25,16 @@ impl SortOrder {
SortOrder::Descending => SortOrder::Ascending, SortOrder::Descending => SortOrder::Ascending,
} }
} }
/// A hack to get a const default.
pub const fn const_default() -> Self {
Self::Ascending
}
} }
impl Default for SortOrder { impl Default for SortOrder {
fn default() -> Self { fn default() -> Self {
Self::Ascending Self::const_default()
} }
} }
@ -195,18 +200,18 @@ where
/// Creates a new [`SortColumn`] with a hard width, which has no shortcut /// Creates a new [`SortColumn`] with a hard width, which has no shortcut
/// and sorts by default in ascending order ([`SortOrder::Ascending`]). /// and sorts by default in ascending order ([`SortOrder::Ascending`]).
pub fn hard(inner: T, width: u16) -> Self { pub const fn hard(inner: T, width: u16) -> Self {
Self { Self {
inner, inner,
bounds: ColumnWidthBounds::Hard(width), bounds: ColumnWidthBounds::Hard(width),
is_hidden: false, is_hidden: false,
default_order: SortOrder::default(), default_order: SortOrder::const_default(),
} }
} }
/// Creates a new [`SortColumn`] with a soft width, which has no shortcut /// Creates a new [`SortColumn`] with a soft width, which has no shortcut
/// and sorts by default in ascending order ([`SortOrder::Ascending`]). /// and sorts by default in ascending order ([`SortOrder::Ascending`]).
pub fn soft(inner: T, max_percentage: Option<f32>) -> Self { pub const fn soft(inner: T, max_percentage: Option<f32>) -> Self {
Self { Self {
inner, inner,
bounds: ColumnWidthBounds::Soft { bounds: ColumnWidthBounds::Soft {
@ -214,7 +219,7 @@ where
max_percentage, max_percentage,
}, },
is_hidden: false, is_hidden: false,
default_order: SortOrder::default(), default_order: SortOrder::const_default(),
} }
} }
@ -225,7 +230,7 @@ where
} }
/// Sets the default sort order to [`SortOrder::Descending`]. /// Sets the default sort order to [`SortOrder::Descending`].
pub fn default_descending(mut self) -> Self { pub const fn default_descending(mut self) -> Self {
self.default_order = SortOrder::Descending; self.default_order = SortOrder::Descending;
self self
} }

View file

@ -268,24 +268,34 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
[flags] [flags]
# Whether to hide the average cpu entry. # Whether to hide the average cpu entry.
#hide_avg_cpu = false #hide_avg_cpu = false
# Whether to use dot markers rather than braille. # Whether to use dot markers rather than braille.
#dot_marker = false #dot_marker = false
# The update rate of the application. # The update rate of the application.
#rate = "1s" #rate = "1s"
# Whether to put the CPU legend to the left. # Whether to put the CPU legend to the left.
#cpu_left_legend = false #cpu_left_legend = false
# Whether to set CPU% on a process to be based on the total CPU or just current usage. # Whether to set CPU% on a process to be based on the total CPU or just current usage.
#current_usage = false #current_usage = false
# Whether to set CPU% on a process to be based on the total CPU or per-core CPU% (not divided by the number of cpus). # Whether to set CPU% on a process to be based on the total CPU or per-core CPU% (not divided by the number of cpus).
#unnormalized_cpu = false #unnormalized_cpu = false
# Whether to group processes with the same name together by default. # Whether to group processes with the same name together by default.
#group_processes = false #group_processes = false
# Whether to make process searching case sensitive by default. # Whether to make process searching case sensitive by default.
#case_sensitive = false #case_sensitive = false
# Whether to make process searching look for matching the entire word by default. # Whether to make process searching look for matching the entire word by default.
#whole_word = false #whole_word = false
# Whether to make process searching use regex by default. # Whether to make process searching use regex by default.
#regex = false #regex = false
# The temperature unit. One of the following, defaults to "c" for Celsius: # The temperature unit. One of the following, defaults to "c" for Celsius:
#temperature_type = "c" #temperature_type = "c"
##temperature_type = "k" ##temperature_type = "k"
@ -293,79 +303,112 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
##temperature_type = "kelvin" ##temperature_type = "kelvin"
##temperature_type = "fahrenheit" ##temperature_type = "fahrenheit"
##temperature_type = "celsius" ##temperature_type = "celsius"
# The default time interval (in milliseconds). # The default time interval (in milliseconds).
#default_time_value = "60s" #default_time_value = "60s"
# The time delta on each zoom in/out action (in milliseconds). # The time delta on each zoom in/out action (in milliseconds).
#time_delta = 15000 #time_delta = 15000
# Hides the time scale. # Hides the time scale.
#hide_time = false #hide_time = false
# Override layout default widget # Override layout default widget
#default_widget_type = "proc" #default_widget_type = "proc"
#default_widget_count = 1 #default_widget_count = 1
# Expand selected widget upon starting the app # Expand selected widget upon starting the app
#expanded = true #expanded = true
# Use basic mode # Use basic mode
#basic = false #basic = false
# Use the old network legend style # Use the old network legend style
#use_old_network_legend = false #use_old_network_legend = false
# Remove space in tables # Remove space in tables
#hide_table_gap = false #hide_table_gap = false
# Show the battery widgets # Show the battery widgets
#battery = false #battery = false
# Disable mouse clicks # Disable mouse clicks
#disable_click = false #disable_click = false
# Show memory values in the processes widget as values by default # Show memory values in the processes widget as values by default
#process_memory_as_value = false #process_memory_as_value = false
# Show tree mode by default in the processes widget. # Show tree mode by default in the processes widget.
#tree = false #tree = false
# Shows an indicator in table widgets tracking where in the list you are. # Shows an indicator in table widgets tracking where in the list you are.
#show_table_scroll_position = false #show_table_scroll_position = false
# Show processes as their commands by default in the process widget. # Show processes as their commands by default in the process widget.
#process_command = false #process_command = false
# Displays the network widget with binary prefixes. # Displays the network widget with binary prefixes.
#network_use_binary_prefix = false #network_use_binary_prefix = false
# Displays the network widget using bytes. # Displays the network widget using bytes.
#network_use_bytes = false #network_use_bytes = false
# Displays the network widget with a log scale. # Displays the network widget with a log scale.
#network_use_log = false #network_use_log = false
# Hides advanced options to stop a process on Unix-like systems. # Hides advanced options to stop a process on Unix-like systems.
#disable_advanced_kill = false #disable_advanced_kill = false
# Hide GPU(s) information # Hide GPU(s) information
#disable_gpu = false #disable_gpu = false
# Shows cache and buffer memory # Shows cache and buffer memory
#enable_cache_memory = false #enable_cache_memory = false
# How much data is stored at once in terms of time. # How much data is stored at once in terms of time.
#retention = "10m" #retention = "10m"
# Where to place the legend for the memory widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right". # Where to place the legend for the memory widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
#memory_legend = "top-right" #memory_legend = "top-right"
# Where to place the legend for the network widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right". # Where to place the legend for the network widget. One of "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right".
#network_legend = "top-right" #network_legend = "top-right"
# Processes widget configuration # Processes widget configuration
#[processes] #[processes]
# The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built): # The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built):
# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU% # PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%
#columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"] #columns = ["PID", "Name", "CPU%", "Mem%", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%"]
# CPU widget configuration # CPU widget configuration
#[cpu] #[cpu]
# One of "all" (default), "average"/"avg" # One of "all" (default), "average"/"avg"
# default = "average" #default = "average"
# Disk widget configuration # Disk widget configuration
#[disk] #[disk]
# The columns shown by the process widget. The following columns are supported:
# Disk, Mount, Used, Free, Total, Used%, Free%, R/s, W/s
#columns = ["Disk", "Mount", "Used", "Free", "Total", "Used%", "R/s", "W/s"]
# By default, there are no disk name filters enabled. These can be turned on to filter out specific data entries if you # By default, there are no disk name filters enabled. These can be turned on to filter out specific data entries if you
# don't want to see them. An example use case is provided below. # don't want to see them. An example use case is provided below.
#[disk.name_filter] #[disk.name_filter]
# Whether to ignore any matches. Defaults to true. # Whether to ignore any matches. Defaults to true.
#is_list_ignored = true #is_list_ignored = true
# A list of filters to try and match. # A list of filters to try and match.
#list = ["/dev/sda\\d+", "/dev/nvme0n1p2"] #list = ["/dev/sda\\d+", "/dev/nvme0n1p2"]
# Whether to use regex. Defaults to false. # Whether to use regex. Defaults to false.
#regex = true #regex = true
# Whether to be case-sensitive. Defaults to false. # Whether to be case-sensitive. Defaults to false.
#case_sensitive = false #case_sensitive = false
# Whether to be require matching the whole word. Defaults to false. # Whether to be require matching the whole word. Defaults to false.
#whole_word = false #whole_word = false
@ -373,51 +416,63 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott
#[disk.mount_filter] #[disk.mount_filter]
# Whether to ignore any matches. Defaults to true. # Whether to ignore any matches. Defaults to true.
#is_list_ignored = true #is_list_ignored = true
# A list of filters to try and match. # A list of filters to try and match.
#list = ["/mnt/.*", "/boot"] #list = ["/mnt/.*", "/boot"]
# Whether to use regex. Defaults to false. # Whether to use regex. Defaults to false.
#regex = true #regex = true
# Whether to be case-sensitive. Defaults to false. # Whether to be case-sensitive. Defaults to false.
#case_sensitive = false #case_sensitive = false
# Whether to be require matching the whole word. Defaults to false. # Whether to be require matching the whole word. Defaults to false.
#whole_word = false #whole_word = false
# Temperature widget configuration # Temperature widget configuration
#[temperature] #[temperature]
# By default, there are no temperature sensor filters enabled. An example use case is provided below. # By default, there are no temperature sensor filters enabled. An example use case is provided below.
#[temperature.sensor_filter] #[temperature.sensor_filter]
# Whether to ignore any matches. Defaults to true. # Whether to ignore any matches. Defaults to true.
#is_list_ignored = true #is_list_ignored = true
# A list of filters to try and match. # A list of filters to try and match.
#list = ["cpu", "wifi"] #list = ["cpu", "wifi"]
# Whether to use regex. Defaults to false. # Whether to use regex. Defaults to false.
#regex = false #regex = false
# Whether to be case-sensitive. Defaults to false. # Whether to be case-sensitive. Defaults to false.
#case_sensitive = false #case_sensitive = false
# Whether to be require matching the whole word. Defaults to false. # Whether to be require matching the whole word. Defaults to false.
#whole_word = false #whole_word = false
# Network widget configuration # Network widget configuration
#[network] #[network]
# By default, there are no network interface filters enabled. An example use case is provided below. # By default, there are no network interface filters enabled. An example use case is provided below.
#[network.interface_filter] #[network.interface_filter]
# Whether to ignore any matches. Defaults to true. # Whether to ignore any matches. Defaults to true.
#is_list_ignored = true #is_list_ignored = true
# A list of filters to try and match. # A list of filters to try and match.
#list = ["virbr0.*"] #list = ["virbr0.*"]
# Whether to use regex. Defaults to false. # Whether to use regex. Defaults to false.
#regex = true #regex = true
# Whether to be case-sensitive. Defaults to false. # Whether to be case-sensitive. Defaults to false.
#case_sensitive = false #case_sensitive = false
# Whether to be require matching the whole word. Defaults to false. # Whether to be require matching the whole word. Defaults to false.
#whole_word = false #whole_word = false
# These are all the components that support custom theming. Note that colour support # These are all the components that support custom theming. Note that colour support
# will depend on terminal support. # will depend on terminal support.
#[styles] # Uncomment if you want to use custom styling #[styles] # Uncomment if you want to use custom styling
# Built-in themes. Valid values are: # Built-in themes. Valid values are:
# - "default" # - "default"
# - "default-light" # - "default-light"

View file

@ -402,15 +402,15 @@ pub fn convert_network_points(
}; };
if need_four_points { if need_four_points {
let rx_display = format!("{:.*}{}", 1, rx_converted_result.0, rx_converted_result.1); let rx_display = format!("{:.1}{}", rx_converted_result.0, rx_converted_result.1);
let total_rx_display = Some(format!( let total_rx_display = Some(format!(
"{:.*}{}", "{:.1}{}",
1, total_rx_converted_result.0, total_rx_converted_result.1 total_rx_converted_result.0, total_rx_converted_result.1
)); ));
let tx_display = format!("{:.*}{}", 1, tx_converted_result.0, tx_converted_result.1); let tx_display = format!("{:.1}{}", tx_converted_result.0, tx_converted_result.1);
let total_tx_display = Some(format!( let total_tx_display = Some(format!(
"{:.*}{}", "{:.1}{}",
1, total_tx_converted_result.0, total_tx_converted_result.1 total_tx_converted_result.0, total_tx_converted_result.1
)); ));
ConvertedNetworkData { ConvertedNetworkData {
rx, rx,
@ -474,35 +474,38 @@ pub fn convert_network_points(
/// Returns a string given a value that is converted to the closest binary /// Returns a string given a value that is converted to the closest binary
/// variant. If the value is greater than a gibibyte, then it will return a /// variant. If the value is greater than a gibibyte, then it will return a
/// decimal place. /// decimal place.
#[inline]
pub fn binary_byte_string(value: u64) -> String { pub fn binary_byte_string(value: u64) -> String {
let converted_values = get_binary_bytes(value); let converted_values = get_binary_bytes(value);
if value >= GIBI_LIMIT { if value >= GIBI_LIMIT {
format!("{:.*}{}", 1, converted_values.0, converted_values.1) format!("{:.1}{}", converted_values.0, converted_values.1)
} else { } else {
format!("{:.*}{}", 0, converted_values.0, converted_values.1) format!("{:.0}{}", converted_values.0, converted_values.1)
} }
} }
/// Returns a string given a value that is converted to the closest SI-variant. /// Returns a string given a value that is converted to the closest SI-variant.
/// If the value is greater than a giga-X, then it will return a decimal place. /// If the value is greater than a giga-X, then it will return a decimal place.
#[inline]
pub fn dec_bytes_per_string(value: u64) -> String { pub fn dec_bytes_per_string(value: u64) -> String {
let converted_values = get_decimal_bytes(value); let converted_values = get_decimal_bytes(value);
if value >= GIGA_LIMIT { if value >= GIGA_LIMIT {
format!("{:.*}{}", 1, converted_values.0, converted_values.1) format!("{:.1}{}", converted_values.0, converted_values.1)
} else { } else {
format!("{:.*}{}", 0, converted_values.0, converted_values.1) format!("{:.0}{}", converted_values.0, converted_values.1)
} }
} }
/// Returns a string given a value that is converted to the closest SI-variant, /// Returns a string given a value that is converted to the closest SI-variant,
/// per second. If the value is greater than a giga-X, then it will return a /// per second. If the value is greater than a giga-X, then it will return a
/// decimal place. /// decimal place.
#[inline]
pub fn dec_bytes_per_second_string(value: u64) -> String { pub fn dec_bytes_per_second_string(value: u64) -> String {
let converted_values = get_decimal_bytes(value); let converted_values = get_decimal_bytes(value);
if value >= GIGA_LIMIT { if value >= GIGA_LIMIT {
format!("{:.*}{}/s", 1, converted_values.0, converted_values.1) format!("{:.1}{}/s", converted_values.0, converted_values.1)
} else { } else {
format!("{:.*}{}/s", 0, converted_values.0, converted_values.1) format!("{:.0}{}/s", converted_values.0, converted_values.1)
} }
} }
@ -511,9 +514,9 @@ pub fn dec_bytes_per_second_string(value: u64) -> String {
pub fn dec_bytes_string(value: u64) -> String { pub fn dec_bytes_string(value: u64) -> String {
let converted_values = get_decimal_bytes(value); let converted_values = get_decimal_bytes(value);
if value >= GIGA_LIMIT { if value >= GIGA_LIMIT {
format!("{:.*}{}", 1, converted_values.0, converted_values.1) format!("{:.1}{}", converted_values.0, converted_values.1)
} else { } else {
format!("{:.*}{}", 0, converted_values.0, converted_values.1) format!("{:.0}{}", converted_values.0, converted_values.1)
} }
} }

View file

@ -279,6 +279,8 @@ fn generate_schema() -> anyhow::Result<()> {
use itertools::Itertools; use itertools::Itertools;
use strum::VariantArray; use strum::VariantArray;
// TODO: Maybe make this case insensitive? See https://stackoverflow.com/a/68639341
let proc_columns = schema.definitions.get_mut("ProcColumn").unwrap(); let proc_columns = schema.definitions.get_mut("ProcColumn").unwrap();
match proc_columns { match proc_columns {
schemars::schema::Schema::Object(proc_columns) => { schemars::schema::Schema::Object(proc_columns) => {
@ -293,6 +295,21 @@ fn generate_schema() -> anyhow::Result<()> {
} }
_ => anyhow::bail!("missing proc columns definition"), _ => anyhow::bail!("missing proc columns definition"),
} }
let disk_columns = schema.definitions.get_mut("DiskColumn").unwrap();
match disk_columns {
schemars::schema::Schema::Object(disk_columns) => {
let enums = disk_columns.enum_values.as_mut().unwrap();
*enums = widgets::DiskColumn::VARIANTS
.iter()
.flat_map(|var| var.get_schema_names())
.sorted()
.map(|v| serde_json::Value::String(v.to_string()))
.dedup()
.collect();
}
_ => anyhow::bail!("missing disk columns definition"),
}
} }
let metadata = schema.schema.metadata.as_mut().unwrap(); let metadata = schema.schema.metadata.as_mut().unwrap();

View file

@ -403,7 +403,11 @@ pub(crate) fn init_app(
Disk => { Disk => {
disk_state_map.insert( disk_state_map.insert(
widget.widget_id, widget.widget_id,
DiskTableWidget::new(&app_config_fields, &styling), DiskTableWidget::new(
&app_config_fields,
&styling,
config.disk.as_ref().map(|cfg| cfg.columns.as_slice()),
),
); );
} }
Temp => { Temp => {

View file

@ -6,7 +6,7 @@ use serde::Deserialize;
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[cfg_attr(test, derive(PartialEq, Eq))] #[cfg_attr(test, derive(PartialEq, Eq))]
pub enum CpuDefault { pub(crate) enum CpuDefault {
#[default] #[default]
All, All,
#[serde(alias = "avg")] #[serde(alias = "avg")]
@ -17,9 +17,9 @@ pub enum CpuDefault {
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))] #[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
pub struct CpuConfig { pub(crate) struct CpuConfig {
#[serde(default)] #[serde(default)]
pub default: CpuDefault, pub(crate) default: CpuDefault,
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,15 +1,45 @@
use serde::Deserialize; use serde::Deserialize;
use crate::options::DiskColumn;
use super::IgnoreList; use super::IgnoreList;
/// Disk configuration. /// Disk configuration.
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))] #[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
pub struct DiskConfig { pub(crate) struct DiskConfig {
/// A filter over the disk names. /// A filter over the disk names.
pub name_filter: Option<IgnoreList>, pub(crate) name_filter: Option<IgnoreList>,
/// A filter over the mount names. /// A filter over the mount names.
pub mount_filter: Option<IgnoreList>, pub(crate) mount_filter: Option<IgnoreList>,
/// A list of disk widget columns.
#[serde(default)]
pub(crate) columns: Vec<DiskColumn>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets
}
#[cfg(test)]
mod test {
use super::DiskConfig;
#[test]
fn empty_column_setting() {
let config = "";
let generated: DiskConfig = toml_edit::de::from_str(config).unwrap();
assert!(generated.columns.is_empty());
}
#[test]
fn valid_disk_column_settings() {
let config = r#"columns = ["disk", "mount", "used", "free", "total", "used%", "free%", "r/s", "w/s"]"#;
toml_edit::de::from_str::<DiskConfig>(config).expect("Should succeed!");
}
#[test]
fn bad_disk_column_settings() {
let config = r#"columns = ["diskk"]"#;
toml_edit::de::from_str::<DiskConfig>(config).expect_err("Should error out!");
}
} }

View file

@ -6,7 +6,7 @@ use super::IgnoreList;
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))] #[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
pub struct NetworkConfig { pub(crate) struct NetworkConfig {
/// A filter over the network interface names. /// A filter over the network interface names.
pub interface_filter: Option<IgnoreList>, pub(crate) interface_filter: Option<IgnoreList>,
} }

View file

@ -6,10 +6,10 @@ use crate::widgets::ProcColumn;
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))] #[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
pub struct ProcessesConfig { pub(crate) struct ProcessesConfig {
/// A list of process widget columns. /// A list of process widget columns.
#[serde(default)] #[serde(default)]
pub columns: Vec<ProcColumn>, pub(crate) columns: Vec<ProcColumn>, // TODO: make this more composable(?) in the future, we might need to rethink how it's done for custom widgets
} }
#[cfg(test)] #[cfg(test)]
@ -32,7 +32,7 @@ mod test {
} }
#[test] #[test]
fn process_column_settings() { fn valid_process_column_config() {
let config = r#" let config = r#"
columns = ["CPU%", "PiD", "user", "MEM", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state"] columns = ["CPU%", "PiD", "user", "MEM", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state"]
"#; "#;
@ -57,13 +57,13 @@ mod test {
} }
#[test] #[test]
fn process_column_settings_2() { fn bad_process_column_config() {
let config = r#"columns = ["MEM", "TWrite", "Cpuz", "read", "wps"]"#; let config = r#"columns = ["MEM", "TWrite", "Cpuz", "read", "wps"]"#;
toml_edit::de::from_str::<ProcessesConfig>(config).expect_err("Should error out!"); toml_edit::de::from_str::<ProcessesConfig>(config).expect_err("Should error out!");
} }
#[test] #[test]
fn process_column_settings_3() { fn valid_process_column_config_2() {
let config = r#"columns = ["Twrite", "T.Write"]"#; let config = r#"columns = ["Twrite", "T.Write"]"#;
let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap(); let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap();
assert_eq!( assert_eq!(

View file

@ -6,7 +6,7 @@ use super::IgnoreList;
#[derive(Clone, Debug, Default, Deserialize)] #[derive(Clone, Debug, Default, Deserialize)]
#[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "generate_schema", derive(schemars::JsonSchema))]
#[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))] #[cfg_attr(test, serde(deny_unknown_fields), derive(PartialEq, Eq))]
pub struct TempConfig { pub(crate) struct TempConfig {
/// A filter over the sensor names. /// A filter over the sensor names.
pub sensor_filter: Option<IgnoreList>, pub(crate) sensor_filter: Option<IgnoreList>,
} }

View file

@ -165,7 +165,7 @@ pub struct CpuWidgetState {
} }
impl CpuWidgetState { impl CpuWidgetState {
pub fn new( pub(crate) fn new(
config: &AppConfigFields, default_selection: CpuDefault, current_display_time: u64, config: &AppConfigFields, default_selection: CpuDefault, current_display_time: u64,
autohide_timer: Option<Instant>, colours: &ColourPalette, autohide_timer: Option<Instant>, colours: &ColourPalette,
) -> Self { ) -> Self {

View file

@ -1,5 +1,7 @@
use std::{borrow::Cow, cmp::max, num::NonZeroU16}; use std::{borrow::Cow, cmp::max, num::NonZeroU16};
use serde::Deserialize;
use crate::{ use crate::{
app::AppConfigFields, app::AppConfigFields,
canvas::components::data_table::{ canvas::components::data_table::{
@ -26,11 +28,7 @@ impl DiskWidgetData {
fn total_space(&self) -> Cow<'static, str> { fn total_space(&self) -> Cow<'static, str> {
if let Some(total_bytes) = self.total_bytes { if let Some(total_bytes) = self.total_bytes {
let converted_total_space = get_decimal_bytes(total_bytes); let converted_total_space = get_decimal_bytes(total_bytes);
format!( format!("{:.0}{}", converted_total_space.0, converted_total_space.1).into()
"{:.*}{}",
0, converted_total_space.0, converted_total_space.1
)
.into()
} else { } else {
"N/A".into() "N/A".into()
} }
@ -39,7 +37,7 @@ impl DiskWidgetData {
fn free_space(&self) -> Cow<'static, str> { fn free_space(&self) -> Cow<'static, str> {
if let Some(free_bytes) = self.free_bytes { if let Some(free_bytes) = self.free_bytes {
let converted_free_space = get_decimal_bytes(free_bytes); let converted_free_space = get_decimal_bytes(free_bytes);
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into() format!("{:.0}{}", converted_free_space.0, converted_free_space.1).into()
} else { } else {
"N/A".into() "N/A".into()
} }
@ -48,7 +46,7 @@ impl DiskWidgetData {
fn used_space(&self) -> Cow<'static, str> { fn used_space(&self) -> Cow<'static, str> {
if let Some(used_bytes) = self.used_bytes { if let Some(used_bytes) = self.used_bytes {
let converted_free_space = get_decimal_bytes(used_bytes); let converted_free_space = get_decimal_bytes(used_bytes);
format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1).into() format!("{:.0}{}", converted_free_space.0, converted_free_space.1).into()
} else { } else {
"N/A".into() "N/A".into()
} }
@ -58,19 +56,16 @@ impl DiskWidgetData {
if let (Some(free_bytes), Some(summed_total_bytes)) = if let (Some(free_bytes), Some(summed_total_bytes)) =
(self.free_bytes, self.summed_total_bytes) (self.free_bytes, self.summed_total_bytes)
{ {
Some(free_bytes as f64 / summed_total_bytes as f64 * 100_f64) if summed_total_bytes > 0 {
Some(free_bytes as f64 / summed_total_bytes as f64 * 100_f64)
} else {
None
}
} else { } else {
None None
} }
} }
fn free_percent_string(&self) -> Cow<'static, str> {
match self.free_percent() {
Some(val) => format!("{val:.1}%").into(),
None => "N/A".into(),
}
}
fn used_percent(&self) -> Option<f64> { fn used_percent(&self) -> Option<f64> {
if let (Some(used_bytes), Some(summed_total_bytes)) = if let (Some(used_bytes), Some(summed_total_bytes)) =
(self.used_bytes, self.summed_total_bytes) (self.used_bytes, self.summed_total_bytes)
@ -84,16 +79,15 @@ impl DiskWidgetData {
None None
} }
} }
fn used_percent_string(&self) -> Cow<'static, str> {
match self.used_percent() {
Some(val) => format!("{val:.1}%").into(),
None => "N/A".into(),
}
}
} }
pub enum DiskWidgetColumn { #[derive(Debug, Clone)]
#[cfg_attr(
feature = "generate_schema",
derive(schemars::JsonSchema, strum::VariantArray)
)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub enum DiskColumn {
Disk, Disk,
Mount, Mount,
Used, Used,
@ -105,45 +99,91 @@ pub enum DiskWidgetColumn {
IoWrite, IoWrite,
} }
impl ColumnHeader for DiskWidgetColumn { impl<'de> Deserialize<'de> for DiskColumn {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?.to_lowercase();
match value.as_str() {
"disk" => Ok(DiskColumn::Disk),
"mount" => Ok(DiskColumn::Mount),
"used" => Ok(DiskColumn::Used),
"free" => Ok(DiskColumn::Free),
"total" => Ok(DiskColumn::Total),
"usedpercent" | "used%" => Ok(DiskColumn::UsedPercent),
"freepercent" | "free%" => Ok(DiskColumn::FreePercent),
"r/s" => Ok(DiskColumn::IoRead),
"w/s" => Ok(DiskColumn::IoWrite),
_ => Err(serde::de::Error::custom(
"doesn't match any disk column name",
)),
}
}
}
impl DiskColumn {
/// An ugly hack to generate the JSON schema.
#[cfg(feature = "generate_schema")]
pub fn get_schema_names(&self) -> &[&'static str] {
match self {
DiskColumn::Disk => &["Disk"],
DiskColumn::Mount => &["Mount"],
DiskColumn::Used => &["Used"],
DiskColumn::Free => &["Free"],
DiskColumn::Total => &["Total"],
DiskColumn::UsedPercent => &["Used%"],
DiskColumn::FreePercent => &["Free%"],
DiskColumn::IoRead => &["R/s", "Read", "Rps"],
DiskColumn::IoWrite => &["W/s", "Write", "Wps"],
}
}
}
impl ColumnHeader for DiskColumn {
fn text(&self) -> Cow<'static, str> { fn text(&self) -> Cow<'static, str> {
match self { match self {
DiskWidgetColumn::Disk => "Disk(d)", DiskColumn::Disk => "Disk(d)",
DiskWidgetColumn::Mount => "Mount(m)", DiskColumn::Mount => "Mount(m)",
DiskWidgetColumn::Used => "Used(u)", DiskColumn::Used => "Used(u)",
DiskWidgetColumn::Free => "Free(n)", DiskColumn::Free => "Free(n)",
DiskWidgetColumn::UsedPercent => "Used%(p)", DiskColumn::Total => "Total(t)",
DiskWidgetColumn::FreePercent => "Free%", DiskColumn::UsedPercent => "Used%(p)",
DiskWidgetColumn::Total => "Total(t)", DiskColumn::FreePercent => "Free%",
DiskWidgetColumn::IoRead => "R/s(r)", DiskColumn::IoRead => "R/s(r)",
DiskWidgetColumn::IoWrite => "W/s(w)", DiskColumn::IoWrite => "W/s(w)",
} }
.into() .into()
} }
} }
impl DataToCell<DiskWidgetColumn> for DiskWidgetData { impl DataToCell<DiskColumn> for DiskWidgetData {
fn to_cell( fn to_cell(
&self, column: &DiskWidgetColumn, _calculated_width: NonZeroU16, &self, column: &DiskColumn, _calculated_width: NonZeroU16,
) -> Option<Cow<'static, str>> { ) -> Option<Cow<'static, str>> {
fn percent_string(value: Option<f64>) -> Cow<'static, str> {
match value {
Some(val) => format!("{val:.1}%").into(),
None => "N/A".into(),
}
}
let text = match column { let text = match column {
DiskWidgetColumn::Disk => self.name.clone(), DiskColumn::Disk => self.name.clone(),
DiskWidgetColumn::Mount => self.mount_point.clone(), DiskColumn::Mount => self.mount_point.clone(),
DiskWidgetColumn::Used => self.used_space(), DiskColumn::Used => self.used_space(),
DiskWidgetColumn::Free => self.free_space(), DiskColumn::Free => self.free_space(),
DiskWidgetColumn::UsedPercent => self.used_percent_string(), DiskColumn::UsedPercent => percent_string(self.used_percent()),
DiskWidgetColumn::FreePercent => self.free_percent_string(), DiskColumn::FreePercent => percent_string(self.free_percent()),
DiskWidgetColumn::Total => self.total_space(), DiskColumn::Total => self.total_space(),
DiskWidgetColumn::IoRead => self.io_read.clone(), DiskColumn::IoRead => self.io_read.clone(),
DiskWidgetColumn::IoWrite => self.io_write.clone(), DiskColumn::IoWrite => self.io_write.clone(),
}; };
Some(text) Some(text)
} }
fn column_widths<C: DataTableColumn<DiskWidgetColumn>>( fn column_widths<C: DataTableColumn<DiskColumn>>(data: &[Self], _columns: &[C]) -> Vec<u16>
data: &[Self], _columns: &[C],
) -> Vec<u16>
where where
Self: Sized, Self: Sized,
{ {
@ -159,63 +199,85 @@ impl DataToCell<DiskWidgetColumn> for DiskWidgetData {
} }
pub struct DiskTableWidget { pub struct DiskTableWidget {
pub table: SortDataTable<DiskWidgetData, DiskWidgetColumn>, pub table: SortDataTable<DiskWidgetData, DiskColumn>,
pub force_update_data: bool, pub force_update_data: bool,
} }
impl SortsRow for DiskWidgetColumn { impl SortsRow for DiskColumn {
type DataType = DiskWidgetData; type DataType = DiskWidgetData;
fn sort_data(&self, data: &mut [Self::DataType], descending: bool) { fn sort_data(&self, data: &mut [Self::DataType], descending: bool) {
match self { match self {
DiskWidgetColumn::Disk => { DiskColumn::Disk => {
data.sort_by(|a, b| sort_partial_fn(descending)(&a.name, &b.name)); data.sort_by(|a, b| sort_partial_fn(descending)(&a.name, &b.name));
} }
DiskWidgetColumn::Mount => { DiskColumn::Mount => {
data.sort_by(|a, b| sort_partial_fn(descending)(&a.mount_point, &b.mount_point)); data.sort_by(|a, b| sort_partial_fn(descending)(&a.mount_point, &b.mount_point));
} }
DiskWidgetColumn::Used => { DiskColumn::Used => {
data.sort_by(|a, b| sort_partial_fn(descending)(&a.used_bytes, &b.used_bytes)); data.sort_by(|a, b| sort_partial_fn(descending)(&a.used_bytes, &b.used_bytes));
} }
DiskWidgetColumn::UsedPercent => { DiskColumn::UsedPercent => {
data.sort_by(|a, b| { data.sort_by(|a, b| {
sort_partial_fn(descending)(&a.used_percent(), &b.used_percent()) sort_partial_fn(descending)(&a.used_percent(), &b.used_percent())
}); });
} }
DiskWidgetColumn::Free => { DiskColumn::Free => {
data.sort_by(|a, b| sort_partial_fn(descending)(&a.free_bytes, &b.free_bytes)); data.sort_by(|a, b| sort_partial_fn(descending)(&a.free_bytes, &b.free_bytes));
} }
DiskWidgetColumn::FreePercent => { DiskColumn::FreePercent => {
data.sort_by(|a, b| { data.sort_by(|a, b| {
sort_partial_fn(descending)(&a.free_percent(), &b.free_percent()) sort_partial_fn(descending)(&a.free_percent(), &b.free_percent())
}); });
} }
DiskWidgetColumn::Total => { DiskColumn::Total => {
data.sort_by(|a, b| sort_partial_fn(descending)(&a.total_bytes, &b.total_bytes)); data.sort_by(|a, b| sort_partial_fn(descending)(&a.total_bytes, &b.total_bytes));
} }
DiskWidgetColumn::IoRead => { DiskColumn::IoRead => {
data.sort_by(|a, b| sort_partial_fn(descending)(&a.io_read, &b.io_read)); data.sort_by(|a, b| sort_partial_fn(descending)(&a.io_read, &b.io_read));
} }
DiskWidgetColumn::IoWrite => { DiskColumn::IoWrite => {
data.sort_by(|a, b| sort_partial_fn(descending)(&a.io_write, &b.io_write)); data.sort_by(|a, b| sort_partial_fn(descending)(&a.io_write, &b.io_write));
} }
} }
} }
} }
impl DiskTableWidget { const fn create_column(column_type: &DiskColumn) -> SortColumn<DiskColumn> {
pub fn new(config: &AppConfigFields, palette: &ColourPalette) -> Self { match column_type {
let columns = [ DiskColumn::Disk => SortColumn::soft(DiskColumn::Disk, Some(0.2)),
SortColumn::soft(DiskWidgetColumn::Disk, Some(0.2)), DiskColumn::Mount => SortColumn::soft(DiskColumn::Mount, Some(0.2)),
SortColumn::soft(DiskWidgetColumn::Mount, Some(0.2)), DiskColumn::Used => SortColumn::hard(DiskColumn::Used, 8).default_descending(),
SortColumn::hard(DiskWidgetColumn::Used, 8).default_descending(), DiskColumn::Free => SortColumn::hard(DiskColumn::Free, 8).default_descending(),
SortColumn::hard(DiskWidgetColumn::Free, 8).default_descending(), DiskColumn::Total => SortColumn::hard(DiskColumn::Total, 9).default_descending(),
SortColumn::hard(DiskWidgetColumn::Total, 9).default_descending(), DiskColumn::UsedPercent => {
SortColumn::hard(DiskWidgetColumn::UsedPercent, 9).default_descending(), SortColumn::hard(DiskColumn::UsedPercent, 9).default_descending()
SortColumn::hard(DiskWidgetColumn::IoRead, 10).default_descending(), }
SortColumn::hard(DiskWidgetColumn::IoWrite, 11).default_descending(), DiskColumn::FreePercent => {
]; SortColumn::hard(DiskColumn::FreePercent, 9).default_descending()
}
DiskColumn::IoRead => SortColumn::hard(DiskColumn::IoRead, 10).default_descending(),
DiskColumn::IoWrite => SortColumn::hard(DiskColumn::IoWrite, 11).default_descending(),
}
}
const fn default_disk_columns() -> [SortColumn<DiskColumn>; 8] {
[
create_column(&DiskColumn::Disk),
create_column(&DiskColumn::Mount),
create_column(&DiskColumn::Used),
create_column(&DiskColumn::Free),
create_column(&DiskColumn::Total),
create_column(&DiskColumn::UsedPercent),
create_column(&DiskColumn::IoRead),
create_column(&DiskColumn::IoWrite),
]
}
impl DiskTableWidget {
pub fn new(
config: &AppConfigFields, palette: &ColourPalette, columns: Option<&[DiskColumn]>,
) -> Self {
let props = SortDataTableProps { let props = SortDataTableProps {
inner: DataTableProps { inner: DataTableProps {
title: Some(" Disks ".into()), title: Some(" Disks ".into()),
@ -231,9 +293,18 @@ impl DiskTableWidget {
let styling = DataTableStyling::from_palette(palette); let styling = DataTableStyling::from_palette(palette);
Self { match columns {
table: SortDataTable::new_sortable(columns, props, styling), Some(columns) => {
force_update_data: false, let columns = columns.iter().map(create_column).collect::<Vec<_>>();
Self {
table: SortDataTable::new_sortable(columns, props, styling),
force_update_data: false,
}
}
None => Self {
table: SortDataTable::new_sortable(default_disk_columns(), props, styling),
force_update_data: false,
},
} }
} }

View file

@ -94,28 +94,14 @@ impl ColumnHeader for ProcColumn {
fn header(&self) -> Cow<'static, str> { fn header(&self) -> Cow<'static, str> {
match self { match self {
ProcColumn::CpuPercent => "CPU%(c)", ProcColumn::CpuPercent => "CPU%(c)".into(),
ProcColumn::MemValue => "Mem(m)", ProcColumn::MemValue => "Mem(m)".into(),
ProcColumn::MemPercent => "Mem%(m)", ProcColumn::MemPercent => "Mem%(m)".into(),
ProcColumn::Pid => "PID(p)", ProcColumn::Pid => "PID(p)".into(),
ProcColumn::Count => "Count", ProcColumn::Name => "Name(n)".into(),
ProcColumn::Name => "Name(n)", ProcColumn::Command => "Command(n)".into(),
ProcColumn::Command => "Command(n)", _ => self.text(),
ProcColumn::ReadPerSecond => "R/s",
ProcColumn::WritePerSecond => "W/s",
ProcColumn::TotalRead => "T.Read",
ProcColumn::TotalWrite => "T.Write",
ProcColumn::State => "State",
ProcColumn::User => "User",
ProcColumn::Time => "Time",
#[cfg(feature = "gpu")]
ProcColumn::GpuMemValue => "GMem",
#[cfg(feature = "gpu")]
ProcColumn::GpuMemPercent => "GMem%",
#[cfg(feature = "gpu")]
ProcColumn::GpuUtilPercent => "GPU%",
} }
.into()
} }
} }
@ -214,7 +200,9 @@ impl<'de> Deserialize<'de> for ProcColumn {
"gmem" | "gmem%" => Ok(ProcColumn::GpuMemPercent), "gmem" | "gmem%" => Ok(ProcColumn::GpuMemPercent),
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
"gpu%" => Ok(ProcColumn::GpuUtilPercent), "gpu%" => Ok(ProcColumn::GpuUtilPercent),
_ => Err(serde::de::Error::custom("doesn't match any column type")), _ => Err(serde::de::Error::custom(
"doesn't match any process column name",
)),
} }
} }
} }

View file

@ -131,3 +131,11 @@ fn test_invalid_process_column() {
.failure() .failure()
.stderr(predicate::str::contains("doesn't match")); .stderr(predicate::str::contains("doesn't match"));
} }
#[test]
fn test_invalid_disk_column() {
btm_command(&["-C", "./tests/invalid_configs/invalid_disk_column.toml"])
.assert()
.failure()
.stderr(predicate::str::contains("doesn't match"));
}

View file

@ -0,0 +1,2 @@
[disk]
columns = ["disk", "fake"]

View file

@ -1,4 +1,5 @@
#:schema none #:schema none
# Adding this to avoid a warning from some schema linters
[styles] [styles]
theme = "gruvbox" theme = "gruvbox"