1use 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#[derive(Args, Debug)]
22pub struct TreeCommand {
23 target: Option<String>,
25
26 #[arg(long, short)]
28 ancestors: bool,
29
30 #[arg(long, short)]
32 json: bool,
33
34 #[arg(long, short, default_value = "10")]
36 depth: usize,
37
38 #[arg(long, short = 'C')]
40 compact: bool,
41
42 #[arg(long)]
44 min_cpu: Option<f32>,
45
46 #[arg(long)]
48 min_mem: Option<f64>,
49
50 #[arg(long)]
52 status: Option<String>,
53
54 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
56 pub in_dir: Option<String>,
57
58 #[arg(long = "by", short = 'b')]
60 pub by_name: Option<String>,
61}
62
63impl TreeCommand {
64 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 let all_processes = Process::find_all()?;
75
76 let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
78
79 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 if self.ancestors {
90 return self.show_ancestors(&printer, &pid_map);
91 }
92
93 let target_processes: Vec<&Process> = if let Some(ref target) = self.target {
95 match parse_target(target) {
97 TargetType::Port(_) | TargetType::Pid(_) => {
98 let resolved = resolve_target(target)?;
100 if resolved.is_empty() {
101 printer.warning(&format!("No process found for '{}'", target));
102 return Ok(());
103 }
104 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 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() };
131
132 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 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 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 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 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 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 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 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 fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
429 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 if chain.len() > 100 {
439 break;
440 }
441 } else {
442 break;
443 }
444 }
445
446 chain.reverse();
448
449 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 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 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}