mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-08-23 11:56:45 +00:00
feature: add option to show zeroed total network usage on startup (#2199)
This adds the ability to run with `--network_start_zeroed` or the `start_zeroed` config option, which will zero out the total network usage so that bottom starts with it at zero, rather than the total network usage reported by the OS since boot.
This commit is contained in:
+7
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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 <POSITION>` | 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 <POSITION>` | 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
+52
-33
@@ -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<MemData>,
|
||||
pub swap_harvest: Option<MemData>,
|
||||
last_update_time: Instant,
|
||||
|
||||
pub(crate) time_series_data: TimeSeriesData,
|
||||
pub(crate) network_harvest: NetworkHarvest,
|
||||
pub(crate) ram_harvest: Option<MemData>,
|
||||
pub(crate) swap_harvest: Option<MemData>,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub cache_harvest: Option<MemData>,
|
||||
pub(crate) cache_harvest: Option<MemData>,
|
||||
#[cfg(feature = "zfs")]
|
||||
pub arc_harvest: Option<MemData>,
|
||||
pub(crate) arc_harvest: Option<MemData>,
|
||||
#[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<DiskWidgetData>,
|
||||
pub temp_data: Vec<TempWidgetData>,
|
||||
prev_io: FxHashMap<(String, String), (u64, u64)>,
|
||||
pub(crate) disk_harvest: Vec<DiskWidgetData>,
|
||||
pub(crate) temp_data: Vec<TempWidgetData>,
|
||||
#[cfg(feature = "battery")]
|
||||
pub battery_harvest: Vec<batteries::BatteryData>,
|
||||
pub(crate) battery_harvest: Vec<batteries::BatteryData>,
|
||||
|
||||
/// Used if we are zeroing out the network data.
|
||||
starting_total_network: Option<TotalNetworkData>,
|
||||
}
|
||||
|
||||
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<StoredData>),
|
||||
Frozen(Box<InnerData>),
|
||||
}
|
||||
|
||||
/// 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<Data>, 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GraphData<'a>> {
|
||||
let show_avg_offset = if show_avg_cpu { AVG_POSITION } else { 0 };
|
||||
let current_scroll_position = cpu_widget_state.table.state.current_index;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<IndexSet<ProcWidgetColumn>> = {
|
||||
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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<bool>,
|
||||
|
||||
/// 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<bool>,
|
||||
}
|
||||
|
||||
@@ -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<DiskWidgetData> = if self.show_unmounted {
|
||||
|
||||
@@ -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<ProcWidgetData> {
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user