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