1use crate::core::{
12 matches_by_filter, parse_target, resolve_in_dir, resolve_target, Process, ProcessStatus,
13 StatusFilter, TargetType,
14};
15use crate::error::Result;
16use crate::ui::{format_memory, plural, uptime_label, Printer};
17use clap::Args;
18use colored::*;
19use serde::Serialize;
20use std::collections::HashMap;
21use std::path::PathBuf;
22
23#[derive(Args, Debug)]
25pub struct TreeCommand {
26 target: Option<String>,
28
29 #[arg(long, short)]
31 ancestors: bool,
32
33 #[arg(long, short = 'v')]
35 verbose: bool,
36
37 #[arg(long, short = 'j')]
39 json: bool,
40
41 #[arg(long, short, default_value = "10")]
43 depth: usize,
44
45 #[arg(long, short = 'C')]
47 compact: bool,
48
49 #[arg(long)]
51 min_cpu: Option<f32>,
52
53 #[arg(long)]
55 min_mem: Option<f64>,
56
57 #[arg(long, value_enum, ignore_case = true)]
59 status: Option<StatusFilter>,
60
61 #[arg(long)]
63 min_uptime: Option<u64>,
64
65 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
67 pub in_dir: Option<String>,
68
69 #[arg(long = "by", short = 'b')]
71 pub by_name: Option<String>,
72}
73
74impl TreeCommand {
75 fn ancestor_pids(all_processes: &[Process]) -> std::collections::HashSet<u32> {
80 let mut pids = std::collections::HashSet::new();
81 let self_pid = std::process::id();
82 pids.insert(self_pid);
83 let mut current = self_pid;
84 for _ in 0..10 {
85 if let Some(parent_pid) = all_processes
86 .iter()
87 .find(|p| p.pid == current)
88 .and_then(|p| p.parent_pid)
89 {
90 pids.insert(parent_pid);
91 current = parent_pid;
92 } else {
93 break;
94 }
95 }
96 pids
97 }
98
99 pub fn execute(&self) -> Result<()> {
101 let printer = Printer::from_flags(self.json, self.verbose);
102
103 let all_processes = Process::find_all()?;
105
106 let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
108
109 let mut children_map: HashMap<u32, Vec<&Process>> = HashMap::new();
111
112 for proc in &all_processes {
113 if let Some(ppid) = proc.parent_pid {
114 children_map.entry(ppid).or_default().push(proc);
115 }
116 }
117
118 if self.ancestors {
120 return self.show_ancestors(&printer, &pid_map);
121 }
122
123 let target_processes: Vec<&Process> = if let Some(ref target) = self.target {
125 match parse_target(target) {
127 TargetType::Port(_) | TargetType::Pid(_) => {
128 let resolved = resolve_target(target)?;
130 if resolved.is_empty() {
131 printer.warning(&format!("No process found for '{}'", target));
132 return Ok(());
133 }
134 let pids: Vec<u32> = resolved.iter().map(|p| p.pid).collect();
136 all_processes
137 .iter()
138 .filter(|p| pids.contains(&p.pid))
139 .collect()
140 }
141 TargetType::Name(ref pattern) => {
142 let ancestor_pids = Self::ancestor_pids(&all_processes);
145 all_processes
146 .iter()
147 .filter(|p| {
148 !ancestor_pids.contains(&p.pid) && matches_by_filter(p, pattern)
149 })
150 .collect()
151 }
152 }
153 } else {
154 Vec::new() };
156
157 let target_processes = if self.target.is_some() {
159 let in_dir_filter = resolve_in_dir(&self.in_dir);
160 target_processes
161 .into_iter()
162 .filter(|p| {
163 if let Some(ref dir_path) = in_dir_filter {
164 if let Some(ref cwd) = p.cwd {
165 if !PathBuf::from(cwd).starts_with(dir_path) {
166 return false;
167 }
168 } else {
169 return false;
170 }
171 }
172 if let Some(ref name) = self.by_name {
173 if !matches_by_filter(p, name) {
174 return false;
175 }
176 }
177 true
178 })
179 .collect()
180 } else {
181 target_processes
182 };
183
184 let matches_filters = |p: &Process| -> bool {
186 if let Some(min_cpu) = self.min_cpu {
187 if p.cpu_percent < min_cpu {
188 return false;
189 }
190 }
191 if let Some(min_mem) = self.min_mem {
192 if p.memory_mb < min_mem {
193 return false;
194 }
195 }
196 if let Some(status) = self.status {
197 if !status.matches(p.status) {
198 return false;
199 }
200 }
201 if let Some(min_uptime) = self.min_uptime {
202 if let Some(start_time) = p.start_time {
203 let now = std::time::SystemTime::now()
204 .duration_since(std::time::UNIX_EPOCH)
205 .map(|d| d.as_secs())
206 .unwrap_or(0);
207 if now.saturating_sub(start_time) < min_uptime {
208 return false;
209 }
210 } else {
211 return false;
212 }
213 }
214 true
215 };
216
217 let has_filters = self.min_cpu.is_some()
219 || self.min_mem.is_some()
220 || self.status.is_some()
221 || self.min_uptime.is_some();
222
223 if self.json {
224 let tree_nodes = if self.target.is_some() {
225 target_processes
226 .iter()
227 .filter(|p| matches_filters(p))
228 .map(|p| self.build_tree_node(p, &children_map, 0))
229 .collect()
230 } else if has_filters {
231 all_processes
233 .iter()
234 .filter(|p| matches_filters(p))
235 .map(|p| self.build_tree_node(p, &children_map, 0))
236 .collect()
237 } else {
238 all_processes
240 .iter()
241 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
242 .map(|p| self.build_tree_node(p, &children_map, 0))
243 .collect()
244 };
245
246 printer.print_json(&TreeOutput {
247 action: "tree",
248 success: true,
249 tree: tree_nodes,
250 });
251 } else if self.target.is_some() {
252 let filtered: Vec<_> = target_processes
253 .into_iter()
254 .filter(|p| matches_filters(p))
255 .collect();
256 if filtered.is_empty() {
257 printer.warning(&format!(
258 "No processes found for '{}'",
259 self.target.as_ref().unwrap()
260 ));
261 return Ok(());
262 }
263
264 println!(
265 "{} Process tree for '{}':\n",
266 "✓".green().bold(),
267 self.target.as_ref().unwrap().cyan()
268 );
269
270 for proc in &filtered {
271 self.print_tree(proc, &children_map, "", true, 0);
272 println!();
273 }
274 } else if has_filters {
275 let filtered: Vec<_> = all_processes
276 .iter()
277 .filter(|p| matches_filters(p))
278 .collect();
279 if filtered.is_empty() {
280 printer.warning("No processes match the specified filters");
281 return Ok(());
282 }
283
284 println!(
285 "{} {} process{} matching filters:\n",
286 "✓".green().bold(),
287 filtered.len().to_string().cyan().bold(),
288 plural(filtered.len())
289 );
290
291 for (i, proc) in filtered.iter().enumerate() {
292 let is_last = i == filtered.len() - 1;
293 self.print_tree(proc, &children_map, "", is_last, 0);
294 }
295 } else {
296 println!("{} Process tree:\n", "✓".green().bold());
297
298 let display_roots: Vec<&Process> = all_processes
300 .iter()
301 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
302 .collect();
303
304 for (i, proc) in display_roots.iter().enumerate() {
305 let is_last = i == display_roots.len() - 1;
306 self.print_tree(proc, &children_map, "", is_last, 0);
307 }
308 }
309
310 Ok(())
311 }
312
313 fn print_tree(
314 &self,
315 proc: &Process,
316 children_map: &HashMap<u32, Vec<&Process>>,
317 prefix: &str,
318 is_last: bool,
319 depth: usize,
320 ) {
321 if depth > self.depth {
322 return;
323 }
324
325 let connector = if is_last { "└── " } else { "├── " };
326
327 if self.compact {
328 println!(
329 "{}{}{}",
330 prefix.bright_black(),
331 connector.bright_black(),
332 proc.pid.to_string().cyan()
333 );
334 } else {
335 let status_indicator = match proc.status {
336 crate::core::ProcessStatus::Running => "●".green(),
337 crate::core::ProcessStatus::Sleeping => "○".blue(),
338 crate::core::ProcessStatus::Stopped => "◐".yellow(),
339 crate::core::ProcessStatus::Zombie => "✗".red(),
340 _ => "?".white(),
341 };
342
343 println!(
344 "{}{}{} {} [{}] {:.1}% {}{}",
345 prefix.bright_black(),
346 connector.bright_black(),
347 status_indicator,
348 proc.name.white().bold(),
349 proc.pid.to_string().cyan(),
350 proc.cpu_percent,
351 format_memory(proc.memory_mb),
352 uptime_label(proc.uptime_secs).bright_black()
353 );
354 }
355
356 let child_prefix = if is_last {
357 format!("{} ", prefix)
358 } else {
359 format!("{}│ ", prefix)
360 };
361
362 if let Some(children) = children_map.get(&proc.pid) {
363 let mut sorted_children: Vec<&&Process> = children.iter().collect();
364 sorted_children.sort_by_key(|p| p.pid);
365
366 for (i, child) in sorted_children.iter().enumerate() {
367 let child_is_last = i == sorted_children.len() - 1;
368 self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
369 }
370 }
371 }
372
373 fn build_tree_node(
374 &self,
375 proc: &Process,
376 children_map: &HashMap<u32, Vec<&Process>>,
377 depth: usize,
378 ) -> TreeNode {
379 let children = if depth < self.depth {
380 children_map
381 .get(&proc.pid)
382 .map(|kids| {
383 kids.iter()
384 .map(|p| self.build_tree_node(p, children_map, depth + 1))
385 .collect()
386 })
387 .unwrap_or_default()
388 } else {
389 Vec::new()
390 };
391
392 TreeNode {
393 pid: proc.pid,
394 name: proc.name.clone(),
395 cpu_percent: proc.cpu_percent,
396 memory_mb: proc.memory_mb,
397 status: proc.status,
398 uptime_secs: proc.uptime_secs,
399 children,
400 }
401 }
402
403 fn show_ancestors(&self, printer: &Printer, pid_map: &HashMap<u32, &Process>) -> Result<()> {
405 use crate::core::{parse_target, resolve_target, TargetType};
406
407 let target = match &self.target {
408 Some(t) => t,
409 None => {
410 printer.warning("--ancestors requires a target (PID, :port, or name)");
411 return Ok(());
412 }
413 };
414
415 let target_processes = match parse_target(target) {
417 TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target)?,
418 TargetType::Name(ref pattern) => {
419 let all_procs: Vec<Process> = pid_map.values().map(|p| (*p).clone()).collect();
420 let ancestor_pids = Self::ancestor_pids(&all_procs);
421 pid_map
422 .values()
423 .filter(|p| !ancestor_pids.contains(&p.pid) && matches_by_filter(p, pattern))
424 .map(|p| (*p).clone())
425 .collect()
426 }
427 };
428
429 if target_processes.is_empty() {
430 printer.warning(&format!("No process found for '{}'", target));
431 return Ok(());
432 }
433
434 if self.json {
435 let ancestry_output: Vec<AncestryNode> = target_processes
436 .iter()
437 .map(|proc| self.build_ancestry_node(proc, pid_map))
438 .collect();
439 printer.print_json(&AncestryOutput {
440 action: "tree",
441 success: true,
442 ancestry: ancestry_output,
443 });
444 } else {
445 println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
446
447 for proc in &target_processes {
448 self.print_ancestry(proc, pid_map);
449 println!();
450 }
451 }
452
453 Ok(())
454 }
455
456 fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
458 let mut chain: Vec<&Process> = Vec::new();
460 let mut current_pid = Some(target.pid);
461
462 while let Some(pid) = current_pid {
463 if let Some(proc) = pid_map.get(&pid) {
464 chain.push(proc);
465 current_pid = proc.parent_pid;
466 if chain.len() > 100 {
468 break;
469 }
470 } else {
471 break;
472 }
473 }
474
475 chain.reverse();
477
478 for (i, proc) in chain.iter().enumerate() {
480 let is_target = proc.pid == target.pid;
481 let indent = " ".repeat(i);
482 let connector = if i == 0 { "" } else { "└── " };
483
484 let status_indicator = match proc.status {
485 ProcessStatus::Running => "●".green(),
486 ProcessStatus::Sleeping => "○".blue(),
487 ProcessStatus::Stopped => "◐".yellow(),
488 ProcessStatus::Zombie => "✗".red(),
489 _ => "?".white(),
490 };
491
492 if is_target {
493 println!(
495 "{}{}{} {} [{}] {:.1}% {}{} {}",
496 indent.bright_black(),
497 connector.bright_black(),
498 status_indicator,
499 proc.name.cyan().bold(),
500 proc.pid.to_string().cyan().bold(),
501 proc.cpu_percent,
502 format_memory(proc.memory_mb),
503 uptime_label(proc.uptime_secs).bright_black(),
504 "← target".yellow()
505 );
506 } else {
507 println!(
508 "{}{}{} {} [{}] {:.1}% {}{}",
509 indent.bright_black(),
510 connector.bright_black(),
511 status_indicator,
512 proc.name.white(),
513 proc.pid.to_string().cyan(),
514 proc.cpu_percent,
515 format_memory(proc.memory_mb),
516 uptime_label(proc.uptime_secs).bright_black()
517 );
518 }
519 }
520 }
521
522 fn build_ancestry_node(
524 &self,
525 target: &Process,
526 pid_map: &HashMap<u32, &Process>,
527 ) -> AncestryNode {
528 let mut chain: Vec<ProcessInfo> = Vec::new();
529 let mut current_pid = Some(target.pid);
530
531 while let Some(pid) = current_pid {
532 if let Some(proc) = pid_map.get(&pid) {
533 chain.push(ProcessInfo {
534 pid: proc.pid,
535 name: proc.name.clone(),
536 cpu_percent: proc.cpu_percent,
537 memory_mb: proc.memory_mb,
538 status: proc.status,
539 uptime_secs: proc.uptime_secs,
540 });
541 current_pid = proc.parent_pid;
542 if chain.len() > 100 {
543 break;
544 }
545 } else {
546 break;
547 }
548 }
549
550 chain.reverse();
551
552 AncestryNode {
553 target_pid: target.pid,
554 target_name: target.name.clone(),
555 depth: chain.len(),
556 chain,
557 }
558 }
559}
560
561#[derive(Serialize)]
562struct AncestryOutput {
563 action: &'static str,
564 success: bool,
565 ancestry: Vec<AncestryNode>,
566}
567
568#[derive(Serialize)]
569struct AncestryNode {
570 target_pid: u32,
571 target_name: String,
572 depth: usize,
573 chain: Vec<ProcessInfo>,
574}
575
576#[derive(Serialize)]
577struct ProcessInfo {
578 pid: u32,
579 name: String,
580 cpu_percent: f32,
581 memory_mb: f64,
582 status: ProcessStatus,
583 #[serde(skip_serializing_if = "Option::is_none")]
584 uptime_secs: Option<u64>,
585}
586
587#[derive(Serialize)]
588struct TreeOutput {
589 action: &'static str,
590 success: bool,
591 tree: Vec<TreeNode>,
592}
593
594#[derive(Serialize)]
595struct TreeNode {
596 pid: u32,
597 name: String,
598 cpu_percent: f32,
599 memory_mb: f64,
600 status: ProcessStatus,
601 #[serde(skip_serializing_if = "Option::is_none")]
602 uptime_secs: Option<u64>,
603 children: Vec<TreeNode>,
604}