diff --git a/build.rs b/build.rs index 58df60fd..1d13c701 100644 --- a/build.rs +++ b/build.rs @@ -7,7 +7,7 @@ use clap_complete::{generate_to, shells::Shell, Generator}; use clap_complete_fig::Fig; use clap_complete_nushell::Nushell; -include!("src/args.rs"); +include!("src/options/args.rs"); fn create_dir(dir: &Path) -> io::Result<()> { let res = fs::create_dir_all(dir); diff --git a/src/app.rs b/src/app.rs index eea35288..f9e7de16 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,11 @@ +pub mod data_farmer; +pub mod filter; +pub mod frozen_state; +pub mod layout_manager; +mod process_killer; +pub mod query; +pub mod states; + use std::{ cmp::{max, min}, time::Instant, @@ -6,6 +14,7 @@ use std::{ use concat_string::concat_string; use data_farmer::*; use filter::*; +use frozen_state::FrozenState; use hashbrown::HashMap; use layout_manager::*; pub use states::*; @@ -23,16 +32,6 @@ use crate::{ Pid, }; -pub mod data_farmer; -pub mod filter; -pub mod frozen_state; -pub mod layout_manager; -mod process_killer; -pub mod query; -pub mod states; - -use frozen_state::FrozenState; - #[derive(Debug, Clone, Eq, PartialEq, Default)] pub enum AxisScaling { #[default] diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index 720092db..3ad819be 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -21,7 +21,7 @@ use hashbrown::HashMap; use crate::data_collection::batteries; use crate::{ data_collection::{cpu, disks, memory, network, processes::ProcessHarvest, temperature, Data}, - utils::{data_prefixes::*, gen_util::get_decimal_bytes}, + utils::{data_prefixes::*, general::get_decimal_bytes}, Pid, }; diff --git a/src/app/states.rs b/src/app/states.rs index f22a0221..8b5f0ce8 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -7,7 +7,7 @@ use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete, UnicodeSegmentati use crate::{ app::{layout_manager::BottomWidgetType, query::*}, constants, - utils::gen_util::str_width, + utils::general::str_width, widgets::{ BatteryWidgetState, CpuWidgetState, DiskTableWidget, MemWidgetState, NetWidgetState, ProcWidgetState, TempWidgetState, diff --git a/src/bin/main.rs b/src/bin/main.rs index a5218efe..4d588a94 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -15,15 +15,12 @@ use std::{ use anyhow::{Context, Result}; use bottom::{ args, - canvas::{ - styling::CanvasStyling, - {self}, - }, + 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::*, + options::config::{get_color_scheme, get_widget_layout, init_app}, panic_hook, read_config, try_drawing, update_data, BottomEvent, }; use crossterm::{ @@ -73,7 +70,7 @@ fn main() -> Result<()> { }; // Create an "app" struct, which will control most of the program and store settings/state - let mut app = build_app( + let mut app = init_app( matches, config, &widget_layout, diff --git a/src/canvas.rs b/src/canvas.rs index 8960f0a0..6b18f1c7 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -1,3 +1,9 @@ +mod dialogs; +mod drawing_utils; +pub mod styling; +pub mod tui_widgets; +mod widgets; + use std::str::FromStr; use itertools::izip; @@ -19,12 +25,6 @@ use crate::{ utils::{error, error::BottomError}, }; -mod dialogs; -mod drawing_utils; -pub mod styling; -pub mod tui_widgets; -mod widgets; - #[derive(Debug)] pub enum ColourScheme { Default, diff --git a/src/canvas/styling.rs b/src/canvas/styling.rs index efd7638f..c317e8a0 100644 --- a/src/canvas/styling.rs +++ b/src/canvas/styling.rs @@ -5,7 +5,7 @@ use tui::style::{Color, Style}; use super::ColourScheme; use crate::{ constants::*, - options::{Config, ConfigColours}, + options::config::{Config, ConfigColours}, utils::error, }; mod colour_utils; diff --git a/src/canvas/tui_widgets/data_table.rs b/src/canvas/tui_widgets/data_table.rs index 4ae23d1d..5fb4a591 100644 --- a/src/canvas/tui_widgets/data_table.rs +++ b/src/canvas/tui_widgets/data_table.rs @@ -1,27 +1,22 @@ +pub mod column; +pub mod data_type; +pub mod draw; +pub mod props; +pub mod sortable; +pub mod state; +pub mod styling; + use std::{convert::TryInto, marker::PhantomData}; -pub mod column; pub use column::*; - -pub mod styling; +pub use data_type::*; +pub use draw::*; +pub use props::DataTableProps; +pub use sortable::*; +pub use state::{DataTableState, ScrollDirection}; pub use styling::*; -pub mod props; -pub use props::DataTableProps; - -pub mod state; -pub use state::{DataTableState, ScrollDirection}; - -pub mod draw; -pub use draw::*; - -pub mod data_type; -pub use data_type::*; - -pub mod sortable; -pub use sortable::*; - -use crate::utils::gen_util::ClampExt; +use crate::utils::general::ClampExt; /// A [`DataTable`] is a component that displays data in a tabular form. /// diff --git a/src/canvas/tui_widgets/data_table/sortable.rs b/src/canvas/tui_widgets/data_table/sortable.rs index 3a3de1fc..f6c3b502 100644 --- a/src/canvas/tui_widgets/data_table/sortable.rs +++ b/src/canvas/tui_widgets/data_table/sortable.rs @@ -8,7 +8,7 @@ use super::{ ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps, DataTableState, DataTableStyling, DataToCell, }; -use crate::utils::gen_util::truncate_to_text; +use crate::utils::general::truncate_to_text; /// Denotes the sort order. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/src/canvas/tui_widgets/time_chart.rs b/src/canvas/tui_widgets/time_chart.rs index 403ef507..747c2ae3 100644 --- a/src/canvas/tui_widgets/time_chart.rs +++ b/src/canvas/tui_widgets/time_chart.rs @@ -16,7 +16,7 @@ use tui::{ }; use unicode_width::UnicodeWidthStr; -use crate::utils::gen_util::partial_ordering; +use crate::utils::general::partial_ordering; /// A single graph point. pub type Point = (f64, f64); diff --git a/src/canvas/widgets/disk_table.rs b/src/canvas/widgets/disk_table.rs index 5b6b2619..fe241d14 100644 --- a/src/canvas/widgets/disk_table.rs +++ b/src/canvas/widgets/disk_table.rs @@ -1,7 +1,7 @@ use tui::{layout::Rect, terminal::Frame}; use crate::{ - app::{self}, + app, canvas::{ tui_widgets::data_table::{DrawInfo, SelectionState}, Painter, diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index 7c8ce913..af1b6d73 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -16,7 +16,7 @@ use crate::{ }, Painter, }, - utils::{data_prefixes::*, data_units::DataUnit, gen_util::partial_ordering}, + utils::{data_prefixes::*, data_units::DataUnit, general::partial_ordering}, }; impl Painter { diff --git a/src/constants.rs b/src/constants.rs index d736e928..6d312aa4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,6 +1,6 @@ use tui::widgets::Borders; -use crate::options::ConfigColours; +use crate::options::config::ConfigColours; // Default widget ID pub const DEFAULT_WIDGET_ID: u64 = 56709; diff --git a/src/data_collection.rs b/src/data_collection.rs index d140e29e..d00d88e0 100644 --- a/src/data_collection.rs +++ b/src/data_collection.rs @@ -1,17 +1,5 @@ //! This is the main file to house data collection functions. -use std::time::{Duration, Instant}; - -#[cfg(any(target_os = "linux", feature = "gpu"))] -use hashbrown::HashMap; -#[cfg(feature = "battery")] -use starship_battery::{Battery, Manager}; -use sysinfo::{System, SystemExt}; - -use self::temperature::TemperatureType; -use super::DataFilters; -use crate::app::layout_manager::UsedWidgets; - #[cfg(feature = "nvidia")] pub mod nvidia; @@ -25,6 +13,18 @@ pub mod network; pub mod processes; pub mod temperature; +use std::time::{Duration, Instant}; + +#[cfg(any(target_os = "linux", feature = "gpu"))] +use hashbrown::HashMap; +#[cfg(feature = "battery")] +use starship_battery::{Battery, Manager}; +use sysinfo::{System, SystemExt}; + +use self::temperature::TemperatureType; +use super::DataFilters; +use crate::app::layout_manager::UsedWidgets; + #[derive(Clone, Debug)] pub struct Data { pub collection_time: Instant, diff --git a/src/data_collection/disks.rs b/src/data_collection/disks.rs index c4a66eff..8d55de98 100644 --- a/src/data_collection/disks.rs +++ b/src/data_collection/disks.rs @@ -1,10 +1,5 @@ //! Data collection about disks (e.g. I/O, usage, space). -use cfg_if::cfg_if; -use hashbrown::HashMap; - -use crate::app::filter::Filter; - cfg_if! { if #[cfg(target_os = "freebsd")] { mod freebsd; @@ -32,6 +27,11 @@ cfg_if! { } } +use cfg_if::cfg_if; +use hashbrown::HashMap; + +use crate::app::filter::Filter; + #[derive(Clone, Debug, Default)] pub struct DiskHarvest { pub name: String, diff --git a/src/data_collection/disks/unix.rs b/src/data_collection/disks/unix.rs index e41b0458..309a0a19 100644 --- a/src/data_collection/disks/unix.rs +++ b/src/data_collection/disks/unix.rs @@ -3,10 +3,7 @@ mod file_systems; -use file_systems::*; - mod usage; -use usage::*; cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { @@ -24,6 +21,9 @@ cfg_if::cfg_if! { } } +use file_systems::*; +use usage::*; + use super::{keep_disk_entry, DiskHarvest}; use crate::data_collection::DataCollector; diff --git a/src/data_collection/disks/unix/linux/mod.rs b/src/data_collection/disks/unix/linux/mod.rs index 40a83267..a54a276e 100644 --- a/src/data_collection/disks/unix/linux/mod.rs +++ b/src/data_collection/disks/unix/linux/mod.rs @@ -1,5 +1,5 @@ -mod partition; -pub(crate) use partition::*; - mod counters; +mod partition; + pub use counters::*; +pub(crate) use partition::*; diff --git a/src/data_collection/disks/unix/macos/io_kit.rs b/src/data_collection/disks/unix/macos/io_kit.rs index 42193fbd..bf585d46 100644 --- a/src/data_collection/disks/unix/macos/io_kit.rs +++ b/src/data_collection/disks/unix/macos/io_kit.rs @@ -1,10 +1,8 @@ mod bindings; - -mod io_iterator; -pub use io_iterator::*; - -mod io_object; -pub use io_object::*; - mod io_disks; +mod io_iterator; +mod io_object; + pub use io_disks::get_disks; +pub use io_iterator::*; +pub use io_object::*; diff --git a/src/data_collection/disks/unix/macos/mod.rs b/src/data_collection/disks/unix/macos/mod.rs index 97d7e154..9cacc03a 100644 --- a/src/data_collection/disks/unix/macos/mod.rs +++ b/src/data_collection/disks/unix/macos/mod.rs @@ -1,4 +1,4 @@ mod counters; -pub use counters::*; - mod io_kit; + +pub use counters::*; diff --git a/src/data_collection/disks/windows.rs b/src/data_collection/disks/windows.rs index 3da4a866..469bf4bf 100644 --- a/src/data_collection/disks/windows.rs +++ b/src/data_collection/disks/windows.rs @@ -1,14 +1,14 @@ //! Disk stats via sysinfo. +mod bindings; + +use bindings::*; use itertools::Itertools; use sysinfo::{DiskExt, SystemExt}; use super::{keep_disk_entry, DiskHarvest}; use crate::data_collection::{disks::IoCounters, DataCollector}; -mod bindings; -use bindings::*; - /// Returns I/O stats. pub(crate) fn io_stats() -> anyhow::Result> { let volume_io = all_volume_io()?; diff --git a/src/data_collection/processes.rs b/src/data_collection/processes.rs index b6c4522d..08b48a29 100644 --- a/src/data_collection/processes.rs +++ b/src/data_collection/processes.rs @@ -3,13 +3,8 @@ //! For Linux, this is handled by a custom set of functions. //! For Windows, macOS, FreeBSD, Android, and Linux, this is handled by sysinfo. -use std::{borrow::Cow, time::Duration}; - use cfg_if::cfg_if; -use super::DataCollector; -use crate::{utils::error, Pid}; - cfg_if! { if #[cfg(target_os = "linux")] { pub mod linux; @@ -36,6 +31,11 @@ cfg_if! { } } +use std::{borrow::Cow, time::Duration}; + +use super::DataCollector; +use crate::{utils::error, Pid}; + #[derive(Debug, Clone, Default)] pub struct ProcessHarvest { /// The pid of the process. diff --git a/src/data_collection/processes/linux.rs b/src/data_collection/processes/linux.rs index 6898f37e..18103f11 100644 --- a/src/data_collection/processes/linux.rs +++ b/src/data_collection/processes/linux.rs @@ -1,6 +1,7 @@ //! Process data collection for Linux. mod process; + use std::{ fs::{self, File}, io::{BufRead, BufReader}, diff --git a/src/data_collection/processes/macos.rs b/src/data_collection/processes/macos.rs index 036279c9..42aae82a 100644 --- a/src/data_collection/processes/macos.rs +++ b/src/data_collection/processes/macos.rs @@ -1,5 +1,7 @@ //! Process data collection for macOS. Uses sysinfo and custom bindings. +mod sysctl_bindings; + use std::{io, process::Command}; use hashbrown::HashMap; @@ -8,7 +10,6 @@ use sysinfo::{PidExt, ProcessExt}; use super::UnixProcessExt; use crate::Pid; -mod sysctl_bindings; pub(crate) struct MacOSProcessExt; diff --git a/src/data_collection/processes/unix.rs b/src/data_collection/processes/unix.rs index 2e79b578..4c79dc0f 100644 --- a/src/data_collection/processes/unix.rs +++ b/src/data_collection/processes/unix.rs @@ -1,6 +1,7 @@ //! Unix-specific parts of process collection. mod user_table; + use cfg_if::cfg_if; pub use user_table::*; diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 5d63a395..f4aee9d6 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -1,13 +1,15 @@ //! This mainly concerns converting collected data into things that the canvas //! can actually handle. +// TODO: Split this up! + use kstring::KString; use crate::{ app::{data_farmer::DataCollection, AxisScaling}, canvas::tui_widgets::time_chart::Point, data_collection::{cpu::CpuDataType, memory::MemHarvest, temperature::TemperatureType}, - utils::{data_prefixes::*, data_units::DataUnit, gen_util::*}, + utils::{data_prefixes::*, data_units::DataUnit, general::*}, widgets::{DiskWidgetData, TempWidgetData}, }; diff --git a/src/lib.rs b/src/lib.rs index d84e25e7..380dbf7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,21 @@ #![deny(clippy::unimplemented)] #![deny(clippy::missing_safety_doc)] +pub mod app; +pub mod utils { + pub mod data_prefixes; + pub mod data_units; + pub mod error; + pub mod general; + pub mod logging; +} +pub mod canvas; +pub mod constants; +pub mod data_collection; +pub mod data_conversion; +pub mod options; +pub mod widgets; + use std::{ boxed::Box, fs, @@ -44,25 +59,9 @@ use crossterm::{ terminal::{disable_raw_mode, LeaveAlternateScreen}, }; use data_conversion::*; -use options::*; +pub use options::args; +use options::config::Config; use utils::error; - -pub mod app; -pub mod utils { - pub mod data_prefixes; - pub mod data_units; - pub mod error; - pub mod gen_util; - pub mod logging; -} -pub mod args; -pub mod canvas; -pub mod constants; -pub mod data_collection; -pub mod data_conversion; -pub mod options; -pub mod widgets; - #[allow(unused_imports)] pub use utils::logging::*; diff --git a/src/options.rs b/src/options.rs index 1f3f46c9..459d4320 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,1149 +1,2 @@ -use std::{ - borrow::Cow, - convert::TryInto, - str::FromStr, - time::{Duration, Instant}, -}; - -use clap::ArgMatches; -use hashbrown::{HashMap, HashSet}; -use indexmap::IndexSet; -use layout_options::*; -use regex::Regex; -use serde::{Deserialize, Serialize}; -#[cfg(feature = "battery")] -use starship_battery::Manager; - -use crate::{ - app::{filter::Filter, layout_manager::*, *}, - canvas::{styling::CanvasStyling, ColourScheme}, - constants::*, - data_collection::temperature::TemperatureType, - utils::{ - data_units::DataUnit, - error::{self, BottomError}, - }, - widgets::*, -}; - -pub mod layout_options; - -mod process_columns; -pub use process_columns::ProcessConfig; - -mod cpu; -use anyhow::{Context, Result}; -pub use cpu::{CpuConfig, CpuDefault}; - -#[derive(Clone, Debug, Default, Deserialize)] -pub struct Config { - pub flags: Option, - pub colors: Option, - pub row: Option>, - pub disk_filter: Option, - pub mount_filter: Option, - pub temp_filter: Option, - pub net_filter: Option, - pub processes: Option, - pub cpu: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(untagged)] -enum StringOrNum { - String(String), - Num(u64), -} - -impl From for StringOrNum { - fn from(value: String) -> Self { - StringOrNum::String(value) - } -} - -impl From for StringOrNum { - fn from(value: u64) -> Self { - StringOrNum::Num(value) - } -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct ConfigFlags { - hide_avg_cpu: Option, - dot_marker: Option, - temperature_type: Option, - rate: Option, - left_legend: Option, - current_usage: Option, - unnormalized_cpu: Option, - group_processes: Option, - case_sensitive: Option, - whole_word: Option, - regex: Option, - basic: Option, - default_time_value: Option, - time_delta: Option, - autohide_time: Option, - hide_time: Option, - default_widget_type: Option, - default_widget_count: Option, - expanded_on_startup: Option, - use_old_network_legend: Option, - hide_table_gap: Option, - battery: Option, - disable_click: Option, - no_write: Option, - /// For built-in colour palettes. - color: Option, - mem_as_value: Option, - tree: Option, - show_table_scroll_position: Option, - process_command: Option, - disable_advanced_kill: Option, - network_use_bytes: Option, - network_use_log: Option, - network_use_binary_prefix: Option, - enable_gpu: Option, - enable_cache_memory: Option, - retention: Option, -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct ConfigColours { - pub table_header_color: Option>, - pub all_cpu_color: Option>, - pub avg_cpu_color: Option>, - pub cpu_core_colors: Option>>, - pub ram_color: Option>, - #[cfg(not(target_os = "windows"))] - pub cache_color: Option>, - pub swap_color: Option>, - pub arc_color: Option>, - pub gpu_core_colors: Option>>, - pub rx_color: Option>, - pub tx_color: Option>, - pub rx_total_color: Option>, // These only affect basic mode. - pub tx_total_color: Option>, // These only affect basic mode. - pub border_color: Option>, - pub highlighted_border_color: Option>, - pub disabled_text_color: Option>, - pub text_color: Option>, - pub selected_text_color: Option>, - pub selected_bg_color: Option>, - pub widget_title_color: Option>, - pub graph_color: Option>, - pub high_battery_color: Option>, - pub medium_battery_color: Option>, - pub low_battery_color: Option>, -} - -impl ConfigColours { - /// Returns `true` if there is a [`ConfigColours`] that is empty or there isn't one at all. - pub fn is_empty(&self) -> bool { - if let Ok(serialized_string) = toml_edit::ser::to_string(self) { - return serialized_string.is_empty(); - } - - true - } -} - -/// Workaround as per https://github.com/serde-rs/serde/issues/1030 -fn default_as_true() -> bool { - true -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -pub struct IgnoreList { - #[serde(default = "default_as_true")] - // TODO: Deprecate and/or rename, current name sounds awful. - // Maybe to something like "deny_entries"? Currently it defaults to a denylist anyways, so maybe "allow_entries"? - pub is_list_ignored: bool, - pub list: Vec, - #[serde(default = "bool::default")] - pub regex: bool, - #[serde(default = "bool::default")] - pub case_sensitive: bool, - #[serde(default = "bool::default")] - pub whole_word: bool, -} - -macro_rules! is_flag_enabled { - ($flag_name:ident, $matches:expr, $config:expr) => { - if $matches.get_flag(stringify!($flag_name)) { - true - } else if let Some(flags) = &$config.flags { - flags.$flag_name.unwrap_or(false) - } else { - false - } - }; - - ($cmd_flag:literal, $cfg_flag:ident, $matches:expr, $config:expr) => { - if $matches.get_flag($cmd_flag) { - true - } else if let Some(flags) = &$config.flags { - flags.$cfg_flag.unwrap_or(false) - } else { - false - } - }; -} - -pub fn build_app( - matches: ArgMatches, config: Config, widget_layout: &BottomLayout, default_widget_id: u64, - default_widget_type_option: &Option, styling: &CanvasStyling, -) -> Result { - use BottomWidgetType::*; - - // Since everything takes a reference, but we want to take ownership here to drop matches/config later... - let matches = &matches; - 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 use_basic_mode = is_flag_enabled!(basic, matches, config); - let expanded_upon_startup = is_flag_enabled!(expanded_on_startup, matches, 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 mut widget_map = HashMap::new(); - let mut cpu_state_map: HashMap = HashMap::new(); - let mut mem_state_map: HashMap = HashMap::new(); - let mut net_state_map: HashMap = HashMap::new(); - let mut proc_state_map: HashMap = HashMap::new(); - let mut temp_state_map: HashMap = HashMap::new(); - let mut disk_state_map: HashMap = HashMap::new(); - let mut battery_state_map: HashMap = HashMap::new(); - - let autohide_timer = if autohide_time { - Some(Instant::now()) - } else { - None - }; - - let mut initial_widget_id: u64 = default_widget_id; - let mut initial_widget_type = Proc; - 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 proc_columns: Option> = { - let columns = config - .processes - .as_ref() - .and_then(|cfg| cfg.columns.clone()); - - match columns { - Some(columns) => { - if columns.is_empty() { - None - } else { - Some(IndexSet::from_iter(columns)) - } - } - None => None, - } - }; - - 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) - .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), - left_legend: is_flag_enabled!(left_legend, matches, config), - use_current_cpu_total: is_flag_enabled!(current_usage, matches, config), - unnormalized_cpu: is_flag_enabled!(unnormalized_cpu, matches, 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), - 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), - is_advanced_kill, - network_scale_type, - network_unit_type, - network_use_binary_prefix, - retention_ms, - }; - - let table_config = ProcTableConfig { - is_case_sensitive, - is_match_whole_word, - is_use_regex, - show_memory_as_values, - is_command: is_default_command, - }; - - for row in &widget_layout.rows { - for col in &row.children { - for col_row in &col.children { - for widget in &col_row.children { - widget_map.insert(widget.widget_id, widget.clone()); - if let Some(default_widget_type) = &default_widget_type_option { - if !is_custom_layout || use_basic_mode { - match widget.widget_type { - BasicCpu => { - if let Cpu = *default_widget_type { - initial_widget_id = widget.widget_id; - initial_widget_type = Cpu; - } - } - BasicMem => { - if let Mem = *default_widget_type { - initial_widget_id = widget.widget_id; - initial_widget_type = Cpu; - } - } - BasicNet => { - if let Net = *default_widget_type { - initial_widget_id = widget.widget_id; - initial_widget_type = Cpu; - } - } - _ => { - if *default_widget_type == widget.widget_type { - initial_widget_id = widget.widget_id; - initial_widget_type = widget.widget_type.clone(); - } - } - } - } - } - - used_widget_set.insert(widget.widget_type.clone()); - - match widget.widget_type { - Cpu => { - cpu_state_map.insert( - widget.widget_id, - CpuWidgetState::new( - &app_config_fields, - config - .cpu - .as_ref() - .map(|cfg| cfg.default) - .unwrap_or_default(), - default_time_value, - autohide_timer, - styling, - ), - ); - } - Mem => { - mem_state_map.insert( - widget.widget_id, - MemWidgetState::init(default_time_value, autohide_timer), - ); - } - Net => { - net_state_map.insert( - widget.widget_id, - NetWidgetState::init(default_time_value, autohide_timer), - ); - } - Proc => { - let mode = if is_grouped { - ProcWidgetMode::Grouped - } else if is_default_tree { - ProcWidgetMode::Tree { - collapsed_pids: Default::default(), - } - } else { - ProcWidgetMode::Normal - }; - - proc_state_map.insert( - widget.widget_id, - ProcWidgetState::new( - &app_config_fields, - mode, - table_config, - styling, - &proc_columns, - ), - ); - } - Disk => { - disk_state_map.insert( - widget.widget_id, - DiskTableWidget::new(&app_config_fields, styling), - ); - } - Temp => { - temp_state_map.insert( - widget.widget_id, - TempWidgetState::new(&app_config_fields, styling), - ); - } - Battery => { - battery_state_map - .insert(widget.widget_id, BatteryWidgetState::default()); - } - _ => {} - } - } - } - } - } - - let basic_table_widget_state = if use_basic_mode { - Some(match initial_widget_type { - Proc | Disk | Temp => BasicTableWidgetState { - currently_displayed_widget_type: initial_widget_type, - currently_displayed_widget_id: initial_widget_id, - widget_id: 100, - left_tlc: None, - left_brc: None, - right_tlc: None, - right_brc: None, - }, - _ => BasicTableWidgetState { - currently_displayed_widget_type: Proc, - currently_displayed_widget_id: DEFAULT_WIDGET_ID, - widget_id: 100, - left_tlc: None, - left_brc: None, - right_tlc: None, - right_brc: None, - }, - }) - } else { - None - }; - - let use_mem = used_widget_set.get(&Mem).is_some() || used_widget_set.get(&BasicMem).is_some(); - 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_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(), - use_temp: used_widget_set.get(&Temp).is_some(), - use_battery: used_widget_set.get(&Battery).is_some(), - }; - - let disk_filter = - get_ignore_list(&config.disk_filter).context("Update 'disk_filter' in your config file")?; - let mount_filter = get_ignore_list(&config.mount_filter) - .context("Update 'mount_filter' in your config file")?; - let temp_filter = - get_ignore_list(&config.temp_filter).context("Update 'temp_filter' in your config file")?; - let net_filter = - get_ignore_list(&config.net_filter).context("Update 'net_filter' in your config file")?; - - let states = AppWidgetStates { - cpu_state: CpuState::init(cpu_state_map), - mem_state: MemState::init(mem_state_map), - net_state: NetState::init(net_state_map), - proc_state: ProcState::init(proc_state_map), - temp_state: TempState::init(temp_state_map), - disk_state: DiskState::init(disk_state_map), - battery_state: BatteryState::init(battery_state_map), - basic_table_widget_state, - }; - - let current_widget = widget_map.get(&initial_widget_id).unwrap().clone(); - let filters = DataFilters { - disk_filter, - mount_filter, - temp_filter, - net_filter, - }; - let is_expanded = expanded_upon_startup && !use_basic_mode; - - Ok(App::new( - app_config_fields, - states, - widget_map, - current_widget, - used_widgets, - filters, - is_expanded, - )) -} - -pub fn get_widget_layout( - matches: &ArgMatches, config: &Config, -) -> error::Result<(BottomLayout, u64, Option)> { - let left_legend = is_flag_enabled!(left_legend, matches, config); - - let (default_widget_type, mut default_widget_count) = - get_default_widget_and_count(matches, config)?; - let mut default_widget_id = 1; - - let bottom_layout = if is_flag_enabled!(basic, matches, config) { - default_widget_id = DEFAULT_WIDGET_ID; - - BottomLayout::init_basic_default(get_use_battery(matches, config)) - } else { - let ref_row: Vec; // Required to handle reference - let rows = match &config.row { - Some(r) => r, - None => { - // This cannot (like it really shouldn't) fail! - ref_row = toml_edit::de::from_str::(if get_use_battery(matches, config) { - DEFAULT_BATTERY_LAYOUT - } else { - DEFAULT_LAYOUT - })? - .row - .unwrap(); - &ref_row - } - }; - - let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs* - let mut total_height_ratio = 0; - - let mut ret_bottom_layout = BottomLayout { - rows: rows - .iter() - .map(|row| { - row.convert_row_to_bottom_row( - &mut iter_id, - &mut total_height_ratio, - &mut default_widget_id, - &default_widget_type, - &mut default_widget_count, - left_legend, - ) - }) - .collect::>>()?, - total_row_height_ratio: total_height_ratio, - }; - - // Confirm that we have at least ONE widget left - if not, error out! - if iter_id > 0 { - ret_bottom_layout.get_movement_mappings(); - // debug!("Bottom layout: {ret_bottom_layout:#?}"); - - ret_bottom_layout - } else { - return Err(BottomError::ConfigError( - "please have at least one widget under the '[[row]]' section.".to_string(), - )); - } - }; - - Ok((bottom_layout, default_widget_id, default_widget_type)) -} - -fn get_update_rate(matches: &ArgMatches, config: &Config) -> error::Result { - let update_rate = if let Some(update_rate) = matches.get_one::("rate") { - try_parse_ms(update_rate)? - } else if let Some(flags) = &config.flags { - if let Some(rate) = &flags.rate { - match rate { - StringOrNum::String(s) => try_parse_ms(s)?, - StringOrNum::Num(n) => *n, - } - } else { - DEFAULT_REFRESH_RATE_IN_MILLISECONDS - } - } else { - DEFAULT_REFRESH_RATE_IN_MILLISECONDS - }; - - if update_rate < 250 { - return Err(BottomError::ConfigError( - "set your update rate to be at least 250 ms.".to_string(), - )); - } - - Ok(update_rate) -} - -fn get_temperature(matches: &ArgMatches, config: &Config) -> error::Result { - if matches.get_flag("fahrenheit") { - return Ok(TemperatureType::Fahrenheit); - } else if matches.get_flag("kelvin") { - return Ok(TemperatureType::Kelvin); - } else if matches.get_flag("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 \"\"." - ))), - }; - } - } - 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") { - return false; - } else if let Some(flags) = &config.flags { - if let Some(avg_cpu) = flags.hide_avg_cpu { - return !avg_cpu; - } - } - - true -} - -fn try_parse_ms(s: &str) -> error::Result { - if let Ok(val) = humantime::parse_duration(s) { - Ok(val.as_millis().try_into()?) - } else if let Ok(val) = s.parse::() { - Ok(val) - } else { - Err(BottomError::ConfigError( - "could not parse as a valid 64-bit unsigned integer or a human time".to_string(), - )) - } -} - -fn get_default_time_value( - matches: &ArgMatches, config: &Config, retention_ms: u64, -) -> error::Result { - let default_time = - if let Some(default_time_value) = matches.get_one::("default_time_value") { - try_parse_ms(default_time_value)? - } else if let Some(flags) = &config.flags { - if let Some(default_time_value) = &flags.default_time_value { - match default_time_value { - StringOrNum::String(s) => try_parse_ms(s)?, - StringOrNum::Num(n) => *n, - } - } else { - DEFAULT_TIME_MILLISECONDS - } - } else { - DEFAULT_TIME_MILLISECONDS - }; - - if default_time < 30000 { - return Err(BottomError::ConfigError( - "set your default value 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 {}.", - humantime::Duration::from(Duration::from_millis(retention_ms)) - ))); - } - - Ok(default_time) -} - -fn get_time_interval( - matches: &ArgMatches, config: &Config, retention_ms: u64, -) -> error::Result { - let time_interval = if let Some(time_interval) = matches.get_one::("time_delta") { - try_parse_ms(time_interval)? - } 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::Num(n) => *n, - } - } else { - TIME_CHANGE_MILLISECONDS - } - } else { - TIME_CHANGE_MILLISECONDS - }; - - if time_interval < 1000 { - return Err(BottomError::ConfigError( - "set your time delta to be at least 1s.".to_string(), - )); - } else if time_interval > retention_ms { - return Err(BottomError::ConfigError(format!( - "set your time delta to be at most {}.", - humantime::Duration::from(Duration::from_millis(retention_ms)) - ))); - } - - Ok(time_interval) -} - -fn get_default_widget_and_count( - matches: &ArgMatches, config: &Config, -) -> error::Result<(Option, u64)> { - let widget_type = if let Some(widget_type) = matches.get_one::("default_widget_type") { - let parsed_widget = widget_type.parse::()?; - if let BottomWidgetType::Empty = parsed_widget { - None - } else { - Some(parsed_widget) - } - } else if let Some(flags) = &config.flags { - if let Some(widget_type) = &flags.default_widget_type { - let parsed_widget = widget_type.parse::()?; - if let BottomWidgetType::Empty = parsed_widget { - None - } else { - Some(parsed_widget) - } - } else { - None - } - } else { - None - }; - - let widget_count = if let Some(widget_count) = matches.get_one::("default_widget_count") - { - Some(widget_count.parse::()?) - } else if let Some(flags) = &config.flags { - flags - .default_widget_count - .map(|widget_count| widget_count.into()) - } 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() - ))?; - Ok((Some(widget_type), widget_count)) - } - (Some(widget_type), None) => Ok((Some(widget_type), 1)), - (None, Some(_widget_count)) => Err(BottomError::ConfigError( - "cannot set 'default_widget_count' by itself, it must be used with 'default_widget_type'.".to_string(), - )), - (None, None) => Ok((None, 1)) - } -} - -#[allow(unused_variables)] -fn get_use_battery(matches: &ArgMatches, config: &Config) -> bool { - #[cfg(feature = "battery")] - { - if matches.get_flag("battery") { - return true; - } else if let Some(flags) = &config.flags { - if let Some(battery) = flags.battery { - return battery; - } - } - - if let Ok(battery_manager) = Manager::new() { - if let Ok(batteries) = battery_manager.batteries() { - if batteries.count() == 0 { - return false; - } - } - } - } - - false -} - -#[allow(unused_variables)] -fn get_enable_gpu(matches: &ArgMatches, config: &Config) -> bool { - #[cfg(feature = "gpu")] - { - if matches.get_flag("enable_gpu") { - return true; - } else if let Some(flags) = &config.flags { - if let Some(enable_gpu) = flags.enable_gpu { - return enable_gpu; - } - } - } - - false -} - -#[allow(unused_variables)] -fn get_enable_cache_memory(matches: &ArgMatches, config: &Config) -> bool { - #[cfg(not(target_os = "windows"))] - { - if matches.get_flag("enable_cache_memory") { - return true; - } else if let Some(flags) = &config.flags { - if let Some(enable_cache_memory) = flags.enable_cache_memory { - return enable_cache_memory; - } - } - } - - false -} - -fn get_ignore_list(ignore_list: &Option) -> error::Result> { - if let Some(ignore_list) = ignore_list { - let list: Result, _> = ignore_list - .list - .iter() - .map(|name| { - let escaped_string: String; - let res = format!( - "{}{}{}{}", - if ignore_list.whole_word { "^" } else { "" }, - if ignore_list.case_sensitive { - "" - } else { - "(?i)" - }, - if ignore_list.regex { - name - } else { - escaped_string = regex::escape(name); - &escaped_string - }, - if ignore_list.whole_word { "$" } else { "" }, - ); - - Regex::new(&res) - }) - .collect(); - - Ok(Some(Filter { - list: list?, - is_list_ignored: ignore_list.is_list_ignored, - })) - } else { - Ok(None) - } -} - -pub fn get_color_scheme(matches: &ArgMatches, config: &Config) -> error::Result { - if let Some(color) = matches.get_one::("color") { - // Highest priority is always command line flags... - return ColourScheme::from_str(color); - } else if let Some(colors) = &config.colors { - if !colors.is_empty() { - // Then, give priority to custom colours... - return Ok(ColourScheme::Custom); - } else if let Some(flags) = &config.flags { - // Last priority is config file flags... - if let Some(color) = &flags.color { - return ColourScheme::from_str(color); - } - } - } else if let Some(flags) = &config.flags { - // Last priority is config file flags... - if let Some(color) = &flags.color { - return ColourScheme::from_str(color); - } - } - - // And lastly, the final case is just "default". - Ok(ColourScheme::Default) -} - -fn get_network_unit_type(matches: &ArgMatches, config: &Config) -> DataUnit { - if matches.get_flag("network_use_bytes") { - return DataUnit::Byte; - } else if let Some(flags) = &config.flags { - if let Some(network_use_bytes) = flags.network_use_bytes { - if network_use_bytes { - return DataUnit::Byte; - } - } - } - - DataUnit::Bit -} - -fn get_network_scale_type(matches: &ArgMatches, config: &Config) -> AxisScaling { - if matches.get_flag("network_use_log") { - return AxisScaling::Log; - } else if let Some(flags) = &config.flags { - if let Some(network_use_log) = flags.network_use_log { - if network_use_log { - return AxisScaling::Log; - } - } - } - - AxisScaling::Linear -} - -fn get_retention(matches: &ArgMatches, config: &Config) -> error::Result { - const DEFAULT_RETENTION_MS: u64 = 600 * 1000; // Keep 10 minutes of data. - - if let Some(retention) = matches.get_one::("retention") { - try_parse_ms(retention) - } else if let Some(flags) = &config.flags { - if let Some(retention) = &flags.retention { - Ok(match retention { - StringOrNum::String(s) => try_parse_ms(s)?, - StringOrNum::Num(n) => *n, - }) - } else { - Ok(DEFAULT_RETENTION_MS) - } - } else { - Ok(DEFAULT_RETENTION_MS) - } -} - -#[cfg(test)] -mod test { - use clap::ArgMatches; - - use super::{get_color_scheme, get_time_interval, get_widget_layout, Config}; - use crate::{ - app::App, - canvas::styling::CanvasStyling, - options::{ - get_default_time_value, get_retention, get_update_rate, try_parse_ms, ConfigFlags, - }, - }; - - #[test] - fn verify_try_parse_ms() { - let a = "100s"; - let b = "100"; - let c = "1 min"; - let d = "1 hour 1 min"; - - assert_eq!(try_parse_ms(a), Ok(100 * 1000)); - assert_eq!(try_parse_ms(b), Ok(100)); - assert_eq!(try_parse_ms(c), Ok(60 * 1000)); - assert_eq!(try_parse_ms(d), Ok(3660 * 1000)); - - let a_bad = "1 test"; - let b_bad = "-100"; - - assert!(try_parse_ms(a_bad).is_err()); - assert!(try_parse_ms(b_bad).is_err()); - } - - #[test] - fn matches_human_times() { - let config = Config::default(); - let app = crate::args::build_app(); - - { - let app = app.clone(); - let delta_args = vec!["btm", "--time_delta", "2 min"]; - let matches = app.get_matches_from(delta_args); - - assert_eq!( - get_time_interval(&matches, &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); - - assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), - Ok(5 * 60 * 1000) - ); - } - } - - #[test] - fn matches_number_times() { - let config = Config::default(); - let app = crate::args::build_app(); - - { - let app = app.clone(); - let delta_args = vec!["btm", "--time_delta", "120000"]; - let matches = app.get_matches_from(delta_args); - - assert_eq!( - get_time_interval(&matches, &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); - - assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), - Ok(5 * 60 * 1000) - ); - } - } - - #[test] - fn config_human_times() { - let app = crate::args::build_app(); - let matches = app.get_matches_from(["btm"]); - - let mut config = Config::default(); - let flags = ConfigFlags { - time_delta: Some("2 min".to_string().into()), - default_time_value: Some("300s".to_string().into()), - rate: Some("1s".to_string().into()), - retention: Some("10m".to_string().into()), - ..Default::default() - }; - - config.flags = Some(flags); - - assert_eq!( - get_time_interval(&matches, &config, 60 * 60 * 1000), - Ok(2 * 60 * 1000) - ); - - assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), - Ok(5 * 60 * 1000) - ); - - assert_eq!(get_update_rate(&matches, &config), Ok(1000)); - - assert_eq!(get_retention(&matches, &config), Ok(600000)); - } - - #[test] - fn config_number_times_as_string() { - let app = crate::args::build_app(); - let matches = app.get_matches_from(["btm"]); - - let mut config = Config::default(); - let flags = ConfigFlags { - time_delta: Some("120000".to_string().into()), - default_time_value: Some("300000".to_string().into()), - rate: Some("1000".to_string().into()), - retention: Some("600000".to_string().into()), - ..Default::default() - }; - - config.flags = Some(flags); - - assert_eq!( - get_time_interval(&matches, &config, 60 * 60 * 1000), - Ok(2 * 60 * 1000) - ); - - assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), - Ok(5 * 60 * 1000) - ); - - assert_eq!(get_update_rate(&matches, &config), Ok(1000)); - - assert_eq!(get_retention(&matches, &config), Ok(600000)); - } - - #[test] - fn config_number_times_as_num() { - let app = crate::args::build_app(); - let matches = app.get_matches_from(["btm"]); - - let mut config = Config::default(); - let flags = ConfigFlags { - time_delta: Some(120000.into()), - default_time_value: Some(300000.into()), - rate: Some(1000.into()), - retention: Some(600000.into()), - ..Default::default() - }; - - config.flags = Some(flags); - - assert_eq!( - get_time_interval(&matches, &config, 60 * 60 * 1000), - Ok(2 * 60 * 1000) - ); - - assert_eq!( - get_default_time_value(&matches, &config, 60 * 60 * 1000), - Ok(5 * 60 * 1000) - ); - - assert_eq!(get_update_rate(&matches, &config), Ok(1000)); - - assert_eq!(get_retention(&matches, &config), Ok(600000)); - } - - fn create_app(config: Config, matches: ArgMatches) -> App { - let (layout, id, ty) = get_widget_layout(&matches, &config).unwrap(); - let styling = - CanvasStyling::new(get_color_scheme(&matches, &config).unwrap(), &config).unwrap(); - - super::build_app(matches, config, &layout, id, &ty, &styling).unwrap() - } - - // 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 default_app = { - let app = app.clone(); - let config = Config::default(); - let matches = app.get_matches_from([""]); - - create_app(config, matches) - }; - - // Skip battery since it's tricky to test depending on the platform/features we're testing with. - let skip = ["help", "version", "celsius", "battery"]; - - for arg in app.get_arguments().collect::>() { - let arg_name = arg - .get_long_and_visible_aliases() - .unwrap() - .first() - .unwrap() - .to_owned(); - - if !arg.get_action().takes_values() && !skip.contains(&arg_name) { - 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); - - if (default_app.app_config_fields == testing_app.app_config_fields) - && default_app.is_expanded == testing_app.is_expanded - && default_app - .states - .proc_state - .widget_states - .iter() - .zip(testing_app.states.proc_state.widget_states.iter()) - .all(|(a, b)| (a.1.test_equality(b.1))) - { - panic!("failed on {arg_name}"); - } - } - } - } -} +pub mod args; +pub mod config; diff --git a/src/args.rs b/src/options/args.rs similarity index 100% rename from src/args.rs rename to src/options/args.rs diff --git a/src/options/config.rs b/src/options/config.rs new file mode 100644 index 00000000..17c2241f --- /dev/null +++ b/src/options/config.rs @@ -0,0 +1,1147 @@ +mod cpu; +pub mod layout; +mod process_columns; + +use std::{ + borrow::Cow, + convert::TryInto, + str::FromStr, + time::{Duration, Instant}, +}; + +use anyhow::{Context, Result}; +use clap::ArgMatches; +pub use cpu::{CpuConfig, CpuDefault}; +use hashbrown::{HashMap, HashSet}; +use indexmap::IndexSet; +use layout::*; +pub use process_columns::ProcessConfig; +use regex::Regex; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "battery")] +use starship_battery::Manager; + +use crate::{ + app::{filter::Filter, layout_manager::*, *}, + canvas::{styling::CanvasStyling, ColourScheme}, + constants::*, + data_collection::temperature::TemperatureType, + utils::{ + data_units::DataUnit, + error::{self, BottomError}, + }, + widgets::*, +}; + +#[derive(Clone, Debug, Default, Deserialize)] +pub struct Config { + pub flags: Option, + pub colors: Option, + pub row: Option>, + pub disk_filter: Option, + pub mount_filter: Option, + pub temp_filter: Option, + pub net_filter: Option, + pub processes: Option, + pub cpu: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +enum StringOrNum { + String(String), + Num(u64), +} + +impl From for StringOrNum { + fn from(value: String) -> Self { + StringOrNum::String(value) + } +} + +impl From for StringOrNum { + fn from(value: u64) -> Self { + StringOrNum::Num(value) + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct ConfigFlags { + hide_avg_cpu: Option, + dot_marker: Option, + temperature_type: Option, + rate: Option, + left_legend: Option, + current_usage: Option, + unnormalized_cpu: Option, + group_processes: Option, + case_sensitive: Option, + whole_word: Option, + regex: Option, + basic: Option, + default_time_value: Option, + time_delta: Option, + autohide_time: Option, + hide_time: Option, + default_widget_type: Option, + default_widget_count: Option, + expanded_on_startup: Option, + use_old_network_legend: Option, + hide_table_gap: Option, + battery: Option, + disable_click: Option, + no_write: Option, + /// For built-in colour palettes. + color: Option, + mem_as_value: Option, + tree: Option, + show_table_scroll_position: Option, + process_command: Option, + disable_advanced_kill: Option, + network_use_bytes: Option, + network_use_log: Option, + network_use_binary_prefix: Option, + enable_gpu: Option, + enable_cache_memory: Option, + retention: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct ConfigColours { + pub table_header_color: Option>, + pub all_cpu_color: Option>, + pub avg_cpu_color: Option>, + pub cpu_core_colors: Option>>, + pub ram_color: Option>, + #[cfg(not(target_os = "windows"))] + pub cache_color: Option>, + pub swap_color: Option>, + pub arc_color: Option>, + pub gpu_core_colors: Option>>, + pub rx_color: Option>, + pub tx_color: Option>, + pub rx_total_color: Option>, // These only affect basic mode. + pub tx_total_color: Option>, // These only affect basic mode. + pub border_color: Option>, + pub highlighted_border_color: Option>, + pub disabled_text_color: Option>, + pub text_color: Option>, + pub selected_text_color: Option>, + pub selected_bg_color: Option>, + pub widget_title_color: Option>, + pub graph_color: Option>, + pub high_battery_color: Option>, + pub medium_battery_color: Option>, + pub low_battery_color: Option>, +} + +impl ConfigColours { + /// Returns `true` if there is a [`ConfigColours`] that is empty or there isn't one at all. + pub fn is_empty(&self) -> bool { + if let Ok(serialized_string) = toml_edit::ser::to_string(self) { + return serialized_string.is_empty(); + } + + true + } +} + +/// Workaround as per https://github.com/serde-rs/serde/issues/1030 +fn default_as_true() -> bool { + true +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct IgnoreList { + #[serde(default = "default_as_true")] + // TODO: Deprecate and/or rename, current name sounds awful. + // Maybe to something like "deny_entries"? Currently it defaults to a denylist anyways, so maybe "allow_entries"? + pub is_list_ignored: bool, + pub list: Vec, + #[serde(default = "bool::default")] + pub regex: bool, + #[serde(default = "bool::default")] + pub case_sensitive: bool, + #[serde(default = "bool::default")] + pub whole_word: bool, +} + +macro_rules! is_flag_enabled { + ($flag_name:ident, $matches:expr, $config:expr) => { + if $matches.get_flag(stringify!($flag_name)) { + true + } else if let Some(flags) = &$config.flags { + flags.$flag_name.unwrap_or(false) + } else { + false + } + }; + + ($cmd_flag:literal, $cfg_flag:ident, $matches:expr, $config:expr) => { + if $matches.get_flag($cmd_flag) { + true + } else if let Some(flags) = &$config.flags { + flags.$cfg_flag.unwrap_or(false) + } else { + false + } + }; +} + +pub fn init_app( + matches: ArgMatches, config: Config, widget_layout: &BottomLayout, default_widget_id: u64, + default_widget_type_option: &Option, styling: &CanvasStyling, +) -> Result { + use BottomWidgetType::*; + + // Since everything takes a reference, but we want to take ownership here to drop matches/config later... + let matches = &matches; + 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 use_basic_mode = is_flag_enabled!(basic, matches, config); + let expanded_upon_startup = is_flag_enabled!(expanded_on_startup, matches, 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 mut widget_map = HashMap::new(); + let mut cpu_state_map: HashMap = HashMap::new(); + let mut mem_state_map: HashMap = HashMap::new(); + let mut net_state_map: HashMap = HashMap::new(); + let mut proc_state_map: HashMap = HashMap::new(); + let mut temp_state_map: HashMap = HashMap::new(); + let mut disk_state_map: HashMap = HashMap::new(); + let mut battery_state_map: HashMap = HashMap::new(); + + let autohide_timer = if autohide_time { + Some(Instant::now()) + } else { + None + }; + + let mut initial_widget_id: u64 = default_widget_id; + let mut initial_widget_type = Proc; + 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 proc_columns: Option> = { + let columns = config + .processes + .as_ref() + .and_then(|cfg| cfg.columns.clone()); + + match columns { + Some(columns) => { + if columns.is_empty() { + None + } else { + Some(IndexSet::from_iter(columns)) + } + } + None => None, + } + }; + + 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) + .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), + left_legend: is_flag_enabled!(left_legend, matches, config), + use_current_cpu_total: is_flag_enabled!(current_usage, matches, config), + unnormalized_cpu: is_flag_enabled!(unnormalized_cpu, matches, 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), + 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), + is_advanced_kill, + network_scale_type, + network_unit_type, + network_use_binary_prefix, + retention_ms, + }; + + let table_config = ProcTableConfig { + is_case_sensitive, + is_match_whole_word, + is_use_regex, + show_memory_as_values, + is_command: is_default_command, + }; + + for row in &widget_layout.rows { + for col in &row.children { + for col_row in &col.children { + for widget in &col_row.children { + widget_map.insert(widget.widget_id, widget.clone()); + if let Some(default_widget_type) = &default_widget_type_option { + if !is_custom_layout || use_basic_mode { + match widget.widget_type { + BasicCpu => { + if let Cpu = *default_widget_type { + initial_widget_id = widget.widget_id; + initial_widget_type = Cpu; + } + } + BasicMem => { + if let Mem = *default_widget_type { + initial_widget_id = widget.widget_id; + initial_widget_type = Cpu; + } + } + BasicNet => { + if let Net = *default_widget_type { + initial_widget_id = widget.widget_id; + initial_widget_type = Cpu; + } + } + _ => { + if *default_widget_type == widget.widget_type { + initial_widget_id = widget.widget_id; + initial_widget_type = widget.widget_type.clone(); + } + } + } + } + } + + used_widget_set.insert(widget.widget_type.clone()); + + match widget.widget_type { + Cpu => { + cpu_state_map.insert( + widget.widget_id, + CpuWidgetState::new( + &app_config_fields, + config + .cpu + .as_ref() + .map(|cfg| cfg.default) + .unwrap_or_default(), + default_time_value, + autohide_timer, + styling, + ), + ); + } + Mem => { + mem_state_map.insert( + widget.widget_id, + MemWidgetState::init(default_time_value, autohide_timer), + ); + } + Net => { + net_state_map.insert( + widget.widget_id, + NetWidgetState::init(default_time_value, autohide_timer), + ); + } + Proc => { + let mode = if is_grouped { + ProcWidgetMode::Grouped + } else if is_default_tree { + ProcWidgetMode::Tree { + collapsed_pids: Default::default(), + } + } else { + ProcWidgetMode::Normal + }; + + proc_state_map.insert( + widget.widget_id, + ProcWidgetState::new( + &app_config_fields, + mode, + table_config, + styling, + &proc_columns, + ), + ); + } + Disk => { + disk_state_map.insert( + widget.widget_id, + DiskTableWidget::new(&app_config_fields, styling), + ); + } + Temp => { + temp_state_map.insert( + widget.widget_id, + TempWidgetState::new(&app_config_fields, styling), + ); + } + Battery => { + battery_state_map + .insert(widget.widget_id, BatteryWidgetState::default()); + } + _ => {} + } + } + } + } + } + + let basic_table_widget_state = if use_basic_mode { + Some(match initial_widget_type { + Proc | Disk | Temp => BasicTableWidgetState { + currently_displayed_widget_type: initial_widget_type, + currently_displayed_widget_id: initial_widget_id, + widget_id: 100, + left_tlc: None, + left_brc: None, + right_tlc: None, + right_brc: None, + }, + _ => BasicTableWidgetState { + currently_displayed_widget_type: Proc, + currently_displayed_widget_id: DEFAULT_WIDGET_ID, + widget_id: 100, + left_tlc: None, + left_brc: None, + right_tlc: None, + right_brc: None, + }, + }) + } else { + None + }; + + let use_mem = used_widget_set.get(&Mem).is_some() || used_widget_set.get(&BasicMem).is_some(); + 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_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(), + use_temp: used_widget_set.get(&Temp).is_some(), + use_battery: used_widget_set.get(&Battery).is_some(), + }; + + let disk_filter = + get_ignore_list(&config.disk_filter).context("Update 'disk_filter' in your config file")?; + let mount_filter = get_ignore_list(&config.mount_filter) + .context("Update 'mount_filter' in your config file")?; + let temp_filter = + get_ignore_list(&config.temp_filter).context("Update 'temp_filter' in your config file")?; + let net_filter = + get_ignore_list(&config.net_filter).context("Update 'net_filter' in your config file")?; + + let states = AppWidgetStates { + cpu_state: CpuState::init(cpu_state_map), + mem_state: MemState::init(mem_state_map), + net_state: NetState::init(net_state_map), + proc_state: ProcState::init(proc_state_map), + temp_state: TempState::init(temp_state_map), + disk_state: DiskState::init(disk_state_map), + battery_state: BatteryState::init(battery_state_map), + basic_table_widget_state, + }; + + let current_widget = widget_map.get(&initial_widget_id).unwrap().clone(); + let filters = DataFilters { + disk_filter, + mount_filter, + temp_filter, + net_filter, + }; + let is_expanded = expanded_upon_startup && !use_basic_mode; + + Ok(App::new( + app_config_fields, + states, + widget_map, + current_widget, + used_widgets, + filters, + is_expanded, + )) +} + +pub fn get_widget_layout( + matches: &ArgMatches, config: &Config, +) -> error::Result<(BottomLayout, u64, Option)> { + let left_legend = is_flag_enabled!(left_legend, matches, config); + + let (default_widget_type, mut default_widget_count) = + get_default_widget_and_count(matches, config)?; + let mut default_widget_id = 1; + + let bottom_layout = if is_flag_enabled!(basic, matches, config) { + default_widget_id = DEFAULT_WIDGET_ID; + + BottomLayout::init_basic_default(get_use_battery(matches, config)) + } else { + let ref_row: Vec; // Required to handle reference + let rows = match &config.row { + Some(r) => r, + None => { + // This cannot (like it really shouldn't) fail! + ref_row = toml_edit::de::from_str::(if get_use_battery(matches, config) { + DEFAULT_BATTERY_LAYOUT + } else { + DEFAULT_LAYOUT + })? + .row + .unwrap(); + &ref_row + } + }; + + let mut iter_id = 0; // A lazy way of forcing unique IDs *shrugs* + let mut total_height_ratio = 0; + + let mut ret_bottom_layout = BottomLayout { + rows: rows + .iter() + .map(|row| { + row.convert_row_to_bottom_row( + &mut iter_id, + &mut total_height_ratio, + &mut default_widget_id, + &default_widget_type, + &mut default_widget_count, + left_legend, + ) + }) + .collect::>>()?, + total_row_height_ratio: total_height_ratio, + }; + + // Confirm that we have at least ONE widget left - if not, error out! + if iter_id > 0 { + ret_bottom_layout.get_movement_mappings(); + // debug!("Bottom layout: {ret_bottom_layout:#?}"); + + ret_bottom_layout + } else { + return Err(BottomError::ConfigError( + "please have at least one widget under the '[[row]]' section.".to_string(), + )); + } + }; + + Ok((bottom_layout, default_widget_id, default_widget_type)) +} + +fn get_update_rate(matches: &ArgMatches, config: &Config) -> error::Result { + let update_rate = if let Some(update_rate) = matches.get_one::("rate") { + try_parse_ms(update_rate)? + } else if let Some(flags) = &config.flags { + if let Some(rate) = &flags.rate { + match rate { + StringOrNum::String(s) => try_parse_ms(s)?, + StringOrNum::Num(n) => *n, + } + } else { + DEFAULT_REFRESH_RATE_IN_MILLISECONDS + } + } else { + DEFAULT_REFRESH_RATE_IN_MILLISECONDS + }; + + if update_rate < 250 { + return Err(BottomError::ConfigError( + "set your update rate to be at least 250 ms.".to_string(), + )); + } + + Ok(update_rate) +} + +fn get_temperature(matches: &ArgMatches, config: &Config) -> error::Result { + if matches.get_flag("fahrenheit") { + return Ok(TemperatureType::Fahrenheit); + } else if matches.get_flag("kelvin") { + return Ok(TemperatureType::Kelvin); + } else if matches.get_flag("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 \"\"." + ))), + }; + } + } + 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") { + return false; + } else if let Some(flags) = &config.flags { + if let Some(avg_cpu) = flags.hide_avg_cpu { + return !avg_cpu; + } + } + + true +} + +fn try_parse_ms(s: &str) -> error::Result { + if let Ok(val) = humantime::parse_duration(s) { + Ok(val.as_millis().try_into()?) + } else if let Ok(val) = s.parse::() { + Ok(val) + } else { + Err(BottomError::ConfigError( + "could not parse as a valid 64-bit unsigned integer or a human time".to_string(), + )) + } +} + +fn get_default_time_value( + matches: &ArgMatches, config: &Config, retention_ms: u64, +) -> error::Result { + let default_time = + if let Some(default_time_value) = matches.get_one::("default_time_value") { + try_parse_ms(default_time_value)? + } else if let Some(flags) = &config.flags { + if let Some(default_time_value) = &flags.default_time_value { + match default_time_value { + StringOrNum::String(s) => try_parse_ms(s)?, + StringOrNum::Num(n) => *n, + } + } else { + DEFAULT_TIME_MILLISECONDS + } + } else { + DEFAULT_TIME_MILLISECONDS + }; + + if default_time < 30000 { + return Err(BottomError::ConfigError( + "set your default value 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 {}.", + humantime::Duration::from(Duration::from_millis(retention_ms)) + ))); + } + + Ok(default_time) +} + +fn get_time_interval( + matches: &ArgMatches, config: &Config, retention_ms: u64, +) -> error::Result { + let time_interval = if let Some(time_interval) = matches.get_one::("time_delta") { + try_parse_ms(time_interval)? + } 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::Num(n) => *n, + } + } else { + TIME_CHANGE_MILLISECONDS + } + } else { + TIME_CHANGE_MILLISECONDS + }; + + if time_interval < 1000 { + return Err(BottomError::ConfigError( + "set your time delta to be at least 1s.".to_string(), + )); + } else if time_interval > retention_ms { + return Err(BottomError::ConfigError(format!( + "set your time delta to be at most {}.", + humantime::Duration::from(Duration::from_millis(retention_ms)) + ))); + } + + Ok(time_interval) +} + +fn get_default_widget_and_count( + matches: &ArgMatches, config: &Config, +) -> error::Result<(Option, u64)> { + let widget_type = if let Some(widget_type) = matches.get_one::("default_widget_type") { + let parsed_widget = widget_type.parse::()?; + if let BottomWidgetType::Empty = parsed_widget { + None + } else { + Some(parsed_widget) + } + } else if let Some(flags) = &config.flags { + if let Some(widget_type) = &flags.default_widget_type { + let parsed_widget = widget_type.parse::()?; + if let BottomWidgetType::Empty = parsed_widget { + None + } else { + Some(parsed_widget) + } + } else { + None + } + } else { + None + }; + + let widget_count = if let Some(widget_count) = matches.get_one::("default_widget_count") + { + Some(widget_count.parse::()?) + } else if let Some(flags) = &config.flags { + flags + .default_widget_count + .map(|widget_count| widget_count.into()) + } 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() + ))?; + Ok((Some(widget_type), widget_count)) + } + (Some(widget_type), None) => Ok((Some(widget_type), 1)), + (None, Some(_widget_count)) => Err(BottomError::ConfigError( + "cannot set 'default_widget_count' by itself, it must be used with 'default_widget_type'.".to_string(), + )), + (None, None) => Ok((None, 1)) + } +} + +#[allow(unused_variables)] +fn get_use_battery(matches: &ArgMatches, config: &Config) -> bool { + #[cfg(feature = "battery")] + { + if matches.get_flag("battery") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(battery) = flags.battery { + return battery; + } + } + + if let Ok(battery_manager) = Manager::new() { + if let Ok(batteries) = battery_manager.batteries() { + if batteries.count() == 0 { + return false; + } + } + } + } + + false +} + +#[allow(unused_variables)] +fn get_enable_gpu(matches: &ArgMatches, config: &Config) -> bool { + #[cfg(feature = "gpu")] + { + if matches.get_flag("enable_gpu") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(enable_gpu) = flags.enable_gpu { + return enable_gpu; + } + } + } + + false +} + +#[allow(unused_variables)] +fn get_enable_cache_memory(matches: &ArgMatches, config: &Config) -> bool { + #[cfg(not(target_os = "windows"))] + { + if matches.get_flag("enable_cache_memory") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(enable_cache_memory) = flags.enable_cache_memory { + return enable_cache_memory; + } + } + } + + false +} + +fn get_ignore_list(ignore_list: &Option) -> error::Result> { + if let Some(ignore_list) = ignore_list { + let list: Result, _> = ignore_list + .list + .iter() + .map(|name| { + let escaped_string: String; + let res = format!( + "{}{}{}{}", + if ignore_list.whole_word { "^" } else { "" }, + if ignore_list.case_sensitive { + "" + } else { + "(?i)" + }, + if ignore_list.regex { + name + } else { + escaped_string = regex::escape(name); + &escaped_string + }, + if ignore_list.whole_word { "$" } else { "" }, + ); + + Regex::new(&res) + }) + .collect(); + + Ok(Some(Filter { + list: list?, + is_list_ignored: ignore_list.is_list_ignored, + })) + } else { + Ok(None) + } +} + +pub fn get_color_scheme(matches: &ArgMatches, config: &Config) -> error::Result { + if let Some(color) = matches.get_one::("color") { + // Highest priority is always command line flags... + return ColourScheme::from_str(color); + } else if let Some(colors) = &config.colors { + if !colors.is_empty() { + // Then, give priority to custom colours... + return Ok(ColourScheme::Custom); + } else if let Some(flags) = &config.flags { + // Last priority is config file flags... + if let Some(color) = &flags.color { + return ColourScheme::from_str(color); + } + } + } else if let Some(flags) = &config.flags { + // Last priority is config file flags... + if let Some(color) = &flags.color { + return ColourScheme::from_str(color); + } + } + + // And lastly, the final case is just "default". + Ok(ColourScheme::Default) +} + +fn get_network_unit_type(matches: &ArgMatches, config: &Config) -> DataUnit { + if matches.get_flag("network_use_bytes") { + return DataUnit::Byte; + } else if let Some(flags) = &config.flags { + if let Some(network_use_bytes) = flags.network_use_bytes { + if network_use_bytes { + return DataUnit::Byte; + } + } + } + + DataUnit::Bit +} + +fn get_network_scale_type(matches: &ArgMatches, config: &Config) -> AxisScaling { + if matches.get_flag("network_use_log") { + return AxisScaling::Log; + } else if let Some(flags) = &config.flags { + if let Some(network_use_log) = flags.network_use_log { + if network_use_log { + return AxisScaling::Log; + } + } + } + + AxisScaling::Linear +} + +fn get_retention(matches: &ArgMatches, config: &Config) -> error::Result { + const DEFAULT_RETENTION_MS: u64 = 600 * 1000; // Keep 10 minutes of data. + + if let Some(retention) = matches.get_one::("retention") { + try_parse_ms(retention) + } else if let Some(flags) = &config.flags { + if let Some(retention) = &flags.retention { + Ok(match retention { + StringOrNum::String(s) => try_parse_ms(s)?, + StringOrNum::Num(n) => *n, + }) + } else { + Ok(DEFAULT_RETENTION_MS) + } + } else { + Ok(DEFAULT_RETENTION_MS) + } +} + +#[cfg(test)] +mod test { + use clap::ArgMatches; + + use super::{get_color_scheme, get_time_interval, get_widget_layout, Config}; + use crate::{ + app::App, + canvas::styling::CanvasStyling, + options::config::{ + get_default_time_value, get_retention, get_update_rate, try_parse_ms, ConfigFlags, + }, + }; + + #[test] + fn verify_try_parse_ms() { + let a = "100s"; + let b = "100"; + let c = "1 min"; + let d = "1 hour 1 min"; + + assert_eq!(try_parse_ms(a), Ok(100 * 1000)); + assert_eq!(try_parse_ms(b), Ok(100)); + assert_eq!(try_parse_ms(c), Ok(60 * 1000)); + assert_eq!(try_parse_ms(d), Ok(3660 * 1000)); + + let a_bad = "1 test"; + let b_bad = "-100"; + + assert!(try_parse_ms(a_bad).is_err()); + assert!(try_parse_ms(b_bad).is_err()); + } + + #[test] + fn matches_human_times() { + let config = Config::default(); + let app = crate::args::build_app(); + + { + let app = app.clone(); + let delta_args = vec!["btm", "--time_delta", "2 min"]; + let matches = app.get_matches_from(delta_args); + + assert_eq!( + get_time_interval(&matches, &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); + + assert_eq!( + get_default_time_value(&matches, &config, 60 * 60 * 1000), + Ok(5 * 60 * 1000) + ); + } + } + + #[test] + fn matches_number_times() { + let config = Config::default(); + let app = crate::args::build_app(); + + { + let app = app.clone(); + let delta_args = vec!["btm", "--time_delta", "120000"]; + let matches = app.get_matches_from(delta_args); + + assert_eq!( + get_time_interval(&matches, &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); + + assert_eq!( + get_default_time_value(&matches, &config, 60 * 60 * 1000), + Ok(5 * 60 * 1000) + ); + } + } + + #[test] + fn config_human_times() { + let app = crate::args::build_app(); + let matches = app.get_matches_from(["btm"]); + + let mut config = Config::default(); + let flags = ConfigFlags { + time_delta: Some("2 min".to_string().into()), + default_time_value: Some("300s".to_string().into()), + rate: Some("1s".to_string().into()), + retention: Some("10m".to_string().into()), + ..Default::default() + }; + + config.flags = Some(flags); + + assert_eq!( + get_time_interval(&matches, &config, 60 * 60 * 1000), + Ok(2 * 60 * 1000) + ); + + assert_eq!( + get_default_time_value(&matches, &config, 60 * 60 * 1000), + Ok(5 * 60 * 1000) + ); + + assert_eq!(get_update_rate(&matches, &config), Ok(1000)); + + assert_eq!(get_retention(&matches, &config), Ok(600000)); + } + + #[test] + fn config_number_times_as_string() { + let app = crate::args::build_app(); + let matches = app.get_matches_from(["btm"]); + + let mut config = Config::default(); + let flags = ConfigFlags { + time_delta: Some("120000".to_string().into()), + default_time_value: Some("300000".to_string().into()), + rate: Some("1000".to_string().into()), + retention: Some("600000".to_string().into()), + ..Default::default() + }; + + config.flags = Some(flags); + + assert_eq!( + get_time_interval(&matches, &config, 60 * 60 * 1000), + Ok(2 * 60 * 1000) + ); + + assert_eq!( + get_default_time_value(&matches, &config, 60 * 60 * 1000), + Ok(5 * 60 * 1000) + ); + + assert_eq!(get_update_rate(&matches, &config), Ok(1000)); + + assert_eq!(get_retention(&matches, &config), Ok(600000)); + } + + #[test] + fn config_number_times_as_num() { + let app = crate::args::build_app(); + let matches = app.get_matches_from(["btm"]); + + let mut config = Config::default(); + let flags = ConfigFlags { + time_delta: Some(120000.into()), + default_time_value: Some(300000.into()), + rate: Some(1000.into()), + retention: Some(600000.into()), + ..Default::default() + }; + + config.flags = Some(flags); + + assert_eq!( + get_time_interval(&matches, &config, 60 * 60 * 1000), + Ok(2 * 60 * 1000) + ); + + assert_eq!( + get_default_time_value(&matches, &config, 60 * 60 * 1000), + Ok(5 * 60 * 1000) + ); + + assert_eq!(get_update_rate(&matches, &config), Ok(1000)); + + assert_eq!(get_retention(&matches, &config), Ok(600000)); + } + + fn create_app(config: Config, matches: ArgMatches) -> App { + let (layout, id, ty) = get_widget_layout(&matches, &config).unwrap(); + let styling = + CanvasStyling::new(get_color_scheme(&matches, &config).unwrap(), &config).unwrap(); + + super::init_app(matches, config, &layout, id, &ty, &styling).unwrap() + } + + // 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 default_app = { + let app = app.clone(); + let config = Config::default(); + let matches = app.get_matches_from([""]); + + create_app(config, matches) + }; + + // Skip battery since it's tricky to test depending on the platform/features we're testing with. + let skip = ["help", "version", "celsius", "battery"]; + + for arg in app.get_arguments().collect::>() { + let arg_name = arg + .get_long_and_visible_aliases() + .unwrap() + .first() + .unwrap() + .to_owned(); + + if !arg.get_action().takes_values() && !skip.contains(&arg_name) { + 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); + + if (default_app.app_config_fields == testing_app.app_config_fields) + && default_app.is_expanded == testing_app.is_expanded + && default_app + .states + .proc_state + .widget_states + .iter() + .zip(testing_app.states.proc_state.widget_states.iter()) + .all(|(a, b)| (a.1.test_equality(b.1))) + { + panic!("failed on {arg_name}"); + } + } + } + } +} diff --git a/src/options/cpu.rs b/src/options/config/cpu.rs similarity index 100% rename from src/options/cpu.rs rename to src/options/config/cpu.rs diff --git a/src/options/layout_options.rs b/src/options/config/layout.rs similarity index 100% rename from src/options/layout_options.rs rename to src/options/config/layout.rs diff --git a/src/options/process_columns.rs b/src/options/config/process_columns.rs similarity index 100% rename from src/options/process_columns.rs rename to src/options/config/process_columns.rs diff --git a/src/utils/gen_util.rs b/src/utils/general.rs similarity index 100% rename from src/utils/gen_util.rs rename to src/utils/general.rs diff --git a/src/widgets.rs b/src/widgets.rs index 8710d29e..167a954e 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,20 +1,15 @@ -pub mod process_table; -pub use process_table::*; - -pub mod temperature_table; -pub use temperature_table::*; - -pub mod disk_table; -pub use disk_table::*; - -pub mod cpu_graph; -pub use cpu_graph::*; - -pub mod net_graph; -pub use net_graph::*; - -pub mod mem_graph; -pub use mem_graph::*; - pub mod battery_widget; +pub mod cpu_graph; +pub mod disk_table; +pub mod mem_graph; +pub mod net_graph; +pub mod process_table; +pub mod temperature_table; + pub use battery_widget::*; +pub use cpu_graph::*; +pub use disk_table::*; +pub use mem_graph::*; +pub use net_graph::*; +pub use process_table::*; +pub use temperature_table::*; diff --git a/src/widgets/cpu_graph.rs b/src/widgets/cpu_graph.rs index d3a304f0..ff52b617 100644 --- a/src/widgets/cpu_graph.rs +++ b/src/widgets/cpu_graph.rs @@ -15,8 +15,8 @@ use crate::{ }, data_collection::cpu::CpuDataType, data_conversion::CpuWidgetData, - options::CpuDefault, - utils::gen_util::truncate_to_text, + options::config::CpuDefault, + utils::general::truncate_to_text, }; #[derive(Default)] diff --git a/src/widgets/disk_table.rs b/src/widgets/disk_table.rs index 467abd6d..7ac2486e 100644 --- a/src/widgets/disk_table.rs +++ b/src/widgets/disk_table.rs @@ -12,7 +12,7 @@ use crate::{ SortColumn, SortDataTable, SortDataTableProps, SortOrder, SortsRow, }, }, - utils::gen_util::{get_decimal_bytes, sort_partial_fn, truncate_to_text}, + utils::general::{get_decimal_bytes, sort_partial_fn, truncate_to_text}, }; #[derive(Clone, Debug)] diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 2818ad79..172597ca 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -1,9 +1,16 @@ +pub mod proc_widget_column; +pub mod proc_widget_data; +mod sort_table; + use std::{borrow::Cow, collections::BTreeMap}; use hashbrown::{HashMap, HashSet}; use indexmap::IndexSet; use itertools::Itertools; +pub use proc_widget_column::*; +pub use proc_widget_data::*; use serde::{de::Error, Deserialize}; +use sort_table::SortTableColumn; use crate::{ app::{ @@ -22,15 +29,6 @@ use crate::{ Pid, }; -pub mod proc_widget_column; -pub use proc_widget_column::*; - -pub mod proc_widget_data; -pub use proc_widget_data::*; - -mod sort_table; -use sort_table::SortTableColumn; - /// ProcessSearchState only deals with process' search's current settings and state. pub struct ProcessSearchState { pub search_state: AppSearchState, diff --git a/src/widgets/process_table/proc_widget_column.rs b/src/widgets/process_table/proc_widget_column.rs index f9ebf18b..9a0a0b94 100644 --- a/src/widgets/process_table/proc_widget_column.rs +++ b/src/widgets/process_table/proc_widget_column.rs @@ -5,7 +5,7 @@ use serde::{de::Error, Deserialize, Serialize}; use super::ProcWidgetData; use crate::{ canvas::tui_widgets::data_table::{ColumnHeader, SortsRow}, - utils::gen_util::sort_partial_fn, + utils::general::sort_partial_fn, }; #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] diff --git a/src/widgets/process_table/proc_widget_data.rs b/src/widgets/process_table/proc_widget_data.rs index 0286a08b..8f71434f 100644 --- a/src/widgets/process_table/proc_widget_data.rs +++ b/src/widgets/process_table/proc_widget_data.rs @@ -15,7 +15,7 @@ use crate::{ }, data_collection::processes::ProcessHarvest, data_conversion::{binary_byte_string, dec_bytes_per_second_string, dec_bytes_string}, - utils::gen_util::truncate_to_text, + utils::general::truncate_to_text, Pid, }; diff --git a/src/widgets/process_table/sort_table.rs b/src/widgets/process_table/sort_table.rs index 53a72b90..da6a08d3 100644 --- a/src/widgets/process_table/sort_table.rs +++ b/src/widgets/process_table/sort_table.rs @@ -4,7 +4,7 @@ use tui::text::Text; use crate::{ canvas::tui_widgets::data_table::{ColumnHeader, DataTableColumn, DataToCell}, - utils::gen_util::truncate_to_text, + utils::general::truncate_to_text, }; pub struct SortTableColumn; diff --git a/src/widgets/temperature_table.rs b/src/widgets/temperature_table.rs index 1446185d..3a8a9db4 100644 --- a/src/widgets/temperature_table.rs +++ b/src/widgets/temperature_table.rs @@ -14,7 +14,7 @@ use crate::{ }, }, data_collection::temperature::TemperatureType, - utils::gen_util::{sort_partial_fn, truncate_to_text}, + utils::general::{sort_partial_fn, truncate_to_text}, }; #[derive(Clone, Debug)] diff --git a/tests/arg_tests.rs b/tests/arg_tests.rs index 3bb05e95..af4df7b8 100644 --- a/tests/arg_tests.rs +++ b/tests/arg_tests.rs @@ -1,8 +1,8 @@ //! These tests are mostly here just to ensure that invalid results will be caught when passing arguments. +mod util; + use assert_cmd::prelude::*; use predicates::prelude::*; - -mod util; use util::*; #[test] diff --git a/tests/invalid_config_tests.rs b/tests/invalid_config_tests.rs index d000a5af..62e2a376 100644 --- a/tests/invalid_config_tests.rs +++ b/tests/invalid_config_tests.rs @@ -1,7 +1,7 @@ +mod util; + use assert_cmd::prelude::*; use predicates::prelude::*; - -mod util; use util::*; // These tests are for testing some config file-specific options. diff --git a/tests/layout_management_tests.rs b/tests/layout_management_tests.rs index 2d4b617b..607eea9e 100644 --- a/tests/layout_management_tests.rs +++ b/tests/layout_management_tests.rs @@ -5,7 +5,7 @@ use bottom::constants::DEFAULT_BATTERY_LAYOUT; use bottom::{ app::layout_manager::{BottomLayout, BottomWidgetType}, constants::{DEFAULT_LAYOUT, DEFAULT_WIDGET_ID}, - options::{layout_options::Row, Config}, + options::config::{layout::Row, Config}, utils::error, }; use toml_edit::de::from_str;