From 4136d204bc2a02be7dfd5369270eae29de331f3d Mon Sep 17 00:00:00 2001 From: Abu Hurayra Niloy <105109331+abuhurayraniloy@users.noreply.github.com> Date: Tue, 22 Sep 2026 03:22:46 +0600 Subject: [PATCH] feature: add process swap column for Linux (#2224) Adds Linux support for process swap tracking + a column in the widget. --- sample_configs/default_config.toml | 2 +- schema/nightly/bottom.json | 2 + src/app.rs | 1 + src/collection.rs | 6 ++ src/collection/processes.rs | 3 + src/collection/processes/linux/mod.rs | 19 ++++- src/collection/processes/linux/process.rs | 81 +++++++++++++++++++- src/collection/processes/unix/process_ext.rs | 1 + src/collection/processes/windows.rs | 1 + src/constants.rs | 2 +- src/lib.rs | 2 + src/options.rs | 3 + src/options/config/process.rs | 5 +- src/widgets/process_table.rs | 29 +++++++ src/widgets/process_table/process_columns.rs | 8 ++ src/widgets/process_table/process_data.rs | 44 ++++++++++- 16 files changed, 199 insertions(+), 10 deletions(-) diff --git a/sample_configs/default_config.toml b/sample_configs/default_config.toml index c75fd97d..4b0e4177 100644 --- a/sample_configs/default_config.toml +++ b/sample_configs/default_config.toml @@ -158,7 +158,7 @@ # Processes widget configuration #[processes] # The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built): -# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%, Nice, Priority +# PID, Name, CPU%, Mem%, Swap, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%, Nice, Priority #columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%", "Priority"] # The default sort column when bottom starts. Accepts any of the column names above. diff --git a/schema/nightly/bottom.json b/schema/nightly/bottom.json index da0fd0cb..05dafbe0 100644 --- a/schema/nightly/bottom.json +++ b/schema/nightly/bottom.json @@ -1039,6 +1039,7 @@ "Read", "Rps", "State", + "Swap", "T.Read", "T.Write", "TRead", @@ -1072,6 +1073,7 @@ "read", "rps", "state", + "swap", "t.read", "t.write", "time", diff --git a/src/app.rs b/src/app.rs index 5b2405c5..0d1316c6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -48,6 +48,7 @@ pub struct AppConfigFields { pub use_current_cpu_total: bool, pub unnormalized_cpu: bool, pub get_process_threads: bool, + pub get_process_swap: bool, pub use_basic_mode: bool, pub default_time_value: u64, pub time_interval: u64, diff --git a/src/collection.rs b/src/collection.rs index ecc57d8d..b89aa39f 100644 --- a/src/collection.rs +++ b/src/collection.rs @@ -168,6 +168,7 @@ pub struct DataCollector { use_current_cpu_total: bool, show_average_cpu: bool, get_process_threads: bool, + get_process_swap: bool, last_list_collection_time: Instant, should_run_less_routine_tasks: bool, @@ -226,6 +227,7 @@ impl DataCollector { use_current_cpu_total: false, unnormalized_cpu: false, get_process_threads: false, + get_process_swap: false, last_collection_time, total_rx: 0, total_tx: 0, @@ -300,6 +302,10 @@ impl DataCollector { self.get_process_threads = get_process_threads; } + pub fn set_get_process_swap(&mut self, get_process_swap: bool) { + self.get_process_swap = get_process_swap; + } + pub fn set_include_unmounted_disks(&mut self, include_unmounted_disks: bool) { self.include_unmounted_disks = include_unmounted_disks; } diff --git a/src/collection/processes.rs b/src/collection/processes.rs index 90fe6c81..4e0c3f8d 100644 --- a/src/collection/processes.rs +++ b/src/collection/processes.rs @@ -106,6 +106,9 @@ pub struct ProcessHarvest { /// Virtual memory. pub virtual_mem: Bytes, + /// Swapped memory. + pub swap_bytes: Option, + /// The name of the process. pub name: String, diff --git a/src/collection/processes/linux/mod.rs b/src/collection/processes/linux/mod.rs index ae84ac80..48439676 100644 --- a/src/collection/processes/linux/mod.rs +++ b/src/collection/processes/linux/mod.rs @@ -142,6 +142,7 @@ fn read_proc( stat, io, cmdline, + swap_bytes, } = process; let ReadProcArgs { @@ -152,6 +153,7 @@ fn read_proc( time_difference_in_secs, system_uptime, get_process_threads: _, + get_process_swap: _, } = args; let process_state_char = stat.state; @@ -272,6 +274,7 @@ fn read_proc( mem_usage_percent, mem_usage, virtual_mem, + swap_bytes, name, command, read_per_sec, @@ -349,6 +352,7 @@ pub(crate) struct ReadProcArgs { pub time_difference_in_secs: u64, pub system_uptime: u64, pub get_process_threads: bool, + pub get_process_swap: bool, } pub(crate) fn linux_process_data( @@ -364,6 +368,8 @@ pub(crate) fn linux_process_data( unnormalized_cpu: collector.unnormalized_cpu, get_process_threads: collector.get_process_threads, }; + let get_swap = collector.get_process_swap; + let prev_process_details = &mut collector.prev_process_details; let user_table = &mut collector.user_table; @@ -420,6 +426,7 @@ pub(crate) fn linux_process_data( time_difference_in_secs, system_uptime: sysinfo::System::uptime(), get_process_threads: get_threads, + get_process_swap: get_swap, }; // TODO: Maybe pre-allocate these buffers in the future w/ routine cleanup. @@ -428,9 +435,12 @@ pub(crate) fn linux_process_data( let mut process_vector: Vec = pids .filter_map(|pid_path| { - if let Ok((process, threads)) = - Process::from_path(pid_path, &mut buffer, args.get_process_threads) - { + if let Ok((process, threads)) = Process::from_path( + pid_path, + &mut buffer, + args.get_process_threads, + args.get_process_swap, + ) { let pid = process.pid; let prev_proc_details = prev_process_details.entry(pid).or_default(); @@ -474,7 +484,8 @@ pub(crate) fn linux_process_data( // Get thread data. for (pid, tid_paths) in process_threads_to_check { for tid_path in tid_paths { - if let Ok((process, _)) = Process::from_path(tid_path, &mut buffer, false) { + // VmSwap is process-wide, so don't collect it for individual threads. + if let Ok((process, _)) = Process::from_path(tid_path, &mut buffer, false, false) { let tid = process.pid; let prev_proc_details = prev_process_details.entry(tid).or_default(); diff --git a/src/collection/processes/linux/process.rs b/src/collection/processes/linux/process.rs index 410628a9..66616f6f 100644 --- a/src/collection/processes/linux/process.rs +++ b/src/collection/processes/linux/process.rs @@ -246,6 +246,33 @@ impl Io { } } +/// Helper that reads the `VmSwap` line from `/proc//status`. +/// +/// NB: `buffer` must be empty. +/// +/// See the [`proc_pid_status(5)`](https://man7.org/linux/man-pages/man5/proc_pid_status.5.html) +/// documentation for details about this file and field. +fn get_swap_bytes(f: File, buffer: &mut String) -> anyhow::Result { + let mut reader = BufReader::new(f); + + while reader.read_line(buffer)? > 0 { + let mut parts = buffer.split_whitespace(); + + if parts.next() == Some("VmSwap:") { + let swap_kib: u64 = parts + .next() + .ok_or_else(|| anyhow!("VmSwap value missing"))? + .parse()?; + + return Ok(swap_kib.saturating_mul(1024)); + } + + buffer.clear(); + } + + Err(anyhow!("VmSwap field not found")) +} + /// A wrapper around a Linux process operations in `/proc/`. /// /// Core documentation based on [proc's manpages](https://man7.org/linux/man-pages/man5/proc.5.html). @@ -255,6 +282,7 @@ pub(crate) struct Process { pub stat: Stat, pub io: Option, pub cmdline: Option, + pub swap_bytes: Option, } #[inline] @@ -277,7 +305,7 @@ impl Process { /// buffer. #[inline] pub(crate) fn from_path( - pid_path: PathBuf, buffer: &mut String, get_threads: bool, + pid_path: PathBuf, buffer: &mut String, get_threads: bool, get_swap: bool, ) -> anyhow::Result<(Process, Vec)> { buffer.clear(); @@ -333,6 +361,16 @@ impl Process { reset(&mut root, buffer); + let swap_bytes = if get_swap && !stat.is_kernel_thread { + let bytes = open_at(&mut root, "status", &pid_dir) + .and_then(|file| get_swap_bytes(file, buffer)) + .ok(); + reset(&mut root, buffer); + bytes + } else { + None + }; + let threads = threads(&mut root, pid, get_threads); Ok(( @@ -342,6 +380,7 @@ impl Process { stat, io, cmdline, + swap_bytes, }, threads, )) @@ -426,6 +465,14 @@ mod tests { Stat::from_file(file, &mut String::new()) } + fn swap_file(status: &str) -> anyhow::Result { + let mut file = tempfile::tempfile()?; + file.write_all(status.as_bytes())?; + file.rewind()?; + + get_swap_bytes(file, &mut String::new()) + } + #[test] fn parse_short_comm() { let stat = stat_from_name("kworker/u16:2").unwrap(); @@ -463,4 +510,36 @@ mod tests { assert!(stat_file("1 (blah)").is_err(), "too short"); assert!(stat_file("1 )(").is_err(), "wrong order"); } + + #[test] + fn parse_swap_bytes() { + let status = "Name:\ttest\nVmSwap:\t4096 kB\n"; + + let swap_bytes = swap_file(status).unwrap(); + + assert_eq!(swap_bytes, 4_194_304); + } + + #[test] + fn parse_zero_swap_bytes() { + let status = "Name:\ttest\nVmSwap:\t0 kB\n"; + + let swap_bytes = swap_file(status).unwrap(); + + assert_eq!(swap_bytes, 0); + } + + #[test] + fn missing_vm_swap_is_an_error() { + let status = "Name:\ttest\nVmSize:\t4096 kB\n"; + + assert!(swap_file(status).is_err()); + } + + #[test] + fn invalid_vm_swap_is_an_error() { + let status = "Name:\ttest\nVmSwap:\tinvalid kB\n"; + + assert!(swap_file(status).is_err()); + } } diff --git a/src/collection/processes/unix/process_ext.rs b/src/collection/processes/unix/process_ext.rs index 2f6925ee..b4bf9b23 100644 --- a/src/collection/processes/unix/process_ext.rs +++ b/src/collection/processes/unix/process_ext.rs @@ -146,6 +146,7 @@ pub(crate) trait UnixProcessExt { }, mem_usage: process_val.memory(), virtual_mem: process_val.virtual_memory(), + swap_bytes: None, cpu_usage_percent: process_cpu_usage, read_per_sec: disk_usage.read_bytes, write_per_sec: disk_usage.written_bytes, diff --git a/src/collection/processes/windows.rs b/src/collection/processes/windows.rs index 5318da96..db64b6dc 100644 --- a/src/collection/processes/windows.rs +++ b/src/collection/processes/windows.rs @@ -136,6 +136,7 @@ pub fn sysinfo_process_data( } as f32, mem_usage: process.memory(), virtual_mem: process.virtual_memory(), + swap_bytes: None, cpu_usage_percent: process_cpu_usage, read_per_sec: disk_usage.read_bytes, write_per_sec: disk_usage.written_bytes, diff --git a/src/constants.rs b/src/constants.rs index d6a29a66..b1ddee41 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -407,7 +407,7 @@ pub(crate) const CONFIG_TEXT: &str = r#"# This is a default config file for bott # Processes widget configuration #[processes] # The columns shown by the process widget. The following columns are supported (the GPU columns are only available if the GPU feature is enabled when built): -# PID, Name, CPU%, Mem%, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%, Nice, Priority +# PID, Name, CPU%, Mem%, Swap, R/s, W/s, T.Read, T.Write, User, State, Time, GMem%, GPU%, Nice, Priority #columns = ["PID", "Name", "CPU%", "Mem%", "Virt", "R/s", "W/s", "T.Read", "T.Write", "User", "State", "GMem%", "GPU%", "Priority"] # The default sort column when bottom starts. Accepts any of the column names above. diff --git a/src/lib.rs b/src/lib.rs index 763a4824..c76a08ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -225,6 +225,7 @@ fn create_collection_thread( let show_average_cpu = app_config_fields.show_average_cpu; let update_sleep = app_config_fields.update_rate; let get_process_threads = app_config_fields.get_process_threads; + let get_process_swap = app_config_fields.get_process_swap; #[cfg(feature = "zfs")] let get_arc_free = app_config_fields.free_arc; let include_unmounted_disks = @@ -238,6 +239,7 @@ fn create_collection_thread( data_collector.set_unnormalized_cpu(unnormalized_cpu); data_collector.set_show_average_cpu(show_average_cpu); data_collector.set_get_process_threads(get_process_threads); + data_collector.set_get_process_swap(get_process_swap); #[cfg(feature = "zfs")] data_collector.set_free_arc_mem(get_arc_free); data_collector.set_include_unmounted_disks(include_unmounted_disks); diff --git a/src/options.rs b/src/options.rs index 407e00ca..c5442041 100644 --- a/src/options.rs +++ b/src/options.rs @@ -481,6 +481,9 @@ pub(crate) fn init_app(args: BottomArgs, config: Config) -> Result<(App, BottomL flags.unnormalized_cpu, ), get_process_threads: is_flag_enabled_in!(get_threads, args.process, config.processes), + get_process_swap: proc_columns + .as_ref() + .is_some_and(|columns| columns.contains(&ProcWidgetColumn::Swap)), use_basic_mode, default_time_value, time_interval: get_time_interval(args, config, retention_ms)?, diff --git a/src/options/config/process.rs b/src/options/config/process.rs index 2913e148..779f3934 100644 --- a/src/options/config/process.rs +++ b/src/options/config/process.rs @@ -96,12 +96,12 @@ mod test { fn valid_process_column_config() { #[cfg(unix)] let config = r#" - columns = ["CPU%", "PiD", "user", "MEM", "virt", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state", "prioRity", "Nice"] + columns = ["CPU%", "PiD", "user", "MEM", "virt", "swap", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state", "prioRity", "Nice"] "#; #[cfg(target_os = "windows")] let config = r#" - columns = ["CPU%", "PiD", "user", "MEM", "virt", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state", "prioRity"] + columns = ["CPU%", "PiD", "user", "MEM", "virt", "swap", "Tread", "T.Write", "Rps", "W/s", "tiMe", "USER", "state", "prioRity"] "#; let generated: ProcessesConfig = toml_edit::de::from_str(config).unwrap(); @@ -113,6 +113,7 @@ mod test { ProcWidgetColumn::User, ProcWidgetColumn::Mem, ProcWidgetColumn::VirtualMem, + ProcWidgetColumn::Swap, ProcWidgetColumn::TotalRead, ProcWidgetColumn::TotalWrite, ProcWidgetColumn::ReadPerSecond, diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs index 91eea3d2..2b0c3809 100644 --- a/src/widgets/process_table.rs +++ b/src/widgets/process_table.rs @@ -144,6 +144,7 @@ fn make_column(column: ProcColumn) -> SortColumn { MemValue => SortColumn::new(MemValue).default_descending(), MemPercent => SortColumn::new(MemPercent).default_descending(), VirtualMem => SortColumn::new(VirtualMem).default_descending(), + Swap => SortColumn::new(Swap).default_descending(), Pid => SortColumn::new(Pid), Count => SortColumn::new(Count), Name => SortColumn::soft(Name, Some(0.3)), @@ -186,6 +187,7 @@ pub enum ProcWidgetColumn { Cpu, Mem, VirtualMem, + Swap, ReadPerSecond, WritePerSecond, TotalRead, @@ -333,6 +335,7 @@ impl ProcWidgetState { } } ProcWidgetColumn::VirtualMem => VirtualMem, + ProcWidgetColumn::Swap => Swap, ProcWidgetColumn::ReadPerSecond => ReadPerSecond, ProcWidgetColumn::WritePerSecond => WritePerSecond, ProcWidgetColumn::TotalRead => TotalRead, @@ -391,6 +394,7 @@ impl ProcWidgetState { CpuPercent => ProcWidgetColumn::Cpu, MemValue | MemPercent => ProcWidgetColumn::Mem, VirtualMem => ProcWidgetColumn::VirtualMem, + Swap => ProcWidgetColumn::Swap, Pid | Count => ProcWidgetColumn::PidOrCount, Name | Command => ProcWidgetColumn::ProcNameOrCommand, ReadPerSecond => ProcWidgetColumn::ReadPerSecond, @@ -840,6 +844,12 @@ impl ProcWidgetState { } } + pwd.swap_bytes = match (pwd.swap_bytes, process.swap_bytes) { + (Some(a), Some(b)) => Some(a + b), + (Some(a), None) | (None, Some(a)) => Some(a), + (None, None) => None, + }; + pwd.rps += process.read_per_sec; pwd.wps += process.write_per_sec; pwd.total_read += process.total_read; @@ -1183,6 +1193,7 @@ mod test { cpu_usage_percent: 0.0, mem_usage: MemUsage::Percent(1.1), virtual_mem: 100, + swap_bytes: Some(100), rps: 0, wps: 0, total_read: 0, @@ -1213,6 +1224,7 @@ mod test { id: "B".into(), cpu_usage_percent: 1.1, mem_usage: MemUsage::Percent(2.2), + swap_bytes: Some(200), ..(a.clone()) }; @@ -1222,6 +1234,7 @@ mod test { id: "C".into(), cpu_usage_percent: 2.2, mem_usage: MemUsage::Percent(0.0), + swap_bytes: Some(50), ..(a.clone()) }; @@ -1231,8 +1244,10 @@ mod test { id: "D".into(), cpu_usage_percent: 0.0, mem_usage: MemUsage::Percent(0.0), + swap_bytes: None, ..(a.clone()) }; + let mut data = vec![d.clone(), b.clone(), c.clone(), a.clone()]; // Assume we had sorted over by pid. @@ -1265,6 +1280,20 @@ mod test { [&c, &d, &a, &b].iter().map(|d| d.pid).collect::>(), data.iter().map(|d| d.pid).collect::>(), ); + + data.sort_by_key(|p| p.pid); + sort_skip_pid_asc(&ProcColumn::Swap, &mut data, SortOrder::Descending); + assert_eq!( + vec![2, 1, 3, 4], + data.iter().map(|process| process.pid).collect::>(), + ); + + data.sort_by_key(|p| p.pid); + sort_skip_pid_asc(&ProcColumn::Swap, &mut data, SortOrder::Ascending); + assert_eq!( + vec![4, 3, 1, 2], + data.iter().map(|process| process.pid).collect::>(), + ); } fn get_columns(table: &ProcessTable) -> Vec { diff --git a/src/widgets/process_table/process_columns.rs b/src/widgets/process_table/process_columns.rs index f13c0b8f..afd01da2 100644 --- a/src/widgets/process_table/process_columns.rs +++ b/src/widgets/process_table/process_columns.rs @@ -19,6 +19,7 @@ pub enum ProcColumn { MemValue, MemPercent, VirtualMem, + Swap, Pid, Count, Name, @@ -54,6 +55,7 @@ impl ProcColumn { // TODO: Change this ProcColumn::MemValue | ProcColumn::MemPercent => &["Mem", "Mem%", "Memory", "Memory%"], ProcColumn::VirtualMem => &["Virt", "Virtual", "VirtMem", "Virtual Memory"], + ProcColumn::Swap => &["Swap"], ProcColumn::ReadPerSecond => &["R/s", "Read", "Rps"], ProcColumn::WritePerSecond => &["W/s", "Write", "Wps"], ProcColumn::TotalRead => &["T.Read", "TRead", "Total Read"], @@ -80,6 +82,7 @@ impl ColumnHeader for ProcColumn { ProcColumn::MemValue => "Mem", ProcColumn::MemPercent => "Mem%", ProcColumn::VirtualMem => "Virt", + ProcColumn::Swap => "Swap", ProcColumn::Pid => "PID", ProcColumn::Count => "Count", ProcColumn::Name => "Name", @@ -136,6 +139,9 @@ impl SortsRow for ProcColumn { ProcColumn::VirtualMem => { data.sort_by(|a, b| sort_partial_fn(descending)(&a.virtual_mem, &b.virtual_mem)); } + ProcColumn::Swap => { + data.sort_by(|a, b| sort_partial_fn(descending)(&a.swap_bytes, &b.swap_bytes)); + } ProcColumn::Pid => { data.sort_by(|a, b| sort_partial_fn(descending)(a.pid, b.pid)); } @@ -209,6 +215,7 @@ impl ProcColumn { "cpu%" => Some(ProcColumn::CpuPercent), "mem" | "mem%" | "memory" | "memory%" => Some(ProcColumn::MemPercent), "virt" | "virtual" | "virtmem" | "virtual memory" => Some(ProcColumn::VirtualMem), + "swap" => Some(ProcColumn::Swap), "pid" => Some(ProcColumn::Pid), "count" => Some(ProcColumn::Count), "name" => Some(ProcColumn::Name), @@ -251,6 +258,7 @@ impl From<&ProcColumn> for ProcWidgetColumn { ProcColumn::CpuPercent => ProcWidgetColumn::Cpu, ProcColumn::MemPercent | ProcColumn::MemValue => ProcWidgetColumn::Mem, ProcColumn::VirtualMem => ProcWidgetColumn::VirtualMem, + ProcColumn::Swap => ProcWidgetColumn::Swap, ProcColumn::ReadPerSecond => ProcWidgetColumn::ReadPerSecond, ProcColumn::WritePerSecond => ProcWidgetColumn::WritePerSecond, ProcColumn::TotalRead => ProcWidgetColumn::TotalRead, diff --git a/src/widgets/process_table/process_data.rs b/src/widgets/process_table/process_data.rs index 77e02886..3ae2ff2a 100644 --- a/src/widgets/process_table/process_data.rs +++ b/src/widgets/process_table/process_data.rs @@ -19,7 +19,7 @@ use crate::{ Painter, components::data_table::{DataTableColumn, DataToCell}, }, - collection::processes::{Pid, ProcessHarvest}, + collection::processes::{Bytes, Pid, ProcessHarvest}, dec_bytes_per_second_string, utils::data_units::{GIBI_LIMIT, GIGA_LIMIT, get_binary_bytes, get_decimal_bytes}, }; @@ -219,6 +219,7 @@ pub struct ProcWidgetData { pub cpu_usage_percent: f32, pub mem_usage: MemUsage, pub virtual_mem: u64, + pub swap_bytes: Option, pub rps: u64, pub wps: u64, pub total_read: u64, @@ -265,6 +266,7 @@ impl ProcWidgetData { cpu_usage_percent: process.cpu_usage_percent, mem_usage, virtual_mem: process.virtual_mem, + swap_bytes: process.swap_bytes, rps: process.read_per_sec, wps: process.write_per_sec, total_read: process.total_read, @@ -309,6 +311,11 @@ impl ProcWidgetData { self.total_read += other.total_read; self.total_write += other.total_write; self.time = self.time.max(other.time); + self.swap_bytes = match (self.swap_bytes, other.swap_bytes) { + (Some(a), Some(b)) => Some(a + b), + (Some(a), None) | (None, Some(a)) => Some(a), + (None, None) => None, + }; #[cfg(feature = "gpu")] { self.gpu_mem_usage = self.gpu_mem_usage + other.gpu_mem_usage; @@ -324,6 +331,10 @@ impl ProcWidgetData { ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent), ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string(), ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem), + ProcColumn::Swap => self + .swap_bytes + .map(binary_byte_string) + .unwrap_or_else(|| "N/A".to_string()), ProcColumn::Pid => self.pid.to_string(), ProcColumn::Count => self.num_similar.to_string(), ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string(), @@ -360,6 +371,11 @@ impl DataToCell for ProcWidgetData { ProcColumn::CpuPercent => format!("{:.1}%", self.cpu_usage_percent).into(), ProcColumn::MemValue | ProcColumn::MemPercent => self.mem_usage.to_string().into(), ProcColumn::VirtualMem => binary_byte_string(self.virtual_mem).into(), + ProcColumn::Swap => self + .swap_bytes + .map(binary_byte_string) + .unwrap_or_else(|| "N/A".to_string()) + .into(), ProcColumn::Pid => self.pid.to_string().into(), ProcColumn::Count => self.num_similar.to_string().into(), ProcColumn::Name | ProcColumn::Command => self.id.to_prefixed_string().into(), @@ -487,4 +503,30 @@ mod test { "10.4TiB".to_string() ); } + + #[test] + fn test_swap_addition() { + let process = ProcessHarvest { + swap_bytes: Some(100), + ..Default::default() + }; + let other = ProcessHarvest { + swap_bytes: Some(200), + ..Default::default() + }; + let mut data = ProcWidgetData::from_data(&process, false, false); + let other = ProcWidgetData::from_data(&other, false, false); + + data.add(&other); + assert_eq!(data.swap_bytes, Some(300)); + + let unknown = ProcWidgetData::from_data(&ProcessHarvest::default(), false, false); + data.add(&unknown); + assert_eq!(data.swap_bytes, Some(300)); + + let known = ProcWidgetData::from_data(&process, false, false); + let mut unknown = ProcWidgetData::from_data(&ProcessHarvest::default(), false, false); + unknown.add(&known); + assert_eq!(unknown.swap_bytes, Some(100)); + } }