use std::path::PathBuf;
use clap::{builder::PossibleValue, *};
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",
];
#[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,
}
#[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_help = "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.",
alias = "autohide-time"
)]
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.",
alias = "config-location",
alias = "config",
)]
pub config_location: Option<PathBuf>,
#[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.",
alias = "default-time-value"
)]
pub default_time_value: Option<String>,
#[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."
},
alias = "default-widget-count"
)]
pub default_widget_count: Option<u64>,
#[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",
],
alias = "default-widget-type"
)]
pub default_widget_type: Option<String>,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Disables mouse clicks.",
long_help = "Disables mouse clicks from interacting with bottom.",
alias = "disable-click"
)]
pub disable_click: bool,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Disables keyboard shortcuts, INCLUDING the ones that stop bottom.",
long_help = "Disables keyboard shortcuts from interacting with bottom. Note this includes keyboard shortcuts to quit bottom.",
alias = "disable-keys"
)]
pub disable_keys: bool,
#[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.",
alias = "dot-marker"
)]
pub dot_marker: bool,
#[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,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Hides spacing between table headers and entries.",
alias = "hide-table-gap"
)]
pub hide_table_gap: bool,
#[arg(long, action = ArgAction::SetTrue, help = "Hides the time scale from being shown.", alias = "hide-time")]
pub hide_time: bool,
#[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<String>,
#[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<String>,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Shows the list scroll position tracker in the widget title for table widgets.",
alias = "show-table-scroll-position"
)]
pub show_table_scroll_position: bool,
#[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.",
alias = "time-delta"
)]
pub time_delta: Option<String>,
}
#[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 when searching.",
long_help = "Enables case sensitivity by default when searching for a process.",
alias = "case-sensitive"
)]
pub case_sensitive: bool,
#[arg(
short = 'u',
long,
action = ArgAction::SetTrue,
help = "Calculates process CPU usage as a percentage of current usage rather than total usage.",
alias = "current-usage"
)]
pub current_usage: bool,
#[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
#[arg(
long,
action = ArgAction::SetTrue,
help = "Hides additional stopping options on Unix-like systems.",
long_help = "Hides additional stopping options on Unix-like systems. Signal 15 (TERM) will be sent when \
stopping a process.",
alias = "disable-advanced-kill"
)]
pub disable_advanced_kill: bool,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Prevents performing any actions that affect the system.",
long_help = "Prevents performing any actions that affect the system. Disables operations such as stopping or sending signals \
to processes.",
alias = "read-only"
)]
pub read_only: bool,
#[cfg(target_os = "linux")]
#[arg(
long,
action = ArgAction::SetTrue,
help = "Hide kernel threads by default.",
alias = "hide-k-threads"
)]
pub hide_k_threads: bool,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Also gather process thread information.",
alias = "get-threads",
)]
pub get_threads: bool,
#[arg(
short = 'g',
long,
action = ArgAction::SetTrue,
help = "Groups processes with the same name by default when searching.",
long_help = "Groups processes with the same name by default when searching. Doesn't do anything if --tree is also set, or \
tree=true in the config.",
alias = "group-processes"
)]
pub group_processes: bool,
#[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.",
alias = "process-memory-as-value"
)]
pub process_memory_as_value: bool,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Shows the full command name instead of the process name by default.",
alias = "process-command"
)]
pub process_command: bool,
#[arg(short = 'R', long, action = ArgAction::SetTrue, help = "Enables regex by default while searching.")]
pub regex: bool,
#[arg(
short = 'T',
long,
action = ArgAction::SetTrue,
help = "Makes the process widget use tree mode by default."
)]
pub tree: bool,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Collapse process tree by default.",
alias = "tree-collapse"
)]
pub tree_collapse: bool,
#[arg(
short = 'n',
long,
action = ArgAction::SetTrue,
help = "Show process CPU% usage without averaging over the number of CPU cores.",
alias = "unnormalized-cpu"
)]
pub unnormalized_cpu: bool,
#[arg(
short = 'W',
long,
action = ArgAction::SetTrue,
help = "Enables whole-word matching by default while searching.",
alias = "whole-word"
)]
pub whole_word: bool,
}
#[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,
#[arg(
short = 'f',
long,
action = ArgAction::SetTrue,
group = "temperature_unit",
help = "Use Fahrenheit as the temperature unit."
)]
pub fahrenheit: bool,
#[arg(
short = 'k',
long,
action = ArgAction::SetTrue,
group = "temperature_unit",
help = "Use Kelvin as the temperature unit."
)]
pub kelvin: bool,
}
#[derive(Clone, Copy, Debug, Default)]
pub enum CpuDefault {
#[default]
All,
Average,
}
impl ValueEnum for CpuDefault {
fn value_variants<'a>() -> &'a [Self] {
&[CpuDefault::All, CpuDefault::Average]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
CpuDefault::All => Some(PossibleValue::new("all")),
CpuDefault::Average => Some(PossibleValue::new("avg").alias("average")),
}
}
}
#[derive(Args, Clone, Debug, Default)]
#[command(next_help_heading = "CPU Options", rename_all = "snake_case")]
pub struct CpuArgs {
#[arg(
short = 'l',
long,
action = ArgAction::SetTrue,
help = "Puts the CPU chart legend on the left side.",
alias = "cpu-left-legend"
)]
pub cpu_left_legend: bool,
#[arg(
long,
help = "Sets which CPU entry type is selected by default.",
value_name = "ENTRY",
value_parser = value_parser!(CpuDefault),
alias = "default-cpu-entry"
)]
pub default_cpu_entry: Option<CpuDefault>,
#[arg(
short = 'a',
long,
action = ArgAction::SetTrue,
help = "Hides the average CPU usage entry.",
alias = "hide-avg-cpu"
)]
pub hide_avg_cpu: bool,
}
#[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.",
alias = "memory-legend"
)]
pub memory_legend: Option<String>,
#[cfg(not(target_os = "windows"))]
#[arg(
long,
action = ArgAction::SetTrue,
help = "Enables collecting and displaying cache and buffer memory.",
alias = "enable-cache-memory"
)]
pub enable_cache_memory: bool,
#[cfg(feature = "zfs")]
#[arg(
long,
action = ArgAction::SetTrue,
help = "Subtract reclaimable ARC from memory.",
alias = "free-arc"
)]
pub free_arc: bool,
}
#[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.",
alias = "network-legend"
)]
pub network_legend: Option<String>,
#[arg(
long,
action = ArgAction::SetTrue,
help = "Displays the network widget using bytes.",
long_help = "Displays the network widget using bytes. Defaults to bits.",
alias = "network-use-bytes"
)]
pub network_use_bytes: bool,
#[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.",
alias = "network-use-binary-prefix"
)]
pub network_use_binary_prefix: bool,
#[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.",
alias = "network-use-log"
)]
pub network_use_log: bool,
#[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.",
alias = "use-old-network-legend"
)]
pub use_old_network_legend: bool,
}
#[cfg(feature = "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,
}
#[cfg(feature = "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 = "Disable collecting and displaying NVIDIA and AMD GPU information.", alias = "disable-gpu")]
pub disable_gpu: bool,
}
#[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 built-in color theme, 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 theme. Currently supported themes 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 theme: Option<String>,
}
#[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: (),
}
pub fn get_args() -> BottomArgs {
BottomArgs::parse()
}
#[cfg(test)]
pub(crate) fn build_cmd() -> Command {
BottomArgs::command()
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
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."
);
let long_help_str = cmd.render_long_help();
assert!(
!long_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."
);
}
#[test]
fn catch_incorrect_long_args() {
let allow_list: HashSet<&str> = vec![].into_iter().collect();
let cmd = build_cmd();
for opt in cmd.get_opts() {
let long_flag = opt.get_long().unwrap();
if !allow_list.contains(long_flag) {
assert!(
long_flag.len() < 30,
"the long help arg '{long_flag}' might be set wrong, please take a look!"
);
}
}
}
#[test]
fn catch_missing_hyphen_alias() {
let cmd = build_cmd();
for opt in cmd.get_opts() {
let long_flag = opt.get_long().unwrap();
if long_flag.contains("_") {
let aliased_version = long_flag.replace("_", "-");
let stored_alias = opt.get_aliases().unwrap_or_else(|| {
panic!("'{long_flag}' should have an alias, if not, it's missing")
});
assert!(
stored_alias.contains(&aliased_version.as_str()),
"'{long_flag}' has an incorrectly defined alias"
);
}
}
}
}