pub mod process_columns; pub mod process_data; pub mod query; mod sort_table; use std::{borrow::Cow, collections::BTreeMap}; use indexmap::IndexSet; use itertools::Itertools; pub use process_columns::*; pub use process_data::*; use query::{ProcessQuery, parse_query}; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use sort_table::SortTableColumn; use crate::{ app::{ AppConfigFields, AppSearchState, data::{InnerData, ProcessData}, }, canvas::components::data_table::{ Column, ColumnHeader, ColumnWidthBounds, DataTable, DataTableColumn, DataTableProps, DataTableStyling, SortColumn, SortDataTable, SortDataTableProps, SortOrder, SortsRow, }, collection::processes::{Pid, ProcessHarvest}, options::config::style::Styles, utils::int_hash::IntHashMap, widgets::query::QueryOptions, }; /// ProcessSearchState only deals with process' search's current settings and /// state. #[derive(Default)] pub struct ProcessSearchState { // TODO: Flatten AppSearchState as it's been generalized further. pub search_state: AppSearchState, pub query_options: QueryOptions, } impl ProcessSearchState { pub fn search_toggle_ignore_case(&mut self) { self.query_options.ignore_case = !self.query_options.ignore_case; } pub fn search_toggle_whole_word(&mut self) { self.query_options.whole_word = !self.query_options.whole_word; } pub fn search_toggle_regex(&mut self) { self.query_options.use_regex = !self.query_options.use_regex; } } /// Whether to expand or collapse by default. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum TreeCollapsed { DefaultCollapse { expanded_pids: HashSet }, DefaultExpand { collapsed_pids: HashSet }, } impl TreeCollapsed { /// Creates a new [`TreeCollapsed`]. pub(crate) fn new(default_collapsed: bool) -> Self { if default_collapsed { TreeCollapsed::DefaultCollapse { expanded_pids: HashSet::default(), } } else { TreeCollapsed::DefaultExpand { collapsed_pids: HashSet::default(), } } } /// Check whether the given PID is collapsed. pub(crate) fn is_collapsed(&self, pid: Pid) -> bool { match self { TreeCollapsed::DefaultCollapse { expanded_pids } => !expanded_pids.contains(&pid), TreeCollapsed::DefaultExpand { collapsed_pids } => collapsed_pids.contains(&pid), } } /// Collapse the given PID. pub(crate) fn collapse(&mut self, pid: Pid) { match self { TreeCollapsed::DefaultCollapse { expanded_pids } => { expanded_pids.remove(&pid); } TreeCollapsed::DefaultExpand { collapsed_pids } => { collapsed_pids.insert(pid); } } } /// Expand the given PID. pub(crate) fn expand(&mut self, pid: Pid) { match self { TreeCollapsed::DefaultCollapse { expanded_pids } => { expanded_pids.insert(pid); } TreeCollapsed::DefaultExpand { collapsed_pids } => { collapsed_pids.remove(&pid); } } } /// Toggle the given PID. pub(crate) fn toggle(&mut self, pid: Pid) { match self { TreeCollapsed::DefaultCollapse { expanded_pids } => { if expanded_pids.contains(&pid) { expanded_pids.remove(&pid); } else { expanded_pids.insert(pid); } } TreeCollapsed::DefaultExpand { collapsed_pids } => { if collapsed_pids.contains(&pid) { collapsed_pids.remove(&pid); } else { collapsed_pids.insert(pid); } } } } } #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum ProcWidgetMode { Tree(TreeCollapsed), Grouped, Normal, } type ProcessTable = SortDataTable; type SortTable = DataTable, SortTableColumn>; type StringPidMap = HashMap>; fn make_column(column: ProcColumn) -> SortColumn { use ProcColumn::*; match column { CpuPercent => SortColumn::new(CpuPercent).default_descending(), MemValue => SortColumn::new(MemValue).default_descending(), MemPercent => SortColumn::new(MemPercent).default_descending(), VirtualMem => SortColumn::new(VirtualMem).default_descending(), Pid => SortColumn::new(Pid), Count => SortColumn::new(Count), Name => SortColumn::soft(Name, Some(0.3)), Command => SortColumn::soft(Command, Some(0.3)), ReadPerSecond => SortColumn::hard(ReadPerSecond, 8).default_descending(), WritePerSecond => SortColumn::hard(WritePerSecond, 8).default_descending(), TotalRead => SortColumn::hard(TotalRead, 8).default_descending(), TotalWrite => SortColumn::hard(TotalWrite, 8).default_descending(), User => SortColumn::soft(User, Some(0.05)), State => SortColumn::hard(State, 9), Time => SortColumn::new(Time), Priority => SortColumn::new(Priority).default_descending(), #[cfg(unix)] Nice => SortColumn::new(Nice), #[cfg(feature = "gpu")] GpuMemValue => SortColumn::new(GpuMemValue).default_descending(), #[cfg(feature = "gpu")] GpuMemPercent => SortColumn::new(GpuMemPercent).default_descending(), #[cfg(feature = "gpu")] GpuUtilPercent => SortColumn::new(GpuUtilPercent).default_descending(), } } #[derive(Clone, Copy, Default)] pub struct ProcTableConfig { pub is_case_sensitive: bool, pub is_match_whole_word: bool, pub is_use_regex: bool, pub show_memory_as_values: bool, pub is_command: bool, pub default_sort: Option, pub sort_order: Option, } pub struct ProcWidgetState { pub(crate) 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, /// The internal column mapping as an [`IndexSet`], to allow us to do quick /// mappings of column type -> index. /// /// NB: This _must_ be kept in sync with the internal `sort_table` when working with it! column_mapping: IndexSet, /// A name-to-pid mapping. pub id_pid_map: StringPidMap, /// The default sort index. default_sort_index: usize, /// The default sort order. default_sort_order: SortOrder, pub is_sort_open: bool, pub force_rerender: bool, pub force_update_data: bool, #[cfg(target_os = "linux")] pub hide_k_threads: bool, } impl ProcWidgetState { fn new_sort_table(config: &AppConfigFields, palette: &Styles) -> SortTable { const COLUMNS: [Column; 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_table_scroll_bar: false, show_current_entry_when_unfocused: false, }; let styling = DataTableStyling::from_palette(palette); DataTable::new(COLUMNS, props, styling) } fn new_process_table( config: &AppConfigFields, colours: &Styles, columns: Vec>, default_index: usize, default_order: SortOrder, ) -> ProcessTable { 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_table_scroll_bar: config.show_table_scroll_bar, show_current_entry_when_unfocused: false, }; let props = SortDataTableProps { inner: inner_props, sort_index: default_index, order: default_order, }; let styling = DataTableStyling::from_palette(colours); DataTable::new_sortable(columns, props, styling) } pub(crate) fn new( config: &AppConfigFields, mode: ProcWidgetMode, table_config: ProcTableConfig, colours: &Styles, config_columns: &Option>, ) -> Self { let process_search_state = { let mut pss = ProcessSearchState::default(); if table_config.is_case_sensitive { // By default it's off. pss.search_toggle_ignore_case(); } if table_config.is_match_whole_word { pss.search_toggle_whole_word(); } if table_config.is_use_regex { pss.search_toggle_regex(); } pss }; let (columns, column_mapping): (Vec>, _) = { use ProcColumn::*; let is_count = matches!(mode, ProcWidgetMode::Grouped); let is_command = table_config.is_command; let mem_as_values = table_config.show_memory_as_values; match config_columns { Some(configured_columns) if !configured_columns.is_empty() => { let columns = configured_columns .into_iter() .map(|p| { // Note that we have to handle "toggle" columns - if a user "splits" a toggle column on their own, // then we won't do anything - but if they don't, then we have to take into account is_count, // is_command, and mem_as_values. fn contains_unique_toggle_column( curr: &ProcColumn, configured_columns: &IndexSet, a: ProcColumn, b: ProcColumn, ) -> bool { (*curr == a && !configured_columns.contains(&b)) || (*curr == b && !configured_columns.contains(&a)) } if contains_unique_toggle_column( p, configured_columns, ProcColumn::Pid, ProcColumn::Count, ) { if is_count { make_column(Count) } else { make_column(Pid) } } else if contains_unique_toggle_column( p, configured_columns, ProcColumn::Name, ProcColumn::Command, ) { if is_command { make_column(Command) } else { make_column(Name) } } else if contains_unique_toggle_column( p, configured_columns, ProcColumn::MemPercent, ProcColumn::MemValue, ) { if mem_as_values { make_column(MemValue) } else { make_column(MemPercent) } } else { make_column(*p) } }) .collect(); (columns, configured_columns.clone()) } _ => { let default_columns = [ if is_count { Count } else { Pid }, if is_command { Command } else { Name }, CpuPercent, if mem_as_values { MemValue } else { MemPercent }, ReadPerSecond, WritePerSecond, TotalRead, TotalWrite, User, State, Time, Priority, // Maybe add nice back as a default when I can figure out how to do the // default configs better for Windows? As currently otherwise there's a // mismatch. ]; let columns = default_columns.into_iter().map(make_column).collect(); (columns, IndexSet::from_iter(default_columns)) } } }; let configured_default_sort = table_config.default_sort.and_then(|col| { column_mapping .get_index_of(&col) .map(|index| (index, columns[index].default_order)) }); let (default_sort_index, mut default_sort_order) = if let Some(pair) = configured_default_sort { pair } else if matches!(mode, ProcWidgetMode::Tree { .. }) { if let Some(index) = column_mapping.get_index_of(&ProcColumn::Pid) { (index, columns[index].default_order) } else { (0, columns[0].default_order) } } else if let Some(index) = column_mapping.get_index_of(&ProcColumn::CpuPercent) { (index, columns[index].default_order) } else { (0, columns[0].default_order) }; // If configured, override it with this. if let Some(order) = table_config.sort_order { default_sort_order = order; } let sort_table = Self::new_sort_table(config, colours); let table = Self::new_process_table( config, colours, columns, default_sort_index, default_sort_order, ); let id_pid_map = HashMap::default(); let mut table = ProcWidgetState { proc_search: process_search_state, table, sort_table, id_pid_map, column_mapping, is_sort_open: false, mode, force_rerender: true, force_update_data: false, default_sort_index, default_sort_order, #[cfg(target_os = "linux")] hide_k_threads: config.hide_k_threads, }; table.sort_table.set_data(table.column_text()); table } #[inline] pub fn is_using_command(&self) -> bool { self.column_mapping .get_index_of(&ProcColumn::Command) .is_some() } #[inline] pub fn is_mem_percent(&self) -> bool { self.column_mapping .get_index_of(&ProcColumn::MemPercent) .is_some() } fn get_query(&self) -> &Option { if self.proc_search.search_state.is_invalid_or_blank_search() { &None } else { &self.proc_search.search_state.query } } /// Update the current table data. /// /// 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: &InnerData) { let data = match &self.mode { ProcWidgetMode::Grouped | ProcWidgetMode::Normal => { self.get_normal_data(&stored_data.process_data.process_harvest) } ProcWidgetMode::Tree(collapse) => self.get_tree_data(collapse, stored_data), }; self.table.set_data(data); self.force_update_data = false; } fn get_tree_data( &self, collapsed: &TreeCollapsed, stored_data: &InnerData, ) -> Vec { const BRANCH_END: char = '└'; const BRANCH_SPLIT: char = '├'; const BRANCH_HORIZONTAL: char = '─'; const SPACED_BRANCH_VERTICAL: &str = "│ "; 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, .. } = &stored_data.process_data; // Only keep a set of the kept PIDs. let kept_pids = stored_data .process_data .process_harvest .iter() .filter_map(|(pid, process)| { if search_query .as_ref() .map(|q| q.check(process, is_using_command)) .unwrap_or(true) { #[cfg(target_os = "linux")] if self.hide_k_threads && process.process_type.is_kernel() { return None; } Some(*pid) } else { None } }) .collect::>(); #[inline] fn is_ancestor_shown( current_process: &ProcessHarvest, kept_pids: &HashSet, process_harvest: &BTreeMap, ) -> bool { if let Some(ppid) = current_process.parent_pid { if kept_pids.contains(&ppid) { true } else if let Some(parent) = process_harvest.get(&ppid) { is_ancestor_shown(parent, kept_pids, process_harvest) } else { false } } else { false } } // A process is shown under the filtered tree if at least one of these // conditions hold: // - The process itself matches. // - The process contains some descendant that matches. // - The process's parent (and only parent, not any ancestor) matches. let filtered_tree = { let mut filtered_tree: IntHashMap> = IntHashMap::default(); // We do a simple DFS traversal to build our filtered parent-to-tree mappings. let mut visited_pids: IntHashMap = IntHashMap::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.contains(&process.pid); 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(); // Show the entry if it is: // - Matches the filter. // - Has at least one child (doesn't have to be direct) that matches the // filter. // - Is the child of a shown process. let is_shown = is_process_matching || !shown_children.is_empty() || is_ancestor_shown(process, &kept_pids, process_harvest); 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 { let is_shown = is_process_matching || is_ancestor_shown(process, &kept_pids, process_harvest); if is_shown { filtered_tree.insert(process.pid, vec![]); } visited_pids.insert(process.pid, is_shown); 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()) .expect("columns should contain the current sort index"); sort_skip_pid_asc(column.inner(), &mut stack, self.table.order()); let mut length_stack = vec![stack.len()]; stack.reverse(); while let (Some(process), Some(siblings_left)) = (stack.pop(), length_stack.last_mut()) { *siblings_left -= 1; let disabled = !kept_pids.contains(&process.pid); let is_last = *siblings_left == 0; if collapsed.is_collapsed(process.pid) { let mut summed_process = process.clone(); let mut has_children = false; 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) }) })); } } has_children = !children_pids.is_empty(); } // This is so that if an entry is "collapsed" but there are no children, avoid // drawing the "+". let prefix = if has_children { if prefixes.is_empty() { "+ ".to_string() } else { format!( "{}{}{} + ", prefixes.join(""), if is_last { BRANCH_END } else { BRANCH_SPLIT }, BRANCH_HORIZONTAL ) } } else if prefixes.is_empty() { String::default() } 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 { SPACED_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, ) -> Vec { 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| { #[cfg(target_os = "linux")] if self.hide_k_threads && process.process_type.is_kernel() { return false; } search_query .as_ref() .map(|query| query.check(process, is_using_command)) .unwrap_or(true) }); let mut id_pid_map: HashMap> = HashMap::default(); let mut filtered_data: Vec = if let ProcWidgetMode::Grouped = self.mode { let mut id_process_mapping: HashMap<&String, ProcWidgetData> = HashMap::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(pwd) = id_process_mapping.get_mut(id) { pwd.cpu_usage_percent += process.cpu_usage_percent; match &mut pwd.mem_usage { MemUsage::Percent(usage) => { *usage += process.mem_usage_percent; } MemUsage::Bytes(usage) => { *usage += process.mem_usage; } } pwd.rps += process.read_per_sec; pwd.wps += process.write_per_sec; pwd.total_read += process.total_read; pwd.total_write += process.total_write; pwd.time = pwd.time.max(process.time); #[cfg(feature = "gpu")] { pwd.gpu_usage += process.gpu_util; match &mut pwd.gpu_mem_usage { MemUsage::Percent(usage) => { *usage += process.gpu_mem_percent; } MemUsage::Bytes(usage) => { *usage += process.gpu_mem; } } } pwd.num_similar += 1; } else { id_process_mapping.insert( id, ProcWidgetData::from_data(process, is_using_command, is_mem_percent), ); } } id_process_mapping.into_values().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()) } /// If there is only one memory "type" column (GPU memory is separate from system memory for this check), then /// this will toggle it to use the percentage type, or vice versa. /// /// If there is already multiple types - e.g. the user defines both [`ProcColumn::MemValue`] _and_ [`ProcColumn::MemPercent`], /// then this will not do anything since toggling is pointless with that configuration. pub fn toggle_mem_percentage(&mut self) { match ( self.column_mapping.get_index_of(&ProcColumn::MemPercent), self.column_mapping.get_index_of(&ProcColumn::MemValue), ) { (None, None) | (Some(_), Some(_)) => {} (None, Some(index)) | (Some(index), None) => { if let Some(mem_column) = self.get_mut_proc_col(index) { let new_value = match mem_column { ProcColumn::MemValue => ProcColumn::MemPercent, ProcColumn::MemPercent => ProcColumn::MemValue, _ => unreachable!(), }; *mem_column = new_value; let _ = self.column_mapping.replace_index(index, new_value); self.sort_table.set_data(self.column_text()); self.force_data_update(); } } } #[cfg(feature = "gpu")] match ( self.column_mapping.get_index_of(&ProcColumn::GpuMemPercent), self.column_mapping.get_index_of(&ProcColumn::GpuMemValue), ) { (None, None) | (Some(_), Some(_)) => {} (None, Some(index)) | (Some(index), None) => { if let Some(mem_column) = self.get_mut_proc_col(index) { let new_value = match mem_column { ProcColumn::GpuMemValue => ProcColumn::GpuMemPercent, ProcColumn::GpuMemPercent => ProcColumn::GpuMemValue, _ => unreachable!(), }; *mem_column = new_value; let _ = self.column_mapping.replace_index(index, new_value); 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; } #[cfg(target_os = "linux")] pub fn toggle_k_thread(&mut self) { self.hide_k_threads = !self.hide_k_threads; self.force_rerender_and_update(); } /// Marks the selected column as hidden, and automatically resets the /// selected column to the default sort index and order. fn hide_column(&mut self, column: ProcColumn) { if let Some(index) = self.column_mapping.get_index_of(&column) && let Some(col) = self.table.columns.get_mut(index) { col.set_hidden(true); if self.table.sort_index() == index { self.table.set_sort_index(self.default_sort_index); self.table.set_order(self.default_sort_order); } } } /// Marks the selected column as shown. fn show_column(&mut self, column: ProcColumn) { if let Some(index) = self.column_mapping.get_index_of(&column) && let Some(col) = self.table.columns.get_mut(index) { col.set_hidden(false); } } /// Select a column. If the column is already selected, then just toggle the /// sort order. pub fn select_column(&mut self, column: ProcColumn) { if let Some(index) = self.column_mapping.get_index_of(&column) { self.table.set_sort_index(index); self.force_data_update(); } } /// Select from multiple columns - if there are multiple hits, it'll take the first one. pub fn select_first_column(&mut self, columns: &[ProcColumn]) { let mut smallest_index: Option = None; for column in columns { if let Some(index) = self.column_mapping.get_index_of(column) { smallest_index = match smallest_index { Some(current) => Some(current.min(index)), None => Some(index), }; } } if let Some(index) = smallest_index { self.table.set_sort_index(index); self.force_data_update(); } } pub fn collapse_current_tree_branch_entry(&mut self) { if let ProcWidgetMode::Tree(collapsed) = &mut self.mode && let Some(process) = self.table.current_item() { collapsed.collapse(process.pid); self.force_data_update(); } } pub fn expand_current_tree_branch_entry(&mut self) { if let ProcWidgetMode::Tree(collapsed) = &mut self.mode && let Some(process) = self.table.current_item() { collapsed.expand(process.pid); self.force_data_update(); } } pub fn toggle_current_tree_branch_entry(&mut self) { if let ProcWidgetMode::Tree(collapsed) = &mut self.mode && let Some(process) = self.table.current_item() { collapsed.toggle(process.pid); self.force_data_update(); } } /// If there is only one command/name "type" column then this will toggle it to use the percentage type, or vice versa. /// /// If there is already multiple types - that is, the user defines both [`ProcColumn::Command`] _and_ [`ProcColumn::Name`], /// then this will not do anything since toggling is pointless with that configuration. pub fn toggle_command(&mut self) { match ( self.column_mapping.get_index_of(&ProcColumn::Command), self.column_mapping.get_index_of(&ProcColumn::Name), ) { (None, None) | (Some(_), Some(_)) => {} (None, Some(index)) | (Some(index), None) => { if let Some(col) = self.table.columns.get_mut(index) { let inner = col.inner_mut(); let current_value = *inner; let new_value = match current_value { ProcColumn::Name => { *inner = ProcColumn::Command; if let ColumnWidthBounds::Soft { max_percentage, .. } = col.bounds_mut() { *max_percentage = Some(0.5); } ProcColumn::Command } 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), }; } ProcColumn::Name } _ => unreachable!(), }; let _ = self.column_mapping.replace_index(index, new_value); self.sort_table.set_data(self.column_text()); self.force_rerender_and_update(); } } } } /// Toggles PID and count. This is meant to be done 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 if the columns exist, the User and /// State columns should be re-enabled, and the mode switched to /// [`ProcWidgetMode::Normal`]. pub(crate) fn toggle_count_pid(&mut self) { if !matches!(self.mode, ProcWidgetMode::Tree { .. }) { match ( self.column_mapping.get_index_of(&ProcColumn::Count), self.column_mapping.get_index_of(&ProcColumn::Pid), ) { // The (Some, Some) case should never happen. (None, None) | (Some(_), Some(_)) => {} (None, Some(index)) | (Some(index), None) => { if let Some(sort_col) = self.table.columns.get_mut(index) { let col = sort_col.inner_mut(); let new_value = match col { ProcColumn::Pid => { *col = ProcColumn::Count; sort_col.default_order = SortOrder::Descending; self.hide_column(ProcColumn::User); self.hide_column(ProcColumn::State); self.mode = ProcWidgetMode::Grouped; ProcColumn::Count } ProcColumn::Count => { *col = ProcColumn::Pid; sort_col.default_order = SortOrder::Ascending; self.show_column(ProcColumn::User); self.show_column(ProcColumn::State); self.mode = ProcWidgetMode::Normal; ProcColumn::Pid } _ => unreachable!(), }; let _ = self.column_mapping.replace_index(index, new_value); self.sort_table.set_data(self.column_text()); self.force_rerender_and_update(); } } } } } pub fn column_text(&self) -> Vec> { self.table .columns .iter() .filter(|c| !c.is_hidden()) .map(|c| c.inner().text()) .collect::>() } pub fn is_search_enabled(&self) -> bool { self.proc_search.search_state.is_enabled } /// Update the current search query. /// /// TODO: Maybe debounce this. pub fn update_query(&mut self) { let current_query = self .proc_search .search_state .input_field_state .current_query(); if current_query.is_empty() { self.proc_search.search_state.is_invalid_search = false; self.proc_search.search_state.error_message = None; } else { match parse_query(current_query, &self.proc_search.query_options) { Ok(parsed_query) => { self.proc_search.search_state.query = Some(parsed_query); 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_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(); } /// Sets the [`ProcWidgetState`]'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(); } #[cfg(test)] pub(crate) fn test_equality(&self, other: &Self) -> bool { self.mode == other.mode && self.proc_search.query_options == other.proc_search.query_options && self .table .columns .iter() .map(|c| c.header()) .collect::>() == other .table .columns .iter() .map(|c| c.header()) .collect::>() } } #[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 std::time::Duration; use super::*; #[cfg(target_os = "linux")] use crate::collection::processes::ProcessType; 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), virtual_mem: 100, rps: 0, wps: 0, total_read: 0, total_write: 0, process_state: "N/A", process_char: '?', #[cfg(unix)] user: Some("root".into()), #[cfg(not(target_family = "unix"))] user: Some("N/A".into()), num_similar: 0, disabled: false, time: Duration::from_secs(0), #[cfg(feature = "gpu")] gpu_mem_usage: MemUsage::Percent(1.1), #[cfg(feature = "gpu")] gpu_usage: 0, #[cfg(target_os = "linux")] process_type: crate::collection::processes::ProcessType::Regular, #[cfg(unix)] nice: 0, priority: -20, }; 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!( [&c, &b, &a, &d].iter().map(|d| d.pid).collect::>(), data.iter().map(|d| d.pid).collect::>(), ); // 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!( [&a, &d, &b, &c].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::MemPercent, &mut data, SortOrder::Descending); assert_eq!( [&b, &a, &c, &d].iter().map(|d| d.pid).collect::>(), data.iter().map(|d| d.pid).collect::>(), ); // Note that the PID ordering for ties is still ascending. data.sort_by_key(|p| p.pid); sort_skip_pid_asc(&ProcColumn::MemPercent, &mut data, SortOrder::Ascending); assert_eq!( [&c, &d, &a, &b].iter().map(|d| d.pid).collect::>(), data.iter().map(|d| d.pid).collect::>(), ); } fn get_columns(table: &ProcessTable) -> Vec { table .columns .iter() .filter_map(|c| { if c.is_hidden() { None } else { Some(*c.inner()) } }) .collect::>() } fn init_state(table_config: ProcTableConfig, columns: &[ProcColumn]) -> ProcWidgetState { let config = AppConfigFields::default(); let styling = Styles::default(); let columns = Some(columns.iter().cloned().collect()); ProcWidgetState::new( &config, ProcWidgetMode::Normal, table_config, &styling, &columns, ) } fn init_default_state(columns: &[ProcColumn]) -> ProcWidgetState { init_state(ProcTableConfig::default(), columns) } #[test] fn default_sort_honoured() { let init_columns = [ ProcColumn::Pid, ProcColumn::Name, ProcColumn::CpuPercent, ProcColumn::MemPercent, ]; let state_default = init_default_state(&init_columns); assert_eq!( state_default.table.sort_index(), 2, "default sort should be CPU (index 2 in init_columns)" ); let table_config = ProcTableConfig { default_sort: Some(ProcColumn::MemPercent), ..Default::default() }; let state_mem = init_state(table_config, &init_columns); assert_eq!(state_mem.table.sort_index(), 3); let table_config = ProcTableConfig { default_sort: Some(ProcColumn::Pid), ..Default::default() }; let state_pid = init_state(table_config, &init_columns); assert_eq!(state_pid.table.sort_index(), 0); } #[test] fn default_sort_falls_back_when_column_absent() { // `default_sort` points at a column the user didn't include. We should // fall back to the built-in default rather than panic or pick column 0. let init_columns = [ProcColumn::Pid, ProcColumn::Name, ProcColumn::CpuPercent]; let table_config = ProcTableConfig { default_sort: Some(ProcColumn::MemPercent), ..Default::default() }; let state = init_state(table_config, &init_columns); assert_eq!(state.table.sort_index(), 2); } #[test] fn select_first_column_picks_first_present() { let init_columns = [ ProcColumn::Pid, ProcColumn::Name, ProcColumn::MemValue, ProcColumn::MemPercent, ]; let mut state = init_default_state(&init_columns); state.select_first_column(&[ProcColumn::MemPercent, ProcColumn::MemValue]); assert_eq!( state.table.sort_index(), 2, "should pick MemValue (index 2) since it comes before MemPercent" ); assert_eq!(state.table.order(), SortOrder::Descending); // Flip the layout so MemPercent comes first. let init_columns = [ ProcColumn::Pid, ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::MemValue, ]; let mut state = init_default_state(&init_columns); state.select_first_column(&[ProcColumn::MemPercent, ProcColumn::MemValue]); assert_eq!( state.table.sort_index(), 2, "should pick MemPercent (index 2) since it comes before MemValue" ); assert_eq!(state.table.order(), SortOrder::Descending); } #[test] fn select_first_column_only_one_present() { let init_columns = [ ProcColumn::Pid, ProcColumn::Name, ProcColumn::CpuPercent, ProcColumn::MemPercent, ]; let mut state = init_default_state(&init_columns); state.select_first_column(&[ProcColumn::MemValue, ProcColumn::MemPercent]); assert_eq!(state.table.sort_index(), 3); assert_eq!(state.table.order(), SortOrder::Descending); } #[test] fn select_first_column_toggles_when_already_selected() { let init_columns = [ ProcColumn::Pid, ProcColumn::Name, ProcColumn::MemValue, ProcColumn::MemPercent, ]; let mut state = init_default_state(&init_columns); state.select_first_column(&[ProcColumn::MemPercent, ProcColumn::MemValue]); assert_eq!(state.table.sort_index(), 2); assert_eq!(state.table.order(), SortOrder::Descending); state.select_first_column(&[ProcColumn::MemPercent, ProcColumn::MemValue]); assert_eq!(state.table.sort_index(), 2, "index should stay put"); assert_eq!( state.table.order(), SortOrder::Ascending, "re-selecting the same column should toggle the order" ); } #[test] fn select_first_column_none_present() { // If none of the candidate columns exist, nothing should change. let init_columns = [ProcColumn::Pid, ProcColumn::Name, ProcColumn::CpuPercent]; let mut state = init_default_state(&init_columns); let before_index = state.table.sort_index(); let before_order = state.table.order(); state.select_first_column(&[ProcColumn::MemValue, ProcColumn::MemPercent]); assert_eq!(state.table.sort_index(), before_index); assert_eq!(state.table.order(), before_order); } #[test] fn custom_columns() { let init_columns = vec![ ProcColumn::Pid, ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::State, ]; let columns = vec![ ProcColumn::Pid, ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::State, ]; let state = init_default_state(&init_columns); assert_eq!(get_columns(&state.table), columns); } #[test] fn toggle_count_pid() { let init_columns = [ ProcColumn::Pid, ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::State, ]; let original_columns = vec![ ProcColumn::Pid, ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::State, ]; let new_columns = vec![ProcColumn::Count, ProcColumn::Name, ProcColumn::MemPercent]; let mut state = init_default_state(&init_columns); assert_eq!(get_columns(&state.table), original_columns); // This should hide the state. state.toggle_count_pid(); assert_eq!(get_columns(&state.table), new_columns); // This should re-reveal the state. state.toggle_count_pid(); assert_eq!(get_columns(&state.table), original_columns); } #[test] fn toggle_count_pid_2() { let init_columns = [ ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::User, ProcColumn::State, ProcColumn::Pid, ]; let original_columns = vec![ ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::User, ProcColumn::State, ProcColumn::Pid, ]; let new_columns = vec![ProcColumn::Name, ProcColumn::MemPercent, ProcColumn::Count]; let mut state = init_default_state(&init_columns); assert_eq!(get_columns(&state.table), original_columns); // This should hide the state. state.toggle_count_pid(); assert_eq!(get_columns(&state.table), new_columns); // This should re-reveal the state. state.toggle_count_pid(); assert_eq!(get_columns(&state.table), original_columns); } #[test] fn toggle_starting_with_command() { let init_columns = [ ProcColumn::Pid, ProcColumn::State, ProcColumn::MemPercent, ProcColumn::Name, ]; let original_columns = [ ProcColumn::Pid, ProcColumn::State, ProcColumn::MemPercent, ProcColumn::Command, ]; let new_columns = [ ProcColumn::Pid, ProcColumn::State, ProcColumn::MemPercent, ProcColumn::Name, ]; let table_config = ProcTableConfig { is_command: true, ..Default::default() }; let mut state = init_state(table_config, &init_columns); assert_eq!(get_columns(&state.table), original_columns); state.toggle_command(); assert_eq!(get_columns(&state.table), new_columns); state.toggle_command(); assert_eq!(get_columns(&state.table), original_columns); } #[test] fn toggle_mem_percentage() { let init_columns = [ ProcColumn::Pid, ProcColumn::MemPercent, ProcColumn::State, ProcColumn::Name, ]; let original_columns = init_columns; let new_columns = [ ProcColumn::Pid, ProcColumn::MemValue, ProcColumn::State, ProcColumn::Name, ]; let mut state = init_default_state(&init_columns); assert_eq!(get_columns(&state.table), original_columns); state.toggle_mem_percentage(); assert_eq!(get_columns(&state.table), new_columns); state.toggle_mem_percentage(); assert_eq!(get_columns(&state.table), original_columns); } #[test] fn toggle_mem_percentage_2() { let init_columns = [ ProcColumn::Pid, ProcColumn::MemValue, ProcColumn::State, ProcColumn::Name, ]; let original_columns = init_columns; let new_columns = [ ProcColumn::Pid, ProcColumn::MemPercent, ProcColumn::State, ProcColumn::Name, ]; let table_config = ProcTableConfig { show_memory_as_values: true, ..Default::default() }; let mut state = init_state(table_config, &init_columns); assert_eq!(get_columns(&state.table), original_columns); state.toggle_mem_percentage(); assert_eq!(get_columns(&state.table), new_columns); state.toggle_mem_percentage(); assert_eq!(get_columns(&state.table), original_columns); } #[test] fn toggle_memory_3() { let init_columns = [ ProcColumn::Pid, ProcColumn::MemPercent, ProcColumn::State, ProcColumn::Name, ]; let original_columns = init_columns; let new_columns = [ ProcColumn::Pid, ProcColumn::MemValue, ProcColumn::State, ProcColumn::Name, ]; let table_config = ProcTableConfig { ..Default::default() }; let mut state = init_state(table_config, &init_columns); assert_eq!(get_columns(&state.table), original_columns); state.toggle_mem_percentage(); assert_eq!(get_columns(&state.table), new_columns); state.toggle_mem_percentage(); assert_eq!(get_columns(&state.table), original_columns); } #[test] fn columns_and_is_using_command() { let init_columns = [ ProcColumn::Pid, ProcColumn::MemPercent, ProcColumn::State, ProcColumn::Command, ]; let original_columns = init_columns; let table_config = ProcTableConfig { is_command: true, ..Default::default() }; let mut state = init_state(table_config, &init_columns); assert_eq!(get_columns(&state.table), original_columns); assert!(state.is_using_command()); state.toggle_command(); assert!(!state.is_using_command()); state.toggle_command(); assert!(state.is_using_command()); } #[test] fn columns_and_is_memory() { let init_columns = [ ProcColumn::Pid, ProcColumn::MemValue, ProcColumn::State, ProcColumn::Name, ]; let original_columns = init_columns; let table_config = ProcTableConfig { show_memory_as_values: true, ..Default::default() }; let mut state = init_state(table_config, &init_columns); assert_eq!(get_columns(&state.table), original_columns); assert!(!state.is_mem_percent()); state.toggle_mem_percentage(); assert!(state.is_mem_percent()); state.toggle_mem_percentage(); assert!(!state.is_mem_percent()); } /// Tests toggling if both mem and mem% columns are configured. #[test] fn double_memory_simultaneous_toggle() { let init_columns = [ ProcColumn::MemPercent, ProcColumn::Pid, ProcColumn::State, ProcColumn::Name, ProcColumn::MemValue, ]; let original_columns = init_columns; let mut state = init_default_state(&init_columns); assert_eq!(get_columns(&state.table), original_columns); state.toggle_mem_percentage(); assert_eq!(get_columns(&state.table), original_columns); state.toggle_mem_percentage(); assert_eq!(get_columns(&state.table), original_columns); } /// Tests toggling if both pid and count columns are configured. #[test] fn pid_and_count_simultaneous_toggle() { let init_columns = [ ProcColumn::Name, ProcColumn::Pid, ProcColumn::MemPercent, ProcColumn::State, ProcColumn::Count, ]; let original_columns = init_columns; let mut state = init_default_state(&init_columns); assert_eq!(get_columns(&state.table), original_columns); // This should hide the state. state.toggle_count_pid(); assert_eq!(get_columns(&state.table), original_columns); // This should re-reveal the state. state.toggle_count_pid(); assert_eq!(get_columns(&state.table), original_columns); } /// Tests toggling if both command and name columns are configured. It should do nothing. #[test] fn command_name_simultaneous_toggle() { let init_columns = [ ProcColumn::Command, ProcColumn::Pid, ProcColumn::State, ProcColumn::MemPercent, ProcColumn::Name, ]; let original_columns = init_columns; let mut state = init_default_state(&init_columns); assert_eq!(get_columns(&state.table), original_columns); state.toggle_command(); assert_eq!(get_columns(&state.table), original_columns); state.toggle_command(); assert_eq!(get_columns(&state.table), original_columns); } /// Sanity test to ensure tree collapse logic works, both when /// enabled-by-default or disabled-by-default. #[test] fn test_tree_collapse() { { let mut collapsed_by_default = TreeCollapsed::new(true); assert!(collapsed_by_default.is_collapsed(1)); collapsed_by_default.collapse(1); assert!(collapsed_by_default.is_collapsed(1)); collapsed_by_default.expand(1); assert!(!collapsed_by_default.is_collapsed(1)); collapsed_by_default.toggle(1); assert!(collapsed_by_default.is_collapsed(1)); collapsed_by_default.toggle(1); assert!(!collapsed_by_default.is_collapsed(1)); } { let mut expanded_by_default = TreeCollapsed::new(false); assert!(!expanded_by_default.is_collapsed(1)); expanded_by_default.collapse(1); assert!(expanded_by_default.is_collapsed(1)); expanded_by_default.expand(1); assert!(!expanded_by_default.is_collapsed(1)); expanded_by_default.toggle(1); assert!(expanded_by_default.is_collapsed(1)); expanded_by_default.toggle(1); assert!(!expanded_by_default.is_collapsed(1)); } } #[cfg(target_os = "linux")] /// Sanity test to ensure kernel thread processes are toggled #[test] fn test_toggle_k_threads() { let init_columns = [ ProcColumn::Name, ProcColumn::Pid, ProcColumn::State, ProcColumn::MemPercent, ]; let mut state = init_default_state(&init_columns); let process_harvest = ProcessHarvest { pid: 1, ..Default::default() }; let k_process_harvest = ProcessHarvest { pid: 2, process_type: ProcessType::Kernel, ..Default::default() }; // test get_normal_data default is filtered by toggle_k_thread let mut normal_proc_harvest: BTreeMap = BTreeMap::new(); normal_proc_harvest.insert(1, process_harvest.clone()); normal_proc_harvest.insert(2, k_process_harvest.clone()); let default_normal_results = state.get_normal_data(&normal_proc_harvest).len(); assert!(default_normal_results == 2); state.toggle_k_thread(); let filtered_normal_results = state.get_normal_data(&normal_proc_harvest).len(); assert!(filtered_normal_results == 1); // test that get_normal_data in grouped mode is still filtered state.mode = ProcWidgetMode::Grouped; let filtered_grouped_results = state.get_normal_data(&normal_proc_harvest).len(); assert!(filtered_grouped_results == 1); // test that get_tree_data is filtered on toggle_k_thread let tree_collapsed = TreeCollapsed::new(false); state.mode = ProcWidgetMode::Tree(tree_collapsed.clone()); state.hide_k_threads = false; let mut tree_proc_data = ProcessData::default(); 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 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(); assert!(default_tree_results == 2); state.toggle_k_thread(); let filtered_tree_results = state .get_tree_data(&tree_collapsed, &tree_stored_data) .len(); assert!(filtered_tree_results == 1); } }