1use crate::core::{
13 classify, matches_by_filter, matches_in_filter, parse_target, resolve_in_dir, resolve_target,
14 Hit, NameMatch, Process, ProcessStatus, StatusFilter, TargetType,
15};
16use crate::error::Result;
17use crate::ui::{format_memory, plural, uptime_label, Printer};
18use clap::Args;
19use colored::*;
20use serde::Serialize;
21use std::collections::HashMap;
22
23#[derive(Args, Debug)]
25pub struct TreeCommand {
26 #[arg(value_parser = crate::core::validate_target_arg)]
28 target: Option<String>,
29
30 #[arg(long, short)]
32 ancestors: bool,
33
34 #[arg(long, short = 'v')]
36 verbose: bool,
37
38 #[arg(long, short = 'j')]
40 json: bool,
41
42 #[arg(long, short, default_value = "10")]
44 depth: usize,
45
46 #[arg(long, short = 'C')]
48 compact: bool,
49
50 #[arg(long)]
52 min_cpu: Option<f32>,
53
54 #[arg(long)]
56 min_mem: Option<f64>,
57
58 #[arg(long, value_enum, ignore_case = true)]
60 status: Option<StatusFilter>,
61
62 #[arg(long)]
64 min_uptime: Option<u64>,
65
66 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
69 pub in_dir: Option<String>,
70
71 #[arg(long = "by", short = 'b')]
73 pub by_name: Option<String>,
74
75 #[arg(long)]
77 pub cmd: bool,
78}
79
80impl TreeCommand {
81 fn ancestor_pids(all_processes: &[Process]) -> std::collections::HashSet<u32> {
87 let mut pids = std::collections::HashSet::new();
88 let self_pid = std::process::id();
89 pids.insert(self_pid);
90 let mut next = Process::find_by_pid(self_pid)
91 .ok()
92 .flatten()
93 .and_then(|me| me.parent_pid);
94 for _ in 0..10 {
95 let Some(parent_pid) = next else { break };
96 pids.insert(parent_pid);
97 next = all_processes
98 .iter()
99 .find(|p| p.pid == parent_pid)
100 .and_then(|p| p.parent_pid);
101 }
102 pids
103 }
104
105 fn named<'a>(
109 all: impl Iterator<Item = &'a Process>,
110 ancestors: &std::collections::HashSet<u32>,
111 pattern: &str,
112 mode: NameMatch,
113 ) -> (Vec<&'a Process>, usize) {
114 let mut others = 0;
115 let named = all
116 .filter(|p| !ancestors.contains(&p.pid))
117 .filter(|p| match classify(pattern, &p.names(), mode) {
118 Hit::Match => true,
119 Hit::CommandLineOnly => {
120 others += 1;
121 false
122 }
123 Hit::No => false,
124 })
125 .collect();
126 (named, others)
127 }
128
129 pub fn execute(&self) -> Result<()> {
131 let printer = Printer::from_flags(self.json, self.verbose);
132
133 let mut all_processes = Process::find_all()?;
135 Process::measure_cpu(&mut all_processes);
136
137 all_processes.sort_by_key(|p| p.pid);
142
143 let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
145
146 let mut children_map: HashMap<u32, Vec<&Process>> = HashMap::new();
148
149 for proc in &all_processes {
150 if let Some(ppid) = proc.parent_pid {
151 children_map.entry(ppid).or_default().push(proc);
152 }
153 }
154
155 if self.ancestors {
157 return self.show_ancestors(&printer, &pid_map, &all_processes);
158 }
159
160 let mode = NameMatch::from_flag(self.cmd);
163 let mut in_command_line = 0;
164 let target_processes: Vec<&Process> = if let Some(ref target) = self.target {
165 match parse_target(target) {
167 TargetType::Port(_) | TargetType::Pid(_) => {
168 let resolved = resolve_target(target, mode)?;
171 let pids: Vec<u32> = resolved.iter().map(|p| p.pid).collect();
173 all_processes
174 .iter()
175 .filter(|p| pids.contains(&p.pid))
176 .collect()
177 }
178 TargetType::Name(ref pattern) => {
179 let ancestor_pids = Self::ancestor_pids(&all_processes);
182 let (named, others) =
183 Self::named(all_processes.iter(), &ancestor_pids, pattern, mode);
184 in_command_line = others;
185 named
186 }
187 }
188 } else {
189 Vec::new() };
191
192 let target_processes = if self.target.is_some() {
194 let in_dir_filter = resolve_in_dir(&self.in_dir);
195 target_processes
196 .into_iter()
197 .filter(|p| {
198 if let Some(ref dir_path) = in_dir_filter {
199 if !matches_in_filter(p, dir_path) {
200 return false;
201 }
202 }
203 if let Some(ref name) = self.by_name {
204 if !matches_by_filter(p, name, mode) {
205 return false;
206 }
207 }
208 true
209 })
210 .collect()
211 } else {
212 target_processes
213 };
214
215 let matches_filters = |p: &Process| self.matches_filters(p);
217
218 let has_filters = self.min_cpu.is_some()
220 || self.min_mem.is_some()
221 || self.status.is_some()
222 || self.min_uptime.is_some();
223
224 if self.json {
225 let tree_nodes = if self.target.is_some() {
226 target_processes
227 .iter()
228 .filter(|p| matches_filters(p))
229 .map(|p| self.build_tree_node(p, &children_map, 0))
230 .collect()
231 } else if has_filters {
232 all_processes
234 .iter()
235 .filter(|p| matches_filters(p))
236 .map(|p| self.build_tree_node(p, &children_map, 0))
237 .collect()
238 } else {
239 all_processes
241 .iter()
242 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
243 .map(|p| self.build_tree_node(p, &children_map, 0))
244 .collect()
245 };
246
247 printer.print_json(&TreeOutput {
248 action: "tree",
249 success: true,
250 tree: tree_nodes,
251 });
252 } else if self.target.is_some() {
253 let filtered: Vec<_> = target_processes
254 .into_iter()
255 .filter(|p| matches_filters(p))
256 .collect();
257 if filtered.is_empty() {
258 let target = self.target.as_ref().unwrap();
259 let mut warning = format!("No processes found for '{target}'");
260 if in_command_line > 0 {
261 warning.push_str("\n ");
262 warning.push_str(&crate::error::command_line_hint(target, in_command_line));
263 }
264 printer.warning(&warning);
265 return Ok(());
266 }
267
268 println!(
269 "{} Process tree for '{}':\n",
270 "✓".green().bold(),
271 self.target.as_ref().unwrap().cyan()
272 );
273
274 for proc in &filtered {
275 self.print_tree(proc, &children_map, "", true, 0);
276 println!();
277 }
278 } else if has_filters {
279 let filtered: Vec<_> = all_processes
280 .iter()
281 .filter(|p| matches_filters(p))
282 .collect();
283 if filtered.is_empty() {
284 printer.warning("No processes match the specified filters");
285 return Ok(());
286 }
287
288 println!(
289 "{} {} process{} matching filters:\n",
290 "✓".green().bold(),
291 filtered.len().to_string().cyan().bold(),
292 plural(filtered.len())
293 );
294
295 for (i, proc) in filtered.iter().enumerate() {
296 let is_last = i == filtered.len() - 1;
297 self.print_tree(proc, &children_map, "", is_last, 0);
298 }
299 } else {
300 println!("{} Process tree:\n", "✓".green().bold());
301
302 let display_roots: Vec<&Process> = all_processes
304 .iter()
305 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
306 .collect();
307
308 for (i, proc) in display_roots.iter().enumerate() {
309 let is_last = i == display_roots.len() - 1;
310 self.print_tree(proc, &children_map, "", is_last, 0);
311 }
312 }
313
314 Ok(())
315 }
316
317 fn matches_filters(&self, p: &Process) -> bool {
319 if let Some(min_cpu) = self.min_cpu {
320 if p.cpu_percent < min_cpu {
321 return false;
322 }
323 }
324 if let Some(min_mem) = self.min_mem {
325 if p.memory_mb < min_mem {
326 return false;
327 }
328 }
329 if let Some(status) = self.status {
330 if !status.matches(p.status) {
331 return false;
332 }
333 }
334 if let Some(min_uptime) = self.min_uptime {
335 if let Some(start_time) = p.start_time {
336 let now = std::time::SystemTime::now()
337 .duration_since(std::time::UNIX_EPOCH)
338 .map(|d| d.as_secs())
339 .unwrap_or(0);
340 if now.saturating_sub(start_time) < min_uptime {
341 return false;
342 }
343 } else {
344 return false;
345 }
346 }
347 true
348 }
349
350 fn print_tree(
351 &self,
352 proc: &Process,
353 children_map: &HashMap<u32, Vec<&Process>>,
354 prefix: &str,
355 is_last: bool,
356 depth: usize,
357 ) {
358 if depth > self.depth {
359 return;
360 }
361
362 let connector = if is_last { "└── " } else { "├── " };
363
364 if self.compact {
365 println!(
366 "{}{}{}",
367 prefix.bright_black(),
368 connector.bright_black(),
369 proc.pid.to_string().cyan()
370 );
371 } else {
372 let status_indicator = status_indicator(proc.status);
373
374 println!(
375 "{}{}{} {} [{}] {:.1}% {}{}",
376 prefix.bright_black(),
377 connector.bright_black(),
378 status_indicator,
379 proc.name.white().bold(),
380 proc.pid.to_string().cyan(),
381 proc.cpu_percent,
382 format_memory(proc.memory_mb),
383 uptime_label(proc.uptime_secs).bright_black()
384 );
385 }
386
387 let child_prefix = if is_last {
388 format!("{} ", prefix)
389 } else {
390 format!("{}│ ", prefix)
391 };
392
393 if let Some(children) = children_map.get(&proc.pid) {
395 for (i, child) in children.iter().enumerate() {
396 let child_is_last = i == children.len() - 1;
397 self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
398 }
399 }
400 }
401
402 fn build_tree_node(
403 &self,
404 proc: &Process,
405 children_map: &HashMap<u32, Vec<&Process>>,
406 depth: usize,
407 ) -> TreeNode {
408 let children = if depth < self.depth {
409 children_map
410 .get(&proc.pid)
411 .map(|kids| {
412 kids.iter()
413 .map(|p| self.build_tree_node(p, children_map, depth + 1))
414 .collect()
415 })
416 .unwrap_or_default()
417 } else {
418 Vec::new()
419 };
420
421 TreeNode {
422 pid: proc.pid,
423 name: proc.name.clone(),
424 cpu_percent: proc.cpu_percent,
425 memory_mb: proc.memory_mb,
426 status: proc.status,
427 uptime_secs: proc.uptime_secs,
428 children,
429 }
430 }
431
432 fn show_ancestors(
435 &self,
436 printer: &Printer,
437 pid_map: &HashMap<u32, &Process>,
438 all_processes: &[Process],
439 ) -> Result<()> {
440 use crate::core::{parse_target, resolve_target, TargetType};
441
442 let mode = NameMatch::from_flag(self.cmd);
443 let mut in_command_line = 0;
444 let target = match &self.target {
445 Some(t) => t,
446 None => {
447 printer.warning("--ancestors requires a target (PID, :port, or name)");
448 return Ok(());
449 }
450 };
451
452 let target_processes = match parse_target(target) {
454 TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target, mode)?,
455 TargetType::Name(ref pattern) => {
456 let ancestor_pids = Self::ancestor_pids(all_processes);
457 let (named, others) =
458 Self::named(all_processes.iter(), &ancestor_pids, pattern, mode);
459 in_command_line = others;
460 named.into_iter().cloned().collect()
461 }
462 };
463
464 if target_processes.is_empty() {
465 let mut warning = format!("No process found for '{}'", target);
466 if in_command_line > 0 {
467 warning.push_str("\n ");
468 warning.push_str(&crate::error::command_line_hint(target, in_command_line));
469 }
470 printer.warning(&warning);
471 return Ok(());
472 }
473
474 if self.json {
475 let ancestry_output: Vec<AncestryNode> = target_processes
476 .iter()
477 .map(|proc| self.build_ancestry_node(proc, pid_map))
478 .collect();
479 printer.print_json(&AncestryOutput {
480 action: "tree",
481 success: true,
482 ancestry: ancestry_output,
483 });
484 } else {
485 println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
486
487 for proc in &target_processes {
488 self.print_ancestry(proc, pid_map);
489 println!();
490 }
491 }
492
493 Ok(())
494 }
495
496 fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
498 let chain = ancestor_chain(target, pid_map);
500
501 for (i, proc) in chain.iter().enumerate() {
503 let is_target = proc.pid == target.pid;
504 let indent = " ".repeat(i);
505 let connector = if i == 0 { "" } else { "└── " };
506
507 let status_indicator = status_indicator(proc.status);
508
509 if is_target {
510 println!(
512 "{}{}{} {} [{}] {:.1}% {}{} {}",
513 indent.bright_black(),
514 connector.bright_black(),
515 status_indicator,
516 proc.name.cyan().bold(),
517 proc.pid.to_string().cyan().bold(),
518 proc.cpu_percent,
519 format_memory(proc.memory_mb),
520 uptime_label(proc.uptime_secs).bright_black(),
521 "← target".yellow()
522 );
523 } else {
524 println!(
525 "{}{}{} {} [{}] {:.1}% {}{}",
526 indent.bright_black(),
527 connector.bright_black(),
528 status_indicator,
529 proc.name.white(),
530 proc.pid.to_string().cyan(),
531 proc.cpu_percent,
532 format_memory(proc.memory_mb),
533 uptime_label(proc.uptime_secs).bright_black()
534 );
535 }
536 }
537 }
538
539 fn build_ancestry_node(
541 &self,
542 target: &Process,
543 pid_map: &HashMap<u32, &Process>,
544 ) -> AncestryNode {
545 let chain: Vec<ProcessInfo> = ancestor_chain(target, pid_map)
546 .into_iter()
547 .map(|proc| ProcessInfo {
548 pid: proc.pid,
549 name: proc.name.clone(),
550 cpu_percent: proc.cpu_percent,
551 memory_mb: proc.memory_mb,
552 status: proc.status,
553 uptime_secs: proc.uptime_secs,
554 })
555 .collect();
556
557 AncestryNode {
558 target_pid: target.pid,
559 target_name: target.name.clone(),
560 depth: chain.len(),
561 chain,
562 }
563 }
564}
565
566fn status_indicator(status: ProcessStatus) -> ColoredString {
568 match status {
569 ProcessStatus::Running => "●".green(),
570 ProcessStatus::Sleeping => "○".blue(),
571 ProcessStatus::Stopped => "◐".yellow(),
572 ProcessStatus::Zombie => "✗".red(),
573 _ => "?".white(),
574 }
575}
576
577fn ancestor_chain<'a>(
582 target: &'a Process,
583 pid_map: &HashMap<u32, &'a Process>,
584) -> Vec<&'a Process> {
585 let mut chain: Vec<&Process> = Vec::new();
586 let mut current_pid = Some(target.pid);
587
588 while let Some(pid) = current_pid {
589 let Some(proc) = pid_map.get(&pid) else { break };
590 chain.push(proc);
591 current_pid = proc.parent_pid;
592 if chain.len() > 100 {
593 break;
594 }
595 }
596
597 chain.reverse();
598 chain
599}
600
601#[derive(Serialize)]
602struct AncestryOutput {
603 action: &'static str,
604 success: bool,
605 ancestry: Vec<AncestryNode>,
606}
607
608#[derive(Serialize)]
609struct AncestryNode {
610 target_pid: u32,
611 target_name: String,
612 depth: usize,
613 chain: Vec<ProcessInfo>,
614}
615
616#[derive(Serialize)]
617struct ProcessInfo {
618 pid: u32,
619 name: String,
620 cpu_percent: f32,
621 memory_mb: f64,
622 status: ProcessStatus,
623 #[serde(skip_serializing_if = "Option::is_none")]
624 uptime_secs: Option<u64>,
625}
626
627#[derive(Serialize)]
628struct TreeOutput {
629 action: &'static str,
630 success: bool,
631 tree: Vec<TreeNode>,
632}
633
634#[derive(Serialize)]
635struct TreeNode {
636 pid: u32,
637 name: String,
638 cpu_percent: f32,
639 memory_mb: f64,
640 status: ProcessStatus,
641 #[serde(skip_serializing_if = "Option::is_none")]
642 uptime_secs: Option<u64>,
643 children: Vec<TreeNode>,
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 fn command() -> TreeCommand {
651 TreeCommand {
652 target: None,
653 ancestors: false,
654 verbose: false,
655 json: false,
656 depth: 10,
657 compact: false,
658 min_cpu: None,
659 min_mem: None,
660 status: None,
661 min_uptime: None,
662 in_dir: None,
663 by_name: None,
664 cmd: false,
665 }
666 }
667
668 fn process(pid: u32, parent_pid: Option<u32>) -> Process {
670 Process {
671 pid,
672 name: format!("p{pid}"),
673 exe_path: None,
674 cwd: None,
675 command: None,
676 argv0: None,
677 raw: None,
678 cpu_percent: 0.0,
679 memory_mb: 0.0,
680 status: ProcessStatus::Sleeping,
681 user: None,
682 parent_pid,
683 start_time: None,
684 uptime_secs: None,
685 }
686 }
687
688 #[test]
689 fn a_name_matches_whole_and_counts_the_processes_only_cmd_would_add() {
690 let mut node = process(1, None);
691 node.name = "node".to_string();
692 let mut editor = process(2, None);
693 editor.name = "vim".to_string();
694 editor.command = Some("vim node-notes.md".to_string());
695 let mut shell = process(3, None);
696 shell.name = "node".to_string();
697 let all = [node, editor, shell];
698 let skipped: std::collections::HashSet<u32> = [3].into();
699
700 let (named, others) = TreeCommand::named(all.iter(), &skipped, "node", NameMatch::Exact);
701 assert_eq!(named.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![1]);
702 assert_eq!(others, 1);
703
704 let (named, others) = TreeCommand::named(all.iter(), &skipped, "node", NameMatch::Command);
705 assert_eq!(named.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![1, 2]);
706 assert_eq!(others, 0);
707
708 let (named, others) = TreeCommand::named(all.iter(), &skipped, "python", NameMatch::Exact);
709 assert!(named.is_empty());
710 assert_eq!(others, 0);
711 }
712
713 #[test]
714 fn a_process_with_no_start_time_fails_min_uptime() {
715 let cmd = TreeCommand {
716 min_uptime: Some(0),
717 ..command()
718 };
719 assert!(!cmd.matches_filters(&process(1, None)));
720 assert!(command().matches_filters(&process(1, None)));
721 }
722
723 #[test]
724 fn every_status_has_its_own_marker() {
725 let color = |status| status_indicator(status).fgcolor;
726 let text = |status| status_indicator(status).trim().to_string();
727 assert_eq!(text(ProcessStatus::Running), "●");
728 assert_eq!(text(ProcessStatus::Sleeping), "○");
729 assert_eq!(text(ProcessStatus::Stopped), "◐");
730 assert_eq!(text(ProcessStatus::Zombie), "✗");
731 assert_eq!(text(ProcessStatus::Dead), "?");
732 assert_eq!(text(ProcessStatus::Unknown), "?");
733 assert_eq!(color(ProcessStatus::Running), Some(Color::Green));
734 assert_eq!(color(ProcessStatus::Sleeping), Some(Color::Blue));
735 assert_eq!(color(ProcessStatus::Stopped), Some(Color::Yellow));
736 assert_eq!(color(ProcessStatus::Zombie), Some(Color::Red));
737 assert_eq!(color(ProcessStatus::Unknown), Some(Color::White));
738 }
739
740 #[test]
741 fn the_chain_runs_from_the_root_down_to_the_target() {
742 let (root, middle, leaf) = (
743 process(1, Some(0)),
744 process(2, Some(1)),
745 process(3, Some(2)),
746 );
747 let map: HashMap<u32, &Process> = [&root, &middle, &leaf]
748 .iter()
749 .map(|p| (p.pid, *p))
750 .collect();
751 let chain: Vec<u32> = ancestor_chain(&leaf, &map).iter().map(|p| p.pid).collect();
752 assert_eq!(chain, vec![1, 2, 3]);
753 }
754
755 #[test]
756 fn a_loop_in_the_parent_links_is_cut() {
757 let (a, b) = (process(1, Some(2)), process(2, Some(1)));
758 let map: HashMap<u32, &Process> = [&a, &b].iter().map(|p| (p.pid, *p)).collect();
759 assert_eq!(ancestor_chain(&a, &map).len(), 101);
760 }
761
762 #[test]
763 fn a_process_that_passes_every_filter_is_kept() {
764 let mut p = process(1, None);
765 p.start_time = Some(0);
766 let cmd = TreeCommand {
767 min_cpu: Some(0.0),
768 min_mem: Some(0.0),
769 status: Some(StatusFilter::Sleeping),
770 min_uptime: Some(1),
771 ..command()
772 };
773 assert!(cmd.matches_filters(&p));
774 let other_status = TreeCommand {
775 status: Some(StatusFilter::Running),
776 ..command()
777 };
778 assert!(!other_status.matches_filters(&p));
779 }
780}