Skip to main content

proc_cli/commands/
tree.rs

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