refactor: use struct for args instead of builder interface (#1472)

* start moving args

* tmp

* refactor config

* port over ags

* update changelog
This commit is contained in:
Clement Tsang 2024-05-27 01:16:37 -04:00 committed by GitHub
parent 71f6136a1e
commit ee2e1fee1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 769 additions and 736 deletions

View File

@ -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

23
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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)?;

View File

@ -26,12 +26,12 @@ 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_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. |

View File

@ -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. |

View File

@ -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.

View File

@ -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"

View File

@ -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::<String>("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)?;

View File

@ -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<Self> {
pub fn new(colour_scheme: ColourScheme, config: &ConfigV1) -> anyhow::Result<Self> {
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();

View File

@ -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.

View File

@ -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<Self, Self::Err> {
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 \"<kelvin|k|celsius|c|fahrenheit|f>\"."
)),
}
}
}
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 {

View File

@ -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<Option<PathBuf>> {
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<PathBuf> {
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<Option<Pat
}
} else {
None
};
Ok(config_path)
}
}
pub fn create_or_get_config(config_path: &Option<PathBuf>) -> error::Result<Config> {
if let Some(path) = config_path {
pub fn get_or_create_config(override_config_path: Option<&Path>) -> error::Result<ConfigV1> {
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<PathBuf>) -> error::Result<Conf
}
fs::File::create(path)?.write_all(CONFIG_TEXT.as_bytes())?;
Ok(Config::default())
Ok(ConfigV1::default())
}
} else {
// Don't write...
Ok(Config::default())
// If we somehow don't have any config path, then just assume the default config but don't write to any file.
Ok(ConfigV1::default())
}
}

View File

@ -13,16 +13,18 @@ use std::{
};
use anyhow::{Context, Result};
use clap::ArgMatches;
pub use colours::ConfigColours;
pub use config::Config;
pub use config::ConfigV1;
use hashbrown::{HashMap, HashSet};
use indexmap::IndexSet;
use regex::Regex;
#[cfg(feature = "battery")]
use starship_battery::Manager;
use self::config::{layout::Row, IgnoreList, StringOrNum};
use self::{
args::BottomArgs,
config::{layout::Row, IgnoreList, StringOrNum},
};
use crate::{
app::{filter::Filter, layout_manager::*, *},
canvas::{components::time_chart::LegendPosition, styling::CanvasStyling, ColourScheme},
@ -36,8 +38,8 @@ use crate::{
};
macro_rules! is_flag_enabled {
($flag_name:ident, $matches:expr, $config:expr) => {
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<BottomWidgetType>, styling: &CanvasStyling,
) -> Result<App> {
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<u64, CpuWidgetState> = 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<IndexSet<ProcWidgetColumn>> = {
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,7 +353,8 @@ pub fn init_app(
};
let is_expanded = expanded && !use_basic_mode;
Ok(App::new(
Ok((
App::new(
app_config_fields,
states,
widget_map,
@ -356,29 +362,31 @@ pub fn init_app(
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<BottomWidgetType>)> {
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<Row>; // 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::<Config>(if get_use_battery(matches, config) {
ref_row = toml_edit::de::from_str::<ConfigV1>(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<u64> {
let update_rate = if let Some(update_rate) = matches.get_one::<String>("rate") {
try_parse_ms(update_rate)?
fn get_update_rate(args: &BottomArgs, config: &ConfigV1) -> error::Result<u64> {
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<u64>
Ok(update_rate)
}
fn get_temperature(matches: &ArgMatches, config: &Config) -> error::Result<TemperatureType> {
if matches.get_flag("fahrenheit") {
fn get_temperature(args: &BottomArgs, config: &ConfigV1) -> error::Result<TemperatureType> {
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 \"<kelvin|k|celsius|c|fahrenheit|f>\"."
))),
};
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,15 +501,18 @@ fn try_parse_ms(s: &str) -> error::Result<u64> {
}
fn get_default_time_value(
matches: &ArgMatches, config: &Config, retention_ms: u64,
args: &BottomArgs, config: &ConfigV1, retention_ms: u64,
) -> error::Result<u64> {
let default_time =
if let Some(default_time_value) = matches.get_one::<String>("default_time_value") {
try_parse_ms(default_time_value)?
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)?,
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 {
@ -517,11 +524,11 @@ fn get_default_time_value(
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<u64> {
let time_interval = if let Some(time_interval) = matches.get_one::<String>("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<BottomWidgetType>, u64)> {
let widget_type = if let Some(widget_type) = matches.get_one::<String>("default_widget_type") {
let widget_type = if let Some(widget_type) = &args.general.default_widget_type {
let parsed_widget = widget_type.parse::<BottomWidgetType>()?;
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::<String>("default_widget_count")
{
Some(widget_count.parse::<u128>()?)
} else if let Some(flags) = &config.flags {
let widget_count: Option<u128> = if let Some(widget_count) = args.general.default_widget_count {
Some(widget_count.into())
} else {
config.flags.as_ref().and_then(|flags| {
flags
.default_widget_count
.map(|widget_count| widget_count.into())
} else {
None
})
};
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<IgnoreList>) -> error::Result<Option<Fil
}
}
pub fn get_color_scheme(matches: &ArgMatches, config: &Config) -> error::Result<ColourScheme> {
if let Some(color) = matches.get_one::<String>("color") {
pub fn get_color_scheme(args: &BottomArgs, config: &ConfigV1) -> error::Result<ColourScheme> {
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<u64> {
fn get_retention(args: &BottomArgs, config: &ConfigV1) -> error::Result<u64> {
const DEFAULT_RETENTION_MS: u64 = 600 * 1000; // Keep 10 minutes of data.
if let Some(retention) = matches.get_one::<String>("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<u64> {
}
}
fn get_network_legend(
matches: &ArgMatches, config: &Config,
fn get_network_legend_position(
args: &BottomArgs, config: &ConfigV1,
) -> error::Result<Option<LegendPosition>> {
let error =
|_| BottomError::ConfigError("network_legend is set to an invalid value".to_string());
if let Some(s) = matches.get_one::<String>("network_legend") {
if let Some(s) = &args.network.network_legend {
match s.to_ascii_lowercase().trim() {
"none" => Ok(None),
position => Ok(Some(position.parse::<LegendPosition>().map_err(error)?)),
position => Ok(Some(position.parse::<LegendPosition>().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::<LegendPosition>().map_err(error)?))
Ok(Some(legend.parse::<LegendPosition>().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<Option<LegendPosition>> {
let error =
|_| BottomError::ConfigError("memory_legend is set to an invalid value".to_string());
if let Some(s) = matches.get_one::<String>("memory_legend") {
if let Some(s) = &args.memory.memory_legend {
match s.to_ascii_lowercase().trim() {
"none" => Ok(None),
position => Ok(Some(position.parse::<LegendPosition>().map_err(error)?)),
position => Ok(Some(position.parse::<LegendPosition>().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::<LegendPosition>().map_err(error)?))
Ok(Some(legend.parse::<LegendPosition>().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

View File

@ -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]
{all-args}"
};
( $( $arg:expr ),+ $(,)? ) => {
{
let mut args = [ $( $arg, )* ];
args.sort_unstable_by(sort_args);
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. \
#[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,
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."
)
.value_hint(ValueHint::AnyPath);
)]
pub config_location: Option<PathBuf>,
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.",
);
#[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<String>,
// 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<u64>,
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<String>,
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' \
#[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>,
// 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' \
#[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>,
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 \
#[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.",
);
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,
])
defaults to 15s."
)]
pub time_delta: Option<String>,
}
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<PossibleValue> {
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<String>,
#[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<String>,
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.");
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 \
#[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_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.");
#[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,
// 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 \
#[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."
);
cmd.arg(battery)
)]
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<String>,
}
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}
/// Returns a [`BottomArgs`].
pub fn get_args() -> BottomArgs {
BottomArgs::parse()
}
{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!(),
};
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"),

View File

@ -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<ConfigFlags>,
pub(crate) colors: Option<ConfigColours>,
pub(crate) row: Option<Vec<Row>>,
@ -71,7 +71,7 @@ pub(crate) struct ConfigFlags {
pub(crate) memory_legend: Option<String>,
/// For built-in colour palettes.
pub(crate) color: Option<String>,
pub(crate) mem_as_value: Option<bool>,
pub(crate) process_memory_as_value: Option<bool>,
pub(crate) tree: Option<bool>,
pub(crate) show_table_scroll_position: Option<bool>,
pub(crate) process_command: Option<bool>,

View File

@ -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::<Config>(DEFAULT_LAYOUT).unwrap().row.unwrap();
let rows = from_str::<ConfigV1>(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::<Config>(DEFAULT_BATTERY_LAYOUT)
let rows = from_str::<ConfigV1>(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::<Config>(DEFAULT_LAYOUT).unwrap().row.unwrap();
let rows = from_str::<ConfigV1>(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::<Config>(proc_layout).unwrap().row.unwrap();
let rows = from_str::<ConfigV1>(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::<Config>(PROC_LAYOUT).unwrap().row.unwrap();
let rows = from_str::<ConfigV1>(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::<Config>(PROC_LAYOUT).unwrap().row.unwrap();
let rows = from_str::<ConfigV1>(PROC_LAYOUT).unwrap().row.unwrap();
let ret_bottom_layout = test_create_layout(&rows, DEFAULT_WIDGET_ID, None, 1, false);
// First proc widget

View File

@ -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),
}

View File

@ -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,

View File

@ -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]