diff --git a/CHANGELOG.md b/CHANGELOG.md index d3a45984..e6273c0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ That said, these are more guidelines rather than hard rules, though the project --- +## 0.14.9/0.15.0 - Unreleased + +### Features + +- [#2199](https://github.com/ClementTsang/bottom/pull/2199): Add option to show zeroed total network usage on startup. + ## 0.14.8 - 2026-08-13 ### Bug Fixes @@ -29,7 +35,7 @@ That said, these are more guidelines rather than hard rules, though the project ### Other -- [#2914](https://github.com/ClementTsang/bottom/pull/2194): Fix schema links + link generation. +- [#2194](https://github.com/ClementTsang/bottom/pull/2194): Fix schema links + link generation. ## 0.14.7 - 2026-07-27 diff --git a/docs/content/configuration/command-line-options.md b/docs/content/configuration/command-line-options.md index 19f16369..42800931 100644 --- a/docs/content/configuration/command-line-options.md +++ b/docs/content/configuration/command-line-options.md @@ -71,14 +71,15 @@ see information on these options by running `btm -h`, or run `btm --help` to dis ## Network Options -| Option | Behaviour | -| ----------------------------- | ------------------------------------------------------- | -| `--network_legend ` | Where to place the legend for the network chart widget. | -| `--network_use_bytes` | Displays the network widget using bytes. | -| `--network_use_binary_prefix` | Displays the network widget with binary prefixes. | -| `--network_use_log` | Displays the network widget with a log scale. | -| `--show_packets` | Displays packet rate and average packet size info. | -| `--use_old_network_legend` | Uses a separate network legend. | +| Option | Behaviour | +| ----------------------------- | -------------------------------------------------------------- | +| `--network_legend ` | Where to place the legend for the network chart widget. | +| `--network_use_bytes` | Displays the network widget using bytes. | +| `--network_use_binary_prefix` | Displays the network widget with binary prefixes. | +| `--network_use_log` | Displays the network widget with a log scale. | +| `--show_packets` | Displays packet rate and average packet size info. | +| `--use_old_network_legend` | Uses a separate network legend. | +| `--network_start_zeroed` | Show total network usage from app startup rather than on boot. | ## Battery Options diff --git a/docs/content/configuration/config-file/network-graph.md b/docs/content/configuration/config-file/network-graph.md index bc85a0a1..6f3c5a20 100644 --- a/docs/content/configuration/config-file/network-graph.md +++ b/docs/content/configuration/config-file/network-graph.md @@ -4,13 +4,14 @@ If you want to change some of the default behaviour of the network graph widget, you can configure some things in the config file. -| Field | Type | Functionality | -| ------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | -| `show_packets` | Boolean | Displays packet rate and average packet size info. | -| `legend_position` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the network widget. | -| `use_bytes` | Boolean | Displays the network widget using bytes. Defaults to bits. | -| `use_log` | Boolean | Displays the network widget with a log scale. Defaults to a non-log scale. | -| `use_binary_prefix` | Boolean | Displays the network widget with a binary prefix (e.g. kibibits) rather than a decimal prefix (e.g. kilobits). Defaults to decimal prefixes. | +| Field | Type | Functionality | +| ------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `show_packets` | Boolean | Displays packet rate and average packet size info. | +| `legend_position` | String (one of ["none", "top-left", "top", "top-right", "left", "right", "bottom-left", "bottom", "bottom-right"]) | Where to place the legend for the network widget. | +| `use_bytes` | Boolean | Displays the network widget using bytes. Defaults to bits. | +| `use_log` | Boolean | Displays the network widget with a log scale. Defaults to a non-log scale. | +| `use_binary_prefix` | Boolean | Displays the network widget with a binary prefix (e.g. kibibits) rather than a decimal prefix (e.g. kilobits). Defaults to decimal prefixes. | +| `start_zeroed` | Boolean | Zeroes out the total network usage ("All") counters such that it shows the total usage since the app is started, rather than the total usage since boot. | ## Filtering Entries diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index 6c977003..fc719c35 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -395,6 +395,9 @@ # Whether to display packet rate and average packet size info. Defaults to false. #show_packets = false +# Zeroes out the total network usage ("All") counters so it shows the total usage\nsince the app is started, rather than the total usage since boot. +#start_zeroed = false + # By default, there are no network interface filters enabled. An example use case is provided below. #[network_graph.interface_filter] # Whether to ignore any matches. Defaults to true. diff --git a/schema/nightly/bottom.json b/schema/nightly/bottom.json index 97f2711c..8d36f366 100644 --- a/schema/nightly/bottom.json +++ b/schema/nightly/bottom.json @@ -930,6 +930,13 @@ "null" ] }, + "start_zeroed": { + "description": "Zeroes out the total network usage (\"All\") counters so it shows the total usage\nsince the app is started, rather than the total usage since boot.", + "type": [ + "boolean", + "null" + ] + }, "use_binary_prefix": { "description": "Displays the network widget with a binary prefix (e.g. kibibits) rather than a decimal\nprefix (e.g. kilobits). Defaults to decimal prefixes.", "type": [ diff --git a/src/app.rs b/src/app.rs index 2f732a4b..2943a663 100644 --- a/src/app.rs +++ b/src/app.rs @@ -76,6 +76,7 @@ pub struct AppConfigFields { pub network_scale_type: AxisScaling, pub network_use_binary_prefix: bool, pub network_show_packets: bool, + pub network_start_zeroed: bool, pub retention_ms: u64, pub dedicated_average_row: bool, pub default_tree_collapse: bool, diff --git a/src/app/data/store.rs b/src/app/data/store.rs index 401541f7..c1903ea9 100644 --- a/src/app/data/store.rs +++ b/src/app/data/store.rs @@ -60,39 +60,49 @@ impl PartialEq for dyn PairKey + '_ { impl Eq for dyn PairKey + '_ {} +#[derive(Debug, Clone)] +pub struct TotalNetworkData { + total_rx: u64, + total_tx: u64, +} + /// A collection of data. This is where we dump data into. /// /// TODO: Maybe reduce visibility of internal data, make it only accessible /// through DataStore? #[derive(Debug, Clone)] -pub struct StoredData { +pub struct InnerData { // FIXME: (points_rework_v1) we could be able to remove this with some more refactoring. - pub last_update_time: Instant, - pub time_series_data: TimeSeriesData, - pub network_harvest: NetworkHarvest, - pub ram_harvest: Option, - pub swap_harvest: Option, + last_update_time: Instant, + + pub(crate) time_series_data: TimeSeriesData, + pub(crate) network_harvest: NetworkHarvest, + pub(crate) ram_harvest: Option, + pub(crate) swap_harvest: Option, #[cfg(not(target_os = "windows"))] - pub cache_harvest: Option, + pub(crate) cache_harvest: Option, #[cfg(feature = "zfs")] - pub arc_harvest: Option, + pub(crate) arc_harvest: Option, #[cfg(feature = "gpu")] - pub gpu_harvest: Vec<(String, MemData)>, - pub cpu_harvest: CpuHarvest, - pub load_avg_harvest: LoadAvgHarvest, - pub process_data: ProcessData, + pub(crate) gpu_harvest: Vec<(String, MemData)>, + pub(crate) cpu_harvest: CpuHarvest, + pub(crate) load_avg_harvest: LoadAvgHarvest, + pub(crate) process_data: ProcessData, /// TODO: (points_rework_v1) Might be a better way to do this without having /// to store here? - pub prev_io: FxHashMap<(String, String), (u64, u64)>, - pub disk_harvest: Vec, - pub temp_data: Vec, + prev_io: FxHashMap<(String, String), (u64, u64)>, + pub(crate) disk_harvest: Vec, + pub(crate) temp_data: Vec, #[cfg(feature = "battery")] - pub battery_harvest: Vec, + pub(crate) battery_harvest: Vec, + + /// Used if we are zeroing out the network data. + starting_total_network: Option, } -impl Default for StoredData { +impl Default for InnerData { fn default() -> Self { - StoredData { + InnerData { last_update_time: Instant::now(), time_series_data: TimeSeriesData::default(), network_harvest: NetworkHarvest::default(), @@ -112,15 +122,12 @@ impl Default for StoredData { arc_harvest: None, #[cfg(feature = "gpu")] gpu_harvest: Vec::default(), + starting_total_network: None, } } } -impl StoredData { - pub fn reset(&mut self) { - *self = StoredData::default(); - } - +impl InnerData { #[allow( clippy::boxed_local, reason = "This avoids warnings on certain platforms (e.g. 32-bit)." @@ -145,7 +152,19 @@ impl StoredData { .add(&data, used_widgets, settings, filters); } - if let Some(network) = data.network { + if let Some(mut network) = data.network { + if settings.network_start_zeroed { + let TotalNetworkData { + total_rx: starting_total_rx, + total_tx: starting_total_tx, + } = self.starting_total_network.get_or_insert(TotalNetworkData { + total_rx: network.total_rx, + total_tx: network.total_tx, + }); + + network.total_rx = network.total_rx.saturating_sub(*starting_total_rx); + network.total_tx = network.total_tx.saturating_sub(*starting_total_tx); + } self.network_harvest = network; } @@ -348,13 +367,13 @@ impl StoredData { pub enum FrozenState { #[default] NotFrozen, - Frozen(Box), + Frozen(Box), } /// What data to share to other parts of the application. pub struct DataStore { frozen_state: FrozenState, - main: StoredData, + inner: InnerData, used_widgets: UsedWidgets, filters: DataFilters, } @@ -364,7 +383,7 @@ impl DataStore { pub fn new(used_widgets: UsedWidgets) -> Self { Self { frozen_state: FrozenState::default(), - main: StoredData::default(), + inner: InnerData::default(), used_widgets, filters: DataFilters::default(), } @@ -374,7 +393,7 @@ impl DataStore { pub fn toggle_frozen(&mut self) { match &self.frozen_state { FrozenState::NotFrozen => { - self.frozen_state = FrozenState::Frozen(Box::new(self.main.clone())); + self.frozen_state = FrozenState::Frozen(Box::new(self.inner.clone())); } FrozenState::Frozen(_) => self.frozen_state = FrozenState::NotFrozen, } @@ -388,9 +407,9 @@ impl DataStore { /// Return a reference to the currently available data. Note that if the /// data is in a frozen state, it will return the snapshot of data from /// when it was frozen. - pub fn get_data(&self) -> &StoredData { + pub fn get_data(&self) -> &InnerData { match &self.frozen_state { - FrozenState::NotFrozen => &self.main, + FrozenState::NotFrozen => &self.inner, FrozenState::Frozen(collected_data) => collected_data, } } @@ -401,18 +420,18 @@ impl DataStore { /// Eat data. pub fn eat_data(&mut self, data: Box, settings: &AppConfigFields) { - self.main + self.inner .eat_data(data, settings, &self.used_widgets, &self.filters); } /// Clean data. pub fn clean_data(&mut self, max_duration: Duration) { - self.main.time_series_data.prune(max_duration); + self.inner.time_series_data.prune(max_duration); } /// Reset data state. pub fn reset(&mut self) { self.frozen_state = FrozenState::NotFrozen; - self.main = StoredData::default(); + self.inner = InnerData::default(); } } diff --git a/src/canvas/widgets/cpu_graph.rs b/src/canvas/widgets/cpu_graph.rs index 85f34fe2..273af59a 100644 --- a/src/canvas/widgets/cpu_graph.rs +++ b/src/canvas/widgets/cpu_graph.rs @@ -4,7 +4,7 @@ use ratatui::{ }; use crate::{ - app::{App, data::StoredData, layout_manager::WidgetDirection}, + app::{App, data::InnerData, layout_manager::WidgetDirection}, canvas::{ Painter, components::{ @@ -118,7 +118,7 @@ impl Painter { } fn generate_points<'a>( - &self, cpu_widget_state: &'a CpuWidgetState, data: &'a StoredData, show_avg_cpu: bool, + &self, cpu_widget_state: &'a CpuWidgetState, data: &'a InnerData, show_avg_cpu: bool, ) -> Vec> { let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 }; let current_scroll_position = cpu_widget_state.table.state.current_index; diff --git a/src/constants.rs b/src/constants.rs index 8fa1a070..e795d97c 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -644,6 +644,9 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott # Whether to display packet rate and average packet size info. Defaults to false. #show_packets = false +# Zeroes out the total network usage ("All") counters so it shows the total usage\nsince the app is started, rather than the total usage since boot. +#start_zeroed = false + # By default, there are no network interface filters enabled. An example use case is provided below. #[network_graph.interface_filter] # Whether to ignore any matches. Defaults to true. diff --git a/src/options.rs b/src/options.rs index 0741d608..7c1b69d6 100644 --- a/src/options.rs +++ b/src/options.rs @@ -405,6 +405,13 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL ); let network_show_packets = is_flag_enabled_in!(show_packets, args.network, config.network_graph); + let network_start_zeroed = if args.network.network_start_zeroed { + true + } else if let Some(network_graph) = &config.network_graph { + network_graph.start_zeroed.unwrap_or(false) + } else { + false + }; let proc_columns: Option> = { config.processes.as_ref().and_then(|cfg| { @@ -498,6 +505,7 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL network_unit_type, network_use_binary_prefix, network_show_packets, + network_start_zeroed, retention_ms, dedicated_average_row: enabled_option_with_deprecated!( false, diff --git a/src/options/args.rs b/src/options/args.rs index e0eec54e..893103cd 100644 --- a/src/options/args.rs +++ b/src/options/args.rs @@ -612,6 +612,15 @@ pub struct NetworkArgs { alias = "show-packets" )] pub show_packets: bool, + + #[arg( + long, + action = ArgAction::SetTrue, + help = "Show total network usage from app startup rather than on boot.", + long_help = "Zeroes out the total network usage (\"All\") counters so it shows the total usage since the app is started, rather than the total usage since boot.", + alias = "network-start-zeroed" + )] + pub network_start_zeroed: bool, } /// Battery arguments/config options. diff --git a/src/options/config/network_graph.rs b/src/options/config/network_graph.rs index 8ea02e97..aa297acf 100644 --- a/src/options/config/network_graph.rs +++ b/src/options/config/network_graph.rs @@ -27,4 +27,8 @@ pub(crate) struct NetworkGraphConfig { /// Displays the network widget with a binary prefix (e.g. kibibits) rather than a decimal /// prefix (e.g. kilobits). Defaults to decimal prefixes. pub(crate) use_binary_prefix: Option, + + /// Zeroes out the total network usage ("All") counters so it shows the total usage + /// since the app is started, rather than the total usage since boot. + pub(crate) start_zeroed: Option, } diff --git a/src/widgets/disk_table.rs b/src/widgets/disk_table.rs index c3356ca2..c3441eaf 100644 --- a/src/widgets/disk_table.rs +++ b/src/widgets/disk_table.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, cmp::max, num::NonZeroU16}; use serde::Deserialize; use crate::{ - app::{AppConfigFields, data::StoredData}, + app::{AppConfigFields, data::InnerData}, canvas::components::data_table::{ ColumnHeader, DataTableColumn, DataTableProps, DataTableStyling, DataToCell, SortColumn, SortDataTable, SortDataTableProps, SortsRow, @@ -376,7 +376,7 @@ impl DiskTableWidget { } /// Update the current table data. - pub fn set_table_data(&mut self, data: &StoredData) { + pub fn set_table_data(&mut self, data: &InnerData) { // Note that the data may contain unmounted disks (e.g. we enable it for another disk widget), // so we have to potentially filter it out here too. let mut data: Vec = if self.show_unmounted { diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 74bf0666..f1aa0eae 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -16,7 +16,7 @@ use sort_table::SortTableColumn; use crate::{ app::{ AppConfigFields, AppSearchState, - data::{ProcessData, StoredData}, + data::{InnerData, ProcessData}, }, canvas::components::data_table::{ Column, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps, @@ -502,7 +502,7 @@ impl ProcWidgetState { /// This function *only* updates the displayed process data. If there is a /// need to update the actual *stored* data, call it before this /// function. - pub fn set_table_data(&mut self, stored_data: &StoredData) { + pub fn set_table_data(&mut self, stored_data: &InnerData) { let data = match &self.mode { ProcWidgetMode::Grouped | ProcWidgetMode::Normal => { self.get_normal_data(&stored_data.process_data.process_harvest) @@ -514,7 +514,7 @@ impl ProcWidgetState { } fn get_tree_data( - &self, collapsed: &TreeCollapsed, stored_data: &StoredData, + &self, collapsed: &TreeCollapsed, stored_data: &InnerData, ) -> Vec { const BRANCH_END: char = '└'; const BRANCH_SPLIT: char = '├'; @@ -1778,10 +1778,8 @@ mod test { tree_proc_data.process_harvest.insert(1, process_harvest); tree_proc_data.process_harvest.insert(2, k_process_harvest); tree_proc_data.orphan_pids = vec![1, 2]; - let tree_stored_data = StoredData { - process_data: tree_proc_data, - ..Default::default() - }; + let mut tree_stored_data = InnerData::default(); + tree_stored_data.process_data = tree_proc_data; let default_tree_results = state .get_tree_data(&tree_collapsed, &tree_stored_data) .len(); diff --git a/tests/valid_configs/network_alias.toml b/tests/valid_configs/network_alias.toml index 9a0bd9c6..42cad755 100644 --- a/tests/valid_configs/network_alias.toml +++ b/tests/valid_configs/network_alias.toml @@ -5,6 +5,7 @@ use_log = false use_bytes = false legend_position = "right" use_binary_prefix = false +start_zeroed = true [network.interface_filter] is_list_ignored = true