monitrs_core/process/mod.rs
1//! Process list logic: ordering, filtering, and tree construction.
2//!
3//! This module is the *list* half of the process view. The row data itself lives
4//! in [`crate::model::ProcessSnapshot`]; nothing here mutates a snapshot, because
5//! §10.4 requires published snapshots to stay immutable. Every entry point
6//! therefore returns indices into the slice it was given, in display order.
7//!
8//! # What §7.2 demands of this module
9//!
10//! * **Stable sorting with a PID/start-time tie-breaker.** Every comparison ends
11//! in a [`crate::model::ProcessIdentity`] comparison, so rows with equal keys
12//! keep the same relative order on every refresh and selection does not jump.
13//! * **Unavailable is not zero** (§26). A metric that was never measured sorts to
14//! the end of the list in *both* directions instead of pretending to be `0`.
15//! * **Plain text filtering** over name, command, PID, and user, with the
16//! user-only and hide-kernel-threads toggles expressed as separate composable
17//! predicates.
18//! * **Tree mode** that preserves parent-child relationships, keeps children
19//! under their parents when sorted, and survives a malformed parent graph.
20//!
21//! # Rendering a tree row
22//!
23//! [`ProcessTree`] emits a flat pre-order list of [`TreeRow`]s carrying a depth,
24//! a last-sibling flag, and (via [`ProcessTree::continuation_flags`]) whether an
25//! ancestor still has siblings below it. That is everything a renderer needs for
26//! the strict-ASCII shape §5.1 requires:
27//!
28//! ```text
29//! systemd
30//! +- sshd
31//! | `- bash
32//! `- cron
33//! ```
34//!
35//! # Example
36//!
37//! ```
38//! use monitrs_core::process::{ProcessFilter, ProcessSort, flat_order};
39//!
40//! # fn view(processes: &[monitrs_core::model::ProcessSnapshot]) -> Vec<usize> {
41//! let filter = ProcessFilter::parse("rustc");
42//! flat_order(processes, &filter, ProcessSort::default())
43//! # }
44//! ```
45
46use std::borrow::Borrow;
47
48use crate::model::ProcessSnapshot;
49
50mod filter;
51mod sort;
52mod tree;
53
54#[cfg(test)]
55mod fixtures;
56
57pub use filter::{FilterPattern, PlainPattern, ProcessFilter, ProcessPredicate};
58pub use sort::{ProcessSort, ProcessSortKey, SortDirection, UnknownSortKey};
59pub use tree::{ProcessTree, TreeRow};
60
61/// The visible rows of the flat process view, in display order.
62///
63/// Filters first and sorts second, and returns indices into `processes` rather
64/// than clones: the snapshot is shared behind an `Arc` and must not be copied per
65/// tick (§10.4, §16.1).
66///
67/// Accepts both `&[ProcessSnapshot]` and `&[&ProcessSnapshot]` so a caller that
68/// already narrowed the list does not have to clone rows to sort them.
69#[must_use]
70pub fn flat_order<P: Borrow<ProcessSnapshot>>(
71 processes: &[P],
72 filter: &ProcessFilter,
73 sort: ProcessSort,
74) -> Vec<usize> {
75 let mut rows: Vec<(usize, &ProcessSnapshot)> = processes
76 .iter()
77 .map(Borrow::borrow)
78 .enumerate()
79 .filter(|(_, process)| filter.matches(process))
80 .collect();
81 rows.sort_by(|(_, left), (_, right)| sort.compare(left, right));
82 rows.into_iter().map(|(index, _)| index).collect()
83}
84
85#[cfg(test)]
86mod tests {
87 use super::fixtures::process;
88 use super::*;
89 use crate::model::ProcessState;
90
91 #[test]
92 fn the_flat_view_filters_before_it_sorts() {
93 let processes = vec![
94 process(1, 1).name("systemd").cpu(1.0).build(),
95 process(2, 2).name("rustc").cpu(50.0).build(),
96 process(3, 3).name("rustc").cpu(90.0).build(),
97 ];
98 let order = flat_order(
99 &processes,
100 &ProcessFilter::parse("rustc"),
101 ProcessSort::default(),
102 );
103 assert_eq!(order, vec![2, 1], "only rustc rows, hottest first");
104 }
105
106 #[test]
107 fn an_inactive_filter_keeps_every_row() {
108 let processes = vec![
109 process(1, 1).cpu(1.0).build(),
110 process(2, 2).cpu(2.0).build(),
111 ];
112 let order = flat_order(&processes, &ProcessFilter::new(), ProcessSort::default());
113 assert_eq!(order.len(), processes.len());
114 }
115
116 #[test]
117 fn the_flat_view_accepts_borrowed_rows_without_cloning_them() {
118 let processes = [
119 process(7, 7).cpu(5.0).build(),
120 process(8, 8).cpu(9.0).build(),
121 ];
122 let borrowed: Vec<&ProcessSnapshot> = processes.iter().collect();
123 let order = flat_order(&borrowed, &ProcessFilter::new(), ProcessSort::default());
124 assert_eq!(order, vec![1, 0]);
125 }
126
127 #[test]
128 fn composed_toggles_and_text_all_apply_to_the_flat_view() {
129 let processes = vec![
130 process(1, 1)
131 .name("kworker/0:1")
132 .user(0, Some("root"))
133 .kernel_thread()
134 .build(),
135 process(2, 2).name("cargo").user(501, Some("gabor")).build(),
136 process(3, 3).name("cargo").user(0, Some("root")).build(),
137 process(4, 4)
138 .name("zsh")
139 .user(501, Some("gabor"))
140 .state(ProcessState::Sleeping)
141 .build(),
142 ];
143 let filter = ProcessFilter::parse("cargo")
144 .with_only_user(Some(501))
145 .with_hidden_kernel_threads(true);
146 let order = flat_order(&processes, &filter, ProcessSort::default());
147 assert_eq!(order, vec![1]);
148 }
149}