diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b05070e..a4e2fbb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,8 @@ jobs: override: true components: clippy + # TODO: Can probably put cache here in the future; I'm worried if this will cause issues with clippy though since cargo check breaks it; maybe wait until 1.52, when fix lands. + - run: cargo clippy --all-targets --workspace -- -D warnings # Compile/check test. diff --git a/.vscode/settings.json b/.vscode/settings.json index a006e543..b6cc5b59 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,7 +26,10 @@ "Mahmoud", "Marcin", "Mousebindings", + "NAS's", "Nonexhaustive", + "PEBI", + "PETA", "PKGBUILD", "PKGBUILDs", "Polishchuk", @@ -90,6 +93,7 @@ "libc", "markdownlint", "memb", + "minmax", "minwindef", "musl", "musleabihf", @@ -102,6 +106,7 @@ "nvme", "paren", "pcpu", + "piasecki", "pids", "pmem", "powerpc", @@ -109,7 +114,9 @@ "ppid", "prepush", "processthreadsapi", + "pvanheus", "regexes", + "rposition", "rsplitn", "runlevel", "rustc", diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc22531..c06685eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#381](https://github.com/ClementTsang/bottom/pull/381): Added a filter in the config file for network interfaces. +- [#392](https://github.com/ClementTsang/bottom/pull/392): Added CPU load averages (1, 5, 15) for Unix-based systems. + - [#406](https://github.com/ClementTsang/bottom/pull/406): Added the Nord colour scheme, as well as a light variant. - [#409](https://github.com/ClementTsang/bottom/pull/409): Added `Ctrl-w` and `Ctrl-h` shortcuts in search, to delete a word and delete a character respectively. @@ -25,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#425](https://github.com/ClementTsang/bottom/pull/425): Added user into the process widget for Unix-based systems. +- [#437](https://github.com/ClementTsang/bottom/pull/437): Redo dynamic network y-axis, add linear scaling, unit type, and prefix options. + ## Changes - [#372](https://github.com/ClementTsang/bottom/pull/372): Hides the SWAP graph and legend in normal mode if SWAP is 0. @@ -37,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#420](https://github.com/ClementTsang/bottom/pull/420): Updated tui-rs, allowing for prettier looking tables! +- [#437](https://github.com/ClementTsang/bottom/pull/437): Add linear interpolation step in drawing step to pr event missing entries on the right side of charts. + ## Bug Fixes - [#416](https://github.com/ClementTsang/bottom/pull/416): Fixes grouped vs ungrouped modes in the processes widget having inconsistent spacing. diff --git a/Cargo.toml b/Cargo.toml index b9351b53..ee88a928 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,8 +48,10 @@ futures = "0.3.12" indexmap = "~1.6" itertools = "0.10.0" once_cell = "1.5.2" +# ordered-float = "2.1.1" regex = "1.4.3" serde = { version = "1.0.123", features = ["derive"] } +# Sysinfo is still used in Linux for the ProcessStatus sysinfo = "0.16.4" thiserror = "1.0.24" toml = "0.5.8" diff --git a/README.md b/README.md index 41205417..74774cc5 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Or, you can just download the binary from the [latest release](https://github.co ### Nightly -You can install pre-release nightly versions [here](https://github.com/ClementTsang/bottom/releases/tag/nightly). Builds are generated every day at 00:00 UTC, based on the most recent commit on the master branch. +You can install pre-release nightly versions [here](https://github.com/ClementTsang/bottom/releases/tag/nightly). Builds are generated every day at 00:00 UTC, based on the most recent commit on the master branch. ### Cargo @@ -252,38 +252,41 @@ Run using `btm`. Use `btm --help` for more information. ``` - --advanced_kill Shows more options when killing a process on Unix-like systems. - --autohide_time Temporarily shows the time scale in graphs. --b, --basic Hides graphs and uses a more basic look. - --battery Shows the battery widget. --S, --case_sensitive Enables case sensitivity by default. --c, --celsius Sets the temperature type to Celsius. - --color <COLOR SCHEME> Use a color scheme, use --help for supported values. --C, --config <CONFIG PATH> Sets the location of the config file. --u, --current_usage Sets process CPU% to be based on current CPU%. --t, --default_time_value <MS> Default time value for graphs in ms. - --default_widget_count <INT> Sets the n'th selected widget type as the default. - --default_widget_type <WIDGET TYPE> Sets the default widget type, use --help for more info. - --disable_click Disables mouse clicks. --m, --dot_marker Uses a dot marker for graphs. --f, --fahrenheit Sets the temperature type to Fahrenheit. --g, --group Groups processes with the same name by default. --h, --help Prints help information. Use --help for more info. --a, --hide_avg_cpu Hides the average CPU usage. - --hide_table_gap Hides the spacing between table headers and entries. - --hide_time Completely hides the time scaling. --k, --kelvin Sets the temperature type to Kelvin. --l, --left_legend Puts the CPU chart legend to the left side. - --mem_as_value Defaults to showing process memory usage by value. - --process_command Show processes as their commands by default. --r, --rate <MS> Sets a refresh rate in ms. --R, --regex Enables regex by default. - --show_table_scroll_position Shows the scroll position tracker in table widgets. --d, --time_delta <MS> The amount in ms changed upon zooming. --T, --tree Defaults to showing the process widget in tree mode. - --use_old_network_legend DEPRECATED - uses the older network legend. --V, --version Prints version information. --W, --whole_word Enables whole-word matching by default. + --advanced_kill Shows more options when killing a process on Unix-like systems. + --autohide_time Temporarily shows the time scale in graphs. + -b, --basic Hides graphs and uses a more basic look. + --battery Shows the battery widget. + -S, --case_sensitive Enables case sensitivity by default. + -c, --celsius Sets the temperature type to Celsius. + --color <COLOR SCHEME> Use a color scheme, use --help for supported values. + -C, --config <CONFIG PATH> Sets the location of the config file. + -u, --current_usage Sets process CPU% to be based on current CPU%. + -t, --default_time_value <MS> Default time value for graphs in ms. + --default_widget_count <INT> Sets the n'th selected widget type as the default. + --default_widget_type <WIDGET TYPE> Sets the default widget type, use --help for more info. + --disable_click Disables mouse clicks. + -m, --dot_marker Uses a dot marker for graphs. + -f, --fahrenheit Sets the temperature type to Fahrenheit. + -g, --group Groups processes with the same name by default. + -h, --help Prints help information. Use --help for more info. + -a, --hide_avg_cpu Hides the average CPU usage. + --hide_table_gap Hides the spacing between table headers and entries. + --hide_time Completely hides the time scaling. + -k, --kelvin Sets the temperature type to Kelvin. + -l, --left_legend Puts the CPU chart legend to the left side. + --mem_as_value Defaults to showing process memory usage by value. + --network_use_binary_prefix Displays the network widget with binary prefixes. + --network_use_bytes Displays the network widget using bytes. + --network_use_log Displays the network widget with a log scale. + --process_command Show processes as their commands by default. + -r, --rate <MS> Sets a refresh rate in ms. + -R, --regex Enables regex by default. + --show_table_scroll_position Shows the scroll position tracker in table widgets. + -d, --time_delta <MS> The amount in ms changed upon zooming. + -T, --tree Defaults to showing the process widget in tree mode. + --use_old_network_legend DEPRECATED - uses the older network legend. + -V, --version Prints version information. + -W, --whole_word Enables whole-word matching by default. ``` ### Keybindings @@ -464,7 +467,7 @@ As yet _another_ process/system visualization and management application, bottom - RAM and swap usage visualization -- Network visualization for receiving and transmitting, on a log-graph scale +- Network visualization for receiving and transmitting - Display information about disk capacity and I/O per second @@ -599,6 +602,9 @@ These are the following supported flag config values, which correspond to the fl | `show_table_scroll_position` | Boolean | Shows the scroll position tracker in table widgets. | | `process_command` | Boolean | Show processes as their commands by default. | | `advanced_kill` | Boolean | Shows more options when killing a process on Unix-like systems. | +| `network_use_binary_prefix` | Boolean | Displays the network widget with binary prefixes. | +| `network_use_bytes` | Boolean | Displays the network widget using bytes. | +| `network_use_log` | Boolean | Displays the network widget with a log scale. | #### Theming diff --git a/src/app.rs b/src/app.rs index 3c0fc9d5..e1f61b7c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -21,6 +21,7 @@ use crate::{ options::Config, options::ConfigFlags, options::WidgetIdEnabled, + units::data_units::DataUnit, utils::error::{BottomError, Result}, Pid, }; @@ -34,6 +35,12 @@ pub mod states; const MAX_SEARCH_LENGTH: usize = 200; +#[derive(Debug, Clone)] +pub enum AxisScaling { + Log, + Linear, +} + /// AppConfigFields is meant to cover basic fields that would normally be set /// by config files or launch options. #[derive(Debug)] @@ -55,6 +62,10 @@ pub struct AppConfigFields { pub no_write: bool, pub show_table_scroll_position: bool, pub is_advanced_kill: bool, + // TODO: Remove these, move network details state-side. + pub network_unit_type: DataUnit, + pub network_scale_type: AxisScaling, + pub network_use_binary_prefix: bool, } /// For filtering out information @@ -708,7 +719,7 @@ impl App { if self.delete_dialog_state.is_showing_dd { if self.dd_err.is_some() { self.close_dd(); - } else if self.delete_dialog_state.selected_signal != KillSignal::CANCEL { + } else if self.delete_dialog_state.selected_signal != KillSignal::Cancel { // If within dd... if self.dd_err.is_none() { // Also ensure that we didn't just fail a dd... @@ -886,7 +897,7 @@ impl App { if kbd_signal > 31 { kbd_signal %= 10; } - self.delete_dialog_state.selected_signal = KillSignal::KILL(kbd_signal); + self.delete_dialog_state.selected_signal = KillSignal::Kill(kbd_signal); if kbd_signal < 10 { self.delete_dialog_state.keyboard_signal_select = kbd_signal; } else { @@ -991,15 +1002,15 @@ impl App { { if self.app_config_fields.is_advanced_kill { match self.delete_dialog_state.selected_signal { - KillSignal::KILL(prev_signal) => { + KillSignal::Kill(prev_signal) => { self.delete_dialog_state.selected_signal = match prev_signal - 1 { - 0 => KillSignal::CANCEL, + 0 => KillSignal::Cancel, // 32+33 are skipped - 33 => KillSignal::KILL(31), - signal => KillSignal::KILL(signal), + 33 => KillSignal::Kill(31), + signal => KillSignal::Kill(signal), }; } - KillSignal::CANCEL => {} + KillSignal::Cancel => {} }; } else { self.delete_dialog_state.selected_signal = KillSignal::default(); @@ -1007,7 +1018,7 @@ impl App { } #[cfg(target_os = "windows")] { - self.delete_dialog_state.selected_signal = KillSignal::KILL(1); + self.delete_dialog_state.selected_signal = KillSignal::Kill(1); } } } @@ -1067,23 +1078,23 @@ impl App { { if self.app_config_fields.is_advanced_kill { let new_signal = match self.delete_dialog_state.selected_signal { - KillSignal::CANCEL => 1, + KillSignal::Cancel => 1, // 32+33 are skipped #[cfg(target_os = "linux")] - KillSignal::KILL(31) => 34, + KillSignal::Kill(31) => 34, #[cfg(target_os = "macos")] - KillSignal::KILL(31) => 31, - KillSignal::KILL(64) => 64, - KillSignal::KILL(signal) => signal + 1, + KillSignal::Kill(31) => 31, + KillSignal::Kill(64) => 64, + KillSignal::Kill(signal) => signal + 1, }; - self.delete_dialog_state.selected_signal = KillSignal::KILL(new_signal); + self.delete_dialog_state.selected_signal = KillSignal::Kill(new_signal); } else { - self.delete_dialog_state.selected_signal = KillSignal::CANCEL; + self.delete_dialog_state.selected_signal = KillSignal::Cancel; } } #[cfg(target_os = "windows")] { - self.delete_dialog_state.selected_signal = KillSignal::CANCEL; + self.delete_dialog_state.selected_signal = KillSignal::Cancel; } } } @@ -1091,15 +1102,15 @@ impl App { pub fn on_page_up(&mut self) { if self.delete_dialog_state.is_showing_dd { let mut new_signal = match self.delete_dialog_state.selected_signal { - KillSignal::CANCEL => 0, - KillSignal::KILL(signal) => max(signal, 8) - 8, + KillSignal::Cancel => 0, + KillSignal::Kill(signal) => max(signal, 8) - 8, }; if new_signal > 23 && new_signal < 33 { new_signal -= 2; } self.delete_dialog_state.selected_signal = match new_signal { - 0 => KillSignal::CANCEL, - sig => KillSignal::KILL(sig), + 0 => KillSignal::Cancel, + sig => KillSignal::Kill(sig), }; } } @@ -1107,13 +1118,13 @@ impl App { pub fn on_page_down(&mut self) { if self.delete_dialog_state.is_showing_dd { let mut new_signal = match self.delete_dialog_state.selected_signal { - KillSignal::CANCEL => 8, - KillSignal::KILL(signal) => min(signal + 8, MAX_SIGNAL), + KillSignal::Cancel => 8, + KillSignal::Kill(signal) => min(signal + 8, MAX_SIGNAL), }; if new_signal > 31 && new_signal < 42 { new_signal += 2; } - self.delete_dialog_state.selected_signal = KillSignal::KILL(new_signal); + self.delete_dialog_state.selected_signal = KillSignal::Kill(new_signal); } } @@ -1672,8 +1683,8 @@ impl App { if let Some(current_selected_processes) = &self.to_delete_process_list { #[cfg(target_family = "unix")] let signal = match self.delete_dialog_state.selected_signal { - KillSignal::KILL(sig) => sig, - KillSignal::CANCEL => 15, // should never happen, so just TERM + KillSignal::Kill(sig) => sig, + KillSignal::Cancel => 15, // should never happen, so just TERM }; for pid in ¤t_selected_processes.1 { #[cfg(target_family = "unix")] @@ -2229,7 +2240,7 @@ impl App { } else if self.help_dialog_state.is_showing_help { self.help_dialog_state.scroll_state.current_scroll_index = 0; } else if self.delete_dialog_state.is_showing_dd { - self.delete_dialog_state.selected_signal = KillSignal::CANCEL; + self.delete_dialog_state.selected_signal = KillSignal::Cancel; } } @@ -2312,7 +2323,7 @@ impl App { .max_scroll_index .saturating_sub(1); } else if self.delete_dialog_state.is_showing_dd { - self.delete_dialog_state.selected_signal = KillSignal::KILL(MAX_SIGNAL); + self.delete_dialog_state.selected_signal = KillSignal::Kill(MAX_SIGNAL); } } @@ -2871,13 +2882,13 @@ impl App { }, ) { Some((_, _, _, _, 0)) => { - self.delete_dialog_state.selected_signal = KillSignal::CANCEL + self.delete_dialog_state.selected_signal = KillSignal::Cancel } Some((_, _, _, _, idx)) => { if *idx > 31 { - self.delete_dialog_state.selected_signal = KillSignal::KILL(*idx + 2) + self.delete_dialog_state.selected_signal = KillSignal::Kill(*idx + 2) } else { - self.delete_dialog_state.selected_signal = KillSignal::KILL(*idx) + self.delete_dialog_state.selected_signal = KillSignal::Kill(*idx) } } _ => {} diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index bfc7ae8d..4a2f470e 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -19,7 +19,7 @@ use std::{time::Instant, vec::Vec}; use crate::app::data_harvester::load_avg::LoadAvgHarvest; use crate::{ data_harvester::{batteries, cpu, disks, load_avg, mem, network, processes, temperature, Data}, - utils::gen_util::get_simple_byte_values, + utils::gen_util::get_decimal_bytes, }; use regex::Regex; @@ -57,7 +57,7 @@ pub struct DataCollection { pub load_avg_harvest: load_avg::LoadAvgHarvest, pub process_harvest: Vec<processes::ProcessHarvest>, pub disk_harvest: Vec<disks::DiskHarvest>, - pub io_harvest: disks::IOHarvest, + pub io_harvest: disks::IoHarvest, pub io_labels_and_prev: Vec<((u64, u64), (u64, u64))>, pub io_labels: Vec<(String, String)>, pub temp_harvest: Vec<temperature::TempHarvest>, @@ -77,7 +77,7 @@ impl Default for DataCollection { load_avg_harvest: load_avg::LoadAvgHarvest::default(), process_harvest: Vec::default(), disk_harvest: Vec::default(), - io_harvest: disks::IOHarvest::default(), + io_harvest: disks::IoHarvest::default(), io_labels_and_prev: Vec::default(), io_labels: Vec::default(), temp_harvest: Vec::default(), @@ -95,7 +95,7 @@ impl DataCollection { self.cpu_harvest = cpu::CpuHarvest::default(); self.process_harvest = Vec::default(); self.disk_harvest = Vec::default(); - self.io_harvest = disks::IOHarvest::default(); + self.io_harvest = disks::IoHarvest::default(); self.io_labels_and_prev = Vec::default(); self.temp_harvest = Vec::default(); self.battery_harvest = Vec::default(); @@ -205,22 +205,15 @@ impl DataCollection { } fn eat_network(&mut self, network: network::NetworkHarvest, new_entry: &mut TimedData) { - // trace!("Eating network."); - // FIXME [NETWORKING; CONFIG]: The ability to config this? - // FIXME [NETWORKING]: Support bits, support switching between decimal and binary units (move the log part to conversion and switch on the fly) // RX - new_entry.rx_data = if network.rx > 0 { - (network.rx as f64).log2() - } else { - 0.0 - }; + if network.rx > 0 { + new_entry.rx_data = network.rx as f64; + } // TX - new_entry.tx_data = if network.tx > 0 { - (network.tx as f64).log2() - } else { - 0.0 - }; + if network.tx > 0 { + new_entry.tx_data = network.tx as f64; + } // In addition copy over latest data for easy reference self.network_harvest = network; @@ -250,7 +243,7 @@ impl DataCollection { } fn eat_disks( - &mut self, disks: Vec<disks::DiskHarvest>, io: disks::IOHarvest, harvested_time: Instant, + &mut self, disks: Vec<disks::DiskHarvest>, io: disks::IoHarvest, harvested_time: Instant, ) { // trace!("Eating disks."); // TODO: [PO] To implement @@ -300,8 +293,8 @@ impl DataCollection { *io_prev = (io_r_pt, io_w_pt); if let Some(io_labels) = self.io_labels.get_mut(itx) { - let converted_read = get_simple_byte_values(r_rate, false); - let converted_write = get_simple_byte_values(w_rate, false); + let converted_read = get_decimal_bytes(r_rate); + let converted_write = get_decimal_bytes(w_rate); *io_labels = ( format!("{:.*}{}/s", 0, converted_read.0, converted_read.1), format!("{:.*}{}/s", 0, converted_write.0, converted_write.1), diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index 3b7a0f63..207bb23a 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -36,7 +36,7 @@ pub struct Data { pub network: Option<network::NetworkHarvest>, pub list_of_processes: Option<Vec<processes::ProcessHarvest>>, pub disks: Option<Vec<disks::DiskHarvest>>, - pub io: Option<disks::IOHarvest>, + pub io: Option<disks::IoHarvest>, pub list_of_batteries: Option<Vec<batteries::BatteryHarvest>>, } diff --git a/src/app/data_harvester/batteries.rs b/src/app/data_harvester/batteries.rs index 66e3c76b..98cf6ae6 100644 --- a/src/app/data_harvester/batteries.rs +++ b/src/app/data_harvester/batteries.rs @@ -20,19 +20,11 @@ pub fn refresh_batteries(manager: &Manager, batteries: &mut [Battery]) -> Vec<Ba Some(BatteryHarvest { secs_until_full: { let optional_time = battery.time_to_full(); - if let Some(time) = optional_time { - Some(f64::from(time.get::<second>()) as i64) - } else { - None - } + optional_time.map(|time| f64::from(time.get::<second>()) as i64) }, secs_until_empty: { let optional_time = battery.time_to_empty(); - if let Some(time) = optional_time { - Some(f64::from(time.get::<second>()) as i64) - } else { - None - } + optional_time.map(|time| f64::from(time.get::<second>()) as i64) }, charge_percent: f64::from(battery.state_of_charge().get::<percent>()), power_consumption_rate_watts: f64::from(battery.energy_rate().get::<watt>()), diff --git a/src/app/data_harvester/disks.rs b/src/app/data_harvester/disks.rs index 246f5b5d..ba1d2d49 100644 --- a/src/app/data_harvester/disks.rs +++ b/src/app/data_harvester/disks.rs @@ -10,21 +10,21 @@ pub struct DiskHarvest { } #[derive(Clone, Debug)] -pub struct IOData { +pub struct IoData { pub read_bytes: u64, pub write_bytes: u64, } -pub type IOHarvest = std::collections::HashMap<String, Option<IOData>>; +pub type IoHarvest = std::collections::HashMap<String, Option<IoData>>; -pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result<Option<IOHarvest>> { +pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result<Option<IoHarvest>> { if !actually_get { return Ok(None); } use futures::StreamExt; - let mut io_hash: std::collections::HashMap<String, Option<IOData>> = + let mut io_hash: std::collections::HashMap<String, Option<IoData>> = std::collections::HashMap::new(); let counter_stream = heim::disk::io_counters().await?; @@ -37,7 +37,7 @@ pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result<Opt // FIXME: [MOUNT POINT] Add the filter here I guess? io_hash.insert( mount_point.to_string(), - Some(IOData { + Some(IoData { read_bytes: io.read_bytes().get::<heim::units::information::byte>(), write_bytes: io.write_bytes().get::<heim::units::information::byte>(), }), diff --git a/src/app/data_harvester/network.rs b/src/app/data_harvester/network.rs index 55d757f6..52dc8468 100644 --- a/src/app/data_harvester/network.rs +++ b/src/app/data_harvester/network.rs @@ -1,6 +1,7 @@ use std::time::Instant; #[derive(Default, Clone, Debug)] +/// All units in bits. pub struct NetworkHarvest { pub rx: u64, pub tx: u64, @@ -47,8 +48,8 @@ pub async fn get_network_data( }; if to_keep { - total_rx += network.get_total_received(); - total_tx += network.get_total_transmitted(); + total_rx += network.get_total_received() * 8; + total_tx += network.get_total_transmitted() * 8; } } @@ -106,8 +107,12 @@ pub async fn get_network_data( }; if to_keep { - total_rx += io.bytes_recv().get::<heim::units::information::byte>(); - total_tx += io.bytes_sent().get::<heim::units::information::byte>(); + // TODO: Use bytes as the default instead, perhaps? + // Since you might have to do a double conversion (bytes -> bits -> bytes) in some cases; + // but if you stick to bytes, then in the bytes, case, you do no conversion, and in the bits case, + // you only do one conversion... + total_rx += io.bytes_recv().get::<heim::units::information::bit>(); + total_tx += io.bytes_sent().get::<heim::units::information::bit>(); } } } diff --git a/src/app/data_harvester/processes.rs b/src/app/data_harvester/processes.rs index 4d7beda0..52e80c27 100644 --- a/src/app/data_harvester/processes.rs +++ b/src/app/data_harvester/processes.rs @@ -2,6 +2,9 @@ use crate::Pid; use std::path::PathBuf; use sysinfo::ProcessStatus; +#[cfg(target_os = "linux")] +use std::path::Path; + #[cfg(target_family = "unix")] use crate::utils::error; @@ -168,7 +171,7 @@ fn cpu_usage_calculation( // SC in case that the parsing will fail due to length: if val.len() <= 10 { - return Err(error::BottomError::InvalidIO(format!( + return Err(error::BottomError::InvalidIo(format!( "CPU parsing will fail due to too short of a return value; saw {} values, expected 10 values.", val.len() ))); @@ -222,8 +225,8 @@ fn get_linux_process_vsize_rss(stat: &[&str]) -> (u64, u64) { #[cfg(target_os = "linux")] /// Preferably use this only on small files. -fn read_path_contents(path: &PathBuf) -> std::io::Result<String> { - Ok(std::fs::read_to_string(path)?) +fn read_path_contents(path: &Path) -> std::io::Result<String> { + std::fs::read_to_string(path) } #[cfg(target_os = "linux")] @@ -272,9 +275,8 @@ fn get_macos_cpu_usage(pids: &[i32]) -> std::io::Result<std::collections::HashMa let output = std::process::Command::new("ps") .args(&["-o", "pid=,pcpu=", "-p"]) .arg( - pids.iter() - .map(i32::to_string) - .intersperse(",".to_string()) + // Has to look like this since otherwise, it you hit a `unstable_name_collisions` warning. + Itertools::intersperse(pids.iter().map(i32::to_string), ",".to_string()) .collect::<String>(), ) .output()?; @@ -298,7 +300,7 @@ fn get_macos_cpu_usage(pids: &[i32]) -> std::io::Result<std::collections::HashMa } #[cfg(target_os = "linux")] -fn get_uid_and_gid(path: &PathBuf) -> (Option<u32>, Option<u32>) { +fn get_uid_and_gid(path: &Path) -> (Option<u32>, Option<u32>) { // FIXME: [OPT] - can we merge our /stat and /status calls? use std::io::prelude::*; use std::io::BufReader; @@ -470,15 +472,15 @@ fn read_proc( Ok(ProcessHarvest { pid, parent_pid, - name, - command, + cpu_usage_percent, mem_usage_percent, mem_usage_bytes, - cpu_usage_percent, - total_read_bytes, - total_write_bytes, + name, + command, read_bytes_per_sec, write_bytes_per_sec, + total_read_bytes, + total_write_bytes, process_state, process_state_char, uid, diff --git a/src/app/data_harvester/temperature.rs b/src/app/data_harvester/temperature.rs index 1c818e31..3786adc7 100644 --- a/src/app/data_harvester/temperature.rs +++ b/src/app/data_harvester/temperature.rs @@ -95,11 +95,7 @@ pub async fn get_temperature_data( while let Some(sensor) = sensor_data.next().await { if let Ok(sensor) = sensor { let component_name = Some(sensor.unit().to_string()); - let component_label = if let Some(label) = sensor.label() { - Some(label.to_string()) - } else { - None - }; + let component_label = sensor.label().map(|label| label.to_string()); let name = match (component_name, component_label) { (Some(name), Some(label)) => format!("{}: {}", name, label), diff --git a/src/app/query.rs b/src/app/query.rs index 5a827aec..a783c658 100644 --- a/src/app/query.rs +++ b/src/app/query.rs @@ -188,11 +188,7 @@ impl ProcessQuery for ProcWidgetState { let initial_or = Or { lhs: And { lhs: Prefix { - or: if let Some(or) = list_of_ors.pop_front() { - Some(Box::new(or)) - } else { - None - }, + or: list_of_ors.pop_front().map(Box::new), compare_prefix: None, regex_prefix: None, }, diff --git a/src/app/states.rs b/src/app/states.rs index 39dd0222..c8af1737 100644 --- a/src/app/states.rs +++ b/src/app/states.rs @@ -42,18 +42,18 @@ pub struct AppScrollWidgetState { #[derive(PartialEq)] pub enum KillSignal { - CANCEL, - KILL(usize), + Cancel, + Kill(usize), } impl Default for KillSignal { #[cfg(target_family = "unix")] fn default() -> Self { - KillSignal::KILL(15) + KillSignal::Kill(15) } #[cfg(target_os = "windows")] fn default() -> Self { - KillSignal::KILL(1) + KillSignal::Kill(1) } } @@ -690,13 +690,29 @@ impl ProcState { pub struct NetWidgetState { pub current_display_time: u64, pub autohide_timer: Option<Instant>, + // pub draw_max_range_cache: f64, + // pub draw_labels_cache: Vec<String>, + // pub draw_time_start_cache: f64, + // TODO: Re-enable these when we move net details state-side! + // pub unit_type: DataUnitTypes, + // pub scale_type: AxisScaling, } impl NetWidgetState { - pub fn init(current_display_time: u64, autohide_timer: Option<Instant>) -> Self { + pub fn init( + current_display_time: u64, + autohide_timer: Option<Instant>, + // unit_type: DataUnitTypes, + // scale_type: AxisScaling, + ) -> Self { NetWidgetState { current_display_time, autohide_timer, + // draw_max_range_cache: 0.0, + // draw_labels_cache: vec![], + // draw_time_start_cache: 0.0, + // unit_type, + // scale_type, } } } diff --git a/src/bin/main.rs b/src/bin/main.rs index 70d02efe..e9854987 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -26,6 +26,10 @@ use crossterm::{ }; use tui::{backend::CrosstermBackend, Terminal}; +// TODO: Add a debugger tool: +// Debugger binary. This isn't implemented yet; the idea for this is to make it easier to troubleshoot bug reports +// by providing a built-in debugger to help gather relevant information to narrow down the problem. + fn main() -> Result<()> { let matches = clap::get_matches(); // let is_debug = matches.is_present("debug"); @@ -178,6 +182,9 @@ fn main() -> Result<()> { false, app.app_config_fields.use_basic_mode || app.app_config_fields.use_old_network_legend, + &app.app_config_fields.network_scale_type, + &app.app_config_fields.network_unit_type, + app.app_config_fields.network_use_binary_prefix, ); app.canvas_data.network_data_rx = network_data.rx; app.canvas_data.network_data_tx = network_data.tx; diff --git a/src/canvas.rs b/src/canvas.rs index b904e217..1916f200 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -9,6 +9,8 @@ use tui::{ Frame, Terminal, }; +// use ordered_float::OrderedFloat; + use canvas_colours::*; use dialogs::*; use screens::*; @@ -54,7 +56,7 @@ pub struct DisplayableData { pub mem_labels: Option<(String, String)>, pub swap_labels: Option<(String, String)>, - pub mem_data: Vec<Point>, + pub mem_data: Vec<Point>, // TODO: Switch this and all data points over to a better data structure... pub swap_data: Vec<Point>, pub load_avg_data: [f32; 3], pub cpu_data: Vec<ConvertedCpuData>, diff --git a/src/canvas/dialogs/dd_dialog.rs b/src/canvas/dialogs/dd_dialog.rs index cd9029c8..0415aa2d 100644 --- a/src/canvas/dialogs/dd_dialog.rs +++ b/src/canvas/dialogs/dd_dialog.rs @@ -72,11 +72,11 @@ impl KillDialog for Painter { ) { if cfg!(target_os = "windows") || !app_state.app_config_fields.is_advanced_kill { let (yes_button, no_button) = match app_state.delete_dialog_state.selected_signal { - KillSignal::KILL(_) => ( + KillSignal::Kill(_) => ( Span::styled("Yes", self.colours.currently_selected_text_style), Span::raw("No"), ), - KillSignal::CANCEL => ( + KillSignal::Cancel => ( Span::raw("Yes"), Span::styled("No", self.colours.currently_selected_text_style), ), @@ -249,8 +249,8 @@ impl KillDialog for Painter { .split(*button_draw_loc)[1]; let mut selected = match app_state.delete_dialog_state.selected_signal { - KillSignal::CANCEL => 0, - KillSignal::KILL(signal) => signal, + KillSignal::Cancel => 0, + KillSignal::Kill(signal) => signal, }; // 32+33 are skipped if selected > 31 { diff --git a/src/canvas/drawing_utils.rs b/src/canvas/drawing_utils.rs index 222ca852..6aee1aa7 100644 --- a/src/canvas/drawing_utils.rs +++ b/src/canvas/drawing_utils.rs @@ -117,6 +117,12 @@ pub fn get_column_widths( filtered_column_widths } +/// FIXME: [command move] This is a greedy method of determining column widths. This is reserved for columns where we are okay with +/// shoving information as far right as required. +// pub fn greedy_get_column_widths() -> Vec<u16> { +// vec![] +// } + pub fn get_search_start_position( num_columns: usize, cursor_direction: &app::CursorDirection, cursor_bar: &mut usize, current_cursor_position: usize, is_force_redraw: bool, @@ -205,3 +211,14 @@ pub fn calculate_basic_use_bars(use_percentage: f64, num_bars_available: usize) num_bars_available, ) } + +/// Interpolates between two points. Mainly used to help fill in tui-rs blanks in certain situations. +/// It is expected point_one is "further left" compared to point_two. +/// A point is two floats, in (x, y) form. x is time, y is value. +pub fn interpolate_points(point_one: &(f64, f64), point_two: &(f64, f64), time: f64) -> f64 { + let delta_x = point_two.0 - point_one.0; + let delta_y = point_two.1 - point_one.1; + let slope = delta_y / delta_x; + + (point_one.1 + (time - point_one.0) * slope).max(0.0) +} diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index 1b79482f..ff6838f2 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -4,7 +4,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::{ app::{layout_manager::WidgetDirection, App}, canvas::{ - drawing_utils::{get_column_widths, get_start_position}, + drawing_utils::{get_column_widths, get_start_position, interpolate_points}, Painter, }, constants::*, @@ -146,32 +146,34 @@ impl CpuGraphWidget for Painter { ]; let y_axis_labels = vec![ - Span::styled("0%", self.colours.graph_style), + Span::styled(" 0%", self.colours.graph_style), Span::styled("100%", self.colours.graph_style), ]; + let time_start = -(cpu_widget_state.current_display_time as f64); + let x_axis = if app_state.app_config_fields.hide_time || (app_state.app_config_fields.autohide_time && cpu_widget_state.autohide_timer.is_none()) { - Axis::default().bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else if let Some(time) = cpu_widget_state.autohide_timer { if std::time::Instant::now().duration_since(time).as_millis() < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 { Axis::default() - .bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) } else { cpu_widget_state.autohide_timer = None; - Axis::default().bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else { Axis::default() - .bounds([-(cpu_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) }; @@ -184,6 +186,59 @@ impl CpuGraphWidget for Painter { let use_dot = app_state.app_config_fields.use_dot; let show_avg_cpu = app_state.app_config_fields.show_average_cpu; let current_scroll_position = cpu_widget_state.scroll_state.current_scroll_position; + + let interpolated_cpu_points = cpu_data + .iter_mut() + .enumerate() + .map(|(itx, cpu)| { + let to_show = if current_scroll_position == ALL_POSITION { + true + } else { + itx == current_scroll_position + }; + + if to_show { + if let Some(end_pos) = cpu + .cpu_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = cpu.cpu_data.get(start_pos); + let inside_point = cpu.cpu_data.get(end_pos); + + if let (Some(outside_point), Some(inside_point)) = + (outside_point, inside_point) + { + let old = *outside_point; + + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); + + if let Some(to_replace) = cpu.cpu_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our data + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + } + } else { + None + } + }) + .collect::<Vec<_>>(); + let dataset_vector: Vec<Dataset<'_>> = if current_scroll_position == ALL_POSITION { cpu_data .iter() @@ -311,6 +366,18 @@ impl CpuGraphWidget for Painter { .y_axis(y_axis), draw_loc, ); + + // Reset interpolated points + cpu_data + .iter_mut() + .zip(interpolated_cpu_points) + .for_each(|(cpu, interpolation)| { + if let Some((index, old_value)) = interpolation { + if let Some(to_replace) = cpu.cpu_data.get_mut(index) { + *to_replace = old_value; + } + } + }); } } diff --git a/src/canvas/widgets/mem_graph.rs b/src/canvas/widgets/mem_graph.rs index 9136b1ba..6b5bc5ab 100644 --- a/src/canvas/widgets/mem_graph.rs +++ b/src/canvas/widgets/mem_graph.rs @@ -1,4 +1,8 @@ -use crate::{app::App, canvas::Painter, constants::*}; +use crate::{ + app::App, + canvas::{drawing_utils::interpolate_points, Painter}, + constants::*, +}; use tui::{ backend::Backend, @@ -22,8 +26,10 @@ impl MemGraphWidget for Painter { &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { if let Some(mem_widget_state) = app_state.mem_state.widget_states.get_mut(&widget_id) { - let mem_data: &[(f64, f64)] = &app_state.canvas_data.mem_data; - let swap_data: &[(f64, f64)] = &app_state.canvas_data.swap_data; + let mem_data: &mut [(f64, f64)] = &mut app_state.canvas_data.mem_data; + let swap_data: &mut [(f64, f64)] = &mut app_state.canvas_data.swap_data; + + let time_start = -(mem_widget_state.current_display_time as f64); let display_time_labels = vec![ Span::styled( @@ -33,7 +39,7 @@ impl MemGraphWidget for Painter { Span::styled("0s".to_string(), self.colours.graph_style), ]; let y_axis_label = vec![ - Span::styled("0%", self.colours.graph_style), + Span::styled(" 0%", self.colours.graph_style), Span::styled("100%", self.colours.graph_style), ]; @@ -41,24 +47,24 @@ impl MemGraphWidget for Painter { || (app_state.app_config_fields.autohide_time && mem_widget_state.autohide_timer.is_none()) { - Axis::default().bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else if let Some(time) = mem_widget_state.autohide_timer { if std::time::Instant::now().duration_since(time).as_millis() < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 { Axis::default() - .bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) } else { mem_widget_state.autohide_timer = None; - Axis::default().bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else { Axis::default() - .bounds([-(mem_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) }; @@ -68,6 +74,75 @@ impl MemGraphWidget for Painter { .bounds([0.0, 100.5]) .labels(y_axis_label); + // Interpolate values to avoid ugly gaps + let interpolated_mem_point = if let Some(end_pos) = mem_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = mem_data.get(start_pos); + let inside_point = mem_data.get(end_pos); + + if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point) + { + let old = *outside_point; + + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); + + if let Some(to_replace) = mem_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our data + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + + let interpolated_swap_point = if let Some(end_pos) = swap_data + .iter() + .position(|(time, _data)| *time >= time_start) + { + if end_pos > 1 { + let start_pos = end_pos - 1; + let outside_point = swap_data.get(start_pos); + let inside_point = swap_data.get(end_pos); + + if let (Some(outside_point), Some(inside_point)) = (outside_point, inside_point) + { + let old = *outside_point; + + let new_point = ( + time_start, + interpolate_points(outside_point, inside_point, time_start), + ); + + if let Some(to_replace) = swap_data.get_mut(start_pos) { + *to_replace = new_point; + Some((start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our data + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + let mut mem_canvas_vec: Vec<Dataset<'_>> = vec![]; if let Some((label_percent, label_frac)) = &app_state.canvas_data.mem_labels { @@ -147,6 +222,19 @@ impl MemGraphWidget for Painter { .hidden_legend_constraints((Constraint::Ratio(3, 4), Constraint::Ratio(3, 4))), draw_loc, ); + + // Now if you're done, reset any interpolated points! + if let Some((index, old_value)) = interpolated_mem_point { + if let Some(to_replace) = mem_data.get_mut(index) { + *to_replace = old_value; + } + } + + if let Some((index, old_value)) = interpolated_swap_point { + if let Some(to_replace) = swap_data.get_mut(index) { + *to_replace = old_value; + } + } } if app_state.should_get_widget_bounds() { diff --git a/src/canvas/widgets/network_graph.rs b/src/canvas/widgets/network_graph.rs index b6ee080b..74509224 100644 --- a/src/canvas/widgets/network_graph.rs +++ b/src/canvas/widgets/network_graph.rs @@ -3,9 +3,13 @@ use std::cmp::max; use unicode_segmentation::UnicodeSegmentation; use crate::{ - app::App, - canvas::{drawing_utils::get_column_widths, Painter}, + app::{App, AxisScaling}, + canvas::{ + drawing_utils::{get_column_widths, interpolate_points}, + Painter, + }, constants::*, + units::data_units::DataUnit, utils::gen_util::*, }; @@ -82,103 +86,344 @@ impl NetworkGraphWidget for Painter { /// Point is of time, data type Point = (f64, f64); + /// Returns the max data point and time given a time. + fn get_max_entry( + rx: &[Point], tx: &[Point], time_start: f64, network_scale_type: &AxisScaling, + network_use_binary_prefix: bool, + ) -> (f64, f64) { + /// Determines a "fake" max value in circumstances where we couldn't find one from the data. + fn calculate_missing_max( + network_scale_type: &AxisScaling, network_use_binary_prefix: bool, + ) -> f64 { + match network_scale_type { + AxisScaling::Log => { + if network_use_binary_prefix { + LOG_KIBI_LIMIT + } else { + LOG_KILO_LIMIT + } + } + AxisScaling::Linear => { + if network_use_binary_prefix { + KIBI_LIMIT_F64 + } else { + KILO_LIMIT_F64 + } + } + } + } + + // First, let's shorten our ranges to actually look. We can abuse the fact that our rx and tx arrays + // are sorted, so we can short-circuit our search to filter out only the relevant data points... + let filtered_rx = if let (Some(rx_start), Some(rx_end)) = ( + rx.iter().position(|(time, _data)| *time >= time_start), + rx.iter().rposition(|(time, _data)| *time <= 0.0), + ) { + Some(&rx[rx_start..=rx_end]) + } else { + None + }; + + let filtered_tx = if let (Some(tx_start), Some(tx_end)) = ( + tx.iter().position(|(time, _data)| *time >= time_start), + tx.iter().rposition(|(time, _data)| *time <= 0.0), + ) { + Some(&tx[tx_start..=tx_end]) + } else { + None + }; + + // Then, find the maximal rx/tx so we know how to scale, and return it. + match (filtered_rx, filtered_tx) { + (None, None) => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + (None, Some(filtered_tx)) => { + match filtered_tx + .iter() + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + time_start, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } + } + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + } + } + (Some(filtered_rx), None) => { + match filtered_rx + .iter() + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + time_start, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } + } + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + } + } + (Some(filtered_rx), Some(filtered_tx)) => { + match filtered_rx + .iter() + .chain(filtered_tx) + .max_by(|(_, data_a), (_, data_b)| get_ordering(data_a, data_b, false)) + { + Some((best_time, max_val)) => { + if *max_val == 0.0 { + ( + *best_time, + calculate_missing_max( + network_scale_type, + network_use_binary_prefix, + ), + ) + } else { + (*best_time, *max_val) + } + } + None => ( + time_start, + calculate_missing_max(network_scale_type, network_use_binary_prefix), + ), + } + } + } + } + /// Returns the required max data point and labels. fn adjust_network_data_point( - rx: &[Point], tx: &[Point], time_start: f64, time_end: f64, + max_entry: f64, network_scale_type: &AxisScaling, network_unit_type: &DataUnit, + network_use_binary_prefix: bool, ) -> (f64, Vec<String>) { - // First, filter and find the maximal rx or tx so we know how to scale - let mut max_val_bytes = 0.0; - let filtered_rx = rx - .iter() - .cloned() - .filter(|(time, _data)| *time >= time_start && *time <= time_end); + // So, we're going with an approach like this for linear data: + // - Main goal is to maximize the amount of information displayed given a specific height. + // We don't want to drown out some data if the ranges are too far though! Nor do we want to filter + // out too much data... + // - Change the y-axis unit (kilo/kibi, mega/mebi...) dynamically based on max load. + // + // The idea is we take the top value, build our scale such that each "point" is a scaled version of that. + // So for example, let's say I use 390 Mb/s. If I drew 4 segments, it would be 97.5, 195, 292.5, 390, and + // probably something like 438.75? + // + // So, how do we do this in tui-rs? Well, if we are using intervals that tie in perfectly to the max + // value we want... then it's actually not that hard. Since tui-rs accepts a vector as labels and will + // properly space them all out... we just work with that and space it out properly. + // + // Dynamic chart idea based off of FreeNAS's chart design. + // + // === + // + // For log data, we just use the old method of log intervals (kilo/mega/giga/etc.). Keep it nice and simple. - let filtered_tx = tx - .iter() - .cloned() - .filter(|(time, _data)| *time >= time_start && *time <= time_end); + // Now just check the largest unit we correspond to... then proceed to build some entries from there! - for (_time, data) in filtered_rx.clone().chain(filtered_tx.clone()) { - if data > max_val_bytes { - max_val_bytes = data; + let unit_char = match network_unit_type { + DataUnit::Byte => "B", + DataUnit::Bit => "b", + }; + + match network_scale_type { + AxisScaling::Linear => { + let (k_limit, m_limit, g_limit, t_limit) = if network_use_binary_prefix { + ( + KIBI_LIMIT_F64, + MEBI_LIMIT_F64, + GIBI_LIMIT_F64, + TEBI_LIMIT_F64, + ) + } else { + ( + KILO_LIMIT_F64, + MEGA_LIMIT_F64, + GIGA_LIMIT_F64, + TERA_LIMIT_F64, + ) + }; + + let bumped_max_entry = max_entry * 1.5; // We use the bumped up version to calculate our unit type. + let (max_value_scaled, unit_prefix, unit_type): (f64, &str, &str) = + if bumped_max_entry < k_limit { + (max_entry, "", unit_char) + } else if bumped_max_entry < m_limit { + ( + max_entry / k_limit, + if network_use_binary_prefix { "Ki" } else { "K" }, + unit_char, + ) + } else if bumped_max_entry < g_limit { + ( + max_entry / m_limit, + if network_use_binary_prefix { "Mi" } else { "M" }, + unit_char, + ) + } else if bumped_max_entry < t_limit { + ( + max_entry / g_limit, + if network_use_binary_prefix { "Gi" } else { "G" }, + unit_char, + ) + } else { + ( + max_entry / t_limit, + if network_use_binary_prefix { "Ti" } else { "T" }, + unit_char, + ) + }; + + // Finally, build an acceptable range starting from there, using the given height! + // Note we try to put more of a weight on the bottom section vs. the top, since the top has less data. + + let base_unit = max_value_scaled; + let labels: Vec<String> = vec![ + format!("0{}{}", unit_prefix, unit_type), + format!("{:.1}", base_unit * 0.5), + format!("{:.1}", base_unit), + format!("{:.1}", base_unit * 1.5), + ] + .into_iter() + .map(|s| format!("{:>5}", s)) // Pull 5 as the longest legend value is generally going to be 5 digits (if they somehow hit over 5 terabits per second) + .collect(); + + (bumped_max_entry, labels) } - } + AxisScaling::Log => { + let (m_limit, g_limit, t_limit) = if network_use_binary_prefix { + (LOG_MEBI_LIMIT, LOG_GIBI_LIMIT, LOG_TEBI_LIMIT) + } else { + (LOG_MEGA_LIMIT, LOG_GIGA_LIMIT, LOG_TERA_LIMIT) + }; - // FIXME [NETWORKING]: Granularity. Just scale up the values. - // FIXME [NETWORKING]: Ability to set fixed scale in config. - // Currently we do 32 -> 33... which skips some gigabit values - let true_max_val: f64; - let mut labels = vec![]; - if max_val_bytes < LOG_KIBI_LIMIT { - true_max_val = LOG_KIBI_LIMIT; - labels = vec!["0B".to_string(), "1KiB".to_string()]; - } else if max_val_bytes < LOG_MEBI_LIMIT { - true_max_val = LOG_MEBI_LIMIT; - labels = vec!["0B".to_string(), "1KiB".to_string(), "1MiB".to_string()]; - } else if max_val_bytes < LOG_GIBI_LIMIT { - true_max_val = LOG_GIBI_LIMIT; - labels = vec![ - "0B".to_string(), - "1KiB".to_string(), - "1MiB".to_string(), - "1GiB".to_string(), - ]; - } else if max_val_bytes < LOG_TEBI_LIMIT { - true_max_val = max_val_bytes.ceil() + 1.0; - let cap_u32 = true_max_val as u32; - - for i in 0..=cap_u32 { - match i { - 0 => labels.push("0B".to_string()), - LOG_KIBI_LIMIT_U32 => labels.push("1KiB".to_string()), - LOG_MEBI_LIMIT_U32 => labels.push("1MiB".to_string()), - LOG_GIBI_LIMIT_U32 => labels.push("1GiB".to_string()), - _ if i == cap_u32 => { - labels.push(format!("{}GiB", 2_u64.pow(cap_u32 - LOG_GIBI_LIMIT_U32))) - } - _ if i == (LOG_GIBI_LIMIT_U32 + cap_u32) / 2 => labels.push(format!( - "{}GiB", - 2_u64.pow(cap_u32 - ((LOG_GIBI_LIMIT_U32 + cap_u32) / 2)) - )), // ~Halfway point - _ => labels.push(String::default()), + fn get_zero(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "{}0{}", + if network_use_binary_prefix { " " } else { " " }, + unit_char + ) } - } - } else { - true_max_val = max_val_bytes.ceil() + 1.0; - let cap_u32 = true_max_val as u32; - for i in 0..=cap_u32 { - match i { - 0 => labels.push("0B".to_string()), - LOG_KIBI_LIMIT_U32 => labels.push("1KiB".to_string()), - LOG_MEBI_LIMIT_U32 => labels.push("1MiB".to_string()), - LOG_GIBI_LIMIT_U32 => labels.push("1GiB".to_string()), - LOG_TEBI_LIMIT_U32 => labels.push("1TiB".to_string()), - _ if i == cap_u32 => { - labels.push(format!("{}GiB", 2_u64.pow(cap_u32 - LOG_TEBI_LIMIT_U32))) - } - _ if i == (LOG_TEBI_LIMIT_U32 + cap_u32) / 2 => labels.push(format!( - "{}TiB", - 2_u64.pow(cap_u32 - ((LOG_TEBI_LIMIT_U32 + cap_u32) / 2)) - )), // ~Halfway point - _ => labels.push(String::default()), + fn get_k(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Ki" } else { "K" }, + unit_char + ) + } + + fn get_m(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Mi" } else { "M" }, + unit_char + ) + } + + fn get_g(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Gi" } else { "G" }, + unit_char + ) + } + + fn get_t(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Ti" } else { "T" }, + unit_char + ) + } + + fn get_p(network_use_binary_prefix: bool, unit_char: &str) -> String { + format!( + "1{}{}", + if network_use_binary_prefix { "Pi" } else { "P" }, + unit_char + ) + } + + if max_entry < m_limit { + ( + m_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + ], + ) + } else if max_entry < g_limit { + ( + g_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + ], + ) + } else if max_entry < t_limit { + ( + t_limit, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + get_t(network_use_binary_prefix, unit_char), + ], + ) + } else { + // I really doubt anyone's transferring beyond petabyte speeds... + ( + if network_use_binary_prefix { + LOG_PEBI_LIMIT + } else { + LOG_PETA_LIMIT + }, + vec![ + get_zero(network_use_binary_prefix, unit_char), + get_k(network_use_binary_prefix, unit_char), + get_m(network_use_binary_prefix, unit_char), + get_g(network_use_binary_prefix, unit_char), + get_t(network_use_binary_prefix, unit_char), + get_p(network_use_binary_prefix, unit_char), + ], + ) } } } - - (true_max_val, labels) } if let Some(network_widget_state) = app_state.net_state.widget_states.get_mut(&widget_id) { - let network_data_rx: &[(f64, f64)] = &app_state.canvas_data.network_data_rx; - let network_data_tx: &[(f64, f64)] = &app_state.canvas_data.network_data_tx; + let network_data_rx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_rx; + let network_data_tx: &mut [(f64, f64)] = &mut app_state.canvas_data.network_data_tx; + + let time_start = -(network_widget_state.current_display_time as f64); - let (max_range, labels) = adjust_network_data_point( - network_data_rx, - network_data_tx, - -(network_widget_state.current_display_time as f64), - 0.0, - ); let display_time_labels = vec![ Span::styled( format!("{}s", network_widget_state.current_display_time / 1000), @@ -190,29 +435,138 @@ impl NetworkGraphWidget for Painter { || (app_state.app_config_fields.autohide_time && network_widget_state.autohide_timer.is_none()) { - Axis::default().bounds([-(network_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else if let Some(time) = network_widget_state.autohide_timer { if std::time::Instant::now().duration_since(time).as_millis() < AUTOHIDE_TIMEOUT_MILLISECONDS as u128 { Axis::default() - .bounds([-(network_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) } else { network_widget_state.autohide_timer = None; - Axis::default() - .bounds([-(network_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } } else if draw_loc.height < TIME_LABEL_HEIGHT_LIMIT { - Axis::default().bounds([-(network_widget_state.current_display_time as f64), 0.0]) + Axis::default().bounds([time_start, 0.0]) } else { Axis::default() - .bounds([-(network_widget_state.current_display_time as f64), 0.0]) + .bounds([time_start, 0.0]) .style(self.colours.graph_style) .labels(display_time_labels) }; + // Interpolate a point for rx and tx between the last value outside of the left bounds and the first value + // inside it. + // Because we assume it is all in order for... basically all our code, we can't just append it, + // and insertion in the middle seems. So instead, we swap *out* the value that is outside with our + // interpolated point, draw and do whatever calculations, then swap back in the old value! + // + // Note there is some re-used work here! For potential optimizations, we could re-use some work here in/from + // get_max_entry... + let interpolated_rx_point = if let Some(rx_end_pos) = network_data_rx + .iter() + .position(|(time, _data)| *time >= time_start) + { + if rx_end_pos > 1 { + let rx_start_pos = rx_end_pos - 1; + let outside_rx_point = network_data_rx.get(rx_start_pos); + let inside_rx_point = network_data_rx.get(rx_end_pos); + + if let (Some(outside_rx_point), Some(inside_rx_point)) = + (outside_rx_point, inside_rx_point) + { + let old = *outside_rx_point; + + let new_point = ( + time_start, + interpolate_points(outside_rx_point, inside_rx_point, time_start), + ); + + // debug!( + // "Interpolated between {:?} and {:?}, got rx for time {:?}: {:?}", + // outside_rx_point, inside_rx_point, time_start, new_point + // ); + + if let Some(to_replace) = network_data_rx.get_mut(rx_start_pos) { + *to_replace = new_point; + Some((rx_start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our network_data_rx + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + + let interpolated_tx_point = if let Some(tx_end_pos) = network_data_tx + .iter() + .position(|(time, _data)| *time >= time_start) + { + if tx_end_pos > 1 { + let tx_start_pos = tx_end_pos - 1; + let outside_tx_point = network_data_tx.get(tx_start_pos); + let inside_tx_point = network_data_tx.get(tx_end_pos); + + if let (Some(outside_tx_point), Some(inside_tx_point)) = + (outside_tx_point, inside_tx_point) + { + let old = *outside_tx_point; + + let new_point = ( + time_start, + interpolate_points(outside_tx_point, inside_tx_point, time_start), + ); + + if let Some(to_replace) = network_data_tx.get_mut(tx_start_pos) { + *to_replace = new_point; + Some((tx_start_pos, old)) + } else { + None // Failed to get mutable reference. + } + } else { + None // Point somehow doesn't exist in our network_data_tx + } + } else { + None // Point is already "leftmost", no need to interpolate. + } + } else { + None // There is no point. + }; + + // TODO: Cache network results: Only update if: + // - Force update (includes time interval change) + // - Old max time is off screen + // - A new time interval is better and does not fit (check from end of vector to last checked; we only want to update if it is TOO big!) + + // Find the maximal rx/tx so we know how to scale, and return it. + + let (_best_time, max_entry) = get_max_entry( + network_data_rx, + network_data_tx, + time_start, + &app_state.app_config_fields.network_scale_type, + app_state.app_config_fields.network_use_binary_prefix, + ); + + let (max_range, labels) = adjust_network_data_point( + max_entry, + &app_state.app_config_fields.network_scale_type, + &app_state.app_config_fields.network_unit_type, + app_state.app_config_fields.network_use_binary_prefix, + ); + + // Cache results. + // network_widget_state.draw_max_range_cache = max_range; + // network_widget_state.draw_time_start_cache = best_time; + // network_widget_state.draw_labels_cache = labels; + let y_axis_labels = labels .iter() .map(|label| Span::styled(label, self.colours.graph_style)) @@ -250,12 +604,12 @@ impl NetworkGraphWidget for Painter { let legend_constraints = if hide_legend { (Constraint::Ratio(0, 1), Constraint::Ratio(0, 1)) } else { - (Constraint::Ratio(3, 4), Constraint::Ratio(3, 4)) + (Constraint::Ratio(1, 1), Constraint::Ratio(3, 4)) }; + // TODO: Add support for clicking on legend to only show that value on chart. let dataset = if app_state.app_config_fields.use_old_network_legend && !hide_legend { - let mut ret_val = vec![]; - ret_val.push( + vec![ Dataset::default() .name(format!("RX: {:7}", app_state.canvas_data.rx_display)) .marker(if app_state.app_config_fields.use_dot { @@ -266,9 +620,6 @@ impl NetworkGraphWidget for Painter { .style(self.colours.rx_style) .data(&network_data_rx) .graph_type(tui::widgets::GraphType::Line), - ); - - ret_val.push( Dataset::default() .name(format!("TX: {:7}", app_state.canvas_data.tx_display)) .marker(if app_state.app_config_fields.use_dot { @@ -279,30 +630,21 @@ impl NetworkGraphWidget for Painter { .style(self.colours.tx_style) .data(&network_data_tx) .graph_type(tui::widgets::GraphType::Line), - ); - ret_val.push( Dataset::default() .name(format!( "Total RX: {:7}", app_state.canvas_data.total_rx_display )) .style(self.colours.total_rx_style), - ); - - ret_val.push( Dataset::default() .name(format!( "Total TX: {:7}", app_state.canvas_data.total_tx_display )) .style(self.colours.total_tx_style), - ); - - ret_val + ] } else { - let mut ret_val = vec![]; - - ret_val.push( + vec![ Dataset::default() .name(&app_state.canvas_data.rx_display) .marker(if app_state.app_config_fields.use_dot { @@ -313,9 +655,6 @@ impl NetworkGraphWidget for Painter { .style(self.colours.rx_style) .data(&network_data_rx) .graph_type(tui::widgets::GraphType::Line), - ); - - ret_val.push( Dataset::default() .name(&app_state.canvas_data.tx_display) .marker(if app_state.app_config_fields.use_dot { @@ -326,9 +665,7 @@ impl NetworkGraphWidget for Painter { .style(self.colours.tx_style) .data(&network_data_tx) .graph_type(tui::widgets::GraphType::Line), - ); - - ret_val + ] }; f.render_widget( @@ -348,10 +685,22 @@ impl NetworkGraphWidget for Painter { .hidden_legend_constraints(legend_constraints), draw_loc, ); + + // Now if you're done, reset any interpolated points! + if let Some((index, old_value)) = interpolated_rx_point { + if let Some(to_replace) = network_data_rx.get_mut(index) { + *to_replace = old_value; + } + } + + if let Some((index, old_value)) = interpolated_tx_point { + if let Some(to_replace) = network_data_tx.get_mut(index) { + *to_replace = old_value; + } + } } } - // TODO: [DEPRECATED] Get rid of this in, like, 0.6...? fn draw_network_labels<B: Backend>( &self, f: &mut Frame<'_, B>, app_state: &mut App, draw_loc: Rect, widget_id: u64, ) { diff --git a/src/clap.rs b/src/clap.rs index de1b5448..98ec58ef 100644 --- a/src/clap.rs +++ b/src/clap.rs @@ -18,6 +18,7 @@ pub fn get_matches() -> clap::ArgMatches<'static> { build_app().get_matches() } +// TODO: Refactor this a bit, it's quite messy atm pub fn build_app() -> App<'static, 'static> { // Temps let kelvin = Arg::with_name("kelvin") @@ -383,6 +384,30 @@ The minimum is 1s (1000), and defaults to 15s (15000).\n\n\n", Defaults to showing the process widget in tree mode.\n\n", ); + let network_use_bytes = Arg::with_name("network_use_bytes") + .long("network_use_bytes") + .help("Displays the network widget using bytes.") + .long_help( + "\ +Displays the network widget using bytes. Defaults to bits.\n\n", + ); + + let network_use_log = Arg::with_name("network_use_log") + .long("network_use_log") + .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.\n\n", + ); + + let network_use_binary_prefix = Arg::with_name("network_use_binary_prefix") + .long("network_use_binary_prefix") + .help("Displays the network widget with binary prefixes.") + .long_help( + "\ +Displays the network widget with binary prefixes (i.e. kibibits, mebibits) rather than a decimal prefix (i.e. kilobits, megabits). Defaults to decimal prefixes.\n\n\n", + ); + App::new(crate_name!()) .setting(AppSettings::UnifiedHelpMessage) .version(crate_version!()) @@ -422,6 +447,9 @@ Defaults to showing the process widget in tree mode.\n\n", .arg(regex) .arg(time_delta) .arg(tree) + .arg(network_use_bytes) + .arg(network_use_log) + .arg(network_use_binary_prefix) .arg(current_usage) .arg(use_old_network_legend) .arg(whole_word) diff --git a/src/constants.rs b/src/constants.rs index fa6d4b6c..c9afbeea 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -474,6 +474,12 @@ pub const OLD_CONFIG_TEXT: &str = r##"# This is a default config file for bottom #show_table_scroll_position = false # Show processes as their commands by default in the process widget. #process_command = false +# Displays the network widget with binary prefixes. +#network_use_binary_prefix = false +# Displays the network widget using bytes. +#network_use_bytes = false +# Displays the network widget with a log scale. +#network_use_log = false # These are all the components that support custom theming. Note that colour support # will depend on terminal support. diff --git a/src/data_conversion.rs b/src/data_conversion.rs index c3665cda..723743e4 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -1,6 +1,6 @@ //! This mainly concerns converting collected data into things that the canvas //! can actually handle. -use crate::Pid; +use crate::{app::AxisScaling, units::data_units::DataUnit, Pid}; use crate::{ app::{data_farmer, data_harvester, App, ProcWidgetState}, utils::{self, gen_util::*}, @@ -118,13 +118,13 @@ pub fn convert_disk_row(current_data: &data_farmer::DataCollection) -> Vec<Vec<S .zip(¤t_data.io_labels) .for_each(|(disk, (io_read, io_write))| { let free_space_fmt = if let Some(free_space) = disk.free_space { - let converted_free_space = get_simple_byte_values(free_space, false); + let converted_free_space = get_decimal_bytes(free_space); format!("{:.*}{}", 0, converted_free_space.0, converted_free_space.1) } else { "N/A".to_string() }; let total_space_fmt = if let Some(total_space) = disk.total_space { - let converted_total_space = get_simple_byte_values(total_space, false); + let converted_total_space = get_decimal_bytes(total_space); format!( "{:.*}{}", 0, converted_total_space.0, converted_total_space.1 @@ -298,18 +298,22 @@ pub fn convert_swap_data_points( pub fn convert_mem_labels( current_data: &data_farmer::DataCollection, ) -> (Option<(String, String)>, Option<(String, String)>) { - fn return_unit_and_numerator_for_kb(mem_total_kb: u64) -> (&'static str, f64) { - if mem_total_kb < 1024 { - // Stay with KB + /// Returns the unit type and denominator for given total amount of memory in kibibytes. + /// + /// Yes, this function is a bit of a lie. But people seem to generally expect, say, GiB when what they actually + /// wanted calculated was GiB. + fn return_unit_and_denominator_for_mem_kib(mem_total_kib: u64) -> (&'static str, f64) { + if mem_total_kib < 1024 { + // Stay with KiB ("KB", 1.0) - } else if mem_total_kb < 1_048_576 { - // Use MB + } else if mem_total_kib < 1_048_576 { + // Use MiB ("MB", 1024.0) - } else if mem_total_kb < 1_073_741_824 { - // Use GB + } else if mem_total_kib < 1_073_741_824 { + // Use GiB ("GB", 1_048_576.0) } else { - // Use TB + // Use TiB ("TB", 1_073_741_824.0) } } @@ -328,15 +332,15 @@ pub fn convert_mem_labels( } ), { - let (unit, numerator) = return_unit_and_numerator_for_kb( + let (unit, denominator) = return_unit_and_denominator_for_mem_kib( current_data.memory_harvest.mem_total_in_kib, ); format!( " {:.1}{}/{:.1}{}", - current_data.memory_harvest.mem_used_in_kib as f64 / numerator, + current_data.memory_harvest.mem_used_in_kib as f64 / denominator, unit, - (current_data.memory_harvest.mem_total_in_kib as f64 / numerator), + (current_data.memory_harvest.mem_total_in_kib as f64 / denominator), unit ) }, @@ -357,7 +361,7 @@ pub fn convert_mem_labels( } ), { - let (unit, numerator) = return_unit_and_numerator_for_kb( + let (unit, numerator) = return_unit_and_denominator_for_mem_kib( current_data.swap_harvest.mem_total_in_kib, ); @@ -377,7 +381,8 @@ pub fn convert_mem_labels( } pub fn get_rx_tx_data_points( - current_data: &data_farmer::DataCollection, is_frozen: bool, + current_data: &data_farmer::DataCollection, is_frozen: bool, network_scale_type: &AxisScaling, + network_unit_type: &DataUnit, network_use_binary_prefix: bool, ) -> (Vec<Point>, Vec<Point>) { let mut rx: Vec<Point> = Vec::new(); let mut tx: Vec<Point> = Vec::new(); @@ -394,8 +399,34 @@ pub fn get_rx_tx_data_points( for (time, data) in ¤t_data.timed_data_vec { let time_from_start: f64 = (current_time.duration_since(*time).as_millis() as f64).floor(); - rx.push((-time_from_start, data.rx_data)); - tx.push((-time_from_start, data.tx_data)); + + let (rx_data, tx_data) = match network_scale_type { + AxisScaling::Log => { + if network_use_binary_prefix { + match network_unit_type { + DataUnit::Byte => { + // As dividing by 8 is equal to subtracting 4 in base 2! + ((data.rx_data).log2() - 4.0, (data.tx_data).log2() - 4.0) + } + DataUnit::Bit => ((data.rx_data).log2(), (data.tx_data).log2()), + } + } else { + match network_unit_type { + DataUnit::Byte => { + ((data.rx_data / 8.0).log10(), (data.tx_data / 8.0).log10()) + } + DataUnit::Bit => ((data.rx_data).log10(), (data.tx_data).log10()), + } + } + } + AxisScaling::Linear => match network_unit_type { + DataUnit::Byte => (data.rx_data / 8.0, data.tx_data / 8.0), + DataUnit::Bit => (data.rx_data, data.tx_data), + }, + }; + + rx.push((-time_from_start, rx_data)); + tx.push((-time_from_start, tx_data)); if *time == current_time { break; } @@ -406,19 +437,62 @@ pub fn get_rx_tx_data_points( pub fn convert_network_data_points( current_data: &data_farmer::DataCollection, is_frozen: bool, need_four_points: bool, + network_scale_type: &AxisScaling, network_unit_type: &DataUnit, + network_use_binary_prefix: bool, ) -> ConvertedNetworkData { - let (rx, tx) = get_rx_tx_data_points(current_data, is_frozen); + let (rx, tx) = get_rx_tx_data_points( + current_data, + is_frozen, + network_scale_type, + network_unit_type, + network_use_binary_prefix, + ); - let total_rx_converted_result: (f64, String); - let rx_converted_result: (f64, String); - let total_tx_converted_result: (f64, String); - let tx_converted_result: (f64, String); + let unit = match network_unit_type { + DataUnit::Byte => "B", + DataUnit::Bit => "b", + }; - rx_converted_result = get_exact_byte_values(current_data.network_harvest.rx, false); - total_rx_converted_result = get_exact_byte_values(current_data.network_harvest.total_rx, false); + let (rx_data, tx_data, total_rx_data, total_tx_data) = match network_unit_type { + DataUnit::Byte => ( + current_data.network_harvest.rx / 8, + current_data.network_harvest.tx / 8, + current_data.network_harvest.total_rx / 8, + current_data.network_harvest.total_tx / 8, + ), + DataUnit::Bit => ( + current_data.network_harvest.rx, + current_data.network_harvest.tx, + current_data.network_harvest.total_rx / 8, // We always make this bytes... + current_data.network_harvest.total_tx / 8, + ), + }; - tx_converted_result = get_exact_byte_values(current_data.network_harvest.tx, false); - total_tx_converted_result = get_exact_byte_values(current_data.network_harvest.total_tx, false); + let (rx_converted_result, total_rx_converted_result): ((f64, String), (f64, String)) = + if network_use_binary_prefix { + ( + get_binary_prefix(rx_data, unit), // If this isn't obvious why there's two functions, one you can configure the unit, the other is always bytes + get_binary_bytes(total_rx_data), + ) + } else { + ( + get_decimal_prefix(rx_data, unit), + get_decimal_bytes(total_rx_data), + ) + }; + + let (tx_converted_result, total_tx_converted_result): ((f64, String), (f64, String)) = + if network_use_binary_prefix { + ( + get_binary_prefix(tx_data, unit), + get_binary_bytes(total_tx_data), + ) + } else { + ( + get_decimal_prefix(tx_data, unit), + get_decimal_bytes(total_tx_data), + ) + }; if need_four_points { let rx_display = format!("{:.*}{}", 1, rx_converted_result.0, rx_converted_result.1); @@ -441,20 +515,42 @@ pub fn convert_network_data_points( } } else { let rx_display = format!( - "RX: {:<9} All: {:<9}", - format!("{:.1}{:3}", rx_converted_result.0, rx_converted_result.1), - format!( - "{:.1}{:3}", - total_rx_converted_result.0, total_rx_converted_result.1 - ) + "RX: {:<8} All: {}", + if network_use_binary_prefix { + format!("{:.1}{:3}", rx_converted_result.0, rx_converted_result.1) + } else { + format!("{:.1}{:2}", rx_converted_result.0, rx_converted_result.1) + }, + if network_use_binary_prefix { + format!( + "{:.1}{:3}", + total_rx_converted_result.0, total_rx_converted_result.1 + ) + } else { + format!( + "{:.1}{:2}", + total_rx_converted_result.0, total_rx_converted_result.1 + ) + } ); let tx_display = format!( - "TX: {:<9} All: {:<9}", - format!("{:.1}{:3}", tx_converted_result.0, tx_converted_result.1), - format!( - "{:.1}{:3}", - total_tx_converted_result.0, total_tx_converted_result.1 - ) + "TX: {:<8} All: {}", + if network_use_binary_prefix { + format!("{:.1}{:3}", tx_converted_result.0, tx_converted_result.1) + } else { + format!("{:.1}{:2}", tx_converted_result.0, tx_converted_result.1) + }, + if network_use_binary_prefix { + format!( + "{:.1}{:3}", + total_tx_converted_result.0, total_tx_converted_result.1 + ) + } else { + format!( + "{:.1}{:2}", + total_tx_converted_result.0, total_tx_converted_result.1 + ) + } ); ConvertedNetworkData { @@ -492,10 +588,10 @@ pub fn convert_process_data( existing_converted_process_data.keys().copied().collect(); for process in ¤t_data.process_harvest { - let converted_rps = get_exact_byte_values(process.read_bytes_per_sec, false); - let converted_wps = get_exact_byte_values(process.write_bytes_per_sec, false); - let converted_total_read = get_exact_byte_values(process.total_read_bytes, false); - let converted_total_write = get_exact_byte_values(process.total_write_bytes, false); + let converted_rps = get_binary_bytes(process.read_bytes_per_sec); + let converted_wps = get_binary_bytes(process.write_bytes_per_sec); + let converted_total_read = get_binary_bytes(process.total_read_bytes); + let converted_total_write = get_binary_bytes(process.total_write_bytes); let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1); let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1); @@ -530,7 +626,7 @@ pub fn convert_process_data( process_entry.cpu_percent_usage = process.cpu_usage_percent; process_entry.mem_percent_usage = process.mem_usage_percent; process_entry.mem_usage_bytes = process.mem_usage_bytes; - process_entry.mem_usage_str = get_exact_byte_values(process.mem_usage_bytes, false); + process_entry.mem_usage_str = get_binary_bytes(process.mem_usage_bytes); process_entry.group_pids = vec![process.pid]; process_entry.read_per_sec = read_per_sec; process_entry.write_per_sec = write_per_sec; @@ -556,7 +652,7 @@ pub fn convert_process_data( cpu_percent_usage: process.cpu_usage_percent, mem_percent_usage: process.mem_usage_percent, mem_usage_bytes: process.mem_usage_bytes, - mem_usage_str: get_exact_byte_values(process.mem_usage_bytes, false), + mem_usage_str: get_binary_bytes(process.mem_usage_bytes), group_pids: vec![process.pid], read_per_sec, write_per_sec, @@ -586,7 +682,7 @@ pub fn convert_process_data( cpu_percent_usage: process.cpu_usage_percent, mem_percent_usage: process.mem_usage_percent, mem_usage_bytes: process.mem_usage_bytes, - mem_usage_str: get_exact_byte_values(process.mem_usage_bytes, false), + mem_usage_str: get_binary_bytes(process.mem_usage_bytes), group_pids: vec![process.pid], read_per_sec, write_per_sec, @@ -1085,10 +1181,10 @@ pub fn group_process_data( .iter() .map(|(identifier, process_details)| { let p = process_details.clone(); - let converted_rps = get_exact_byte_values(p.read_per_sec as u64, false); - let converted_wps = get_exact_byte_values(p.write_per_sec as u64, false); - let converted_total_read = get_exact_byte_values(p.total_read as u64, false); - let converted_total_write = get_exact_byte_values(p.total_write as u64, false); + let converted_rps = get_binary_bytes(p.read_per_sec as u64); + let converted_wps = get_binary_bytes(p.write_per_sec as u64); + let converted_total_read = get_binary_bytes(p.total_read as u64); + let converted_total_write = get_binary_bytes(p.total_write as u64); let read_per_sec = format!("{:.*}{}/s", 0, converted_rps.0, converted_rps.1); let write_per_sec = format!("{:.*}{}/s", 0, converted_wps.0, converted_wps.1); @@ -1107,7 +1203,7 @@ pub fn group_process_data( cpu_percent_usage: p.cpu_percent_usage, mem_percent_usage: p.mem_percent_usage, mem_usage_bytes: p.mem_usage_bytes, - mem_usage_str: get_exact_byte_values(p.mem_usage_bytes, false), + mem_usage_str: get_binary_bytes(p.mem_usage_bytes), group_pids: p.group_pids, read_per_sec, write_per_sec, diff --git a/src/lib.rs b/src/lib.rs index a3b951f2..4db58883 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,7 @@ pub mod clap; pub mod constants; pub mod data_conversion; pub mod options; +pub mod units; #[cfg(target_family = "windows")] pub type Pid = usize; @@ -326,7 +327,13 @@ pub fn handle_force_redraws(app: &mut App) { } if app.net_state.force_update.is_some() { - let (rx, tx) = get_rx_tx_data_points(&app.data_collection, app.is_frozen); + let (rx, tx) = get_rx_tx_data_points( + &app.data_collection, + app.is_frozen, + &app.app_config_fields.network_scale_type, + &app.app_config_fields.network_unit_type, + app.app_config_fields.network_use_binary_prefix, + ); app.canvas_data.network_data_rx = rx; app.canvas_data.network_data_tx = tx; app.net_state.force_update = None; @@ -352,18 +359,21 @@ pub fn update_all_process_lists(app: &mut App) { } fn update_final_process_list(app: &mut App, widget_id: u64) { - let process_states = match app.proc_state.widget_states.get(&widget_id) { - Some(process_state) => Some(( - process_state - .process_search_state - .search_state - .is_invalid_or_blank_search(), - process_state.is_using_command, - process_state.is_grouped, - process_state.is_tree_mode, - )), - None => None, - }; + let process_states = app + .proc_state + .widget_states + .get(&widget_id) + .map(|process_state| { + ( + process_state + .process_search_state + .search_state + .is_invalid_or_blank_search(), + process_state.is_using_command, + process_state.is_grouped, + process_state.is_tree_mode, + ) + }); if let Some((is_invalid_or_blank, is_using_command, is_grouped, is_tree)) = process_states { if !app.is_frozen { diff --git a/src/options.rs b/src/options.rs index badc32b8..7855a8da 100644 --- a/src/options.rs +++ b/src/options.rs @@ -12,6 +12,7 @@ use crate::{ app::{layout_manager::*, *}, canvas::ColourScheme, constants::*, + units::data_units::DataUnit, utils::error::{self, BottomError}, }; @@ -157,6 +158,15 @@ pub struct ConfigFlags { #[builder(default, setter(strip_option))] pub advanced_kill: Option<bool>, + + #[builder(default, setter(strip_option))] + pub network_use_bytes: Option<bool>, + + #[builder(default, setter(strip_option))] + pub network_use_log: Option<bool>, + + #[builder(default, setter(strip_option))] + pub network_use_binary_prefix: Option<bool>, } #[derive(Clone, Default, Debug, Deserialize, Serialize)] @@ -265,6 +275,10 @@ pub fn build_app( let is_default_command = get_is_default_process_command(matches, config); let is_advanced_kill = get_is_using_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 = get_network_use_binary_prefix(matches, config); + for row in &widget_layout.rows { for col in &row.children { for col_row in &col.children { @@ -319,7 +333,12 @@ pub fn build_app( Net => { net_state_map.insert( widget.widget_id, - NetWidgetState::init(default_time_value, autohide_timer), + NetWidgetState::init( + default_time_value, + autohide_timer, + // network_unit_type.clone(), + // network_scale_type.clone(), + ), ); } Proc => { @@ -404,6 +423,9 @@ pub fn build_app( no_write: false, show_table_scroll_position: get_show_table_scroll_position(matches, config), is_advanced_kill, + network_scale_type, + network_unit_type, + network_use_binary_prefix, }; let used_widgets = UsedWidgets { @@ -818,11 +840,9 @@ fn get_default_widget_and_count( let widget_count = if let Some(widget_count) = matches.value_of("default_widget_count") { Some(widget_count.parse::<u128>()?) } else if let Some(flags) = &config.flags { - if let Some(widget_count) = flags.default_widget_count { - Some(widget_count as u128) - } else { - None - } + flags + .default_widget_count + .map(|widget_count| widget_count as u128) } else { None }; @@ -1031,3 +1051,42 @@ fn get_is_using_advanced_kill(matches: &clap::ArgMatches<'static>, config: &Conf } false } + +fn get_network_unit_type(matches: &clap::ArgMatches<'static>, config: &Config) -> DataUnit { + if matches.is_present("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: &clap::ArgMatches<'static>, config: &Config) -> AxisScaling { + if matches.is_present("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_network_use_binary_prefix(matches: &clap::ArgMatches<'static>, config: &Config) -> bool { + if matches.is_present("network_use_binary_prefix") { + return true; + } else if let Some(flags) = &config.flags { + if let Some(network_use_binary_prefix) = flags.network_use_binary_prefix { + return network_use_binary_prefix; + } + } + false +} diff --git a/src/units.rs b/src/units.rs new file mode 100644 index 00000000..937154de --- /dev/null +++ b/src/units.rs @@ -0,0 +1 @@ +pub mod data_units; diff --git a/src/units/data_units.rs b/src/units/data_units.rs new file mode 100644 index 00000000..d0b7c1e1 --- /dev/null +++ b/src/units/data_units.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone)] +pub enum DataUnit { + Byte, + Bit, +} diff --git a/src/utils/error.rs b/src/utils/error.rs index 1fc75bd7..c9273438 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -10,7 +10,7 @@ pub type Result<T> = result::Result<T, BottomError>; pub enum BottomError { /// An error when there is an IO exception. #[error("IO exception, {0}")] - InvalidIO(String), + InvalidIo(String), /// An error when the heim library encounters a problem. #[error("Error caused by Heim, {0}")] InvalidHeim(String), @@ -39,7 +39,7 @@ pub enum BottomError { impl From<std::io::Error> for BottomError { fn from(err: std::io::Error) -> Self { - BottomError::InvalidIO(err.to_string()) + BottomError::InvalidIo(err.to_string()) } } diff --git a/src/utils/gen_util.rs b/src/utils/gen_util.rs index 2e13968f..e0265290 100644 --- a/src/utils/gen_util.rs +++ b/src/utils/gen_util.rs @@ -9,15 +9,26 @@ pub const MEBI_LIMIT: u64 = 1_048_576; pub const GIBI_LIMIT: u64 = 1_073_741_824; pub const TEBI_LIMIT: u64 = 1_099_511_627_776; +pub const KILO_LIMIT_F64: f64 = 1000.0; +pub const MEGA_LIMIT_F64: f64 = 1_000_000.0; +pub const GIGA_LIMIT_F64: f64 = 1_000_000_000.0; +pub const TERA_LIMIT_F64: f64 = 1_000_000_000_000.0; +pub const KIBI_LIMIT_F64: f64 = 1024.0; +pub const MEBI_LIMIT_F64: f64 = 1_048_576.0; +pub const GIBI_LIMIT_F64: f64 = 1_073_741_824.0; +pub const TEBI_LIMIT_F64: f64 = 1_099_511_627_776.0; + pub const LOG_KILO_LIMIT: f64 = 3.0; pub const LOG_MEGA_LIMIT: f64 = 6.0; pub const LOG_GIGA_LIMIT: f64 = 9.0; pub const LOG_TERA_LIMIT: f64 = 12.0; +pub const LOG_PETA_LIMIT: f64 = 15.0; pub const LOG_KIBI_LIMIT: f64 = 10.0; pub const LOG_MEBI_LIMIT: f64 = 20.0; pub const LOG_GIBI_LIMIT: f64 = 30.0; pub const LOG_TEBI_LIMIT: f64 = 40.0; +pub const LOG_PEBI_LIMIT: f64 = 50.0; pub const LOG_KILO_LIMIT_U32: u32 = 3; pub const LOG_MEGA_LIMIT_U32: u32 = 6; @@ -29,40 +40,12 @@ pub const LOG_MEBI_LIMIT_U32: u32 = 20; pub const LOG_GIBI_LIMIT_U32: u32 = 30; pub const LOG_TEBI_LIMIT_U32: u32 = 40; -pub fn float_min(a: f32, b: f32) -> f32 { - match a.partial_cmp(&b) { - Some(x) => match x { - Ordering::Greater => b, - Ordering::Less => a, - Ordering::Equal => a, - }, - None => a, - } -} - -pub fn float_max(a: f32, b: f32) -> f32 { - match a.partial_cmp(&b) { - Some(x) => match x { - Ordering::Greater => a, - Ordering::Less => b, - Ordering::Equal => a, - }, - None => a, - } -} - -/// Returns a tuple containing the value and the unit. In units of 1024. -/// This only supports up to a tebibyte. -pub fn get_exact_byte_values(bytes: u64, spacing: bool) -> (f64, String) { +/// Returns a tuple containing the value and the unit in bytes. In units of 1024. +/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if +/// `spacing` is true. +pub fn get_binary_bytes(bytes: u64) -> (f64, String) { match bytes { - b if b < KIBI_LIMIT => ( - bytes as f64, - if spacing { - " B".to_string() - } else { - "B".to_string() - }, - ), + b if b < KIBI_LIMIT => (bytes as f64, "B".to_string()), b if b < MEBI_LIMIT => (bytes as f64 / 1024.0, "KiB".to_string()), b if b < GIBI_LIMIT => (bytes as f64 / 1_048_576.0, "MiB".to_string()), b if b < TERA_LIMIT => (bytes as f64 / 1_073_741_824.0, "GiB".to_string()), @@ -70,18 +53,12 @@ pub fn get_exact_byte_values(bytes: u64, spacing: bool) -> (f64, String) { } } -/// Returns a tuple containing the value and the unit. In units of 1000. -/// This only supports up to a terabyte. Note the "byte" unit will have a space appended to match the others. -pub fn get_simple_byte_values(bytes: u64, spacing: bool) -> (f64, String) { +/// Returns a tuple containing the value and the unit in bytes. In units of 1000. +/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if +/// `spacing` is true. +pub fn get_decimal_bytes(bytes: u64) -> (f64, String) { match bytes { - b if b < KILO_LIMIT => ( - bytes as f64, - if spacing { - " B".to_string() - } else { - "B".to_string() - }, - ), + b if b < KILO_LIMIT => (bytes as f64, "B".to_string()), b if b < MEGA_LIMIT => (bytes as f64 / 1000.0, "KB".to_string()), b if b < GIGA_LIMIT => (bytes as f64 / 1_000_000.0, "MB".to_string()), b if b < TERA_LIMIT => (bytes as f64 / 1_000_000_000.0, "GB".to_string()), @@ -89,21 +66,49 @@ pub fn get_simple_byte_values(bytes: u64, spacing: bool) -> (f64, String) { } } +/// Returns a tuple containing the value and the unit. In units of 1024. +/// This only supports up to a tebi. Note the "single" unit will have a space appended to match the others if +/// `spacing` is true. +pub fn get_binary_prefix(quantity: u64, unit: &str) -> (f64, String) { + match quantity { + b if b < KIBI_LIMIT => (quantity as f64, unit.to_string()), + b if b < MEBI_LIMIT => (quantity as f64 / 1024.0, format!("Ki{}", unit)), + b if b < GIBI_LIMIT => (quantity as f64 / 1_048_576.0, format!("Mi{}", unit)), + b if b < TERA_LIMIT => (quantity as f64 / 1_073_741_824.0, format!("Gi{}", unit)), + _ => (quantity as f64 / 1_099_511_627_776.0, format!("Ti{}", unit)), + } +} + +/// Returns a tuple containing the value and the unit. In units of 1000. +/// This only supports up to a tera. Note the "single" unit will have a space appended to match the others if +/// `spacing` is true. +pub fn get_decimal_prefix(quantity: u64, unit: &str) -> (f64, String) { + match quantity { + b if b < KILO_LIMIT => (quantity as f64, unit.to_string()), + b if b < MEGA_LIMIT => (quantity as f64 / 1000.0, format!("K{}", unit)), + b if b < GIGA_LIMIT => (quantity as f64 / 1_000_000.0, format!("M{}", unit)), + b if b < TERA_LIMIT => (quantity as f64 / 1_000_000_000.0, format!("G{}", unit)), + _ => (quantity as f64 / 1_000_000_000_000.0, format!("T{}", unit)), + } +} + /// Gotta get partial ordering? No problem, here's something to deal with it~ +/// +/// Note that https://github.com/reem/rust-ordered-float exists, maybe move to it one day? IDK. pub fn get_ordering<T: std::cmp::PartialOrd>( - a_val: T, b_val: T, descending_order: bool, + a_val: T, b_val: T, reverse_order: bool, ) -> std::cmp::Ordering { match a_val.partial_cmp(&b_val) { Some(x) => match x { Ordering::Greater => { - if descending_order { + if reverse_order { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater } } Ordering::Less => { - if descending_order { + if reverse_order { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less