//! 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. // TODO: New sections are misaligned! See if we can get that fixed. use std::path::PathBuf; use clap::*; use indoc::indoc; 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!(), }; const CHART_WIDGET_POSITIONS: [&str; 9] = [ "none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right", ]; /// 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, #[command(flatten)] pub process: ProcessArgs, #[command(flatten)] pub temperature: TemperatureArgs, #[command(flatten)] pub cpu: CpuArgs, #[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, 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: Option, #[arg( short = 'b', long, 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: Option, #[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 #[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. 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) | +---------+---------+-------------+---------+ 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, #[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. For example, suppose we have a layout that looks like: +-------------------+-----------------------+ | CPU (1) | CPU (2) | +---------+---------+-------------+---------+ | Process | CPU (3) | Temperature | CPU (4) | +---------+---------+-------------+---------+ Then, setting '--default_widget_type temperature' will make the temperature widget selected by default." }, value_parser = [ "cpu", "mem", "net", "network", "proc", "process", "processes", "temp", "temperature", "disk", #[cfg(feature = "battery")] "batt", #[cfg(feature = "battery")] "battery", ], )] pub default_widget_type: Option, #[arg( long, help = "Disables mouse clicks.", long_help = "Disables mouse clicks from interacting with bottom." )] pub disable_click: Option, // TODO: Change this to accept a string with the type of marker. #[arg( short = 'm', long, 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: Option, #[arg( short = 'e', long, 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: Option, #[arg(long, help = "Hides spacing between table headers and entries.")] pub hide_table_gap: Option, #[arg(long, help = "Hides the time scale from being shown.")] pub hide_time: Option, #[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, #[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, #[arg( long, help = "Shows the list scroll position tracker in the widget title for table widgets." )] pub show_table_scroll_position: Option, #[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, } /// 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, help = "Enables case sensitivity by default.", long_help = "Enables case sensitivity by default when searching for a process." )] pub case_sensitive: Option, // TODO: Rename this. #[arg( short = 'u', long, help = "Calculates process CPU usage as a percentage of current usage rather than total usage." )] pub current_usage: Option, // TODO: Disable this on Windows? #[arg( long, 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: Option, #[arg( short = 'g', long, help = "Groups processes with the same name by default." )] pub group_processes: Option, #[arg( long, 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 mem_as_value: Option, #[arg( long, help = "Shows the full command name instead of the process name by default." )] pub process_command: Option, #[arg(short = 'R', long, help = "Enables regex by default while searching.")] pub regex: Option, #[arg( short = 'T', long, help = "Makes the process widget use tree mode by default." )] pub tree: Option, #[arg( short = 'n', long, help = "Show process CPU% usage without averaging over the number of CPU cores." )] pub unnormalized_cpu: Option, #[arg( short = 'W', long, help = "Enables whole-word matching by default while searching." )] pub whole_word: Option, } /// 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, 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, #[arg( short = 'f', long, group = "temperature_unit", help = "Use Fahrenheit as the temperature unit." )] pub fahrenheit: bool, #[arg( short = 'k', long, group = "temperature_unit", help = "Use Kelvin as the temperature unit." )] pub kelvin: bool, } /// 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, } impl From<&str> for CpuDefault { fn from(value: &str) -> Self { match value.to_ascii_lowercase().as_str() { "all" => CpuDefault::All, "avg" | "average" => CpuDefault::Average, _ => CpuDefault::All, } } } /// 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 = ["all", "avg"], default_value = "all" )] pub default_cpu_entry: CpuDefault, #[arg(short = 'a', long, help = "Hides the average CPU usage entry.")] pub hide_avg_cpu: Option, // TODO: Maybe rename this or fix this? Should this apply to all "left legends"? #[arg( short = 'l', long, help = "Puts the CPU chart legend on the left side." )] pub left_legend: Option, } /// 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"))] #[arg( long, help = "Enables collecting and displaying cache and buffer memory." )] pub enable_cache_memory: Option, } /// 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, // TODO: Rename some of these to remove the network prefix for serde. #[arg( long, help = "Displays the network widget using bytes.", long_help = "Displays the network widget using bytes. Defaults to bits." )] pub network_use_bytes: Option, #[arg( long, 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: Option, #[arg( long, 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: Option, #[arg( long, 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: Option, } /// Battery arguments/config options. #[cfg(feature = "battery")] #[derive(Args, Clone, Debug, Default)] #[command(next_help_heading = "Battery Options", rename_all = "snake_case")] pub struct BatteryArgs { #[arg( long, 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: Option, } /// GPU arguments/config options. #[cfg(feature = "gpu")] #[derive(Args, Clone, Debug, Default)] #[command(next_help_heading = "GPU Options", rename_all = "snake_case")] pub struct GpuArgs { #[arg(long, help = "Enable collecting and displaying GPU usage.")] pub enable_gpu: Option, } /// 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! { "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! { "Use a pre-defined color scheme. Currently supported values are: - default - default-light (default but adjusted for lighter backgrounds) - gruvbox (a bright theme with 'retro groove' colors) - gruvbox-light (gruvbox but adjusted for lighter backgrounds) - nord (an arctic, north-bluish color palette) - nord-light (nord but adjusted for lighter backgrounds)" } )] pub color: Option, } /// 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: (), #[arg(short = 'v', long, action = ArgAction::Version, help = "Prints version information.")] version: (), } fn other_args(cmd: Command) -> Command { let cmd = cmd.next_help_heading("Other Options"); 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]) } /// Returns a [`BottomArgs`]. pub fn get_args() -> BottomArgs { BottomArgs::parse() } /// Returns an [`Command`] based off of [`BottomArgs`]. #[cfg(test)] pub(crate) fn build_cmd() -> Command { BottomArgs::command() } #[cfg(test)] mod test { use super::*; #[test] fn verify_cli() { build_cmd().debug_assert(); } #[test] fn no_default_help_heading() { let mut cmd = build_cmd(); let help_str = cmd.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." ); } }