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