From 6b6217655479df639c9ad7f6324939eae4f81cea Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Wed, 10 Jan 2024 19:07:22 -0500 Subject: refactor: rearrange/resort args (#1376) * clean up Cargo.toml * some small cleanup * refactor: group together similar args in the help generation and code This groups together related arguments in both the help text and the code itself. * update changelog * clippy * builder pattern instead --- CHANGELOG.md | 3 +- Cargo.lock | 16 +- Cargo.toml | 25 +- build.rs | 7 +- src/options/args.rs | 900 +++++++++++++++++++++++++++------------------- src/options/args.template | 8 + 6 files changed, 562 insertions(+), 397 deletions(-) create mode 100644 src/options/args.template diff --git a/CHANGELOG.md b/CHANGELOG.md index 2227e9d6..5ecab8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changes -- [#1344](https://github.com/ClementTsang/bottom/pull/1344): Change the `group` command line argument to `group_processes` for consistency with the config file option. +- [#1344](https://github.com/ClementTsang/bottom/pull/1344): Change the `group` command line-argument to `group_processes` for consistency with the config file option. +- [#1376](https://github.com/ClementTsang/bottom/pull/1376): Group together related command-line arguments in `-h` and `--help`. ### Bug Fixes diff --git a/Cargo.lock b/Cargo.lock index 1dd09a57..30e79e1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,18 +239,18 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.4.11" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "33e92c5c1a78c62968ec57dbc2440366a2d6e5a23faf829970ff1585dc6b18e2" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "f4323769dc8a61e2c39ad7dc26f6f2800524691a44d74fe3d1071a5c24db6370" dependencies = [ "anstream", "anstyle", @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a51919c5608a32e34ea1d6be321ad070065e17613e168c5b6977024290f2630b" +checksum = "97aeaa95557bd02f23fbb662f981670c3d20c5a26e69f7354b28f57092437fcd" dependencies = [ "clap", ] @@ -296,9 +296,9 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clap_mangen" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3be86020147691e1d2ef58f75346a3d4d94807bfc473e377d52f09f0f7d77f7" +checksum = "10b5db60b3310cdb376fbeb8826e875a38080d0c61bdec0a91a3da8338948736" dependencies = [ "clap", "roff", diff --git a/Cargo.toml b/Cargo.toml index 883a9fb3..20dbdf00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,23 +64,20 @@ strip = false [features] battery = ["starship-battery"] -gpu = ["nvidia"] nvidia = ["nvml-wrapper"] +gpu = ["nvidia"] zfs = [] -# Including logging for debugging purposes. +# Logging for debugging performance. logging = ["fern", "log", "time/local-offset"] -# The features we use on deploy. Logging is not included as that is primarily (for now) just for debugging locally. -deploy = ["battery", "gpu", "zfs"] - -default = ["deploy"] +default = ["battery", "gpu", "zfs"] [dependencies] anyhow = "1.0.75" backtrace = "0.3.69" cfg-if = "1.0.0" -clap = { version = "4.4.11", features = ["default", "cargo", "wrap_help"] } +clap = { version = "4.4.14", features = ["default", "cargo", "wrap_help"] } concat-string = "1.0.1" crossterm = "0.27.0" ctrlc = { version = "3.4.1", features = ["termination"] } @@ -93,7 +90,9 @@ indexmap = "2.1.0" itertools = "0.12.0" kstring = { version = "2.0.0", features = ["arc"] } log = { version = "0.4.20", optional = true } -nvml-wrapper = { version = "0.9.0", optional = true, features = ["legacy-functions"] } +nvml-wrapper = { version = "0.9.0", optional = true, features = [ + "legacy-functions", +] } regex = "1.10.2" serde = { version = "=1.0.193", features = ["derive"] } starship-battery = { version = "0.8.2", optional = true } @@ -133,15 +132,17 @@ filedescriptor = "0.8.2" [dev-dependencies] assert_cmd = "2.0.12" -cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] } +cargo-husky = { version = "1.5.0", default-features = false, features = [ + "user-hooks", +] } predicates = "3.0.3" [build-dependencies] -clap = { version = "4.4.11", features = ["default", "cargo", "wrap_help"] } -clap_complete = "4.4.5" +clap = { version = "4.4.14", features = ["default", "cargo", "wrap_help"] } +clap_complete = "4.4.6" clap_complete_fig = "4.4.2" clap_complete_nushell = "4.4.2" -clap_mangen = "0.2.15" +clap_mangen = "0.2.16" [package.metadata.deb] section = "utility" diff --git a/build.rs b/build.rs index 1d13c701..5ebf6155 100644 --- a/build.rs +++ b/build.rs @@ -1,13 +1,18 @@ +#[allow(dead_code)] +#[path = "src/options/args.rs"] +mod args; + use std::{ env, fs, io, path::{Path, PathBuf}, }; +use clap::Command; use clap_complete::{generate_to, shells::Shell, Generator}; use clap_complete_fig::Fig; use clap_complete_nushell::Nushell; -include!("src/options/args.rs"); +use crate::args::build_app; fn create_dir(dir: &Path) -> io::Result<()> { let res = fs::create_dir_all(dir); diff --git a/src/options/args.rs b/src/options/args.rs index 2c524da8..30fe88c9 100644 --- a/src/options/args.rs +++ b/src/options/args.rs @@ -1,16 +1,9 @@ -use clap::{builder::PossibleValuesParser, *}; - -const TEMPLATE: &str = "\ -{name} {version} -{author} - -{about} - -{usage-heading} {usage} +//! Argument parsing via clap. +//! +//! Note that you probably want to keep this as a single file so the build script doesn't +//! trip all over itself. -{all-args}"; - -const USAGE: &str = "btm [OPTIONS]"; +use clap::{builder::PossibleValuesParser, *}; const DEFAULT_WIDGET_TYPE_STR: &str = { #[cfg(feature = "battery")] @@ -87,191 +80,236 @@ pub fn get_matches() -> ArgMatches { build_app().get_matches() } -pub fn build_app() -> Command { - // Temps - let kelvin = Arg::new("kelvin") - .short('k') - .long("kelvin") - .action(ArgAction::SetTrue) - .help("Sets the temperature type to Kelvin.") - .long_help("Sets the temperature type to Kelvin."); - - let fahrenheit = Arg::new("fahrenheit") - .short('f') - .long("fahrenheit") - .action(ArgAction::SetTrue) - .help("Sets the temperature type to Fahrenheit.") - .long_help("Sets the temperature type to Fahrenheit."); - - let celsius = Arg::new("celsius") - .short('c') - .long("celsius") - .action(ArgAction::SetTrue) - .help("Sets the temperature type to Celsius.") - .long_help("Sets the temperature type to Celsius. This is the default option."); - - // All flags. These are in alphabetical order - 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.", - ); +trait CommandBuilder { + fn general_args(self) -> Self; - 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. Design is largely inspired by htop's.", - ); + fn style_args(self) -> Self; - let case_sensitive = Arg::new("case_sensitive") - .short('S') - .long("case_sensitive") - .action(ArgAction::SetTrue) - .help("Enables case sensitivity by default.") - .long_help("When searching for a process, enables case sensitivity by default."); - - let current_usage = Arg::new("current_usage") - .short('u') - .long("current_usage") - .action(ArgAction::SetTrue) - .help("Sets process CPU% to be based on current CPU%.") - .long_help("Sets process CPU% usage to be based on the current system CPU% usage rather than total CPU usage."); - - let unnormalized_cpu = Arg::new("unnormalized_cpu") - .short('n') - .long("unnormalized_cpu") - .action(ArgAction::SetTrue) - .help("Show process CPU% usage without normalizing over the number of cores.") - .long_help( - "Shows all process CPU% usage without averaging over the number of CPU cores in the system.", - ); + fn temperature_args(self) -> Self; - 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 the program."); - - 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."); - - let group_processes = Arg::new("group_processes") - .short('g') - .long("group_processes") - .action(ArgAction::SetTrue) - .help("Groups processes with the same name by default.") - .long_help("Groups processes with the same name by default."); - - let hide_avg_cpu = Arg::new("hide_avg_cpu") - .short('a') - .long("hide_avg_cpu") - .action(ArgAction::SetTrue) - .help("Hides the average CPU usage.") - .long_help("Hides the average CPU usage from being shown."); - - let hide_table_gap = Arg::new("hide_table_gap") - .long("hide_table_gap") - .action(ArgAction::SetTrue) - .help("Hides spacing between table headers and entries.") - .long_help("Hides the spacing between table headers and entries."); - - let hide_time = Arg::new("hide_time") - .long("hide_time") - .action(ArgAction::SetTrue) - .help("Hides the time scale.") - .long_help("Completely hides the time scale from being shown."); - - let process_command = Arg::new("process_command") - .long("process_command") - .action(ArgAction::SetTrue) - .help("Show processes as their commands by default.") - .long_help("Show processes as their commands by default in the process widget."); - - let left_legend = Arg::new("left_legend") - .short('l') - .long("left_legend") - .action(ArgAction::SetTrue) - .help("Puts the CPU chart legend to the left side.") - .long_help("Puts the CPU chart legend to the left side rather than the right side."); - - let regex = Arg::new("regex") - .short('R') - .long("regex") - .action(ArgAction::SetTrue) - .help("Enables regex by default.") - .long_help("When searching for a process, enables regex by default."); - - let disable_advanced_kill = Arg::new("disable_advanced_kill") - .long("disable_advanced_kill") - .action(ArgAction::SetTrue) - .help("Hides advanced process killing.") - .long_help("Hides advanced options to stop a process on Unix-like systems. The only option shown is 15 (TERM)."); - - 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."); - - let use_old_network_legend = Arg::new("use_old_network_legend") - .long("use_old_network_legend") - .action(ArgAction::SetTrue) - .help("DEPRECATED - uses a separate network legend.") - .long_help( - "DEPRECATED - uses an older (pre-0.4), separate network widget legend. This display is not \ - tested anymore and could be broken.", - ); + fn process_args(self) -> Self; - let whole_word = Arg::new("whole_word") - .short('W') - .long("whole_word") - .action(ArgAction::SetTrue) - .help("Enables whole-word matching by default.") - .long_help( - "When searching for a process, return results that match the entire query by default.", - ); + fn cpu_args(self) -> Self; + + fn mem_args(self) -> Self; + + fn network_args(self) -> Self; + + fn battery_args(self) -> Self; + + fn gpu_args(self) -> Self; + + fn other(self) -> Self; +} + +impl CommandBuilder for Command { + fn general_args(self) -> Command { + const HEADING: &str = "General Options"; + + 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.", + ) + .help_heading(HEADING); + + 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. Design is largely inspired by htop's.", + ) + .help_heading(HEADING); + + 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 the program.") + .help_heading(HEADING); + + 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.") + .help_heading(HEADING); + + let hide_table_gap = Arg::new("hide_table_gap") + .long("hide_table_gap") + .action(ArgAction::SetTrue) + .help("Hides spacing between table headers and entries.") + .long_help("Hides the spacing between table headers and entries.") + .help_heading(HEADING); + + let hide_time = Arg::new("hide_time") + .long("hide_time") + .action(ArgAction::SetTrue) + .help("Hides the time scale.") + .long_help("Completely hides the time scale from being shown.") + .help_heading(HEADING); + + let left_legend = Arg::new("left_legend") + .short('l') + .long("left_legend") + .action(ArgAction::SetTrue) + .help("Puts the CPU chart legend to the left side.") + .long_help("Puts the CPU chart legend to the left side rather than the right side.") + .help_heading(HEADING); + + 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.", + ) + .help_heading(HEADING); + + 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, one is created.", + ) + .value_hint(ValueHint::AnyPath) + .help_heading(HEADING); + + 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. Takes a number in milliseconds or a human duration (e.g. 60s). The minimum time is 30s, and the default is 60s.", + ) + .help_heading(HEADING); + + // 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("INT") + .help("Sets the n'th selected widget type as the default.") + .long_help( + "\ +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. + +This reads from left to right, top to bottom. For example, suppose +we have a layout that looks like: ++-------------------+-----------------------+ +| CPU (1) | CPU (2) | ++---------+---------+-------------+---------+ +| Process | CPU (3) | Temperature | CPU (4) | ++---------+---------+-------------+---------+ - // All options. Again, alphabetical order. - 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, one is created.", - ) - .value_hint(ValueHint::AnyPath); - - // TODO: File an issue with manpage, it cannot render charts correctly. - let color = Arg::new("color") - .long("color") - .action(ArgAction::Set) - .value_name("COLOR SCHEME") - .value_parser(PossibleValuesParser::new([ - "default", - "default-light", - "gruvbox", - "gruvbox-light", - "nord", - "nord-light", - ])) - .hide_possible_values(true) - .help("Use a color scheme, use --help for info.") - .long_help( - "\ +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. + ", + ) + .help_heading(HEADING); + + let default_widget_type = Arg::new("default_widget_type") + .long("default_widget_type") + .action(ArgAction::Set) + .value_name("WIDGET TYPE") + .help("Sets the default widget type, use --help for info.") + .long_help(DEFAULT_WIDGET_TYPE_STR) + .help_heading(HEADING); + + let expanded_on_startup = Arg::new("expanded_on_startup") + .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. Same as pressing \"e\" inside the app. Use with \"default_widget_type\" and \"default_widget_count\" to select desired expanded widget. This flag has no effect in basic mode (--basic)") + .help_heading(HEADING); + + let rate = Arg::new("rate") + .short('r') + .long("rate") + .action(ArgAction::Set) + .value_name("TIME") + .help("Sets the data refresh rate.") + .long_help("Sets the data refresh rate. Takes a number in milliseconds or a human duration (e.g. 5s). The minimum is 250ms, and defaults to 1000ms. Smaller values may take more computer resources.") + .help_heading(HEADING); + + 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.") + .help_heading(HEADING); + + let retention = Arg::new("retention") + .long("retention") + .action(ArgAction::Set) + .value_name("TIME") + .help("The timespan of data stored.") + .long_help("How much data is stored at once in terms of time. Takes a number in milliseconds or a human duration (e.g. 20m), with a minimum of 1 minute. Note higher values will take up more memory. Defaults to 10 minutes.") + .help_heading(HEADING); + + let mut args = [ + autohide_time, + basic, + disable_click, + dot_marker, + hide_table_gap, + hide_time, + left_legend, + show_table_scroll_position, + config_location, + default_time_value, + default_widget_count, + default_widget_type, + expanded_on_startup, + rate, + time_delta, + retention, + ]; + args.sort_unstable(); + + self.args(args) + } + + fn style_args(self) -> Command { + const HEADING: &str = "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("COLOR SCHEME") + .value_parser(PossibleValuesParser::new([ + "default", + "default-light", + "gruvbox", + "gruvbox-light", + "nord", + "nord-light", + ])) + .hide_possible_values(true) + .help("Use a color scheme, use --help for info.") + .long_help( + "\ Use a pre-defined color scheme. Currently supported values are: +------------------------------------------------------------+ | default | @@ -287,214 +325,315 @@ Use a pre-defined color scheme. Currently supported values are: | nord-light (nord but for use with light backgrounds) | +------------------------------------------------------------+ Defaults to \"default\". -", - ); + ", + ) + .help_heading(HEADING); - 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."); - - 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. Takes a number in milliseconds or a human duration (e.g. 60s). The minimum time is 30s, and the default is 60s.", - ); + self.arg(color) + } - // 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("INT") - .help("Sets the n'th selected widget type as the default.") - .long_help( - "\ -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. + fn temperature_args(self) -> Command { + const HEADING: &str = "Temperature Options"; + + let celsius = Arg::new("celsius") + .short('c') + .long("celsius") + .action(ArgAction::SetTrue) + .help("Use Celsius as the temperature unit.") + .long_help("Use Celsius as the temperature unit. This is the default option.") + .help_heading(HEADING); + + let fahrenheit = Arg::new("fahrenheit") + .short('f') + .long("fahrenheit") + .action(ArgAction::SetTrue) + .help("Use Fahrenheit as the temperature unit.") + .help_heading(HEADING); + + let kelvin = Arg::new("kelvin") + .short('k') + .long("kelvin") + .action(ArgAction::SetTrue) + .help("Use Kelvin as the temperature unit.") + .help_heading(HEADING); + + let temperature_group = ArgGroup::new("TEMPERATURE_TYPE").args([ + celsius.get_id(), + fahrenheit.get_id(), + kelvin.get_id(), + ]); + + let args = [celsius, fahrenheit, kelvin]; + + self.args(args).group(temperature_group) + } -This reads from left to right, top to bottom. For example, suppose -we have a layout that looks like: -+-------------------+-----------------------+ -| CPU (1) | CPU (2) | -+---------+---------+-------------+---------+ -| Process | CPU (3) | Temperature | CPU (4) | -+---------+---------+-------------+---------+ + fn process_args(self) -> Command { + const HEADING: &str = "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("When searching for a process, enables case sensitivity by default.") + .help_heading(HEADING); + + let current_usage = Arg::new("current_usage") + .short('u') + .long("current_usage") + .action(ArgAction::SetTrue) + .help("Sets process CPU% to be based on current CPU%.") + .long_help("Sets process CPU% usage to be based on the current system CPU% usage rather than total CPU usage.") + .help_heading(HEADING); + + let unnormalized_cpu = Arg::new("unnormalized_cpu") + .short('n') + .long("unnormalized_cpu") + .action(ArgAction::SetTrue) + .help("Show process CPU% usage without normalizing over the number of cores.") + .long_help( + "Shows all process CPU% usage without averaging over the number of CPU cores in the system.", + ) + .help_heading(HEADING); + + let group_processes = Arg::new("group_processes") + .short('g') + .long("group_processes") + .action(ArgAction::SetTrue) + .help("Groups processes with the same name by default.") + .long_help("Groups processes with the same name by default.") + .help_heading(HEADING); + + let process_command = Arg::new("process_command") + .long("process_command") + .action(ArgAction::SetTrue) + .help("Show processes as their commands by default.") + .long_help("Show processes as their commands by default in the process widget.") + .help_heading(HEADING); + + let regex = Arg::new("regex") + .short('R') + .long("regex") + .action(ArgAction::SetTrue) + .help("Enables regex by default.") + .long_help("When searching for a process, enables regex by default.") + .help_heading(HEADING); + + let disable_advanced_kill = Arg::new("disable_advanced_kill") + .long("disable_advanced_kill") + .action(ArgAction::SetTrue) + .help("Hides advanced process killing.") + .long_help("Hides advanced options to stop a process on Unix-like systems. The only option shown is 15 (TERM).") + .help_heading(HEADING); + + let whole_word = Arg::new("whole_word") + .short('W') + .long("whole_word") + .action(ArgAction::SetTrue) + .help("Enables whole-word matching by default.") + .long_help( + "When searching for a process, return results that match the entire query by default.", + ) + .help_heading(HEADING); + + let tree = Arg::new("tree") + .short('T') + .long("tree") + .action(ArgAction::SetTrue) + .help("Defaults the process widget be in tree mode.") + .long_help("Defaults to showing the process widget in tree mode.") + .help_heading(HEADING); + + let mut args = [ + case_sensitive, + current_usage, + unnormalized_cpu, + group_processes, + process_command, + regex, + whole_word, + disable_advanced_kill, + tree, + ]; + args.sort_unstable(); + + self.args(args) + } -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. -", - ); + fn cpu_args(self) -> Command { + const HEADING: &str = "CPU Options"; - let default_widget_type = Arg::new("default_widget_type") - .long("default_widget_type") - .action(ArgAction::Set) - .value_name("WIDGET TYPE") - .help("Sets the default widget type, use --help for info.") - .long_help(DEFAULT_WIDGET_TYPE_STR); - - let expanded_on_startup = Arg::new("expanded_on_startup") - .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. Same as pressing \"e\" inside the app. Use with \"default_widget_type\" and \"default_widget_count\" to select desired expanded widget. This flag has no effect in basic mode (--basic)"); - - let rate = Arg::new("rate") - .short('r') - .long("rate") - .action(ArgAction::Set) - .value_name("TIME") - .help("Sets the data refresh rate.") - .long_help("Sets the data refresh rate. Takes a number in milliseconds or a human duration (e.g. 5s). The minimum is 250ms, and defaults to 1000ms. Smaller values may take more computer resources."); - - 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."); - - let tree = Arg::new("tree") - .short('T') - .long("tree") - .action(ArgAction::SetTrue) - .help("Defaults the process widget be in tree mode.") - .long_help("Defaults to showing the process widget in tree mode."); - - 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."); - - 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."); - - 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 (i.e. kibibits, mebibits) rather than a decimal prefix (i.e. kilobits, megabits). Defaults to decimal prefixes.", - ); + let hide_avg_cpu = Arg::new("hide_avg_cpu") + .short('a') + .long("hide_avg_cpu") + .action(ArgAction::SetTrue) + .help("Hides the average CPU usage.") + .long_help("Hides the average CPU usage from being shown.") + .help_heading(HEADING); - let retention = Arg::new("retention") - .long("retention") - .action(ArgAction::Set) - .value_name("TIME") - .help("The timespan of data stored.") - .long_help("How much data is stored at once in terms of time. Takes a number in milliseconds or a human duration (e.g. 20m), with a minimum of 1 minute. Note higher values will take up more memory. Defaults to 10 minutes."); + self.arg(hide_avg_cpu) + } - let version = Arg::new("version") - .short('V') - .long("version") - .action(ArgAction::Version) - .help("Prints version information."); + fn mem_args(self) -> Command { + const HEADING: &str = "Memory Options"; - const VERSION: &str = match option_env!("NIGHTLY_VERSION") { - Some(nightly_version) => nightly_version, - None => crate_version!(), - }; + 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.") + .help_heading(HEADING); - let temperature_group = ArgGroup::new("TEMPERATURE_TYPE").args([ - kelvin.get_id(), - fahrenheit.get_id(), - celsius.get_id(), - ]); - - let mut args = [ - version, - kelvin, - fahrenheit, - celsius, - autohide_time, - basic, - case_sensitive, - process_command, - config_location, - color, - mem_as_value, - default_time_value, - default_widget_count, - default_widget_type, - disable_click, - dot_marker, - group_processes, - hide_avg_cpu, - hide_table_gap, - hide_time, - show_table_scroll_position, - left_legend, - disable_advanced_kill, - rate, - regex, - time_delta, - tree, - network_use_bytes, - network_use_log, - network_use_binary_prefix, - current_usage, - unnormalized_cpu, - use_old_network_legend, - whole_word, - retention, - expanded_on_startup, - #[cfg(feature = "battery")] + #[cfg(not(target_os = "windows"))] { - Arg::new("battery") - .long("battery") + let enable_cache_memory = Arg::new("enable_cache_memory") + .long("enable_cache_memory") .action(ArgAction::SetTrue) - .help("Shows the battery widget.") - .long_help( - "Shows the battery widget in default or basic mode. No effect on custom layouts.", - ) - }, + .help("Enable collecting and displaying cache and buffer memory.") + .help_heading(HEADING); + + self.args([mem_as_value, enable_cache_memory]) + } + #[cfg(target_os = "windows")] + { + self.arg(mem_as_value) + } + } + + fn network_args(self) -> Command { + const HEADING: &str = "Network Options"; + + let use_old_network_legend = Arg::new("use_old_network_legend") + .long("use_old_network_legend") + .action(ArgAction::SetTrue) + .help("DEPRECATED - uses a separate network legend.") + .long_help( + "DEPRECATED - uses an older (pre-0.4), separate network widget legend. This display is not \ + tested anymore and could be broken.", + ) + .help_heading(HEADING); + + 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.") + .help_heading(HEADING); + + 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.") + .help_heading(HEADING); + + 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 (i.e. kibibits, mebibits) rather than a decimal prefix (i.e. kilobits, megabits). Defaults to decimal prefixes.", + ) + .help_heading(HEADING); + + let mut args = [ + use_old_network_legend, + network_use_bytes, + network_use_log, + network_use_binary_prefix, + ]; + + args.sort_unstable(); + + self.args(args) + } + + fn battery_args(self) -> Command { + #[cfg(feature = "battery")] + { + let battery = Arg::new("battery") + .long("battery") + .action(ArgAction::SetTrue) + .help("Shows the battery widget.") + .long_help( + "Shows the battery widget in default or basic mode. No effect on custom layouts.", + ) + .help_heading("Battery Options"); + + self.arg(battery) + } + #[cfg(not(feature = "battery"))] + { + self + } + } + + fn gpu_args(self) -> Command { #[cfg(feature = "gpu")] { - Arg::new("enable_gpu") + let enable_gpu = Arg::new("enable_gpu") .long("enable_gpu") .action(ArgAction::SetTrue) .help("Enable collecting and displaying GPU usage.") - }, - #[cfg(not(target_os = "windows"))] + .help_heading("GPU Options"); + + self.arg(enable_gpu) + } + #[cfg(not(feature = "gpu"))] { - Arg::new("enable_cache_memory") - .long("enable_cache_memory") - .action(ArgAction::SetTrue) - .help("Enable collecting and displaying cache and buffer memory.") - }, - ]; + self + } + } + + fn other(self) -> Command { + const HEADING: &str = "Other Options"; + + let version = Arg::new("version") + .short('V') + .long("version") + .action(ArgAction::Version) + .help("Prints version information.") + .help_heading(HEADING); - // Manually sort the arguments. - args.sort_by(|a, b| { - let a = a.get_long().unwrap_or(a.get_id().as_str()); - let b = b.get_long().unwrap_or(b.get_id().as_str()); + let help = Arg::new("help") + .short('h') + .long("help") + .action(ArgAction::Help) + .help("Prints help (see more with '--help').") + .help_heading(HEADING); - a.cmp(b) - }); + self.args([version, help]) + } +} + +pub fn build_app() -> Command { + const TEMPLATE: &str = include_str!("./args.template"); + const USAGE: &str = "btm [OPTIONS]"; + const VERSION: &str = match option_env!("NIGHTLY_VERSION") { + Some(nightly_version) => nightly_version, + None => crate_version!(), + }; Command::new(crate_name!()) - .version(VERSION) .author(crate_authors!()) .about(crate_description!()) + .disable_help_flag(true) + .disable_version_flag(true) .color(ColorChoice::Auto) - .override_usage(USAGE) .help_template(TEMPLATE) - .disable_version_flag(true) - .args(args) - .group(temperature_group) + .override_usage(USAGE) + .version(VERSION) + .general_args() + .style_args() + .temperature_args() + .process_args() + .cpu_args() + .mem_args() + .network_args() + .battery_args() + .gpu_args() + .other() } #[cfg(test)] @@ -505,4 +644,15 @@ mod test { fn verify_cli() { build_app().debug_assert(); } + + #[test] + fn help_heading_no_default() { + let mut app = build_app(); + let help_str = app.render_help(); + + assert!( + !help_str.to_string().contains("\nOptions:\n"), + "the default 'Options' heading should not exist; if it does then an argument is missing a help heading." + ); + } } diff --git a/src/options/args.template b/src/options/args.template new file mode 100644 index 00000000..c4712b35 --- /dev/null +++ b/src/options/args.template @@ -0,0 +1,8 @@ +{name} {version} +{author} + +{about} + +{usage-heading} {usage} + +{all-args} \ No newline at end of file -- cgit v1.2.3