From ee2e1fee1c6758ee0ec33af28f9d51f1c423599e Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Mon, 27 May 2024 01:16:37 -0400 Subject: [PATCH] refactor: use struct for args instead of builder interface (#1472) * start moving args * tmp * refactor config * port over ags * update changelog --- CHANGELOG.md | 6 +- Cargo.lock | 23 +- Cargo.toml | 9 +- build.rs | 6 +- .../configuration/command-line-options.md | 24 +- .../configuration/config-file/flags.md | 6 +- sample_configs/default_config.toml | 2 +- schema/v1.0/bottom.json | 2 +- src/bin/main.rs | 34 +- src/canvas/styling.rs | 8 +- src/constants.rs | 2 +- src/data_collection/temperature.rs | 17 + src/lib.rs | 26 +- src/options.rs | 397 ++++---- src/options/args.rs | 888 +++++++++--------- src/options/config.rs | 4 +- src/options/config/layout.rs | 14 +- src/utils/error.rs | 9 +- src/widgets/process_table.rs | 12 +- tests/integration/arg_tests.rs | 16 +- 20 files changed, 769 insertions(+), 736 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c60d394..0fd7a9c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1436](https://github.com/ClementTsang/bottom/pull/1436): Use actual "swap" value for Windows. - [#1441](https://github.com/ClementTsang/bottom/pull/1441): The following arguments have changed names: - `--left_legend/-l` is now `--cpu_left_legend`. -- [#1441](https://github.com/ClementTsang/bottom/pull/1441): The following config arguments have changed names: +- [#1441](https://github.com/ClementTsang/bottom/pull/1441): The following config fields have changed names: - `expanded_on_startup` is now `expanded`. - `left_legend` is now `cpu_left_legend`. +- [#1472](https://github.com/ClementTsang/bottom/pull/1472): The following arguments have changed names: + - `mem_as_value` is now `process_memory_as_value`. +- [#1472](https://github.com/ClementTsang/bottom/pull/1472): The following config fields have changed names: + - `mem_as_value` is now `process_memory_as_value`. ### Bug Fixes diff --git a/Cargo.lock b/Cargo.lock index c00631ae..62308eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,6 +258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -302,6 +303,18 @@ dependencies = [ "clap_complete", ] +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.7.0" @@ -514,7 +527,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn", @@ -604,6 +617,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "humantime" version = "2.1.0" @@ -1333,7 +1352,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", diff --git a/Cargo.toml b/Cargo.toml index aaca85f4..75bc8984 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,9 @@ exclude = [ "desktop/", "docs/", "sample_configs/", + "schema", "scripts/", + "wix/", ".all-contributorsrc", ".cirrus.yml", ".gitignore", @@ -29,6 +31,9 @@ exclude = [ "codecov.yml", "CONTRIBUTING.md", "Cross.toml", + "debug.log", + "flamegraph.svg", + "perf.data", "rustfmt.toml", ] rust-version = "1.74.0" # The oldest version I've tested that should still build - note this is not an official MSRV! @@ -75,7 +80,7 @@ default = ["deploy"] anyhow = "1.0.86" backtrace = "0.3.71" cfg-if = "1.0.0" -clap = { version = "4.5.4", features = ["default", "cargo", "wrap_help"] } +clap = { version = "4.5.4", features = ["default", "cargo", "wrap_help", "derive"] } concat-string = "1.0.1" crossterm = "0.27.0" ctrlc = { version = "3.4.4", features = ["termination"] } @@ -135,7 +140,7 @@ predicates = "3.1.0" portable-pty = "0.8.1" [build-dependencies] -clap = { version = "4.5.4", features = ["default", "cargo", "wrap_help"] } +clap = { version = "4.5.4", features = ["default", "cargo", "wrap_help", "derive"] } clap_complete = "4.5.2" clap_complete_nushell = "4.5.1" clap_complete_fig = "4.5.0" diff --git a/build.rs b/build.rs index 5ebf6155..dc894181 100644 --- a/build.rs +++ b/build.rs @@ -7,12 +7,12 @@ use std::{ path::{Path, PathBuf}, }; -use clap::Command; +use clap::{Command, CommandFactory}; use clap_complete::{generate_to, shells::Shell, Generator}; use clap_complete_fig::Fig; use clap_complete_nushell::Nushell; -use crate::args::build_app; +use crate::args::BottomArgs; fn create_dir(dir: &Path) -> io::Result<()> { let res = fs::create_dir_all(dir); @@ -48,7 +48,7 @@ fn btm_generate() -> io::Result<()> { create_dir(&manpage_out_dir)?; // Generate completions - let mut app = build_app(); + let mut app = BottomArgs::command(); generate_completions(Shell::Bash, &mut app, &completion_out_dir)?; generate_completions(Shell::Zsh, &mut app, &completion_out_dir)?; generate_completions(Shell::Fish, &mut app, &completion_out_dir)?; diff --git a/docs/content/configuration/command-line-options.md b/docs/content/configuration/command-line-options.md index 9d95fc96..e32a059b 100644 --- a/docs/content/configuration/command-line-options.md +++ b/docs/content/configuration/command-line-options.md @@ -25,18 +25,18 @@ see information on these options by running `btm -h`, or run `btm --help` to dis ## Process Options -| Option | Behaviour | -| ------------------------- | -------------------------------------------------------------------------------------- | -| `-S, --case_sensitive` | Enables case sensitivity by default. | -| `-u, --current_usage` | Calculates process CPU usage as a percentage of current usage rather than total usage. | -| `--disable_advanced_kill` | Hides additional stopping options Unix-like systems. | -| `-g, --group_processes` | Groups processes with the same name by default. | -| `--mem_as_value` | Defaults to showing process memory usage by value. | -| `--process_command` | Shows the full command name instead of the process name by default. | -| `-R, --regex` | Enables regex by default while searching. | -| `-T, --tree` | Makes the process widget use tree mode by default. | -| `-n, --unnormalized_cpu` | Show process CPU% usage without averaging over the number of CPU cores. | -| `-W, --whole_word` | Enables whole-word matching by default while searching. | +| Option | Behaviour | +| --------------------------- | -------------------------------------------------------------------------------------- | +| `-S, --case_sensitive` | Enables case sensitivity by default. | +| `-u, --current_usage` | Calculates process CPU usage as a percentage of current usage rather than total usage. | +| `--disable_advanced_kill` | Hides additional stopping options Unix-like systems. | +| `-g, --group_processes` | Groups processes with the same name by default. | +| `--process_memory_as_value` | Defaults to showing process memory usage by value. | +| `--process_command` | Shows the full command name instead of the process name by default. | +| `-R, --regex` | Enables regex by default while searching. | +| `-T, --tree` | Makes the process widget use tree mode by default. | +| `-n, --unnormalized_cpu` | Show process CPU% usage without averaging over the number of CPU cores. | +| `-W, --whole_word` | Enables whole-word matching by default while searching. | ## Temperature Options diff --git a/docs/content/configuration/config-file/flags.md b/docs/content/configuration/config-file/flags.md index 4b6792cb..87c825b8 100644 --- a/docs/content/configuration/config-file/flags.md +++ b/docs/content/configuration/config-file/flags.md @@ -18,7 +18,7 @@ each time: | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | `hide_avg_cpu` | Boolean | Hides the average CPU usage. | | `dot_marker` | Boolean | Uses a dot marker for graphs. | -| `cpu_left_legend` | Boolean | Puts the CPU chart legend to the left side. | +| `cpu_left_legend` | Boolean | Puts the CPU chart legend to the left side. | | `current_usage` | Boolean | Sets process CPU% to be based on current CPU%. | | `group_processes` | Boolean | Groups processes with the same name by default. | | `case_sensitive` | Boolean | Enables case sensitivity by default. | @@ -37,7 +37,7 @@ each time: | `disable_click` | Boolean | Disables mouse clicks. | | `color` | String (one of ["default", "default-light", "gruvbox", "gruvbox-light", "nord", "nord-light"]) | Use a color scheme, use --help for supported values. | | `enable_cache_memory` | Boolean | Enable cache and buffer memory stats (not available on Windows). | -| `mem_as_value` | Boolean | Defaults to showing process memory usage by value. | +| `process_memory_as_value` | Boolean | Defaults to showing process memory usage by value. | | `tree` | Boolean | Defaults to showing the process widget in tree mode. | | `show_table_scroll_position` | Boolean | Shows the scroll position tracker in table widgets. | | `process_command` | Boolean | Show processes as their commands by default. | @@ -48,6 +48,6 @@ each time: | `enable_gpu` | Boolean | Shows the GPU widgets. | | `retention` | String (human readable time, such as "10m", "1h", etc.) | How much data is stored at once in terms of time. | | `unnormalized_cpu` | Boolean | Show process CPU% without normalizing over the number of cores. | -| `expanded` | Boolean | Expand the default widget upon starting the app. | +| `expanded` | Boolean | Expand the default widget upon starting the app. | | `memory_legend` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the memory widget. | | `network_legend` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the network widget. | diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index 3c20e4f4..4c8bd8ce 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -58,7 +58,7 @@ # Built-in themes. Valid values are "default", "default-light", "gruvbox", "gruvbox-light", "nord", "nord-light" #color = "default" # Show memory values in the processes widget as values by default -#mem_as_value = false +#process_memory_as_value = false # Show tree mode by default in the processes widget. #tree = false # Shows an indicator in table widgets tracking where in the list you are. diff --git a/schema/v1.0/bottom.json b/schema/v1.0/bottom.json index 807862d9..adfb8b7f 100644 --- a/schema/v1.0/bottom.json +++ b/schema/v1.0/bottom.json @@ -184,7 +184,7 @@ "description": "Built-in themes", "type": "string" }, - "mem_as_value": { + "process_memory_as_value": { "default": false, "description": "Show memory values in the processes widget as values by default", "type": "boolean" diff --git a/src/bin/main.rs b/src/bin/main.rs index c7841b4d..f22e828b 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -17,11 +17,10 @@ use bottom::{ args, canvas::{self, styling::CanvasStyling}, check_if_terminal, cleanup_terminal, create_collection_thread, create_input_thread, - create_or_get_config, data_conversion::*, - handle_key_event_or_break, handle_mouse_event, - options::{get_color_scheme, get_widget_layout, init_app}, - panic_hook, read_config, try_drawing, update_data, BottomEvent, + get_or_create_config, handle_key_event_or_break, handle_mouse_event, + options::{get_color_scheme, init_app}, + panic_hook, try_drawing, update_data, BottomEvent, }; use crossterm::{ event::{EnableBracketedPaste, EnableMouseCapture}, @@ -37,7 +36,7 @@ use tui::{backend::CrosstermBackend, Terminal}; fn main() -> Result<()> { // let _profiler = dhat::Profiler::new_heap(); - let matches = args::get_matches(); + let args = args::get_args(); #[cfg(feature = "logging")] { @@ -50,34 +49,17 @@ fn main() -> Result<()> { } // Read from config file. - let config = { - let config_path = read_config(matches.get_one::("config_location")) - .context("Unable to access the given config file location.")?; - - create_or_get_config(&config_path) - .context("Unable to properly parse or create the config file.")? - }; - - // Get widget layout separately - let (widget_layout, default_widget_id, default_widget_type_option) = - get_widget_layout(&matches, &config) - .context("Found an issue while trying to build the widget layout.")?; + let config = get_or_create_config(args.general.config_location.as_deref()) + .context("Unable to parse or create the config file.")?; // FIXME: Should move this into build app or config let styling = { - let colour_scheme = get_color_scheme(&matches, &config)?; + let colour_scheme = get_color_scheme(&args, &config)?; CanvasStyling::new(colour_scheme, &config)? }; // Create an "app" struct, which will control most of the program and store settings/state - let mut app = init_app( - matches, - config, - &widget_layout, - default_widget_id, - &default_widget_type_option, - &styling, - )?; + let (mut app, widget_layout) = init_app(args, config, &styling)?; // Create painter and set colours. let mut painter = canvas::Painter::init(widget_layout, styling)?; diff --git a/src/canvas/styling.rs b/src/canvas/styling.rs index 88273352..22a88a71 100644 --- a/src/canvas/styling.rs +++ b/src/canvas/styling.rs @@ -5,7 +5,7 @@ use colour_utils::*; use tui::style::{Color, Style}; use super::ColourScheme; -pub use crate::options::Config; +pub use crate::options::ConfigV1; use crate::{constants::*, options::colours::ConfigColours, utils::error}; pub struct CanvasStyling { @@ -124,7 +124,7 @@ macro_rules! try_set_colour_list { } impl CanvasStyling { - pub fn new(colour_scheme: ColourScheme, config: &Config) -> anyhow::Result { + pub fn new(colour_scheme: ColourScheme, config: &ConfigV1) -> anyhow::Result { let mut canvas_colours = Self::default(); match colour_scheme { @@ -236,7 +236,7 @@ mod test { use tui::style::{Color, Style}; use super::{CanvasStyling, ColourScheme}; - use crate::options::Config; + use crate::options::ConfigV1; #[test] fn default_selected_colour_works() { @@ -282,7 +282,7 @@ mod test { #[test] fn built_in_colour_schemes_work() { - let config = Config::default(); + let config = ConfigV1::default(); CanvasStyling::new(ColourScheme::Default, &config).unwrap(); CanvasStyling::new(ColourScheme::DefaultLight, &config).unwrap(); CanvasStyling::new(ColourScheme::Gruvbox, &config).unwrap(); diff --git a/src/constants.rs b/src/constants.rs index 50e66ab7..19873250 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -576,7 +576,7 @@ pub const CONFIG_TEXT: &str = r#"# This is a default config file for bottom. Al # Built-in themes. Valid values are "default", "default-light", "gruvbox", "gruvbox-light", "nord", "nord-light" #color = "default" # Show memory values in the processes widget as values by default -#mem_as_value = false +#process_memory_as_value = false # Show tree mode by default in the processes widget. #tree = false # Shows an indicator in table widgets tracking where in the list you are. diff --git a/src/data_collection/temperature.rs b/src/data_collection/temperature.rs index db490027..b85d1f81 100644 --- a/src/data_collection/temperature.rs +++ b/src/data_collection/temperature.rs @@ -13,6 +13,8 @@ cfg_if::cfg_if! { } } +use std::str::FromStr; + use crate::app::filter::Filter; #[derive(Default, Debug, Clone)] @@ -29,6 +31,21 @@ pub enum TemperatureType { Fahrenheit, } +impl FromStr for TemperatureType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "fahrenheit" | "f" => Ok(TemperatureType::Fahrenheit), + "kelvin" | "k" => Ok(TemperatureType::Kelvin), + "celsius" | "c" => Ok(TemperatureType::Celsius), + _ => Err(format!( + "\"{s}\" is an invalid temperature type, use \"\"." + )), + } + } +} + impl TemperatureType { /// Given a temperature in Celsius, covert it if necessary for a different unit. pub fn convert_temp_unit(&self, temp_celsius: f32) -> f32 { diff --git a/src/lib.rs b/src/lib.rs index 64b41b1e..f80317e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,7 +35,7 @@ use std::{ fs, io::{stderr, stdout, Write}, panic::PanicInfo, - path::PathBuf, + path::{Path, PathBuf}, sync::{ mpsc::{Receiver, Sender}, Arc, Condvar, Mutex, @@ -61,7 +61,7 @@ use crossterm::{ }; use data_conversion::*; pub use options::args; -use options::Config; +use options::ConfigV1; use utils::error; #[allow(unused_imports)] pub use utils::logging::*; @@ -202,9 +202,9 @@ pub fn handle_key_event_or_break( false } -pub fn read_config(config_location: Option<&String>) -> error::Result> { - let config_path = if let Some(conf_loc) = config_location { - Some(PathBuf::from(conf_loc.as_str())) +pub fn get_config_path(override_config_path: Option<&Path>) -> Option { + if let Some(conf_loc) = override_config_path { + Some(conf_loc.to_path_buf()) } else if cfg!(target_os = "windows") { if let Some(home_path) = dirs::config_dir() { let mut path = home_path; @@ -232,13 +232,13 @@ pub fn read_config(config_location: Option<&String>) -> error::Result) -> error::Result { - if let Some(path) = config_path { +pub fn get_or_create_config(override_config_path: Option<&Path>) -> error::Result { + let config_path = get_config_path(override_config_path); + + if let Some(path) = &config_path { if let Ok(config_string) = fs::read_to_string(path) { Ok(toml_edit::de::from_str(config_string.as_str())?) } else { @@ -247,11 +247,11 @@ pub fn create_or_get_config(config_path: &Option) -> error::Result { - if $matches.get_flag(stringify!($flag_name)) { + ($flag_name:ident, $arg:expr, $config:expr) => { + if $arg.$flag_name { true } else if let Some(flags) = &$config.flags { flags.$flag_name.unwrap_or(false) @@ -58,29 +60,34 @@ macro_rules! is_flag_enabled { } pub fn init_app( - matches: ArgMatches, config: Config, widget_layout: &BottomLayout, default_widget_id: u64, - default_widget_type_option: &Option, styling: &CanvasStyling, -) -> Result { + args: BottomArgs, config: ConfigV1, styling: &CanvasStyling, +) -> Result<(App, BottomLayout)> { use BottomWidgetType::*; // Since everything takes a reference, but we want to take ownership here to drop matches/config later... - let matches = &matches; + let args = &args; let config = &config; - let retention_ms = - get_retention(matches, config).context("Update `retention` in your config file.")?; - let autohide_time = is_flag_enabled!(autohide_time, matches, config); - let default_time_value = get_default_time_value(matches, config, retention_ms) - .context("Update 'default_time_value' in your config file.")?; + let (widget_layout, default_widget_id, default_widget_type_option) = + get_widget_layout(args, config) + .context("Found an issue while trying to build the widget layout.")?; - let use_basic_mode = is_flag_enabled!(basic, matches, config); - let expanded = is_flag_enabled!(expanded, matches, config); + let retention_ms = get_retention(args, config)?; + let autohide_time = is_flag_enabled!(autohide_time, args.general, config); + let default_time_value = get_default_time_value(args, config, retention_ms)?; + + let use_basic_mode = is_flag_enabled!(basic, args.general, config); + let expanded = is_flag_enabled!(expanded, args.general, config); // For processes - let is_grouped = is_flag_enabled!(group_processes, matches, config); - let is_case_sensitive = is_flag_enabled!(case_sensitive, matches, config); - let is_match_whole_word = is_flag_enabled!(whole_word, matches, config); - let is_use_regex = is_flag_enabled!(regex, matches, config); + let is_grouped = is_flag_enabled!(group_processes, args.process, config); + let is_case_sensitive = is_flag_enabled!(case_sensitive, args.process, config); + let is_match_whole_word = is_flag_enabled!(whole_word, args.process, config); + let is_use_regex = is_flag_enabled!(regex, args.process, config); + let is_default_tree = is_flag_enabled!(tree, args.process, config); + let is_default_command = is_flag_enabled!(process_command, args.process, config); + let is_advanced_kill = !(is_flag_enabled!(disable_advanced_kill, args.process, config)); + let process_memory_as_value = is_flag_enabled!(process_memory_as_value, args.process, config); let mut widget_map = HashMap::new(); let mut cpu_state_map: HashMap = HashMap::new(); @@ -102,14 +109,10 @@ pub fn init_app( let is_custom_layout = config.row.is_some(); let mut used_widget_set = HashSet::new(); - let show_memory_as_values = is_flag_enabled!(mem_as_value, matches, config); - let is_default_tree = is_flag_enabled!(tree, matches, config); - let is_default_command = is_flag_enabled!(process_command, matches, config); - let is_advanced_kill = !(is_flag_enabled!(disable_advanced_kill, matches, config)); - - let network_unit_type = get_network_unit_type(matches, config); - let network_scale_type = get_network_scale_type(matches, config); - let network_use_binary_prefix = is_flag_enabled!(network_use_binary_prefix, matches, config); + let network_unit_type = get_network_unit_type(args, config); + let network_scale_type = get_network_scale_type(args, config); + let network_use_binary_prefix = + is_flag_enabled!(network_use_binary_prefix, args.network, config); let proc_columns: Option> = { let columns = config.processes.as_ref().map(|cfg| cfg.columns.clone()); @@ -126,32 +129,34 @@ pub fn init_app( } }; - let network_legend_position = get_network_legend(matches, config)?; - let memory_legend_position = get_memory_legend(matches, config)?; + let network_legend_position = get_network_legend_position(args, config)?; + let memory_legend_position = get_memory_legend_position(args, config)?; // TODO: Can probably just reuse the options struct. let app_config_fields = AppConfigFields { - update_rate: get_update_rate(matches, config) - .context("Update 'rate' in your config file.")?, - temperature_type: get_temperature(matches, config) + update_rate: get_update_rate(args, config)?, + temperature_type: get_temperature(args, config) .context("Update 'temperature_type' in your config file.")?, - show_average_cpu: get_show_average_cpu(matches, config), - use_dot: is_flag_enabled!(dot_marker, matches, config), - cpu_left_legend: is_flag_enabled!(cpu_left_legend, matches, config), - use_current_cpu_total: is_flag_enabled!(current_usage, matches, config), - unnormalized_cpu: is_flag_enabled!(unnormalized_cpu, matches, config), + show_average_cpu: get_show_average_cpu(args, config), + use_dot: is_flag_enabled!(dot_marker, args.general, config), + cpu_left_legend: is_flag_enabled!(cpu_left_legend, args.cpu, config), + use_current_cpu_total: is_flag_enabled!(current_usage, args.process, config), + unnormalized_cpu: is_flag_enabled!(unnormalized_cpu, args.process, config), use_basic_mode, default_time_value, - time_interval: get_time_interval(matches, config, retention_ms) - .context("Update 'time_delta' in your config file.")?, - hide_time: is_flag_enabled!(hide_time, matches, config), + time_interval: get_time_interval(args, config, retention_ms)?, + hide_time: is_flag_enabled!(hide_time, args.general, config), autohide_time, - use_old_network_legend: is_flag_enabled!(use_old_network_legend, matches, config), - table_gap: u16::from(!(is_flag_enabled!(hide_table_gap, matches, config))), - disable_click: is_flag_enabled!(disable_click, matches, config), - enable_gpu: get_enable_gpu(matches, config), - enable_cache_memory: get_enable_cache_memory(matches, config), - show_table_scroll_position: is_flag_enabled!(show_table_scroll_position, matches, config), + use_old_network_legend: is_flag_enabled!(use_old_network_legend, args.network, config), + table_gap: u16::from(!(is_flag_enabled!(hide_table_gap, args.general, config))), + disable_click: is_flag_enabled!(disable_click, args.general, config), + enable_gpu: get_enable_gpu(args, config), + enable_cache_memory: get_enable_cache_memory(args, config), + show_table_scroll_position: is_flag_enabled!( + show_table_scroll_position, + args.general, + config + ), is_advanced_kill, memory_legend_position, network_legend_position, @@ -165,7 +170,7 @@ pub fn init_app( is_case_sensitive, is_match_whole_word, is_use_regex, - show_memory_as_values, + show_memory_as_values: process_memory_as_value, is_command: is_default_command, }; @@ -310,8 +315,8 @@ pub fn init_app( let used_widgets = UsedWidgets { use_cpu: used_widget_set.get(&Cpu).is_some() || used_widget_set.get(&BasicCpu).is_some(), use_mem, - use_cache: use_mem && get_enable_cache_memory(matches, config), - use_gpu: get_enable_gpu(matches, config), + use_cache: use_mem && get_enable_cache_memory(args, config), + use_gpu: get_enable_gpu(args, config), use_net: used_widget_set.get(&Net).is_some() || used_widget_set.get(&BasicNet).is_some(), use_proc: used_widget_set.get(&Proc).is_some(), use_disk: used_widget_set.get(&Disk).is_some(), @@ -348,37 +353,40 @@ pub fn init_app( }; let is_expanded = expanded && !use_basic_mode; - Ok(App::new( - app_config_fields, - states, - widget_map, - current_widget, - used_widgets, - filters, - is_expanded, + Ok(( + App::new( + app_config_fields, + states, + widget_map, + current_widget, + used_widgets, + filters, + is_expanded, + ), + widget_layout, )) } pub fn get_widget_layout( - matches: &ArgMatches, config: &Config, + args: &BottomArgs, config: &ConfigV1, ) -> error::Result<(BottomLayout, u64, Option)> { - let cpu_left_legend = is_flag_enabled!(cpu_left_legend, matches, config); + let cpu_left_legend = is_flag_enabled!(cpu_left_legend, args.cpu, config); let (default_widget_type, mut default_widget_count) = - get_default_widget_and_count(matches, config)?; + get_default_widget_and_count(args, config)?; let mut default_widget_id = 1; - let bottom_layout = if is_flag_enabled!(basic, matches, config) { + let bottom_layout = if is_flag_enabled!(basic, args.general, config) { default_widget_id = DEFAULT_WIDGET_ID; - BottomLayout::init_basic_default(get_use_battery(matches, config)) + BottomLayout::init_basic_default(get_use_battery(args, config)) } else { let ref_row: Vec; // Required to handle reference let rows = match &config.row { Some(r) => r, None => { // This cannot (like it really shouldn't) fail! - ref_row = toml_edit::de::from_str::(if get_use_battery(matches, config) { + ref_row = toml_edit::de::from_str::(if get_use_battery(args, config) { DEFAULT_BATTERY_LAYOUT } else { DEFAULT_LAYOUT @@ -423,13 +431,17 @@ pub fn get_widget_layout( Ok((bottom_layout, default_widget_id, default_widget_type)) } -fn get_update_rate(matches: &ArgMatches, config: &Config) -> error::Result { - let update_rate = if let Some(update_rate) = matches.get_one::("rate") { - try_parse_ms(update_rate)? +fn get_update_rate(args: &BottomArgs, config: &ConfigV1) -> error::Result { + let update_rate = if let Some(update_rate) = &args.general.rate { + try_parse_ms(update_rate).map_err(|_| { + BottomError::ArgumentError("set your update rate to be valid".to_string()) + })? } else if let Some(flags) = &config.flags { if let Some(rate) = &flags.rate { match rate { - StringOrNum::String(s) => try_parse_ms(s)?, + StringOrNum::String(s) => try_parse_ms(s).map_err(|_| { + BottomError::ConfigError("set your update rate to be valid".to_string()) + })?, StringOrNum::Num(n) => *n, } } else { @@ -448,32 +460,24 @@ fn get_update_rate(matches: &ArgMatches, config: &Config) -> error::Result Ok(update_rate) } -fn get_temperature(matches: &ArgMatches, config: &Config) -> error::Result { - if matches.get_flag("fahrenheit") { +fn get_temperature(args: &BottomArgs, config: &ConfigV1) -> error::Result { + if args.temperature.fahrenheit { return Ok(TemperatureType::Fahrenheit); - } else if matches.get_flag("kelvin") { + } else if args.temperature.kelvin { return Ok(TemperatureType::Kelvin); - } else if matches.get_flag("celsius") { + } else if args.temperature.celsius { return Ok(TemperatureType::Celsius); } else if let Some(flags) = &config.flags { if let Some(temp_type) = &flags.temperature_type { - // Give lowest priority to config. - return match temp_type.as_str() { - "fahrenheit" | "f" => Ok(TemperatureType::Fahrenheit), - "kelvin" | "k" => Ok(TemperatureType::Kelvin), - "celsius" | "c" => Ok(TemperatureType::Celsius), - _ => Err(BottomError::ConfigError(format!( - "\"{temp_type}\" is an invalid temperature type, use \"\"." - ))), - }; + return TemperatureType::from_str(temp_type).map_err(BottomError::ConfigError); } } Ok(TemperatureType::Celsius) } -/// Yes, this function gets whether to show average CPU (true) or not (false) -fn get_show_average_cpu(matches: &ArgMatches, config: &Config) -> bool { - if matches.get_flag("hide_avg_cpu") { +/// Yes, this function gets whether to show average CPU (true) or not (false). +fn get_show_average_cpu(args: &BottomArgs, config: &ConfigV1) -> bool { + if args.cpu.hide_avg_cpu { return false; } else if let Some(flags) = &config.flags { if let Some(avg_cpu) = flags.hide_avg_cpu { @@ -497,31 +501,34 @@ fn try_parse_ms(s: &str) -> error::Result { } fn get_default_time_value( - matches: &ArgMatches, config: &Config, retention_ms: u64, + args: &BottomArgs, config: &ConfigV1, retention_ms: u64, ) -> error::Result { - let default_time = - if let Some(default_time_value) = matches.get_one::("default_time_value") { - try_parse_ms(default_time_value)? - } else if let Some(flags) = &config.flags { - if let Some(default_time_value) = &flags.default_time_value { - match default_time_value { - StringOrNum::String(s) => try_parse_ms(s)?, - StringOrNum::Num(n) => *n, - } - } else { - DEFAULT_TIME_MILLISECONDS + let default_time = if let Some(default_time_value) = &args.general.default_time_value { + try_parse_ms(default_time_value).map_err(|_| { + BottomError::ArgumentError("set your default time to be valid".to_string()) + })? + } else if let Some(flags) = &config.flags { + if let Some(default_time_value) = &flags.default_time_value { + match default_time_value { + StringOrNum::String(s) => try_parse_ms(s).map_err(|_| { + BottomError::ConfigError("set your default time to be valid".to_string()) + })?, + StringOrNum::Num(n) => *n, } } else { DEFAULT_TIME_MILLISECONDS - }; + } + } else { + DEFAULT_TIME_MILLISECONDS + }; if default_time < 30000 { return Err(BottomError::ConfigError( - "set your default value to be at least 30s.".to_string(), + "set your default time to be at least 30s.".to_string(), )); } else if default_time > retention_ms { return Err(BottomError::ConfigError(format!( - "set your default value to be at most {}.", + "set your default time to be at most {}.", humantime::Duration::from(Duration::from_millis(retention_ms)) ))); } @@ -530,14 +537,18 @@ fn get_default_time_value( } fn get_time_interval( - matches: &ArgMatches, config: &Config, retention_ms: u64, + args: &BottomArgs, config: &ConfigV1, retention_ms: u64, ) -> error::Result { - let time_interval = if let Some(time_interval) = matches.get_one::("time_delta") { - try_parse_ms(time_interval)? + let time_interval = if let Some(time_interval) = &args.general.time_delta { + try_parse_ms(time_interval).map_err(|_| { + BottomError::ArgumentError("set your time delta to be valid".to_string()) + })? } else if let Some(flags) = &config.flags { if let Some(time_interval) = &flags.time_delta { match time_interval { - StringOrNum::String(s) => try_parse_ms(s)?, + StringOrNum::String(s) => try_parse_ms(s).map_err(|_| { + BottomError::ArgumentError("set your time delta to be valid".to_string()) + })?, StringOrNum::Num(n) => *n, } } else { @@ -562,9 +573,9 @@ fn get_time_interval( } fn get_default_widget_and_count( - matches: &ArgMatches, config: &Config, + args: &BottomArgs, config: &ConfigV1, ) -> error::Result<(Option, u64)> { - let widget_type = if let Some(widget_type) = matches.get_one::("default_widget_type") { + let widget_type = if let Some(widget_type) = &args.general.default_widget_type { let parsed_widget = widget_type.parse::()?; if let BottomWidgetType::Empty = parsed_widget { None @@ -586,21 +597,20 @@ fn get_default_widget_and_count( None }; - let widget_count = if let Some(widget_count) = matches.get_one::("default_widget_count") - { - Some(widget_count.parse::()?) - } else if let Some(flags) = &config.flags { - flags - .default_widget_count - .map(|widget_count| widget_count.into()) + let widget_count: Option = if let Some(widget_count) = args.general.default_widget_count { + Some(widget_count.into()) } else { - None + config.flags.as_ref().and_then(|flags| { + flags + .default_widget_count + .map(|widget_count| widget_count.into()) + }) }; match (widget_type, widget_count) { (Some(widget_type), Some(widget_count)) => { let widget_count = widget_count.try_into().map_err(|_| BottomError::ConfigError( - "set your widget count to be at most unsigned INT_MAX.".to_string() + "set your widget count to be at most 18446744073709551615.".to_string() ))?; Ok((Some(widget_type), widget_count)) } @@ -613,9 +623,10 @@ fn get_default_widget_and_count( } #[allow(unused_variables)] -fn get_use_battery(matches: &ArgMatches, config: &Config) -> bool { +fn get_use_battery(args: &BottomArgs, config: &ConfigV1) -> bool { #[cfg(feature = "battery")] { + // TODO: Move this so it's dynamic in the app itself and automatically hide if there are no batteries? if let Ok(battery_manager) = Manager::new() { if let Ok(batteries) = battery_manager.batteries() { if batteries.count() == 0 { @@ -624,7 +635,7 @@ fn get_use_battery(matches: &ArgMatches, config: &Config) -> bool { } } - if matches.get_flag("battery") { + if args.battery.battery { return true; } else if let Some(flags) = &config.flags { if let Some(battery) = flags.battery { @@ -637,10 +648,10 @@ fn get_use_battery(matches: &ArgMatches, config: &Config) -> bool { } #[allow(unused_variables)] -fn get_enable_gpu(matches: &ArgMatches, config: &Config) -> bool { +fn get_enable_gpu(args: &BottomArgs, config: &ConfigV1) -> bool { #[cfg(feature = "gpu")] { - if matches.get_flag("enable_gpu") { + if args.gpu.enable_gpu { return true; } else if let Some(flags) = &config.flags { if let Some(enable_gpu) = flags.enable_gpu { @@ -653,10 +664,10 @@ fn get_enable_gpu(matches: &ArgMatches, config: &Config) -> bool { } #[allow(unused_variables)] -fn get_enable_cache_memory(matches: &ArgMatches, config: &Config) -> bool { +fn get_enable_cache_memory(args: &BottomArgs, config: &ConfigV1) -> bool { #[cfg(not(target_os = "windows"))] { - if matches.get_flag("enable_cache_memory") { + if args.memory.enable_cache_memory { return true; } else if let Some(flags) = &config.flags { if let Some(enable_cache_memory) = flags.enable_cache_memory { @@ -705,8 +716,8 @@ fn get_ignore_list(ignore_list: &Option) -> error::Result error::Result { - if let Some(color) = matches.get_one::("color") { +pub fn get_color_scheme(args: &BottomArgs, config: &ConfigV1) -> error::Result { + if let Some(color) = &args.style.color { // Highest priority is always command line flags... return ColourScheme::from_str(color); } else if let Some(colors) = &config.colors { @@ -730,8 +741,8 @@ pub fn get_color_scheme(matches: &ArgMatches, config: &Config) -> error::Result< Ok(ColourScheme::Default) } -fn get_network_unit_type(matches: &ArgMatches, config: &Config) -> DataUnit { - if matches.get_flag("network_use_bytes") { +fn get_network_unit_type(args: &BottomArgs, config: &ConfigV1) -> DataUnit { + if args.network.network_use_bytes { return DataUnit::Byte; } else if let Some(flags) = &config.flags { if let Some(network_use_bytes) = flags.network_use_bytes { @@ -744,8 +755,8 @@ fn get_network_unit_type(matches: &ArgMatches, config: &Config) -> DataUnit { DataUnit::Bit } -fn get_network_scale_type(matches: &ArgMatches, config: &Config) -> AxisScaling { - if matches.get_flag("network_use_log") { +fn get_network_scale_type(args: &BottomArgs, config: &ConfigV1) -> AxisScaling { + if args.network.network_use_log { return AxisScaling::Log; } else if let Some(flags) = &config.flags { if let Some(network_use_log) = flags.network_use_log { @@ -758,15 +769,18 @@ fn get_network_scale_type(matches: &ArgMatches, config: &Config) -> AxisScaling AxisScaling::Linear } -fn get_retention(matches: &ArgMatches, config: &Config) -> error::Result { +fn get_retention(args: &BottomArgs, config: &ConfigV1) -> error::Result { const DEFAULT_RETENTION_MS: u64 = 600 * 1000; // Keep 10 minutes of data. - if let Some(retention) = matches.get_one::("retention") { + if let Some(retention) = &args.general.retention { try_parse_ms(retention) + .map_err(|_| BottomError::ArgumentError("`retention` is an invalid value".to_string())) } else if let Some(flags) = &config.flags { if let Some(retention) = &flags.retention { Ok(match retention { - StringOrNum::String(s) => try_parse_ms(s)?, + StringOrNum::String(s) => try_parse_ms(s).map_err(|_| { + BottomError::ConfigError("`retention` is an invalid value".to_string()) + })?, StringOrNum::Num(n) => *n, }) } else { @@ -777,19 +791,21 @@ fn get_retention(matches: &ArgMatches, config: &Config) -> error::Result { } } -fn get_network_legend( - matches: &ArgMatches, config: &Config, +fn get_network_legend_position( + args: &BottomArgs, config: &ConfigV1, ) -> error::Result> { - let error = - |_| BottomError::ConfigError("network_legend is set to an invalid value".to_string()); - if let Some(s) = matches.get_one::("network_legend") { + if let Some(s) = &args.network.network_legend { match s.to_ascii_lowercase().trim() { "none" => Ok(None), - position => Ok(Some(position.parse::().map_err(error)?)), + position => Ok(Some(position.parse::().map_err(|_| { + BottomError::ArgumentError("`network_legend` is an invalid value".to_string()) + })?)), } } else if let Some(flags) = &config.flags { if let Some(legend) = &flags.network_legend { - Ok(Some(legend.parse::().map_err(error)?)) + Ok(Some(legend.parse::().map_err(|_| { + BottomError::ConfigError("`network_legend` is an invalid value".to_string()) + })?)) } else { Ok(Some(LegendPosition::default())) } @@ -798,19 +814,21 @@ fn get_network_legend( } } -fn get_memory_legend( - matches: &ArgMatches, config: &Config, +fn get_memory_legend_position( + args: &BottomArgs, config: &ConfigV1, ) -> error::Result> { - let error = - |_| BottomError::ConfigError("memory_legend is set to an invalid value".to_string()); - if let Some(s) = matches.get_one::("memory_legend") { + if let Some(s) = &args.memory.memory_legend { match s.to_ascii_lowercase().trim() { "none" => Ok(None), - position => Ok(Some(position.parse::().map_err(error)?)), + position => Ok(Some(position.parse::().map_err(|_| { + BottomError::ArgumentError("`memory_legend` is an invalid value".to_string()) + })?)), } } else if let Some(flags) = &config.flags { if let Some(legend) = &flags.memory_legend { - Ok(Some(legend.parse::().map_err(error)?)) + Ok(Some(legend.parse::().map_err(|_| { + BottomError::ConfigError("`memory_legend` is an invalid value".to_string()) + })?)) } else { Ok(Some(LegendPosition::default())) } @@ -821,11 +839,12 @@ fn get_memory_legend( #[cfg(test)] mod test { - use clap::ArgMatches; + use clap::Parser; - use super::{get_color_scheme, get_time_interval, get_widget_layout, Config}; + use super::{get_color_scheme, get_time_interval, ConfigV1}; use crate::{ app::App, + args::BottomArgs, canvas::styling::CanvasStyling, options::{ config::ConfigFlags, get_default_time_value, get_retention, get_update_rate, @@ -854,26 +873,24 @@ mod test { #[test] fn matches_human_times() { - let config = Config::default(); - let app = crate::args::build_app(); + let config = ConfigV1::default(); { - let app = app.clone(); let delta_args = vec!["btm", "--time_delta", "2 min"]; - let matches = app.get_matches_from(delta_args); + let args = BottomArgs::parse_from(delta_args); assert_eq!( - get_time_interval(&matches, &config, 60 * 60 * 1000), + get_time_interval(&args, &config, 60 * 60 * 1000), Ok(2 * 60 * 1000) ); } { let default_time_args = vec!["btm", "--default_time_value", "300s"]; - let matches = app.get_matches_from(default_time_args); + let args = BottomArgs::parse_from(default_time_args); assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), + get_default_time_value(&args, &config, 60 * 60 * 1000), Ok(5 * 60 * 1000) ); } @@ -881,26 +898,24 @@ mod test { #[test] fn matches_number_times() { - let config = Config::default(); - let app = crate::args::build_app(); + let config = ConfigV1::default(); { - let app = app.clone(); let delta_args = vec!["btm", "--time_delta", "120000"]; - let matches = app.get_matches_from(delta_args); + let args = BottomArgs::parse_from(delta_args); assert_eq!( - get_time_interval(&matches, &config, 60 * 60 * 1000), + get_time_interval(&args, &config, 60 * 60 * 1000), Ok(2 * 60 * 1000) ); } { let default_time_args = vec!["btm", "--default_time_value", "300000"]; - let matches = app.get_matches_from(default_time_args); + let args = BottomArgs::parse_from(default_time_args); assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), + get_default_time_value(&args, &config, 60 * 60 * 1000), Ok(5 * 60 * 1000) ); } @@ -908,10 +923,9 @@ mod test { #[test] fn config_human_times() { - let app = crate::args::build_app(); - let matches = app.get_matches_from(["btm"]); + let args = BottomArgs::parse_from(["btm"]); - let mut config = Config::default(); + let mut config = ConfigV1::default(); let flags = ConfigFlags { time_delta: Some("2 min".to_string().into()), default_time_value: Some("300s".to_string().into()), @@ -923,26 +937,25 @@ mod test { config.flags = Some(flags); assert_eq!( - get_time_interval(&matches, &config, 60 * 60 * 1000), + get_time_interval(&args, &config, 60 * 60 * 1000), Ok(2 * 60 * 1000) ); assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), + get_default_time_value(&args, &config, 60 * 60 * 1000), Ok(5 * 60 * 1000) ); - assert_eq!(get_update_rate(&matches, &config), Ok(1000)); + assert_eq!(get_update_rate(&args, &config), Ok(1000)); - assert_eq!(get_retention(&matches, &config), Ok(600000)); + assert_eq!(get_retention(&args, &config), Ok(600000)); } #[test] fn config_number_times_as_string() { - let app = crate::args::build_app(); - let matches = app.get_matches_from(["btm"]); + let args = BottomArgs::parse_from(["btm"]); - let mut config = Config::default(); + let mut config = ConfigV1::default(); let flags = ConfigFlags { time_delta: Some("120000".to_string().into()), default_time_value: Some("300000".to_string().into()), @@ -954,26 +967,25 @@ mod test { config.flags = Some(flags); assert_eq!( - get_time_interval(&matches, &config, 60 * 60 * 1000), + get_time_interval(&args, &config, 60 * 60 * 1000), Ok(2 * 60 * 1000) ); assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), + get_default_time_value(&args, &config, 60 * 60 * 1000), Ok(5 * 60 * 1000) ); - assert_eq!(get_update_rate(&matches, &config), Ok(1000)); + assert_eq!(get_update_rate(&args, &config), Ok(1000)); - assert_eq!(get_retention(&matches, &config), Ok(600000)); + assert_eq!(get_retention(&args, &config), Ok(600000)); } #[test] fn config_number_times_as_num() { - let app = crate::args::build_app(); - let matches = app.get_matches_from(["btm"]); + let args = BottomArgs::parse_from(["btm"]); - let mut config = Config::default(); + let mut config = ConfigV1::default(); let flags = ConfigFlags { time_delta: Some(120000.into()), default_time_value: Some(300000.into()), @@ -985,41 +997,35 @@ mod test { config.flags = Some(flags); assert_eq!( - get_time_interval(&matches, &config, 60 * 60 * 1000), + get_time_interval(&args, &config, 60 * 60 * 1000), Ok(2 * 60 * 1000) ); assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), + get_default_time_value(&args, &config, 60 * 60 * 1000), Ok(5 * 60 * 1000) ); - assert_eq!(get_update_rate(&matches, &config), Ok(1000)); + assert_eq!(get_update_rate(&args, &config), Ok(1000)); - assert_eq!(get_retention(&matches, &config), Ok(600000)); + assert_eq!(get_retention(&args, &config), Ok(600000)); } - fn create_app(config: Config, matches: ArgMatches) -> App { - let (layout, id, ty) = get_widget_layout(&matches, &config).unwrap(); + fn create_app(args: BottomArgs) -> App { + let config = ConfigV1::default(); let styling = - CanvasStyling::new(get_color_scheme(&matches, &config).unwrap(), &config).unwrap(); + CanvasStyling::new(get_color_scheme(&args, &config).unwrap(), &config).unwrap(); - super::init_app(matches, config, &layout, id, &ty, &styling).unwrap() + super::init_app(args, config, &styling).unwrap().0 } // TODO: There's probably a better way to create clap options AND unify together to avoid the possibility of // typos/mixing up. Use proc macros to unify on one struct? #[test] fn verify_cli_options_build() { - let app = crate::args::build_app(); + let app = crate::args::build_cmd(); - let default_app = { - let app = app.clone(); - let config = Config::default(); - let matches = app.get_matches_from([""]); - - create_app(config, matches) - }; + let default_app = create_app(BottomArgs::parse_from(["btm"])); // Skip battery since it's tricky to test depending on the platform/features we're testing with. let skip = ["help", "version", "celsius", "battery"]; @@ -1036,11 +1042,8 @@ mod test { let arg = format!("--{arg_name}"); let arguments = vec!["btm", &arg]; - let app = app.clone(); - let config = Config::default(); - let matches = app.get_matches_from(arguments); - - let testing_app = create_app(config, matches); + let args = BottomArgs::parse_from(arguments); + let testing_app = create_app(args); if (default_app.app_config_fields == testing_app.app_config_fields) && default_app.is_expanded == testing_app.is_expanded diff --git a/src/options/args.rs b/src/options/args.rs index a9d550bb..92afa846 100644 --- a/src/options/args.rs +++ b/src/options/args.rs @@ -5,41 +5,28 @@ // TODO: New sections are misaligned! See if we can get that fixed. -use std::cmp::Ordering; +use std::path::PathBuf; -use clap::*; +use clap::{builder::PossibleValue, *}; use indoc::indoc; -pub fn get_matches() -> ArgMatches { - build_app().get_matches() -} +const TEMPLATE: &str = indoc! { + "{name} {version} + {author} -/// Returns an [`Ordering`] for two [`Arg`] values. -/// -/// Note this assumes that they both have a _long_ name, and will -/// panic if either are missing! -fn sort_args(a: &Arg, b: &Arg) -> Ordering { - let a = a.get_long().unwrap(); - let b = b.get_long().unwrap(); + {about} - a.cmp(b) -} + {usage-heading} {usage} -/// Create an array of [`Arg`] values. If there is more than one value, then -/// they will be sorted by their long name. Note this sort will panic if -/// any [`Arg`] does not have a long name! -macro_rules! args { - ( $arg:expr $(,)?) => { - [$arg] - }; - ( $( $arg:expr ),+ $(,)? ) => { - { - let mut args = [ $( $arg, )* ]; - args.sort_unstable_by(sort_args); - args - } - }; -} + {all-args}" +}; + +const USAGE: &str = "btm [OPTIONS]"; + +const VERSION: &str = match option_env!("NIGHTLY_VERSION") { + Some(nightly_version) => nightly_version, + None => crate_version!(), +}; const CHART_WIDGET_POSITIONS: [&str; 9] = [ "none", @@ -53,57 +40,104 @@ const CHART_WIDGET_POSITIONS: [&str; 9] = [ "bottom-right", ]; -fn general_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("General Options"); +/// Represents the arguments that can be passed in to bottom. +#[derive(Parser, Debug)] +#[command( + name = crate_name!(), + version = VERSION, + author = crate_authors!(), + about = crate_description!(), + disable_help_flag = true, + disable_version_flag = true, + color = ColorChoice::Auto, + help_template = TEMPLATE, + override_usage = USAGE, +)] +pub struct BottomArgs { + #[command(flatten)] + pub general: GeneralArgs, - let autohide_time = Arg::new("autohide_time") - .long("autohide_time") - .action(ArgAction::SetTrue) - .help("Temporarily shows the time scale in graphs.") - .long_help( - "Automatically hides the time scale in graphs after being shown for a brief moment when zoomed \ - in/out. If time is disabled via --hide_time then this will have no effect." - ); + #[command(flatten)] + pub process: ProcessArgs, - let basic = Arg::new("basic") - .short('b') - .long("basic") - .action(ArgAction::SetTrue) - .help("Hides graphs and uses a more basic look.") - .long_help("Hides graphs and uses a more basic look, largely inspired by htop's design."); + #[command(flatten)] + pub temperature: TemperatureArgs, - let config_location = Arg::new("config_location") - .short('C') - .long("config") - .action(ArgAction::Set) - .value_name("CONFIG PATH") - .help("Sets the location of the config file.") - .long_help( - "Sets the location of the config file. Expects a config file in the TOML format. \ - If it doesn't exist, a default config file is created at the path. If no path is provided, \ - the default config location will be used." - ) - .value_hint(ValueHint::AnyPath); + #[command(flatten)] + pub cpu: CpuArgs, - let default_time_value = Arg::new("default_time_value") - .short('t') - .long("default_time_value") - .action(ArgAction::Set) - .value_name("TIME") - .help("Default time value for graphs.") - .long_help( - "Default time value for graphs. Either a number in milliseconds or a 'human duration' \ - (e.g. 60s, 10m). Defaults to 60s, must be at least 30s.", - ); + #[command(flatten)] + pub memory: MemoryArgs, + + #[command(flatten)] + pub network: NetworkArgs, + + #[cfg(feature = "battery")] + #[command(flatten)] + pub battery: BatteryArgs, + + #[cfg(feature = "gpu")] + #[command(flatten)] + pub gpu: GpuArgs, + + #[command(flatten)] + pub style: StyleArgs, + + #[command(flatten)] + pub other: OtherArgs, +} + +/// General arguments/config options. +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "General Options", rename_all = "snake_case")] +pub struct GeneralArgs { + #[arg( + long, + action = ArgAction::SetTrue, + help = "Temporarily shows the time scale in graphs.", + long = "Automatically hides the time scale in graphs after being shown for a brief moment when zoomed \ + in/out. If time is disabled using --hide_time then this will have no effect." + )] + pub autohide_time: bool, + + #[arg( + short = 'b', + long, + action = ArgAction::SetTrue, + help = "Hides graphs and uses a more basic look.", + long_help = "Hides graphs and uses a more basic look, largely inspired by htop's design." + )] + pub basic: bool, + + #[arg( + short = 'C', + long, + value_name = "PATH", + value_hint = ValueHint::AnyPath, + help = "Sets the location of the config file.", + long_help = "Sets the location of the config file. Expects a config file in the TOML format. \ + If it doesn't exist, a default config file is created at the path. If no path is provided, \ + the default config location will be used." + )] + pub config_location: Option, + + #[arg( + short = 't', + long, + value_name = "TIME", + help = "Default time value for graphs.", + long_help = "Default time value for graphs. Either a number in milliseconds or a 'human duration' \ + (e.g. 60s, 10m). Defaults to 60s, must be at least 30s." + )] + pub default_time_value: Option, // TODO: Charts are broken in the manpage - let default_widget_count = Arg::new("default_widget_count") - .long("default_widget_count") - .action(ArgAction::Set) - .requires_all(["default_widget_type"]) - .value_name("N") - .help("Sets the N'th selected widget type as the default.") - .long_help(indoc! { + #[arg( + long, + requires_all = ["default_widget_type"], + value_name = "N", + help = "Sets the N'th selected widget type as the default.", + long_help = indoc! { "Sets the N'th selected widget type to use as the default widget. Requires 'default_widget_type' to also be \ set, and defaults to 1. @@ -117,14 +151,15 @@ fn general_args(cmd: Command) -> Command { And we set our default widget type to 'CPU'. If we set '--default_widget_count 1', then it would use the \ CPU (1) as the default widget. If we set '--default_widget_count 3', it would use CPU (3) as the default \ instead." - }); + } + )] + pub default_widget_count: Option, - let default_widget_type = Arg::new("default_widget_type") - .long("default_widget_type") - .action(ArgAction::Set) - .value_name("WIDGET") - .help("Sets the default widget type, use `--help` for info.") - .long_help(indoc!{ + #[arg( + long, + value_name = "WIDGET", + help = "Sets the default widget type. Use --help for more info.", + long_help = indoc!{ "Sets which widget type to use as the default widget. For the default \ layout, this defaults to the 'process' widget. For a custom layout, it defaults \ to the first widget it sees. @@ -136,9 +171,9 @@ fn general_args(cmd: Command) -> Command { | Process | CPU (3) | Temperature | CPU (4) | +---------+---------+-------------+---------+ - Setting '--default_widget_type temperature' will make the temperature widget selected by default." - }) - .value_parser([ + Then, setting '--default_widget_type temperature' will make the temperature widget selected by default." + }, + value_parser = [ "cpu", "mem", "net", @@ -153,359 +188,363 @@ fn general_args(cmd: Command) -> Command { "batt", #[cfg(feature = "battery")] "battery", - ]); + ], + )] + pub default_widget_type: Option, - let disable_click = Arg::new("disable_click") - .long("disable_click") - .action(ArgAction::SetTrue) - .help("Disables mouse clicks.") - .long_help("Disables mouse clicks from interacting with bottom."); + #[arg( + long, + action = ArgAction::SetTrue, + help = "Disables mouse clicks.", + long_help = "Disables mouse clicks from interacting with bottom." + )] + pub disable_click: bool, // TODO: Change this to accept a string with the type of marker. - let dot_marker = Arg::new("dot_marker") - .short('m') - .long("dot_marker") - .action(ArgAction::SetTrue) - .help("Uses a dot marker for graphs.") - .long_help("Uses a dot marker for graphs as opposed to the default braille marker."); + #[arg( + short = 'm', + long, + action = ArgAction::SetTrue, + help = "Uses a dot marker for graphs.", + long_help = "Uses a dot marker for graphs as opposed to the default braille marker." + )] + pub dot_marker: bool, - let expanded = Arg::new("expanded") - .short('e') - .long("expanded") - .action(ArgAction::SetTrue) - .help("Expand the default widget upon starting the app.") - .long_help("Expand the default widget upon starting the app. This flag has no effect in basic mode (--basic)."); + #[arg( + short = 'e', + long, + action = ArgAction::SetTrue, + help = "Expand the default widget upon starting the app.", + long_help = "Expand the default widget upon starting the app. This flag has no effect in basic mode (--basic)." + )] + pub expanded: bool, - let hide_table_gap = Arg::new("hide_table_gap") - .long("hide_table_gap") - .action(ArgAction::SetTrue) - .help("Hides spacing between table headers and entries."); + #[arg(long, action = ArgAction::SetTrue, help = "Hides spacing between table headers and entries.")] + pub hide_table_gap: bool, - let hide_time = Arg::new("hide_time") - .long("hide_time") - .action(ArgAction::SetTrue) - .help("Hides the time scale from being shown."); + #[arg(long, action = ArgAction::SetTrue, help = "Hides the time scale from being shown.")] + pub hide_time: bool, - let rate = Arg::new("rate") - .short('r') - .long("rate") - .action(ArgAction::Set) - .value_name("TIME") - .help("Sets how often data is refreshed.") - .long_help( - "Sets how often data is refreshed. Either a number in milliseconds or a 'human duration' \ - (e.g. 1s, 1m). Defaults to 1s, must be at least 250ms. Smaller values may result in \ - higher system resource usage." - ); + #[arg( + short = 'r', + long, + value_name = "TIME", + help = "Sets how often data is refreshed.", + long_help = "Sets how often data is refreshed. Either a number in milliseconds or a 'human duration' \ + (e.g. 1s, 1m). Defaults to 1s, must be at least 250ms. Smaller values may result in \ + higher system resource usage." + )] + pub rate: Option, - // TODO: Unify how we do defaults. - let retention = Arg::new("retention") - .long("retention") - .action(ArgAction::Set) - .value_name("TIME") - .help("How far back data will be stored up to.") - .long_help( - "How far back data will be stored up to. Either a number in milliseconds or a 'human duration' \ - (e.g. 10m, 1h). Defaults to 10 minutes, and must be at least 1 minute. Larger values \ - may result in higher memory usage." - ); + #[arg( + long, + value_name = "TIME", + help = "How far back data will be stored up to.", + long_help = "How far back data will be stored up to. Either a number in milliseconds or a 'human duration' \ + (e.g. 10m, 1h). Defaults to 10 minutes, and must be at least 1 minute. Larger values \ + may result in higher memory usage." + )] + pub retention: Option, - let show_table_scroll_position = Arg::new("show_table_scroll_position") - .long("show_table_scroll_position") - .action(ArgAction::SetTrue) - .help("Shows the scroll position tracker in table widgets.") - .long_help("Shows the list scroll position tracker in the widget title for table widgets."); + #[arg( + long, + action = ArgAction::SetTrue, + help = "Shows the list scroll position tracker in the widget title for table widgets." + )] + pub show_table_scroll_position: bool, - let time_delta = Arg::new("time_delta") - .short('d') - .long("time_delta") - .action(ArgAction::Set) - .value_name("TIME") - .help("The amount of time changed upon zooming.") - .long_help( - "The amount of time changed when zooming in/out. Takes a number in \ - milliseconds or a human duration (e.g. 30s). The minimum is 1s, and \ - defaults to 15s.", - ); - - cmd.args(args![ - autohide_time, - basic, - config_location, - default_widget_count, - default_time_value, - default_widget_type, - disable_click, - dot_marker, - expanded, - hide_table_gap, - hide_time, - rate, - retention, - show_table_scroll_position, - time_delta, - ]) + #[arg( + short = 'd', + long, + value_name = "TIME", + help = "The amount of time changed upon zooming.", + long_help = "The amount of time changed when zooming in/out. Takes a number in \ + milliseconds or a human duration (e.g. 30s). The minimum is 1s, and \ + defaults to 15s." + )] + pub time_delta: Option, } -fn process_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("Process Options"); - - let case_sensitive = Arg::new("case_sensitive") - .short('S') - .long("case_sensitive") - .action(ArgAction::SetTrue) - .help("Enables case sensitivity by default.") - .long_help("Enables case sensitivity by default when searching for a process."); +/// Process arguments/config options. +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "Process Options", rename_all = "snake_case")] +pub struct ProcessArgs { + #[arg( + short = 'S', + long, + action = ArgAction::SetTrue, + help = "Enables case sensitivity by default.", + long_help = "Enables case sensitivity by default when searching for a process." + )] + pub case_sensitive: bool, // TODO: Rename this. - let current_usage = Arg::new("current_usage") - .short('u') - .long("current_usage") - .action(ArgAction::SetTrue) - .help("Calculates process CPU usage as a percentage of current usage rather than total usage."); + #[arg( + short = 'u', + long, + action = ArgAction::SetTrue, + help = "Calculates process CPU usage as a percentage of current usage rather than total usage." + )] + pub current_usage: bool, // TODO: Disable this on Windows? - let disable_advanced_kill = Arg::new("disable_advanced_kill") - .long("disable_advanced_kill") - .action(ArgAction::SetTrue) - .help("Hides additional stopping options Unix-like systems.") - .long_help( - "Hides additional stopping options Unix-like systems. Signal 15 (TERM) will be sent when \ - stopping a process.", - ); + #[arg( + long, + action = ArgAction::SetTrue, + help = "Hides additional stopping options Unix-like systems.", + long_help = "Hides additional stopping options Unix-like systems. Signal 15 (TERM) will be sent when \ + stopping a process." + )] + pub disable_advanced_kill: bool, - let group_processes = Arg::new("group_processes") - .short('g') - .long("group_processes") - .action(ArgAction::SetTrue) - .help("Groups processes with the same name by default."); + #[arg( + short = 'g', + long, + action = ArgAction::SetTrue, + help = "Groups processes with the same name by default." + )] + pub group_processes: bool, - let mem_as_value = Arg::new("mem_as_value") - .long("mem_as_value") - .action(ArgAction::SetTrue) - .help("Defaults to showing process memory usage by value.") - .long_help("Defaults to showing process memory usage by value. Otherwise, it defaults to showing it by percentage."); + #[arg( + long, + action = ArgAction::SetTrue, + help = "Defaults to showing process memory usage by value.", + long_help = "Defaults to showing process memory usage by value. Otherwise, it defaults to showing it by percentage." + )] + pub process_memory_as_value: bool, - let process_command = Arg::new("process_command") - .long("process_command") - .action(ArgAction::SetTrue) - .help("Shows the full command name instead of the process name by default."); + #[arg( + long, + action = ArgAction::SetTrue, + help = "Shows the full command name instead of the process name by default." + )] + pub process_command: bool, - let regex = Arg::new("regex") - .short('R') - .long("regex") - .action(ArgAction::SetTrue) - .help("Enables regex by default while searching."); + #[arg(short = 'R', long, action = ArgAction::SetTrue, help = "Enables regex by default while searching.")] + pub regex: bool, - let tree = Arg::new("tree") - .short('T') - .long("tree") - .action(ArgAction::SetTrue) - .help("Makes the process widget use tree mode by default."); + #[arg( + short = 'T', + long, + action = ArgAction::SetTrue, + help = "Makes the process widget use tree mode by default." + )] + pub tree: bool, - let unnormalized_cpu = Arg::new("unnormalized_cpu") - .short('n') - .long("unnormalized_cpu") - .action(ArgAction::SetTrue) - .help("Show process CPU% usage without averaging over the number of CPU cores."); + #[arg( + short = 'n', + long, + action = ArgAction::SetTrue, + help = "Show process CPU% usage without averaging over the number of CPU cores." + )] + pub unnormalized_cpu: bool, - let whole_word = Arg::new("whole_word") - .short('W') - .long("whole_word") - .action(ArgAction::SetTrue) - .help("Enables whole-word matching by default while searching."); - - let args = args![ - case_sensitive, - current_usage, - disable_advanced_kill, - group_processes, - mem_as_value, - process_command, - regex, - tree, - unnormalized_cpu, - whole_word, - ]; - - cmd.args(args) + #[arg( + short = 'W', + long, + action = ArgAction::SetTrue, + help = "Enables whole-word matching by default while searching." + )] + pub whole_word: bool, } -fn temperature_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("Temperature Options"); +/// Temperature arguments/config options. +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "Temperature Options", rename_all = "snake_case")] +#[group(id = "temperature_unit", multiple = false)] +pub struct TemperatureArgs { + #[arg( + short = 'c', + long, + action = ArgAction::SetTrue, + group = "temperature_unit", + help = "Use Celsius as the temperature unit. Default.", + long_help = "Use Celsius as the temperature unit. This is the default option." + )] + pub celsius: bool, - let celsius = Arg::new("celsius") - .short('c') - .long("celsius") - .action(ArgAction::SetTrue) - .help("Use Celsius as the temperature unit. Default.") - .long_help("Use Celsius as the temperature unit. This is the default option."); + #[arg( + short = 'f', + long, + action = ArgAction::SetTrue, + group = "temperature_unit", + help = "Use Fahrenheit as the temperature unit." + )] + pub fahrenheit: bool, - let fahrenheit = Arg::new("fahrenheit") - .short('f') - .long("fahrenheit") - .action(ArgAction::SetTrue) - .help("Use Fahrenheit as the temperature unit."); - - let kelvin = Arg::new("kelvin") - .short('k') - .long("kelvin") - .action(ArgAction::SetTrue) - .help("Use Kelvin as the temperature unit."); - - let temperature_group = ArgGroup::new("TEMPERATURE_TYPE").args([ - celsius.get_id(), - fahrenheit.get_id(), - kelvin.get_id(), - ]); - - cmd.args(args![celsius, fahrenheit, kelvin]) - .group(temperature_group) + #[arg( + short = 'k', + long, + action = ArgAction::SetTrue, + group = "temperature_unit", + help = "Use Kelvin as the temperature unit." + )] + pub kelvin: bool, } -fn cpu_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("CPU Options"); - - // let default_cpu_entry = Arg::new(""); - - let hide_avg_cpu = Arg::new("hide_avg_cpu") - .short('a') - .long("hide_avg_cpu") - .action(ArgAction::SetTrue) - .help("Hides the average CPU usage entry."); - - let cpu_left_legend = Arg::new("cpu_left_legend") - .long("cpu_left_legend") - .action(ArgAction::SetTrue) - .help("Puts the CPU chart legend on the left side."); - - cmd.args(args![hide_avg_cpu, cpu_left_legend]) +/// The default selection of the CPU widget. If the given selection is invalid, +/// we will fall back to all. +#[derive(Clone, Copy, Debug, Default)] +pub enum CpuDefault { + #[default] + All, + Average, } -fn mem_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("Memory Options"); +impl ValueEnum for CpuDefault { + fn value_variants<'a>() -> &'a [Self] { + &[CpuDefault::All, CpuDefault::Average] + } - let memory_legend = Arg::new("memory_legend") - .long("memory_legend") - .action(ArgAction::Set) - .value_name("POSITION") - .ignore_case(true) - .help("Where to place the legend for the memory chart widget.") - .value_parser(CHART_WIDGET_POSITIONS); + fn to_possible_value(&self) -> Option { + match self { + CpuDefault::All => Some(PossibleValue::new("all")), + CpuDefault::Average => Some(PossibleValue::new("avg").alias("average")), + } + } +} + +/// CPU arguments/config options. +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "CPU Options", rename_all = "snake_case")] +pub struct CpuArgs { + #[arg( + long, + help = "Sets which CPU entry type is selected by default.", + value_name = "ENTRY", + value_parser = value_parser!(CpuDefault), + default_value = "all" + )] + pub default_cpu_entry: CpuDefault, + + #[arg(short = 'a', long, action = ArgAction::SetTrue, help = "Hides the average CPU usage entry.")] + pub hide_avg_cpu: bool, + + // TODO: Maybe rename this or fix this? Should this apply to all "left legends"? + #[arg( + short = 'l', + long, + action = ArgAction::SetTrue, + help = "Puts the CPU chart legend on the left side." + )] + pub cpu_left_legend: bool, +} + +/// Memory argument/config options. +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "Memory Options", rename_all = "snake_case")] +pub struct MemoryArgs { + #[arg( + long, + value_parser = CHART_WIDGET_POSITIONS, + value_name = "POSITION", + ignore_case = true, + help = "Where to place the legend for the memory chart widget.", + )] + pub memory_legend: Option, #[cfg(not(target_os = "windows"))] - { - let enable_cache_memory = Arg::new("enable_cache_memory") - .long("enable_cache_memory") - .action(ArgAction::SetTrue) - .help("Enable collecting and displaying cache and buffer memory."); - - cmd.args(args![enable_cache_memory, memory_legend]) - } - #[cfg(target_os = "windows")] - { - cmd.arg(memory_legend) - } + #[arg( + long, + action = ArgAction::SetTrue, + help = "Enables collecting and displaying cache and buffer memory." + )] + pub enable_cache_memory: bool, } -fn network_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("Network Options"); +/// Network arguments/config options. +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "Network Options", rename_all = "snake_case")] +pub struct NetworkArgs { + #[arg( + long, + value_parser = CHART_WIDGET_POSITIONS, + value_name = "POSITION", + ignore_case = true, + help = "Where to place the legend for the network chart widget.", + )] + pub network_legend: Option, - let network_legend = Arg::new("network_legend") - .long("network_legend") - .action(ArgAction::Set) - .value_name("POSITION") - .ignore_case(true) - .help("Where to place the legend for the network chart widget.") - .value_parser(CHART_WIDGET_POSITIONS); + // TODO: Rename some of these to remove the network prefix for serde. + #[arg( + long, + action = ArgAction::SetTrue, + help = "Displays the network widget using bytes.", + long_help = "Displays the network widget using bytes. Defaults to bits." + )] + pub network_use_bytes: bool, - let network_use_bytes = Arg::new("network_use_bytes") - .long("network_use_bytes") - .action(ArgAction::SetTrue) - .help("Displays the network widget using bytes.") - .long_help("Displays the network widget using bytes. Defaults to bits."); + #[arg( + long, + action = ArgAction::SetTrue, + help = "Displays the network widget with binary prefixes.", + long_help = "Displays the network widget with binary prefixes (e.g. kibibits, mebibits) rather than a decimal \ + prefixes (e.g. kilobits, megabits). Defaults to decimal prefixes." + )] + pub network_use_binary_prefix: bool, - let network_use_binary_prefix = Arg::new("network_use_binary_prefix") - .long("network_use_binary_prefix") - .action(ArgAction::SetTrue) - .help("Displays the network widget with binary prefixes.") - .long_help( - "Displays the network widget with binary prefixes (e.g. kibibits, mebibits) rather than a decimal \ - prefixes (e.g. kilobits, megabits). Defaults to decimal prefixes." - ); + #[arg( + long, + action = ArgAction::SetTrue, + help = "Displays the network widget with a log scale.", + long_help = "Displays the network widget with a log scale. Defaults to a non-log scale." + )] + pub network_use_log: bool, - let network_use_log = Arg::new("network_use_log") - .long("network_use_log") - .action(ArgAction::SetTrue) - .help("Displays the network widget with a log scale.") - .long_help("Displays the network widget with a log scale. Defaults to a non-log scale."); - - // TODO: Change this to be configured as network graph type? - let use_old_network_legend = Arg::new("use_old_network_legend") - .long("use_old_network_legend") - .action(ArgAction::SetTrue) - .help("(DEPRECATED) Uses a separated network legend.") - .long_help("(DEPRECATED) Uses separated network widget legend. This display is not tested and may be broken."); - - cmd.args(args![ - network_legend, - network_use_bytes, - network_use_log, - network_use_binary_prefix, - use_old_network_legend, - ]) + #[arg( + long, + action = ArgAction::SetTrue, + help = "(DEPRECATED) Uses a separate network legend.", + long_help = "(DEPRECATED) Uses separate network widget legend. This display is not tested and may be broken." + )] + pub use_old_network_legend: bool, } +/// Battery arguments/config options. #[cfg(feature = "battery")] -fn battery_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("Battery Options"); - - let battery = Arg::new("battery") - .long("battery") - .action(ArgAction::SetTrue) - .help("Shows the battery widget in non-custom layouts.") - .long_help( - "Shows the battery widget in default or basic mode, if there is as battery available. This \ - has no effect on custom layouts; if the battery widget is desired for a custom layout, explicitly \ - specify it." - ); - - cmd.arg(battery) +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "Battery Options", rename_all = "snake_case")] +pub struct BatteryArgs { + #[arg( + long, + action = ArgAction::SetTrue, + help = "Shows the battery widget in non-custom layouts.", + long_help = "Shows the battery widget in default or basic mode, if there is as battery available. This \ + has no effect on custom layouts; if the battery widget is desired for a custom layout, explicitly \ + specify it." + )] + pub battery: bool, } +/// GPU arguments/config options. #[cfg(feature = "gpu")] -fn gpu_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("GPU Options"); - - let enable_gpu = Arg::new("enable_gpu") - .long("enable_gpu") - .action(ArgAction::SetTrue) - .help("Enable collecting and displaying GPU usage."); - - cmd.arg(enable_gpu) +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "GPU Options", rename_all = "snake_case")] +pub struct GpuArgs { + #[arg(long, action = ArgAction::SetTrue, help = "Enable collecting and displaying GPU usage.")] + pub enable_gpu: bool, } -fn style_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("Style Options"); - - // TODO: File an issue with manpage, it cannot render charts correctly. - let color = Arg::new("color") - .long("color") - .action(ArgAction::Set) - .value_name("SCHEME") - .value_parser([ +/// Style arguments/config options. +#[derive(Args, Clone, Debug, Default)] +#[command(next_help_heading = "Style Options", rename_all = "snake_case")] +pub struct StyleArgs { + #[arg( + long, + value_name = "SCHEME", + value_parser = [ "default", "default-light", "gruvbox", "gruvbox-light", "nord", "nord-light", - ]) - .hide_possible_values(true) - .help(indoc! { + ], + hide_possible_values = true, + help = indoc! { "Use a color scheme, use `--help` for info on the colors. [possible values: default, default-light, gruvbox, gruvbox-light, nord, nord-light]", - }) - .long_help(indoc! { + }, + long_help = indoc! { "Use a pre-defined color scheme. Currently supported values are: - default - default-light (default but adjusted for lighter backgrounds) @@ -513,72 +552,31 @@ fn style_args(cmd: Command) -> Command { - gruvbox-light (gruvbox but adjusted for lighter backgrounds) - nord (an arctic, north-bluish color palette) - nord-light (nord but adjusted for lighter backgrounds)" - }); - - cmd.arg(color) + } + )] + pub color: Option, } -fn other_args(cmd: Command) -> Command { - let cmd = cmd.next_help_heading("Other Options"); +/// Other arguments. This just handle options that are for help/version displaying. +#[derive(Args, Clone, Debug)] +#[command(next_help_heading = "Other Options", rename_all = "snake_case")] +pub struct OtherArgs { + #[arg(short = 'h', long, action = ArgAction::Help, help = "Prints help info (for more details use `--help`.")] + help: (), - let help = Arg::new("help") - .short('h') - .long("help") - .action(ArgAction::Help) - .help("Prints help info (for more details use `--help`."); - - let version = Arg::new("version") - .short('V') - .long("version") - .action(ArgAction::Version) - .help("Prints version information."); - - cmd.args([help, version]) + #[arg(short = 'v', long, action = ArgAction::Version, help = "Prints version information.")] + version: (), } -pub fn build_app() -> Command { - const TEMPLATE: &str = indoc! { - "{name} {version} - {author} - - {about} - - {usage-heading} {usage} - - {all-args}" - }; - const USAGE: &str = "btm [OPTIONS]"; - const VERSION: &str = match option_env!("NIGHTLY_VERSION") { - Some(nightly_version) => nightly_version, - None => crate_version!(), - }; +/// Returns a [`BottomArgs`]. +pub fn get_args() -> BottomArgs { + BottomArgs::parse() +} - let cmd = Command::new(crate_name!()) - .author(crate_authors!()) - .about(crate_description!()) - .disable_help_flag(true) - .disable_version_flag(true) - .color(ColorChoice::Auto) - .help_template(TEMPLATE) - .override_usage(USAGE) - .version(VERSION); - - [ - general_args, - process_args, - temperature_args, - cpu_args, - mem_args, - network_args, - #[cfg(feature = "battery")] - battery_args, - #[cfg(feature = "gpu")] - gpu_args, - style_args, - other_args, - ] - .into_iter() - .fold(cmd, |c, f| f(c)) +/// Returns an [`Command`] based off of [`BottomArgs`]. +#[cfg(test)] +pub(crate) fn build_cmd() -> Command { + BottomArgs::command() } #[cfg(test)] @@ -587,13 +585,13 @@ mod test { #[test] fn verify_cli() { - build_app().debug_assert(); + build_cmd().debug_assert(); } #[test] fn no_default_help_heading() { - let mut app = build_app(); - let help_str = app.render_help(); + let mut cmd = build_cmd(); + let help_str = cmd.render_help(); assert!( !help_str.to_string().contains("\nOptions:\n"), diff --git a/src/options/config.rs b/src/options/config.rs index 5fe29fe2..975472f1 100644 --- a/src/options/config.rs +++ b/src/options/config.rs @@ -10,7 +10,7 @@ use self::{cpu::CpuConfig, layout::Row, process_columns::ProcessConfig}; use super::ConfigColours; #[derive(Clone, Debug, Default, Deserialize)] -pub struct Config { +pub struct ConfigV1 { pub(crate) flags: Option, pub(crate) colors: Option, pub(crate) row: Option>, @@ -71,7 +71,7 @@ pub(crate) struct ConfigFlags { pub(crate) memory_legend: Option, /// For built-in colour palettes. pub(crate) color: Option, - pub(crate) mem_as_value: Option, + pub(crate) process_memory_as_value: Option, pub(crate) tree: Option, pub(crate) show_table_scroll_position: Option, pub(crate) process_command: Option, diff --git a/src/options/config/layout.rs b/src/options/config/layout.rs index 04105bd8..b88448fa 100644 --- a/src/options/config/layout.rs +++ b/src/options/config/layout.rs @@ -239,7 +239,7 @@ mod test { use super::*; use crate::{ constants::{DEFAULT_LAYOUT, DEFAULT_WIDGET_ID}, - options::Config, + options::ConfigV1, utils::error, }; @@ -293,7 +293,7 @@ mod test { #[test] /// Tests the default setup. fn test_default_movement() { - let rows = from_str::(DEFAULT_LAYOUT).unwrap().row.unwrap(); + let rows = from_str::(DEFAULT_LAYOUT).unwrap().row.unwrap(); let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false); // Simple tests for the top CPU widget @@ -367,7 +367,7 @@ mod test { fn test_default_battery_movement() { use crate::constants::DEFAULT_BATTERY_LAYOUT; - let rows = from_str::(DEFAULT_BATTERY_LAYOUT) + let rows = from_str::(DEFAULT_BATTERY_LAYOUT) .unwrap() .row .unwrap(); @@ -413,7 +413,7 @@ mod test { #[test] /// Tests using cpu_left_legend. fn test_cpu_left_legend() { - let rows = from_str::(DEFAULT_LAYOUT).unwrap().row.unwrap(); + let rows = from_str::(DEFAULT_LAYOUT).unwrap().row.unwrap(); let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, true); // Legend @@ -473,7 +473,7 @@ mod test { type="proc" "#; - let rows = from_str::(proc_layout).unwrap().row.unwrap(); + let rows = from_str::(proc_layout).unwrap().row.unwrap(); let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs* let mut total_height_ratio = 0; let mut default_widget_count = 1; @@ -506,7 +506,7 @@ mod test { #[test] /// Tests default widget by setting type and count. fn test_default_widget_by_option() { - let rows = from_str::(PROC_LAYOUT).unwrap().row.unwrap(); + let rows = from_str::(PROC_LAYOUT).unwrap().row.unwrap(); let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs* let mut total_height_ratio = 0; let mut default_widget_count = 3; @@ -538,7 +538,7 @@ mod test { #[test] fn test_proc_custom_layout() { - let rows = from_str::(PROC_LAYOUT).unwrap().row.unwrap(); + let rows = from_str::(PROC_LAYOUT).unwrap().row.unwrap(); let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false); // First proc widget diff --git a/src/utils/error.rs b/src/utils/error.rs index 74fdbe69..532ad283 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -17,21 +17,22 @@ pub enum BottomError { /// An error to represent generic errors. #[error("Error, {0}")] GenericError(String), + #[cfg(feature = "fern")] /// An error to represent errors with fern. #[error("Fern error, {0}")] FernError(String), + /// An error to represent invalid command-line arguments. + #[error("Invalid argument, {0}")] + ArgumentError(String), /// An error to represent errors with the config. #[error("Configuration file error, {0}")] ConfigError(String), /// An error to represent errors with converting between data types. #[error("Conversion error, {0}")] ConversionError(String), - /// An error to represent errors with querying. + /// An error to represent errors with a query. #[error("Query error, {0}")] QueryError(Cow<'static, str>), - /// An error that just signifies something minor went wrong; no message. - #[error("Minor error.")] - MinorError, #[error("Error casting integers {0}")] TryFromIntError(#[from] std::num::TryFromIntError), } diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 49087fb3..8d2c5ba1 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -255,7 +255,7 @@ impl ProcWidgetState { let is_count = matches!(mode, ProcWidgetMode::Grouped); let is_command = table_config.is_command; - let mem_vals = table_config.show_memory_as_values; + let mem_as_values = table_config.show_memory_as_values; match config_columns { Some(columns) if !columns.is_empty() => columns @@ -278,7 +278,7 @@ impl ProcWidgetState { } ProcWidgetColumn::Cpu => CpuPercent, ProcWidgetColumn::Mem => { - if mem_vals { + if mem_as_values { MemoryVal } else { MemoryPercent @@ -293,7 +293,7 @@ impl ProcWidgetState { ProcWidgetColumn::Time => Time, #[cfg(feature = "gpu")] ProcWidgetColumn::GpuMem => { - if mem_vals { + if mem_as_values { GpuMem } else { GpuMemPercent @@ -311,7 +311,11 @@ impl ProcWidgetState { if is_count { Count } else { Pid }, if is_command { Command } else { Name }, CpuPercent, - if mem_vals { MemoryVal } else { MemoryPercent }, + if mem_as_values { + MemoryVal + } else { + MemoryPercent + }, ReadPerSecond, WritePerSecond, TotalRead, diff --git a/tests/integration/arg_tests.rs b/tests/integration/arg_tests.rs index 5bb8a6bb..033e51ff 100644 --- a/tests/integration/arg_tests.rs +++ b/tests/integration/arg_tests.rs @@ -24,7 +24,9 @@ fn test_large_default_time() { .arg("18446744073709551616") .assert() .failure() - .stderr(predicate::str::contains("could not parse")); + .stderr(predicate::str::contains( + "set your default time to be valid", + )); } #[test] @@ -35,7 +37,7 @@ fn test_small_default_time() { .assert() .failure() .stderr(predicate::str::contains( - "set your default value to be at least", + "set your default time to be at least", )); } @@ -46,7 +48,7 @@ fn test_large_delta_time() { .arg("18446744073709551616") .assert() .failure() - .stderr(predicate::str::contains("could not parse")); + .stderr(predicate::str::contains("set your time delta to be valid")); } #[test] @@ -68,7 +70,7 @@ fn test_large_rate() { .arg("18446744073709551616") .assert() .failure() - .stderr(predicate::str::contains("could not parse")); + .stderr(predicate::str::contains("set your update rate")); } #[test] @@ -89,7 +91,7 @@ fn test_invalid_rate() { .arg("100-1000") .assert() .failure() - .stderr(predicate::str::contains("could not parse")); + .stderr(predicate::str::contains("set your update rate")); } #[test] @@ -121,9 +123,7 @@ fn test_invalid_default_widget_2() { .arg("18446744073709551616") .assert() .failure() - .stderr(predicate::str::contains( - "set your widget count to be at most unsigned INT_MAX", - )); + .stderr(predicate::str::contains("number too large")); } #[test]