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