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