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