1use bincode::{Decode, Encode};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode)]
6pub struct ProcessInfo {
7 pub pid: u32,
8 pub parent_pid: Option<u32>,
9 pub name: String,
10 pub command: String,
11 pub user: String,
12 pub cpu_percent: f32,
13 pub memory_bytes: u64,
14 pub memory_percent: f32,
15 pub status: String,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SortField {
21 Cpu,
22 Mem,
23 Pid,
24 Name,
25 User,
26}
27
28impl std::fmt::Display for SortField {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 SortField::Cpu => write!(f, "cpu"),
32 SortField::Mem => write!(f, "mem"),
33 SortField::Pid => write!(f, "pid"),
34 SortField::Name => write!(f, "name"),
35 SortField::User => write!(f, "user"),
36 }
37 }
38}
39
40impl std::str::FromStr for SortField {
41 type Err = String;
42
43 fn from_str(s: &str) -> Result<Self, Self::Err> {
44 match s.to_lowercase().as_str() {
45 "cpu" => Ok(SortField::Cpu),
46 "mem" | "memory" => Ok(SortField::Mem),
47 "pid" => Ok(SortField::Pid),
48 "name" => Ok(SortField::Name),
49 "user" => Ok(SortField::User),
50 _ => Err(format!(
51 "invalid sort field '{s}': expected one of cpu, mem, pid, name, user"
52 )),
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy)]
59pub enum SortOrder {
60 Asc,
61 Desc,
62}
63
64pub fn sort_processes(procs: &mut [ProcessInfo], field: SortField, order: SortOrder) {
67 match field {
71 SortField::Name => procs.sort_by_cached_key(|p| p.name.to_lowercase()),
72 SortField::User => procs.sort_by_cached_key(|p| p.user.to_lowercase()),
73 SortField::Cpu => procs.sort_unstable_by(|a, b| {
74 a.cpu_percent
75 .partial_cmp(&b.cpu_percent)
76 .unwrap_or(std::cmp::Ordering::Equal)
77 }),
78 SortField::Mem => procs.sort_unstable_by_key(|p| p.memory_bytes),
79 SortField::Pid => procs.sort_unstable_by_key(|p| p.pid),
80 }
81
82 if matches!(order, SortOrder::Desc) {
83 procs.reverse();
84 }
85}
86
87pub fn filter_processes(procs: &[ProcessInfo], pattern: &str) -> Vec<ProcessInfo> {
90 if pattern.is_empty() {
91 return procs.to_vec();
92 }
93 let lower = pattern.to_lowercase();
94 procs
95 .iter()
96 .filter(|p| {
97 p.name.to_lowercase().contains(&lower) || p.command.to_lowercase().contains(&lower)
98 })
99 .cloned()
100 .collect()
101}
102
103#[derive(Debug, Clone)]
105pub struct TreeNode {
106 pub process: ProcessInfo,
107 pub children: Vec<TreeNode>,
108 pub depth: usize,
109}
110
111pub fn build_process_tree(procs: &[ProcessInfo]) -> Vec<TreeNode> {
117 use std::collections::HashMap;
118
119 if procs.is_empty() {
120 return Vec::new();
121 }
122
123 let mut children_map: HashMap<u32, Vec<usize>> = HashMap::with_capacity(procs.len());
125 let pid_set: std::collections::HashSet<u32> = procs.iter().map(|p| p.pid).collect();
126
127 for (i, p) in procs.iter().enumerate() {
128 match p.parent_pid {
129 Some(ppid) if ppid > 0 && pid_set.contains(&ppid) => {
130 children_map.entry(ppid).or_default().push(i);
131 }
132 _ => {} }
134 }
135
136 let roots: Vec<usize> = procs
138 .iter()
139 .enumerate()
140 .filter(|(_, p)| match p.parent_pid {
141 None | Some(0) => true,
142 Some(ppid) => !pid_set.contains(&ppid),
143 })
144 .map(|(i, _)| i)
145 .collect();
146
147 const MAX_DEPTH: usize = 256;
148
149 fn build_subtree(
150 idx: usize,
151 depth: usize,
152 procs: &[ProcessInfo],
153 children_map: &HashMap<u32, Vec<usize>>,
154 ) -> TreeNode {
155 let p = &procs[idx];
156 let children = if depth < MAX_DEPTH {
157 children_map
158 .get(&p.pid)
159 .map(|indices| indices.as_slice())
160 .unwrap_or_default()
161 .iter()
162 .map(|&ci| build_subtree(ci, depth + 1, procs, children_map))
163 .collect()
164 } else {
165 Vec::new() };
167 TreeNode {
168 process: p.clone(),
169 children,
170 depth,
171 }
172 }
173
174 roots
175 .iter()
176 .map(|&ri| build_subtree(ri, 0, procs, &children_map))
177 .collect()
178}
179
180pub fn flatten_tree(roots: &[TreeNode]) -> Vec<(ProcessInfo, usize)> {
183 let mut result = Vec::new();
184 fn walk(node: &TreeNode, out: &mut Vec<(ProcessInfo, usize)>) {
185 out.push((node.process.clone(), node.depth));
186 for child in &node.children {
187 walk(child, out);
188 }
189 }
190 for root in roots {
191 walk(root, &mut result);
192 }
193 result
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 fn make_proc(pid: u32, name: &str, cmd: &str, user: &str, cpu: f32, mem: u64) -> ProcessInfo {
201 ProcessInfo {
202 pid,
203 parent_pid: None,
204 name: name.to_string(),
205 command: cmd.to_string(),
206 user: user.to_string(),
207 cpu_percent: cpu,
208 memory_bytes: mem,
209 memory_percent: 0.0,
210 status: "Running".to_string(),
211 }
212 }
213
214 fn cpu_procs() -> Vec<ProcessInfo> {
215 vec![
216 make_proc(1, "a", "", "alice", 10.0, 100),
217 make_proc(2, "b", "", "bob", 50.0, 200),
218 make_proc(3, "c", "", "carol", 30.0, 300),
219 make_proc(4, "d", "", "dave", 90.0, 400),
220 make_proc(5, "e", "", "eve", 20.0, 500),
221 ]
222 }
223
224 #[test]
225 fn test_sort_by_cpu_desc() {
226 let mut procs = cpu_procs();
227 sort_processes(&mut procs, SortField::Cpu, SortOrder::Desc);
228 let cpus: Vec<f32> = procs.iter().map(|p| p.cpu_percent).collect();
229 assert_eq!(cpus, vec![90.0, 50.0, 30.0, 20.0, 10.0]);
230 }
231
232 #[test]
233 fn test_sort_by_cpu_asc() {
234 let mut procs = cpu_procs();
235 sort_processes(&mut procs, SortField::Cpu, SortOrder::Asc);
236 let cpus: Vec<f32> = procs.iter().map(|p| p.cpu_percent).collect();
237 assert_eq!(cpus, vec![10.0, 20.0, 30.0, 50.0, 90.0]);
238 }
239
240 #[test]
241 fn test_sort_by_name_asc() {
242 let mut procs = vec![
243 make_proc(1, "Zsh", "", "u", 0.0, 0),
244 make_proc(2, "apache", "", "u", 0.0, 0),
245 make_proc(3, "Bash", "", "u", 0.0, 0),
246 ];
247 sort_processes(&mut procs, SortField::Name, SortOrder::Asc);
248 let names: Vec<&str> = procs.iter().map(|p| p.name.as_str()).collect();
249 assert_eq!(names, vec!["apache", "Bash", "Zsh"]);
251 }
252
253 #[test]
254 fn test_sort_by_mem_desc() {
255 let mut procs = cpu_procs();
256 sort_processes(&mut procs, SortField::Mem, SortOrder::Desc);
257 let mems: Vec<u64> = procs.iter().map(|p| p.memory_bytes).collect();
258 assert_eq!(mems, vec![500, 400, 300, 200, 100]);
259 }
260
261 #[test]
262 fn test_sort_by_pid_asc() {
263 let mut procs = vec![
264 make_proc(30, "c", "", "u", 0.0, 0),
265 make_proc(10, "a", "", "u", 0.0, 0),
266 make_proc(20, "b", "", "u", 0.0, 0),
267 ];
268 sort_processes(&mut procs, SortField::Pid, SortOrder::Asc);
269 let pids: Vec<u32> = procs.iter().map(|p| p.pid).collect();
270 assert_eq!(pids, vec![10, 20, 30]);
271 }
272
273 #[test]
274 fn test_sort_by_user_asc() {
275 let mut procs = vec![
276 make_proc(1, "a", "", "Zara", 0.0, 0),
277 make_proc(2, "b", "", "alice", 0.0, 0),
278 make_proc(3, "c", "", "Bob", 0.0, 0),
279 ];
280 sort_processes(&mut procs, SortField::User, SortOrder::Asc);
281 let users: Vec<&str> = procs.iter().map(|p| p.user.as_str()).collect();
282 assert_eq!(users, vec!["alice", "Bob", "Zara"]);
283 }
284
285 #[test]
286 fn test_sort_empty_list() {
287 let mut procs: Vec<ProcessInfo> = vec![];
288 sort_processes(&mut procs, SortField::Cpu, SortOrder::Desc);
290 assert!(procs.is_empty());
291 }
292
293 #[test]
294 fn test_sort_single_element() {
295 let mut procs = vec![make_proc(42, "solo", "/bin/solo", "root", 5.0, 1024)];
296 sort_processes(&mut procs, SortField::Cpu, SortOrder::Desc);
297 assert_eq!(procs.len(), 1);
298 assert_eq!(procs[0].pid, 42);
299 }
300
301 #[test]
302 fn test_filter_case_insensitive() {
303 let procs = vec![
304 make_proc(1, "Firefox", "/usr/bin/firefox", "u", 0.0, 0),
305 make_proc(2, "firefox-esr", "/usr/bin/firefox-esr", "u", 0.0, 0),
306 make_proc(3, "bash", "/bin/bash", "u", 0.0, 0),
307 ];
308 let result = filter_processes(&procs, "fire");
309 assert_eq!(result.len(), 2);
310 let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
311 assert!(names.contains(&"Firefox"));
312 assert!(names.contains(&"firefox-esr"));
313 }
314
315 #[test]
316 fn test_filter_empty_pattern() {
317 let procs = cpu_procs();
318 let result = filter_processes(&procs, "");
319 assert_eq!(result.len(), procs.len());
320 }
321
322 #[test]
323 fn test_filter_no_match() {
324 let procs = cpu_procs();
325 let result = filter_processes(&procs, "zzznomatch");
326 assert!(result.is_empty());
327 }
328
329 #[test]
330 fn test_filter_matches_command_field() {
331 let procs = vec![
332 make_proc(1, "proc1", "/usr/bin/rustc --edition 2021", "u", 0.0, 0),
333 make_proc(2, "proc2", "/bin/bash", "u", 0.0, 0),
334 ];
335 let result = filter_processes(&procs, "rustc");
337 assert_eq!(result.len(), 1);
338 assert_eq!(result[0].pid, 1);
339 }
340
341 fn make_proc_with_parent(pid: u32, ppid: Option<u32>, name: &str) -> ProcessInfo {
344 ProcessInfo {
345 pid,
346 parent_pid: ppid,
347 name: name.to_string(),
348 command: String::new(),
349 user: "u".to_string(),
350 cpu_percent: 0.0,
351 memory_bytes: 0,
352 memory_percent: 0.0,
353 status: "Running".to_string(),
354 }
355 }
356
357 #[test]
358 fn test_tree_parent_child() {
359 let procs = vec![
360 make_proc_with_parent(1, Some(0), "init"),
361 make_proc_with_parent(100, Some(1), "sshd"),
362 make_proc_with_parent(200, Some(100), "bash"),
363 ];
364 let tree = build_process_tree(&procs);
365 assert_eq!(tree.len(), 1, "should have one root (init)");
366 assert_eq!(tree[0].process.pid, 1);
367 assert_eq!(tree[0].children.len(), 1, "init should have 1 child");
368 assert_eq!(tree[0].children[0].process.pid, 100);
369 assert_eq!(
370 tree[0].children[0].children.len(),
371 1,
372 "sshd should have 1 child"
373 );
374 assert_eq!(tree[0].children[0].children[0].process.pid, 200);
375 }
376
377 #[test]
378 fn test_tree_depth_increments() {
379 let procs = vec![
380 make_proc_with_parent(1, Some(0), "init"),
381 make_proc_with_parent(10, Some(1), "child"),
382 make_proc_with_parent(100, Some(10), "grandchild"),
383 ];
384 let tree = build_process_tree(&procs);
385 assert_eq!(tree[0].depth, 0);
386 assert_eq!(tree[0].children[0].depth, 1);
387 assert_eq!(tree[0].children[0].children[0].depth, 2);
388 }
389
390 #[test]
391 fn test_tree_orphan_as_root() {
392 let procs = vec![make_proc_with_parent(500, Some(999), "orphan")];
393 let tree = build_process_tree(&procs);
394 assert_eq!(tree.len(), 1, "orphan with missing parent should be a root");
395 assert_eq!(tree[0].process.pid, 500);
396 assert_eq!(tree[0].depth, 0);
397 }
398
399 #[test]
400 fn test_tree_multiple_roots() {
401 let procs = vec![
402 make_proc_with_parent(1, Some(0), "init"),
403 make_proc_with_parent(2, Some(0), "kthreadd"),
404 ];
405 let tree = build_process_tree(&procs);
406 assert_eq!(
407 tree.len(),
408 2,
409 "two processes with PPID=0 should both be roots"
410 );
411 }
412
413 #[test]
414 fn test_tree_empty_list() {
415 let tree = build_process_tree(&[]);
416 assert!(tree.is_empty());
417 }
418
419 #[test]
420 fn test_tree_single_process() {
421 let procs = vec![make_proc_with_parent(42, None, "solo")];
422 let tree = build_process_tree(&procs);
423 assert_eq!(tree.len(), 1);
424 assert!(tree[0].children.is_empty());
425 }
426
427 #[test]
428 fn test_flatten_tree_order() {
429 let procs = vec![
430 make_proc_with_parent(1, Some(0), "init"),
431 make_proc_with_parent(10, Some(1), "child_a"),
432 make_proc_with_parent(20, Some(1), "child_b"),
433 make_proc_with_parent(100, Some(10), "grandchild"),
434 ];
435 let tree = build_process_tree(&procs);
436 let flat = flatten_tree(&tree);
437 let pids: Vec<u32> = flat.iter().map(|(p, _)| p.pid).collect();
438 assert_eq!(pids, vec![1, 10, 100, 20]);
440 }
441
442 #[test]
443 fn test_flatten_tree_depth_values() {
444 let procs = vec![
445 make_proc_with_parent(1, Some(0), "init"),
446 make_proc_with_parent(10, Some(1), "child"),
447 make_proc_with_parent(100, Some(10), "grandchild"),
448 ];
449 let tree = build_process_tree(&procs);
450 let flat = flatten_tree(&tree);
451 let depths: Vec<usize> = flat.iter().map(|(_, d)| *d).collect();
452 assert_eq!(depths, vec![0, 1, 2]);
453 }
454
455 #[test]
458 fn test_sort_field_from_str_valid() {
459 assert_eq!("cpu".parse::<SortField>().unwrap(), SortField::Cpu);
460 assert_eq!("mem".parse::<SortField>().unwrap(), SortField::Mem);
461 assert_eq!("memory".parse::<SortField>().unwrap(), SortField::Mem);
462 assert_eq!("pid".parse::<SortField>().unwrap(), SortField::Pid);
463 assert_eq!("name".parse::<SortField>().unwrap(), SortField::Name);
464 assert_eq!("user".parse::<SortField>().unwrap(), SortField::User);
465 }
466
467 #[test]
468 fn test_sort_field_from_str_case_insensitive() {
469 assert_eq!("CPU".parse::<SortField>().unwrap(), SortField::Cpu);
470 assert_eq!("Mem".parse::<SortField>().unwrap(), SortField::Mem);
471 assert_eq!("PID".parse::<SortField>().unwrap(), SortField::Pid);
472 }
473
474 #[test]
475 fn test_sort_field_from_str_invalid() {
476 let err = "invalid".parse::<SortField>().unwrap_err();
477 assert!(err.contains("invalid sort field"));
478 assert!(err.contains("cpu, mem, pid, name, user"));
479 }
480
481 #[test]
482 fn test_sort_field_display_roundtrip() {
483 for &field in &[
484 SortField::Cpu,
485 SortField::Mem,
486 SortField::Pid,
487 SortField::Name,
488 SortField::User,
489 ] {
490 let s = field.to_string();
491 assert_eq!(s.parse::<SortField>().unwrap(), field);
492 }
493 }
494
495 #[test]
496 fn test_tree_preserves_all_processes() {
497 let procs = vec![
498 make_proc_with_parent(1, Some(0), "init"),
499 make_proc_with_parent(2, Some(0), "kthreadd"),
500 make_proc_with_parent(10, Some(1), "a"),
501 make_proc_with_parent(11, Some(1), "b"),
502 make_proc_with_parent(20, Some(2), "c"),
503 make_proc_with_parent(100, Some(10), "d"),
504 make_proc_with_parent(101, Some(10), "e"),
505 make_proc_with_parent(200, Some(20), "f"),
506 make_proc_with_parent(500, Some(999), "orphan"),
507 make_proc_with_parent(42, None, "none_parent"),
508 ];
509 let tree = build_process_tree(&procs);
510 let flat = flatten_tree(&tree);
511 assert_eq!(flat.len(), procs.len(), "all processes must be in the tree");
512 }
513}