mirror of
https://github.com/ClementTsang/bottom.git
synced 2026-10-12 12:19:09 +00:00
1851 lines
64 KiB
Rust
1851 lines
64 KiB
Rust
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<Pid> },
|
|
DefaultExpand { collapsed_pids: HashSet<Pid> },
|
|
}
|
|
|
|
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<ProcWidgetData, ProcColumn>;
|
|
type SortTable = DataTable<Cow<'static, str>, SortTableColumn>;
|
|
type StringPidMap = HashMap<String, Vec<Pid>>;
|
|
|
|
fn make_column(column: ProcColumn) -> SortColumn<ProcColumn> {
|
|
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<ProcColumn>,
|
|
pub sort_order: Option<SortOrder>,
|
|
}
|
|
|
|
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<ProcColumn>,
|
|
|
|
/// 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<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_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<SortColumn<ProcColumn>>,
|
|
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<IndexSet<ProcColumn>>,
|
|
) -> 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<SortColumn<ProcColumn>>, _) = {
|
|
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<ProcColumn>,
|
|
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<ProcessQuery> {
|
|
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<ProcWidgetData> {
|
|
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::<HashSet<_>>();
|
|
|
|
#[inline]
|
|
fn is_ancestor_shown(
|
|
current_process: &ProcessHarvest, kept_pids: &HashSet<Pid>,
|
|
process_harvest: &BTreeMap<Pid, ProcessHarvest>,
|
|
) -> 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<Pid, Vec<Pid>> = IntHashMap::default();
|
|
|
|
// We do a simple DFS traversal to build our filtered parent-to-tree mappings.
|
|
let mut visited_pids: IntHashMap<Pid, bool> = 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<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| {
|
|
#[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<String, Vec<Pid>> = HashMap::default();
|
|
let mut filtered_data: Vec<ProcWidgetData> = 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<usize> = 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<Cow<'static, str>> {
|
|
self.table
|
|
.columns
|
|
.iter()
|
|
.filter(|c| !c.is_hidden())
|
|
.map(|c| c.inner().text())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
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::<Vec<_>>()
|
|
== other
|
|
.table
|
|
.columns
|
|
.iter()
|
|
.map(|c| c.header())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
}
|
|
|
|
#[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::<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!(
|
|
[&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::MemPercent, &mut data, SortOrder::Descending);
|
|
assert_eq!(
|
|
[&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::MemPercent, &mut data, SortOrder::Ascending);
|
|
assert_eq!(
|
|
[&c, &d, &a, &b].iter().map(|d| d.pid).collect::<Vec<_>>(),
|
|
data.iter().map(|d| d.pid).collect::<Vec<_>>(),
|
|
);
|
|
}
|
|
|
|
fn get_columns(table: &ProcessTable) -> Vec<ProcColumn> {
|
|
table
|
|
.columns
|
|
.iter()
|
|
.filter_map(|c| {
|
|
if c.is_hidden() {
|
|
None
|
|
} else {
|
|
Some(*c.inner())
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
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<Pid, ProcessHarvest> = 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);
|
|
}
|
|
}
|