1use 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#[derive(Args, Debug)]
24pub struct TreeCommand {
25 target: Option<String>,
27
28 #[arg(long, short)]
30 ancestors: bool,
31
32 #[arg(long, short = 'v')]
34 verbose: bool,
35
36 #[arg(long, short = 'j')]
38 json: bool,
39
40 #[arg(long, short, default_value = "10")]
42 depth: usize,
43
44 #[arg(long, short = 'C')]
46 compact: bool,
47
48 #[arg(long)]
50 min_cpu: Option<f32>,
51
52 #[arg(long)]
54 min_mem: Option<f64>,
55
56 #[arg(long)]
58 status: Option<String>,
59
60 #[arg(long)]
62 min_uptime: Option<u64>,
63
64 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
66 pub in_dir: Option<String>,
67
68 #[arg(long = "by", short = 'b')]
70 pub by_name: Option<String>,
71}
72
73impl TreeCommand {
74 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 let all_processes = Process::find_all()?;
85
86 let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
88
89 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 if self.ancestors {
100 return self.show_ancestors(&printer, &pid_map);
101 }
102
103 let target_processes: Vec<&Process> = if let Some(ref target) = self.target {
105 match parse_target(target) {
107 TargetType::Port(_) | TargetType::Pid(_) => {
108 let resolved = resolve_target(target)?;
110 if resolved.is_empty() {
111 printer.warning(&format!("No process found for '{}'", target));
112 return Ok(());
113 }
114 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 let pattern_lower = pattern.to_lowercase();
124 let self_pid = std::process::id();
125 all_processes
126 .iter()
127 .filter(|p| {
128 p.pid != self_pid
129 && (p.name.to_lowercase().contains(&pattern_lower)
130 || p.command
131 .as_ref()
132 .map(|c| c.to_lowercase().contains(&pattern_lower))
133 .unwrap_or(false))
134 })
135 .collect()
136 }
137 }
138 } else {
139 Vec::new() };
141
142 let target_processes = if self.target.is_some() {
144 let in_dir_filter = resolve_in_dir(&self.in_dir);
145 target_processes
146 .into_iter()
147 .filter(|p| {
148 if let Some(ref dir_path) = in_dir_filter {
149 if let Some(ref cwd) = p.cwd {
150 if !PathBuf::from(cwd).starts_with(dir_path) {
151 return false;
152 }
153 } else {
154 return false;
155 }
156 }
157 if let Some(ref name) = self.by_name {
158 if !p.name.to_lowercase().contains(&name.to_lowercase()) {
159 return false;
160 }
161 }
162 true
163 })
164 .collect()
165 } else {
166 target_processes
167 };
168
169 let matches_filters = |p: &Process| -> bool {
171 if let Some(min_cpu) = self.min_cpu {
172 if p.cpu_percent < min_cpu {
173 return false;
174 }
175 }
176 if let Some(min_mem) = self.min_mem {
177 if p.memory_mb < min_mem {
178 return false;
179 }
180 }
181 if let Some(ref status) = self.status {
182 let status_match = match status.to_lowercase().as_str() {
183 "running" => matches!(p.status, ProcessStatus::Running),
184 "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
185 "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
186 "zombie" => matches!(p.status, ProcessStatus::Zombie),
187 _ => true,
188 };
189 if !status_match {
190 return false;
191 }
192 }
193 if let Some(min_uptime) = self.min_uptime {
194 if let Some(start_time) = p.start_time {
195 let now = std::time::SystemTime::now()
196 .duration_since(std::time::UNIX_EPOCH)
197 .map(|d| d.as_secs())
198 .unwrap_or(0);
199 if now.saturating_sub(start_time) < min_uptime {
200 return false;
201 }
202 } else {
203 return false;
204 }
205 }
206 true
207 };
208
209 let has_filters = self.min_cpu.is_some()
211 || self.min_mem.is_some()
212 || self.status.is_some()
213 || self.min_uptime.is_some();
214
215 if self.json {
216 let tree_nodes = if self.target.is_some() {
217 target_processes
218 .iter()
219 .filter(|p| matches_filters(p))
220 .map(|p| self.build_tree_node(p, &children_map, 0))
221 .collect()
222 } else if has_filters {
223 all_processes
225 .iter()
226 .filter(|p| matches_filters(p))
227 .map(|p| self.build_tree_node(p, &children_map, 0))
228 .collect()
229 } else {
230 all_processes
232 .iter()
233 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
234 .map(|p| self.build_tree_node(p, &children_map, 0))
235 .collect()
236 };
237
238 printer.print_json(&TreeOutput {
239 action: "tree",
240 success: true,
241 tree: tree_nodes,
242 });
243 } else if self.target.is_some() {
244 let filtered: Vec<_> = target_processes
245 .into_iter()
246 .filter(|p| matches_filters(p))
247 .collect();
248 if filtered.is_empty() {
249 printer.warning(&format!(
250 "No processes found for '{}'",
251 self.target.as_ref().unwrap()
252 ));
253 return Ok(());
254 }
255
256 println!(
257 "{} Process tree for '{}':\n",
258 "✓".green().bold(),
259 self.target.as_ref().unwrap().cyan()
260 );
261
262 for proc in &filtered {
263 self.print_tree(proc, &children_map, "", true, 0);
264 println!();
265 }
266 } else if has_filters {
267 let filtered: Vec<_> = all_processes
268 .iter()
269 .filter(|p| matches_filters(p))
270 .collect();
271 if filtered.is_empty() {
272 printer.warning("No processes match the specified filters");
273 return Ok(());
274 }
275
276 println!(
277 "{} {} process{} matching filters:\n",
278 "✓".green().bold(),
279 filtered.len().to_string().cyan().bold(),
280 if filtered.len() == 1 { "" } else { "es" }
281 );
282
283 for (i, proc) in filtered.iter().enumerate() {
284 let is_last = i == filtered.len() - 1;
285 self.print_tree(proc, &children_map, "", is_last, 0);
286 }
287 } else {
288 println!("{} Process tree:\n", "✓".green().bold());
289
290 let display_roots: Vec<&Process> = all_processes
292 .iter()
293 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
294 .collect();
295
296 for (i, proc) in display_roots.iter().enumerate() {
297 let is_last = i == display_roots.len() - 1;
298 self.print_tree(proc, &children_map, "", is_last, 0);
299 }
300 }
301
302 Ok(())
303 }
304
305 fn print_tree(
306 &self,
307 proc: &Process,
308 children_map: &HashMap<u32, Vec<&Process>>,
309 prefix: &str,
310 is_last: bool,
311 depth: usize,
312 ) {
313 if depth > self.depth {
314 return;
315 }
316
317 let connector = if is_last { "└── " } else { "├── " };
318
319 if self.compact {
320 println!(
321 "{}{}{}",
322 prefix.bright_black(),
323 connector.bright_black(),
324 proc.pid.to_string().cyan()
325 );
326 } else {
327 let status_indicator = match proc.status {
328 crate::core::ProcessStatus::Running => "●".green(),
329 crate::core::ProcessStatus::Sleeping => "○".blue(),
330 crate::core::ProcessStatus::Stopped => "◐".yellow(),
331 crate::core::ProcessStatus::Zombie => "✗".red(),
332 _ => "?".white(),
333 };
334
335 println!(
336 "{}{}{} {} [{}] {:.1}% {}",
337 prefix.bright_black(),
338 connector.bright_black(),
339 status_indicator,
340 proc.name.white().bold(),
341 proc.pid.to_string().cyan(),
342 proc.cpu_percent,
343 format_memory(proc.memory_mb)
344 );
345 }
346
347 let child_prefix = if is_last {
348 format!("{} ", prefix)
349 } else {
350 format!("{}│ ", prefix)
351 };
352
353 if let Some(children) = children_map.get(&proc.pid) {
354 let mut sorted_children: Vec<&&Process> = children.iter().collect();
355 sorted_children.sort_by_key(|p| p.pid);
356
357 for (i, child) in sorted_children.iter().enumerate() {
358 let child_is_last = i == sorted_children.len() - 1;
359 self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
360 }
361 }
362 }
363
364 fn build_tree_node(
365 &self,
366 proc: &Process,
367 children_map: &HashMap<u32, Vec<&Process>>,
368 depth: usize,
369 ) -> TreeNode {
370 let children = if depth < self.depth {
371 children_map
372 .get(&proc.pid)
373 .map(|kids| {
374 kids.iter()
375 .map(|p| self.build_tree_node(p, children_map, depth + 1))
376 .collect()
377 })
378 .unwrap_or_default()
379 } else {
380 Vec::new()
381 };
382
383 TreeNode {
384 pid: proc.pid,
385 name: proc.name.clone(),
386 cpu_percent: proc.cpu_percent,
387 memory_mb: proc.memory_mb,
388 status: format!("{:?}", proc.status),
389 children,
390 }
391 }
392
393 fn show_ancestors(&self, printer: &Printer, pid_map: &HashMap<u32, &Process>) -> Result<()> {
395 use crate::core::{parse_target, resolve_target, TargetType};
396
397 let target = match &self.target {
398 Some(t) => t,
399 None => {
400 printer.warning("--ancestors requires a target (PID, :port, or name)");
401 return Ok(());
402 }
403 };
404
405 let target_processes = match parse_target(target) {
407 TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target)?,
408 TargetType::Name(ref pattern) => {
409 let pattern_lower = pattern.to_lowercase();
410 let self_pid = std::process::id();
411 pid_map
412 .values()
413 .filter(|p| {
414 p.pid != self_pid
415 && (p.name.to_lowercase().contains(&pattern_lower)
416 || p.command
417 .as_ref()
418 .map(|c| c.to_lowercase().contains(&pattern_lower))
419 .unwrap_or(false))
420 })
421 .map(|p| (*p).clone())
422 .collect()
423 }
424 };
425
426 if target_processes.is_empty() {
427 printer.warning(&format!("No process found for '{}'", target));
428 return Ok(());
429 }
430
431 if self.json {
432 let ancestry_output: Vec<AncestryNode> = target_processes
433 .iter()
434 .map(|proc| self.build_ancestry_node(proc, pid_map))
435 .collect();
436 printer.print_json(&AncestryOutput {
437 action: "ancestry",
438 success: true,
439 ancestry: ancestry_output,
440 });
441 } else {
442 println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
443
444 for proc in &target_processes {
445 self.print_ancestry(proc, pid_map);
446 println!();
447 }
448 }
449
450 Ok(())
451 }
452
453 fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
455 let mut chain: Vec<&Process> = 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(proc);
462 current_pid = proc.parent_pid;
463 if chain.len() > 100 {
465 break;
466 }
467 } else {
468 break;
469 }
470 }
471
472 chain.reverse();
474
475 for (i, proc) in chain.iter().enumerate() {
477 let is_target = proc.pid == target.pid;
478 let indent = " ".repeat(i);
479 let connector = if i == 0 { "" } else { "└── " };
480
481 let status_indicator = match proc.status {
482 ProcessStatus::Running => "●".green(),
483 ProcessStatus::Sleeping => "○".blue(),
484 ProcessStatus::Stopped => "◐".yellow(),
485 ProcessStatus::Zombie => "✗".red(),
486 _ => "?".white(),
487 };
488
489 if is_target {
490 println!(
492 "{}{}{} {} [{}] {:.1}% {} {}",
493 indent.bright_black(),
494 connector.bright_black(),
495 status_indicator,
496 proc.name.cyan().bold(),
497 proc.pid.to_string().cyan().bold(),
498 proc.cpu_percent,
499 format_memory(proc.memory_mb),
500 "← target".yellow()
501 );
502 } else {
503 println!(
504 "{}{}{} {} [{}] {:.1}% {}",
505 indent.bright_black(),
506 connector.bright_black(),
507 status_indicator,
508 proc.name.white(),
509 proc.pid.to_string().cyan(),
510 proc.cpu_percent,
511 format_memory(proc.memory_mb)
512 );
513 }
514 }
515 }
516
517 fn build_ancestry_node(
519 &self,
520 target: &Process,
521 pid_map: &HashMap<u32, &Process>,
522 ) -> AncestryNode {
523 let mut chain: Vec<ProcessInfo> = Vec::new();
524 let mut current_pid = Some(target.pid);
525
526 while let Some(pid) = current_pid {
527 if let Some(proc) = pid_map.get(&pid) {
528 chain.push(ProcessInfo {
529 pid: proc.pid,
530 name: proc.name.clone(),
531 cpu_percent: proc.cpu_percent,
532 memory_mb: proc.memory_mb,
533 status: format!("{:?}", proc.status),
534 });
535 current_pid = proc.parent_pid;
536 if chain.len() > 100 {
537 break;
538 }
539 } else {
540 break;
541 }
542 }
543
544 chain.reverse();
545
546 AncestryNode {
547 target_pid: target.pid,
548 target_name: target.name.clone(),
549 depth: chain.len(),
550 chain,
551 }
552 }
553}
554
555#[derive(Serialize)]
556struct AncestryOutput {
557 action: &'static str,
558 success: bool,
559 ancestry: Vec<AncestryNode>,
560}
561
562#[derive(Serialize)]
563struct AncestryNode {
564 target_pid: u32,
565 target_name: String,
566 depth: usize,
567 chain: Vec<ProcessInfo>,
568}
569
570#[derive(Serialize)]
571struct ProcessInfo {
572 pid: u32,
573 name: String,
574 cpu_percent: f32,
575 memory_mb: f64,
576 status: String,
577}
578
579#[derive(Serialize)]
580struct TreeOutput {
581 action: &'static str,
582 success: bool,
583 tree: Vec<TreeNode>,
584}
585
586#[derive(Serialize)]
587struct TreeNode {
588 pid: u32,
589 name: String,
590 cpu_percent: f32,
591 memory_mb: f64,
592 status: String,
593 children: Vec<TreeNode>,
594}