diff options
Diffstat (limited to 'src/widgets/process_table.rs')
-rw-r--r-- | src/widgets/process_table.rs | 958 |
1 files changed, 958 insertions, 0 deletions
diff --git a/src/widgets/process_table.rs b/src/widgets/process_table.rs new file mode 100644 index 00000000..390158ec --- /dev/null +++ b/src/widgets/process_table.rs @@ -0,0 +1,958 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use const_format::formatcp; +use fxhash::{FxHashMap, FxHashSet}; +use itertools::Itertools; + +use crate::{ + app::{ + data_farmer::{DataCollection, ProcessData}, + data_harvester::processes::ProcessHarvest, + query::*, + AppConfigFields, AppSearchState, + }, + canvas::canvas_colours::CanvasColours, + components::data_table::{ + Column, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps, + DataTableStyling, SortColumn, SortDataTable, SortDataTableProps, SortOrder, SortsRow, + }, + Pid, +}; + +pub mod proc_widget_column; +pub use proc_widget_column::*; + +pub mod proc_widget_data; +pub use proc_widget_data::*; + +mod sort_table; +use sort_table::SortTableColumn; +use unicode_segmentation::GraphemeIncomplete; + +/// ProcessSearchState only deals with process' search's current settings and state. +pub struct ProcessSearchState { + pub search_state: AppSearchState, + pub is_ignoring_case: bool, + pub is_searching_whole_word: bool, + pub is_searching_with_regex: bool, +} + +impl Default for ProcessSearchState { + fn default() -> Self { + ProcessSearchState { + search_state: AppSearchState::default(), + is_ignoring_case: true, + is_searching_whole_word: false, + is_searching_with_regex: false, + } + } +} + +impl ProcessSearchState { + pub fn search_toggle_ignore_case(&mut self) { + self.is_ignoring_case = !self.is_ignoring_case; + } + + pub fn search_toggle_whole_word(&mut self) { + self.is_searching_whole_word = !self.is_searching_whole_word; + } + + pub fn search_toggle_regex(&mut self) { + self.is_searching_with_regex = !self.is_searching_with_regex; + } +} + +#[derive(Clone, Debug)] +pub enum ProcWidgetMode { + Tree { collapsed_pids: FxHashSet<Pid> }, + Grouped, + Normal, +} + +type ProcessTable = SortDataTable<ProcWidgetData, ProcColumn>; +type SortTable = DataTable<Cow<'static, str>, SortTableColumn>; +type StringPidMap = FxHashMap<String, Vec<Pid>>; + +pub struct ProcWidget { + pub mode: ProcWidgetMode, + + /// The state of the search box. + pub proc_search: ProcessSearchState, + + /// The state of the main table. + pub table: ProcessTable, + + /// The state of the togglable table that controls sorting. + pub sort_table: SortTable, + + /// A name-to-pid mapping. + pub id_pid_map: StringPidMap, + + pub is_sort_open: bool, + pub force_rerender: bool, + pub force_update_data: bool, +} + +impl ProcWidget { + pub const PID_OR_COUNT: usize = 0; + pub const PROC_NAME_OR_CMD: usize = 1; + pub const CPU: usize = 2; + pub const MEM: usize = 3; + pub const RPS: usize = 4; + pub const WPS: usize = 5; + pub const T_READ: usize = 6; + pub const T_WRITE: usize = 7; + #[cfg(target_family = "unix")] + pub const USER: usize = 8; + #[cfg(target_family = "unix")] + pub const STATE: usize = 9; + #[cfg(not(target_family = "unix"))] + pub const STATE: usize = 8; + + fn new_sort_table(config: &AppConfigFields, colours: &CanvasColours) -> SortTable { + const COLUMNS: [Column<SortTableColumn>; 1] = [Column::hard(SortTableColumn, 7)]; + + let props = DataTableProps { + title: None, + table_gap: config.table_gap, + left_to_right: true, + is_basic: false, + show_table_scroll_position: false, + show_current_entry_when_unfocused: false, + }; + + let styling = DataTableStyling::from_colours(colours); + + DataTable::new(COLUMNS, props, styling) + } + + fn new_process_table( + config: &AppConfigFields, colours: &CanvasColours, mode: &ProcWidgetMode, is_count: bool, + is_command: bool, show_memory_as_values: bool, + ) -> ProcessTable { + let (default_index, default_order) = if matches!(mode, ProcWidgetMode::Tree { .. }) { + (Self::PID_OR_COUNT, SortOrder::Ascending) + } else { + (Self::CPU, SortOrder::Descending) + }; + + let columns = { + use ProcColumn::*; + + let pid_or_count = SortColumn::new(if is_count { Count } else { Pid }); + let name_or_cmd = SortColumn::soft(if is_command { Command } else { Name }, Some(0.3)); + let cpu = SortColumn::new(CpuPercent).default_descending(); + let mem = SortColumn::new(if show_memory_as_values { + MemoryVal + } else { + MemoryPercent + }) + .default_descending(); + let rps = SortColumn::hard(ReadPerSecond, 8).default_descending(); + let wps = SortColumn::hard(WritePerSecond, 8).default_descending(); + let tr = SortColumn::hard(TotalRead, 8).default_descending(); + let tw = SortColumn::hard(TotalWrite, 8).default_descending(); + let state = SortColumn::hard(State, 7); + + vec![ + pid_or_count, + name_or_cmd, + cpu, + mem, + rps, + wps, + tr, + tw, + #[cfg(target_family = "unix")] + SortColumn::soft(User, Some(0.05)), + state, + ] + }; + + let inner_props = DataTableProps { + title: Some(" Processes ".into()), + table_gap: config.table_gap, + left_to_right: true, + is_basic: config.use_basic_mode, + show_table_scroll_position: config.show_table_scroll_position, + show_current_entry_when_unfocused: false, + }; + let props = SortDataTableProps { + inner: inner_props, + sort_index: default_index, + order: default_order, + }; + + let styling = DataTableStyling::from_colours(colours); + + DataTable::new_sortable(columns, props, styling) + } + + pub fn new( + config: &AppConfigFields, mode: ProcWidgetMode, is_case_sensitive: bool, + is_match_whole_word: bool, is_use_regex: bool, show_memory_as_values: bool, + is_command: bool, colours: &CanvasColours, + ) -> Self { + let process_search_state = { + let mut pss = ProcessSearchState::default(); + + if is_case_sensitive { + // By default it's off + pss.search_toggle_ignore_case(); + } + if is_match_whole_word { + pss.search_toggle_whole_word(); + } + if is_use_regex { + pss.search_toggle_regex(); + } + + pss + }; + + let is_count = matches!(mode, ProcWidgetMode::Grouped); + let sort_table = Self::new_sort_table(config, colours); + let table = Self::new_process_table( + config, + colours, + &mode, + is_count, + is_command, + show_memory_as_values, + ); + + let id_pid_map = FxHashMap::default(); + + let mut table = ProcWidget { + proc_search: process_search_state, + table, + sort_table, + id_pid_map, + is_sort_open: false, + mode, + force_rerender: true, + force_update_data: false, + }; + table.sort_table.set_data(table.column_text()); + + table + } + + pub fn is_using_command(&self) -> bool { + self.table + .columns + .get(ProcWidget::PROC_NAME_OR_CMD) + .map(|col| matches!(col.inner(), ProcColumn::Command)) + .unwrap_or(false) + } + + pub fn is_mem_percent(&self) -> bool { + self.table + .columns + .get(ProcWidget::MEM) + .map(|col| matches!(col.inner(), ProcColumn::MemoryPercent)) + .unwrap_or(false) + } + + fn get_query(&self) -> &Option<Query> { + if self.proc_search.search_state.is_invalid_or_blank_search() { + &None + } else { + &self.proc_search.search_state.query + } + } + + /// 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 ingest_data(&mut self, data_collection: &DataCollection) { + let data = match &self.mode { + ProcWidgetMode::Grouped | ProcWidgetMode::Normal => { + self.get_normal_data(&data_collection.process_data.process_harvest) + } + ProcWidgetMode::Tree { collapsed_pids } => { + self.get_tree_data(collapsed_pids, data_collection) + } + }; + self.table.set_data(data); + } + + fn get_tree_data( + &self, collapsed_pids: &FxHashSet<Pid>, data_collection: &DataCollection, + ) -> Vec<ProcWidgetData> { + const BRANCH_END: char = '└'; + const BRANCH_VERTICAL: char = '│'; + const BRANCH_SPLIT: char = '├'; + const BRANCH_HORIZONTAL: char = '─'; + + let search_query = self.get_query(); + let is_using_command = self.is_using_command(); + let is_mem_percent = self.is_mem_percent(); + + let ProcessData { + process_harvest, + process_parent_mapping, + orphan_pids, + .. + } = &data_collection.process_data; + + let kept_pids = data_collection + .process_data + .process_harvest + .iter() + .map(|(pid, process)| { + ( + *pid, + search_query + .as_ref() + .map(|q| q.check(process, is_using_command)) + .unwrap_or(true), + ) + }) + .collect::<FxHashMap<_, _>>(); + + let filtered_tree = { + let mut filtered_tree = FxHashMap::default(); + + // We do a simple BFS traversal to build our filtered parent-to-tree mappings. + let mut visited_pids = FxHashMap::default(); + let mut stack = orphan_pids + .iter() + .filter_map(|process| process_harvest.get(process)) + .collect_vec(); + + while let Some(process) = stack.last() { + let is_process_matching = *kept_pids.get(&process.pid).unwrap_or(&false); + + if let Some(children_pids) = process_parent_mapping.get(&process.pid) { + if children_pids + .iter() + .all(|pid| visited_pids.contains_key(pid)) + { + let shown_children = children_pids + .iter() + .filter(|pid| visited_pids.get(*pid).copied().unwrap_or(false)) + .collect_vec(); + let is_shown = is_process_matching || !shown_children.is_empty(); + visited_pids.insert(process.pid, is_shown); + + if is_shown { + filtered_tree.insert( + process.pid, + shown_children + .into_iter() + .filter_map(|pid| { + process_harvest.get(pid).map(|process| process.pid) + }) + .collect_vec(), + ); + } + + stack.pop(); + } else { + children_pids + .iter() + .filter_map(|process| process_harvest.get(process)) + .rev() + .for_each(|process| { + stack.push(process); + }); + } + } else { + if is_process_matching { + filtered_tree.insert(process.pid, vec![]); + } + + visited_pids.insert(process.pid, is_process_matching); + stack.pop(); + } + } + + filtered_tree + }; + + let mut data = vec![]; + let mut prefixes = vec![]; + let mut stack = orphan_pids + .iter() + .filter_map(|pid| { + if filtered_tree.contains_key(pid) { + process_harvest.get(pid).map(|process| { + ProcWidgetData::from_data(process, is_using_command, is_mem_percent) + }) + } else { + None + } + }) + .collect_vec(); + + stack.sort_unstable_by_key(|p| p.pid); + + let column = self.table.columns.get(self.table.sort_index()).unwrap(); + sort_skip_pid_asc(column.inner(), &mut stack, self.table.order()); + + stack.reverse(); + + let mut length_stack = vec![stack.len()]; + + while let (Some(process), Some(siblings_left)) = (stack.pop(), length_stack.last_mut()) { + *siblings_left -= 1; + + let disabled = !*kept_pids.get(&process.pid).unwrap_or(&false); + let is_last = *siblings_left == 0; + + if collapsed_pids.contains(&process.pid) { + let mut summed_process = process.clone(); + + if let Some(children_pids) = filtered_tree.get(&process.pid) { + let mut sum_queue = children_pids + .iter() + .filter_map(|child| { + process_harvest.get(child).map(|p| { + ProcWidgetData::from_data(p, is_using_command, is_mem_percent) + }) + }) + .collect_vec(); + + while let Some(process) = sum_queue.pop() { + summed_process.add(&process); + + if let Some(pids) = filtered_tree.get(&process.pid) { + sum_queue.extend(pids.iter().filter_map(|child| { + process_harvest.get(child).map(|p| { + ProcWidgetData::from_data(p, is_using_command, is_mem_percent) + }) + })); + } + } + } + + let prefix = if prefixes.is_empty() { + "+ ".to_string() + } else { + format!( + "{}{}{} + ", + prefixes.join(""), + if is_last { BRANCH_END } else { BRANCH_SPLIT }, + BRANCH_HORIZONTAL + ) + }; + + data.push(summed_process.prefix(Some(prefix)).disabled(disabled)); + } else { + let prefix = if prefixes.is_empty() { + String::default() + } else { + format!( + "{}{}{} ", + prefixes.join(""), + if is_last { BRANCH_END } else { BRANCH_SPLIT }, + BRANCH_HORIZONTAL + ) + }; + let pid = process.pid; + data.push(process.prefix(Some(prefix)).disabled(disabled)); + + if let Some(children_pids) = filtered_tree.get(&pid) { + if prefixes.is_empty() { + prefixes.push(""); + } else { + prefixes.push(if is_last { + " " + } else { + formatcp!("{} ", BRANCH_VERTICAL) + }); + } + + let mut children = children_pids + .iter() + .filter_map(|child_pid| { + process_harvest.get(child_pid).map(|p| { + ProcWidgetData::from_data(p, is_using_command, is_mem_percent) + }) + }) + .collect_vec(); + + column.sort_by(&mut children, self.table.order().rev()); + + length_stack.push(children.len()); + stack.extend(children); + } + } + + while let Some(children_left) = length_stack.last() { + if *children_left == 0 { + length_stack.pop(); + prefixes.pop(); + } else { + break; + } + } + } + + data + } + + fn get_normal_data( + &mut self, process_harvest: &BTreeMap<Pid, ProcessHarvest>, + ) -> Vec<ProcWidgetData> { + let search_query = self.get_query(); + let is_using_command = self.is_using_command(); + let is_mem_percent = self.is_mem_percent(); + + let filtered_iter = process_harvest.values().filter(|process| { + search_query + .as_ref() + .map(|query| query.check(process, is_using_command)) + .unwrap_or(true) + }); + + let mut id_pid_map: FxHashMap<String, Vec<Pid>> = FxHashMap::default(); + let mut filtered_data: Vec<ProcWidgetData> = if let ProcWidgetMode::Grouped = self.mode { + let mut id_process_mapping: FxHashMap<&String, ProcessHarvest> = FxHashMap::default(); + for process in filtered_iter { + let id = if is_using_command { + &process.command + } else { + &process.name + }; + let pid = process.pid; + + if let Some(entry) = id_pid_map.get_mut(id) { + entry.push(pid); + } else { + id_pid_map.insert(id.clone(), vec![pid]); + } + + if let Some(grouped_process_harvest) = id_process_mapping.get_mut(id) { + grouped_process_harvest.add(process); + } else { + // FIXME: [PERF] could maybe eliminate an allocation here in the grouped mode... or maybe just avoid the entire transformation step, making an alloc fine. + id_process_mapping.insert(id, process.clone()); + } + } + + id_process_mapping + .values() + .map(|process| { + let id = if is_using_command { + &process.command + } else { + &process.name + }; + + let num_similar = id_pid_map.get(id).map(|val| val.len()).unwrap_or(1) as u64; + + ProcWidgetData::from_data(process, is_using_command, is_mem_percent) + .num_similar(num_similar) + }) + .collect() + } else { + filtered_iter + .map(|process| ProcWidgetData::from_data(process, is_using_command, is_mem_percent)) + .collect() + }; + + self.id_pid_map = id_pid_map; + + if let Some(column) = self.table.columns.get(self.table.sort_index()) { + sort_skip_pid_asc(column.inner(), &mut filtered_data, self.table.order()); + } + + filtered_data + } + + #[inline(always)] + fn get_mut_proc_col(&mut self, index: usize) -> Option<&mut ProcColumn> { + self.table.columns.get_mut(index).map(|col| col.inner_mut()) + } + + pub fn toggle_mem_percentage(&mut self) { + if let Some(mem) = self.get_mut_proc_col(Self::MEM) { + match mem { + ProcColumn::MemoryVal => { + *mem = ProcColumn::MemoryPercent; + } + ProcColumn::MemoryPercent => { + *mem = ProcColumn::MemoryVal; + } + _ => unreachable!(), + } + + self.sort_table.set_data(self.column_text()); + self.force_data_update(); + } + } + + /// Forces an update of the data stored. + #[inline] + pub fn force_data_update(&mut self) { + self.force_update_data = true; + } + + /// Forces an entire rerender and update of the data stored. + #[inline] + pub fn force_rerender_and_update(&mut self) { + self.force_rerender = true; + self.force_update_data = true; + } + + /// Marks the selected column as hidden, and automatically resets the selected column to CPU + /// and descending if that column was selected. + fn hide_column(&mut self, index: usize) { + if let Some(col) = self.table.columns.get_mut(index) { + col.is_hidden = true; + + if self.table.sort_index() == index { + self.table.set_sort_index(Self::CPU); + self.table.set_order(SortOrder::Descending); + } + } + } + + /// Marks the selected column as shown. + fn show_column(&mut self, index: usize) { + if let Some(col) = self.table.columns.get_mut(index) { + col.is_hidden = false; + } + } + + /// Select a column. If the column is already selected, then just toggle the sort order. + pub fn select_column(&mut self, new_sort_index: usize) { + self.table.set_sort_index(new_sort_index); + self.force_data_update(); + } + + pub fn toggle_current_tree_branch_entry(&mut self) { + if let ProcWidgetMode::Tree { collapsed_pids } = &mut self.mode { + if let Some(process) = self.table.current_item() { + let pid = process.pid; + + if !collapsed_pids.remove(&pid) { + collapsed_pids.insert(pid); + } + self.force_data_update(); + } + } + } + + pub fn toggle_command(&mut self) { + if let Some(col) = self.table.columns.get_mut(Self::PROC_NAME_OR_CMD) { + let inner = col.inner_mut(); + match inner { + ProcColumn::Name => { + *inner = ProcColumn::Command; + if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() { + *max_percentage = Some(0.5); + } + } + ProcColumn::Command => { + *inner = ProcColumn::Name; + if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() { + *max_percentage = match self.mode { + ProcWidgetMode::Tree { .. } => Some(0.5), + ProcWidgetMode::Grouped | ProcWidgetMode::Normal => Some(0.3), + }; + } + } + _ => unreachable!(), + } + self.sort_table.set_data(self.column_text()); + self.force_rerender_and_update(); + } + } + + /// Toggles the appropriate columns/settings when tab is pressed. + /// + /// If count is enabled, we should set the mode to [`ProcWidgetMode::Grouped`], and switch off the User and State + /// columns. We should also move the user off of the columns if they were selected, as those columns are now hidden + /// (handled by internal method calls), and go back to the "defaults". + /// + /// Otherwise, if count is disabled, then the User and State columns should be re-enabled, and the mode switched + /// to [`ProcWidgetMode::Normal`]. + pub fn on_tab(&mut self) { + if !matches!(self.mode, ProcWidgetMode::Tree { .. }) { + if let Some(sort_col) = self.table.columns.get_mut(Self::PID_OR_COUNT) { + let col = sort_col.inner_mut(); + match col { + ProcColumn::Pid => { + *col = ProcColumn::Count; + sort_col.default_order = SortOrder::Descending; + + #[cfg(target_family = "unix")] + self.hide_column(Self::USER); + self.hide_column(Self::STATE); + self.mode = ProcWidgetMode::Grouped; + } + ProcColumn::Count => { + *col = ProcColumn::Pid; + sort_col.default_order = SortOrder::Ascending; + + #[cfg(target_family = "unix")] + self.show_column(Self::USER); + self.show_column(Self::STATE); + self.mode = ProcWidgetMode::Normal; + } + _ => unreachable!(), + } + + self.sort_table.set_data(self.column_text()); + self.force_rerender_and_update(); + } + } + } + + pub fn column_text(&self) -> Vec<Cow<'static, str>> { + self.table + .columns + .iter() + .filter(|c| !c.is_hidden) + .map(|c| c.inner().text()) + .collect::<Vec<_>>() + } + + pub fn get_search_cursor_position(&self) -> usize { + self.proc_search.search_state.grapheme_cursor.cur_cursor() + } + + pub fn get_char_cursor_position(&self) -> usize { + self.proc_search.search_state.char_cursor_position + } + + pub fn is_search_enabled(&self) -> bool { + self.proc_search.search_state.is_enabled + } + + pub fn get_current_search_query(&self) -> &String { + &self.proc_search.search_state.current_search_query + } + + pub fn update_query(&mut self) { + if self + .proc_search + .search_state + .current_search_query + .is_empty() + { + self.proc_search.search_state.is_blank_search = true; + self.proc_search.search_state.is_invalid_search = false; + self.proc_search.search_state.error_message = None; + } else { + match parse_query( + &self.proc_search.search_state.current_search_query, + self.proc_search.is_searching_whole_word, + self.proc_search.is_ignoring_case, + self.proc_search.is_searching_with_regex, + ) { + Ok(parsed_query) => { + self.proc_search.search_state.query = Some(parsed_query); + self.proc_search.search_state.is_blank_search = false; + self.proc_search.search_state.is_invalid_search = false; + self.proc_search.search_state.error_message = None; + } + Err(err) => { + self.proc_search.search_state.is_blank_search = false; + self.proc_search.search_state.is_invalid_search = true; + self.proc_search.search_state.error_message = Some(err.to_string()); + } + } + } + self.table.state.display_start_index = 0; + self.table.state.current_index = 0; + + self.force_data_update(); + } + + pub fn clear_search(&mut self) { + self.proc_search.search_state.reset(); + self.force_data_update(); + } + + pub fn search_walk_forward(&mut self, start_position: usize) { + // TODO: Add tests for this. + let chunk = &self.proc_search.search_state.current_search_query[start_position..]; + + match self + .proc_search + .search_state + .grapheme_cursor + .next_boundary(chunk, start_position) + { + Ok(_) => {} + Err(err) => match err { + GraphemeIncomplete::PreContext(ctx) => { + // Provide the entire string as context. Not efficient but should resolve failures. + self.proc_search + .search_state + .grapheme_cursor + .provide_context( + &self.proc_search.search_state.current_search_query[0..ctx], + 0, + ); + + self.proc_search + .search_state + .grapheme_cursor + .next_boundary(chunk, start_position) + .unwrap(); + } + _ => Err(err).unwrap(), + }, + } + } + + pub fn search_walk_back(&mut self, start_position: usize) { + // TODO: Add tests for this. + let chunk = &self.proc_search.search_state.current_search_query[..start_position]; + match self + .proc_search + .search_state + .grapheme_cursor + .prev_boundary(chunk, 0) + { + Ok(_) => {} + Err(err) => match err { + GraphemeIncomplete::PreContext(ctx) => { + // Provide the entire string as context. Not efficient but should resolve failures. + self.proc_search + .search_state + .grapheme_cursor + .provide_context( + &self.proc_search.search_state.current_search_query[0..ctx], + 0, + ); + + self.proc_search + .search_state + .grapheme_cursor + .prev_boundary(chunk, 0) + .unwrap(); + } + _ => Err(err).unwrap(), + }, + } + } + + /// Returns the number of columns *enabled*. Note this differs from *visible* - a column may be enabled but not + /// visible (e.g. off screen). + pub fn num_enabled_columns(&self) -> usize { + self.table.columns.iter().filter(|c| !c.is_hidden).count() + } + + /// Sets the [`ProcWidget`]'s current sort index to whatever was in the sort table if possible, then closes the + /// sort table. + pub(crate) fn use_sort_table_value(&mut self) { + self.table.set_sort_index(self.sort_table.current_index()); + + self.is_sort_open = false; + self.force_rerender_and_update(); + } +} + +#[inline] +fn sort_skip_pid_asc(column: &ProcColumn, data: &mut [ProcWidgetData], order: SortOrder) { + let descending = matches!(order, SortOrder::Descending); + match column { + ProcColumn::Pid if !descending => {} + _ => { + column.sort_data(data, descending); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::widgets::MemUsage; + + #[test] + fn test_proc_sort() { + let a = ProcWidgetData { + pid: 1, + ppid: None, + id: "A".into(), + cpu_usage_percent: 0.0, + mem_usage: MemUsage::Percent(1.1), + rps: 0, + wps: 0, + total_read: 0, + total_write: 0, + process_state: "N/A".to_string(), + process_char: '?', + #[cfg(target_family = "unix")] + user: "root".to_string(), + num_similar: 0, + disabled: false, + }; + + let b = ProcWidgetData { + pid: 2, + ppid: Some(1), + id: "B".into(), + cpu_usage_percent: 1.1, + mem_usage: MemUsage::Percent(2.2), + ..(a.clone()) + }; + + let c = ProcWidgetData { + pid: 3, + ppid: Some(1), + id: "C".into(), + cpu_usage_percent: 2.2, + mem_usage: MemUsage::Percent(0.0), + ..(a.clone()) + }; + + let d = ProcWidgetData { + pid: 4, + ppid: Some(2), + id: "D".into(), + cpu_usage_percent: 0.0, + mem_usage: MemUsage::Percent(0.0), + ..(a.clone()) + }; + let mut data = vec![d.clone(), b.clone(), c.clone(), a.clone()]; + + // Assume we had sorted over by pid. + data.sort_by_key(|p| p.pid); + sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Descending); + assert_eq!( + vec![&c, &b, &a, &d] + .iter() + .map(|d| (d.pid)) + .collect::<Vec<_>>(), + data.iter().map(|d| (d.pid)).collect::<Vec<_>>(), + ); + + // Note that the PID ordering for ties is still ascending. + data.sort_by_key(|p| p.pid); + sort_skip_pid_asc(&ProcColumn::CpuPercent, &mut data, SortOrder::Ascending); + assert_eq!( + vec![&a, &d, &b, &c] + .iter() + .map(|d| (d.pid)) + .collect::<Vec<_>>(), + data.iter().map(|d| (d.pid)).collect::<Vec<_>>(), + ); + + data.sort_by_key(|p| p.pid); + sort_skip_pid_asc(&ProcColumn::MemoryPercent, &mut data, SortOrder::Descending); + assert_eq!( + vec![&b, &a, &c, &d] + .iter() + .map(|d| (d.pid)) + .collect::<Vec<_>>(), + data.iter().map(|d| (d.pid)).collect::<Vec<_>>(), + ); + + // Note that the PID ordering for ties is still ascending. + data.sort_by_key(|p| p.pid); + sort_skip_pid_asc(&ProcColumn::MemoryPercent, &mut data, SortOrder::Ascending); + assert_eq!( + vec![&c, &d, &a, &b] + .iter() + .map(|d| (d.pid)) + .collect::<Vec<_>>(), + data.iter().map(|d| (d.pid)).collect::<Vec<_>>(), + ); + } +} |