Skip to main content

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}