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 and parent to avoid
123                    // false positives from proc's own args in the shell command line)
124                    let pattern_lower = pattern.to_lowercase();
125                    let self_pid = std::process::id();
126                    let parent_pid = all_processes
127                        .iter()
128                        .find(|p| p.pid == self_pid)
129                        .and_then(|p| p.parent_pid);
130                    all_processes
131                        .iter()
132                        .filter(|p| {
133                            p.pid != self_pid
134                                && Some(p.pid) != parent_pid
135                                && (p.name.to_lowercase().contains(&pattern_lower)
136                                    || p.command
137                                        .as_ref()
138                                        .map(|c| c.to_lowercase().contains(&pattern_lower))
139                                        .unwrap_or(false))
140                        })
141                        .collect()
142                }
143            }
144        } else {
145            Vec::new() // Will show full tree
146        };
147
148        // Apply --in and --by filters (only for targeted mode)
149        let target_processes = if self.target.is_some() {
150            let in_dir_filter = resolve_in_dir(&self.in_dir);
151            target_processes
152                .into_iter()
153                .filter(|p| {
154                    if let Some(ref dir_path) = in_dir_filter {
155                        if let Some(ref cwd) = p.cwd {
156                            if !PathBuf::from(cwd).starts_with(dir_path) {
157                                return false;
158                            }
159                        } else {
160                            return false;
161                        }
162                    }
163                    if let Some(ref name) = self.by_name {
164                        if !p.name.to_lowercase().contains(&name.to_lowercase()) {
165                            return false;
166                        }
167                    }
168                    true
169                })
170                .collect()
171        } else {
172            target_processes
173        };
174
175        // Apply resource filters if specified
176        let matches_filters = |p: &Process| -> bool {
177            if let Some(min_cpu) = self.min_cpu {
178                if p.cpu_percent < min_cpu {
179                    return false;
180                }
181            }
182            if let Some(min_mem) = self.min_mem {
183                if p.memory_mb < min_mem {
184                    return false;
185                }
186            }
187            if let Some(ref status) = self.status {
188                let status_match = match status.to_lowercase().as_str() {
189                    "running" => matches!(p.status, ProcessStatus::Running),
190                    "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
191                    "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
192                    "zombie" => matches!(p.status, ProcessStatus::Zombie),
193                    _ => true,
194                };
195                if !status_match {
196                    return false;
197                }
198            }
199            if let Some(min_uptime) = self.min_uptime {
200                if let Some(start_time) = p.start_time {
201                    let now = std::time::SystemTime::now()
202                        .duration_since(std::time::UNIX_EPOCH)
203                        .map(|d| d.as_secs())
204                        .unwrap_or(0);
205                    if now.saturating_sub(start_time) < min_uptime {
206                        return false;
207                    }
208                } else {
209                    return false;
210                }
211            }
212            true
213        };
214
215        // Apply filters to target processes or find filtered roots
216        let has_filters = self.min_cpu.is_some()
217            || self.min_mem.is_some()
218            || self.status.is_some()
219            || self.min_uptime.is_some();
220
221        if self.json {
222            let tree_nodes = if self.target.is_some() {
223                target_processes
224                    .iter()
225                    .filter(|p| matches_filters(p))
226                    .map(|p| self.build_tree_node(p, &children_map, 0))
227                    .collect()
228            } else if has_filters {
229                // Show only processes matching filters
230                all_processes
231                    .iter()
232                    .filter(|p| matches_filters(p))
233                    .map(|p| self.build_tree_node(p, &children_map, 0))
234                    .collect()
235            } else {
236                // Show full tree from roots
237                all_processes
238                    .iter()
239                    .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
240                    .map(|p| self.build_tree_node(p, &children_map, 0))
241                    .collect()
242            };
243
244            printer.print_json(&TreeOutput {
245                action: "tree",
246                success: true,
247                tree: tree_nodes,
248            });
249        } else if self.target.is_some() {
250            let filtered: Vec<_> = target_processes
251                .into_iter()
252                .filter(|p| matches_filters(p))
253                .collect();
254            if filtered.is_empty() {
255                printer.warning(&format!(
256                    "No processes found for '{}'",
257                    self.target.as_ref().unwrap()
258                ));
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                if filtered.len() == 1 { "" } else { "es" }
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            // Find processes with PID 1 or no parent as roots
297            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 print_tree(
312        &self,
313        proc: &Process,
314        children_map: &HashMap<u32, Vec<&Process>>,
315        prefix: &str,
316        is_last: bool,
317        depth: usize,
318    ) {
319        if depth > self.depth {
320            return;
321        }
322
323        let connector = if is_last { "└── " } else { "├── " };
324
325        if self.compact {
326            println!(
327                "{}{}{}",
328                prefix.bright_black(),
329                connector.bright_black(),
330                proc.pid.to_string().cyan()
331            );
332        } else {
333            let status_indicator = match proc.status {
334                crate::core::ProcessStatus::Running => "●".green(),
335                crate::core::ProcessStatus::Sleeping => "○".blue(),
336                crate::core::ProcessStatus::Stopped => "◐".yellow(),
337                crate::core::ProcessStatus::Zombie => "✗".red(),
338                _ => "?".white(),
339            };
340
341            println!(
342                "{}{}{} {} [{}] {:.1}% {}",
343                prefix.bright_black(),
344                connector.bright_black(),
345                status_indicator,
346                proc.name.white().bold(),
347                proc.pid.to_string().cyan(),
348                proc.cpu_percent,
349                format_memory(proc.memory_mb)
350            );
351        }
352
353        let child_prefix = if is_last {
354            format!("{}    ", prefix)
355        } else {
356            format!("{}│   ", prefix)
357        };
358
359        if let Some(children) = children_map.get(&proc.pid) {
360            let mut sorted_children: Vec<&&Process> = children.iter().collect();
361            sorted_children.sort_by_key(|p| p.pid);
362
363            for (i, child) in sorted_children.iter().enumerate() {
364                let child_is_last = i == sorted_children.len() - 1;
365                self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
366            }
367        }
368    }
369
370    fn build_tree_node(
371        &self,
372        proc: &Process,
373        children_map: &HashMap<u32, Vec<&Process>>,
374        depth: usize,
375    ) -> TreeNode {
376        let children = if depth < self.depth {
377            children_map
378                .get(&proc.pid)
379                .map(|kids| {
380                    kids.iter()
381                        .map(|p| self.build_tree_node(p, children_map, depth + 1))
382                        .collect()
383                })
384                .unwrap_or_default()
385        } else {
386            Vec::new()
387        };
388
389        TreeNode {
390            pid: proc.pid,
391            name: proc.name.clone(),
392            cpu_percent: proc.cpu_percent,
393            memory_mb: proc.memory_mb,
394            status: format!("{:?}", proc.status),
395            children,
396        }
397    }
398
399    /// Show ancestry (path UP to root) for target processes
400    fn show_ancestors(&self, printer: &Printer, pid_map: &HashMap<u32, &Process>) -> Result<()> {
401        use crate::core::{parse_target, resolve_target, TargetType};
402
403        let target = match &self.target {
404            Some(t) => t,
405            None => {
406                printer.warning("--ancestors requires a target (PID, :port, or name)");
407                return Ok(());
408            }
409        };
410
411        // Resolve target to processes
412        let target_processes = match parse_target(target) {
413            TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target)?,
414            TargetType::Name(ref pattern) => {
415                let pattern_lower = pattern.to_lowercase();
416                let self_pid = std::process::id();
417                let parent_pid = pid_map.get(&self_pid).and_then(|p| p.parent_pid);
418                pid_map
419                    .values()
420                    .filter(|p| {
421                        p.pid != self_pid
422                            && Some(p.pid) != parent_pid
423                            && (p.name.to_lowercase().contains(&pattern_lower)
424                                || p.command
425                                    .as_ref()
426                                    .map(|c| c.to_lowercase().contains(&pattern_lower))
427                                    .unwrap_or(false))
428                    })
429                    .map(|p| (*p).clone())
430                    .collect()
431            }
432        };
433
434        if target_processes.is_empty() {
435            printer.warning(&format!("No process found for '{}'", target));
436            return Ok(());
437        }
438
439        if self.json {
440            let ancestry_output: Vec<AncestryNode> = target_processes
441                .iter()
442                .map(|proc| self.build_ancestry_node(proc, pid_map))
443                .collect();
444            printer.print_json(&AncestryOutput {
445                action: "ancestry",
446                success: true,
447                ancestry: ancestry_output,
448            });
449        } else {
450            println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
451
452            for proc in &target_processes {
453                self.print_ancestry(proc, pid_map);
454                println!();
455            }
456        }
457
458        Ok(())
459    }
460
461    /// Trace and print ancestry from root down to target
462    fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
463        // Build the ancestor chain (from target up to root)
464        let mut chain: Vec<&Process> = Vec::new();
465        let mut current_pid = Some(target.pid);
466
467        while let Some(pid) = current_pid {
468            if let Some(proc) = pid_map.get(&pid) {
469                chain.push(proc);
470                current_pid = proc.parent_pid;
471                // Prevent infinite loops
472                if chain.len() > 100 {
473                    break;
474                }
475            } else {
476                break;
477            }
478        }
479
480        // Reverse to print from root to target
481        chain.reverse();
482
483        // Print the chain
484        for (i, proc) in chain.iter().enumerate() {
485            let is_target = proc.pid == target.pid;
486            let indent = "    ".repeat(i);
487            let connector = if i == 0 { "" } else { "└── " };
488
489            let status_indicator = match proc.status {
490                ProcessStatus::Running => "●".green(),
491                ProcessStatus::Sleeping => "○".blue(),
492                ProcessStatus::Stopped => "◐".yellow(),
493                ProcessStatus::Zombie => "✗".red(),
494                _ => "?".white(),
495            };
496
497            if is_target {
498                // Highlight the target
499                println!(
500                    "{}{}{} {} [{}] {:.1}% {}  {}",
501                    indent.bright_black(),
502                    connector.bright_black(),
503                    status_indicator,
504                    proc.name.cyan().bold(),
505                    proc.pid.to_string().cyan().bold(),
506                    proc.cpu_percent,
507                    format_memory(proc.memory_mb),
508                    "← target".yellow()
509                );
510            } else {
511                println!(
512                    "{}{}{} {} [{}] {:.1}% {}",
513                    indent.bright_black(),
514                    connector.bright_black(),
515                    status_indicator,
516                    proc.name.white(),
517                    proc.pid.to_string().cyan(),
518                    proc.cpu_percent,
519                    format_memory(proc.memory_mb)
520                );
521            }
522        }
523    }
524
525    /// Build ancestry node for JSON output
526    fn build_ancestry_node(
527        &self,
528        target: &Process,
529        pid_map: &HashMap<u32, &Process>,
530    ) -> AncestryNode {
531        let mut chain: Vec<ProcessInfo> = Vec::new();
532        let mut current_pid = Some(target.pid);
533
534        while let Some(pid) = current_pid {
535            if let Some(proc) = pid_map.get(&pid) {
536                chain.push(ProcessInfo {
537                    pid: proc.pid,
538                    name: proc.name.clone(),
539                    cpu_percent: proc.cpu_percent,
540                    memory_mb: proc.memory_mb,
541                    status: format!("{:?}", proc.status),
542                });
543                current_pid = proc.parent_pid;
544                if chain.len() > 100 {
545                    break;
546                }
547            } else {
548                break;
549            }
550        }
551
552        chain.reverse();
553
554        AncestryNode {
555            target_pid: target.pid,
556            target_name: target.name.clone(),
557            depth: chain.len(),
558            chain,
559        }
560    }
561}
562
563#[derive(Serialize)]
564struct AncestryOutput {
565    action: &'static str,
566    success: bool,
567    ancestry: Vec<AncestryNode>,
568}
569
570#[derive(Serialize)]
571struct AncestryNode {
572    target_pid: u32,
573    target_name: String,
574    depth: usize,
575    chain: Vec<ProcessInfo>,
576}
577
578#[derive(Serialize)]
579struct ProcessInfo {
580    pid: u32,
581    name: String,
582    cpu_percent: f32,
583    memory_mb: f64,
584    status: String,
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: String,
601    children: Vec<TreeNode>,
602}