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();
125 let self_pid = std::process::id();
126 let parent_pid = all_processes
127 .iter()
128 .find(|p| p.pid == self_pid)
129 .and_then(|p| p.parent_pid);
130 all_processes
131 .iter()
132 .filter(|p| {
133 p.pid != self_pid
134 && Some(p.pid) != parent_pid
135 && (p.name.to_lowercase().contains(&pattern_lower)
136 || p.command
137 .as_ref()
138 .map(|c| c.to_lowercase().contains(&pattern_lower))
139 .unwrap_or(false))
140 })
141 .collect()
142 }
143 }
144 } else {
145 Vec::new() };
147
148 let target_processes = if self.target.is_some() {
150 let in_dir_filter = resolve_in_dir(&self.in_dir);
151 target_processes
152 .into_iter()
153 .filter(|p| {
154 if let Some(ref dir_path) = in_dir_filter {
155 if let Some(ref cwd) = p.cwd {
156 if !PathBuf::from(cwd).starts_with(dir_path) {
157 return false;
158 }
159 } else {
160 return false;
161 }
162 }
163 if let Some(ref name) = self.by_name {
164 if !p.name.to_lowercase().contains(&name.to_lowercase()) {
165 return false;
166 }
167 }
168 true
169 })
170 .collect()
171 } else {
172 target_processes
173 };
174
175 let matches_filters = |p: &Process| -> bool {
177 if let Some(min_cpu) = self.min_cpu {
178 if p.cpu_percent < min_cpu {
179 return false;
180 }
181 }
182 if let Some(min_mem) = self.min_mem {
183 if p.memory_mb < min_mem {
184 return false;
185 }
186 }
187 if let Some(ref status) = self.status {
188 let status_match = match status.to_lowercase().as_str() {
189 "running" => matches!(p.status, ProcessStatus::Running),
190 "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
191 "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
192 "zombie" => matches!(p.status, ProcessStatus::Zombie),
193 _ => true,
194 };
195 if !status_match {
196 return false;
197 }
198 }
199 if let Some(min_uptime) = self.min_uptime {
200 if let Some(start_time) = p.start_time {
201 let now = std::time::SystemTime::now()
202 .duration_since(std::time::UNIX_EPOCH)
203 .map(|d| d.as_secs())
204 .unwrap_or(0);
205 if now.saturating_sub(start_time) < min_uptime {
206 return false;
207 }
208 } else {
209 return false;
210 }
211 }
212 true
213 };
214
215 let has_filters = self.min_cpu.is_some()
217 || self.min_mem.is_some()
218 || self.status.is_some()
219 || self.min_uptime.is_some();
220
221 if self.json {
222 let tree_nodes = if self.target.is_some() {
223 target_processes
224 .iter()
225 .filter(|p| matches_filters(p))
226 .map(|p| self.build_tree_node(p, &children_map, 0))
227 .collect()
228 } else if has_filters {
229 all_processes
231 .iter()
232 .filter(|p| matches_filters(p))
233 .map(|p| self.build_tree_node(p, &children_map, 0))
234 .collect()
235 } else {
236 all_processes
238 .iter()
239 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
240 .map(|p| self.build_tree_node(p, &children_map, 0))
241 .collect()
242 };
243
244 printer.print_json(&TreeOutput {
245 action: "tree",
246 success: true,
247 tree: tree_nodes,
248 });
249 } else if self.target.is_some() {
250 let filtered: Vec<_> = target_processes
251 .into_iter()
252 .filter(|p| matches_filters(p))
253 .collect();
254 if filtered.is_empty() {
255 printer.warning(&format!(
256 "No processes found for '{}'",
257 self.target.as_ref().unwrap()
258 ));
259 return Ok(());
260 }
261
262 println!(
263 "{} Process tree for '{}':\n",
264 "✓".green().bold(),
265 self.target.as_ref().unwrap().cyan()
266 );
267
268 for proc in &filtered {
269 self.print_tree(proc, &children_map, "", true, 0);
270 println!();
271 }
272 } else if has_filters {
273 let filtered: Vec<_> = all_processes
274 .iter()
275 .filter(|p| matches_filters(p))
276 .collect();
277 if filtered.is_empty() {
278 printer.warning("No processes match the specified filters");
279 return Ok(());
280 }
281
282 println!(
283 "{} {} process{} matching filters:\n",
284 "✓".green().bold(),
285 filtered.len().to_string().cyan().bold(),
286 if filtered.len() == 1 { "" } else { "es" }
287 );
288
289 for (i, proc) in filtered.iter().enumerate() {
290 let is_last = i == filtered.len() - 1;
291 self.print_tree(proc, &children_map, "", is_last, 0);
292 }
293 } else {
294 println!("{} Process tree:\n", "✓".green().bold());
295
296 let display_roots: Vec<&Process> = all_processes
298 .iter()
299 .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
300 .collect();
301
302 for (i, proc) in display_roots.iter().enumerate() {
303 let is_last = i == display_roots.len() - 1;
304 self.print_tree(proc, &children_map, "", is_last, 0);
305 }
306 }
307
308 Ok(())
309 }
310
311 fn print_tree(
312 &self,
313 proc: &Process,
314 children_map: &HashMap<u32, Vec<&Process>>,
315 prefix: &str,
316 is_last: bool,
317 depth: usize,
318 ) {
319 if depth > self.depth {
320 return;
321 }
322
323 let connector = if is_last { "└── " } else { "├── " };
324
325 if self.compact {
326 println!(
327 "{}{}{}",
328 prefix.bright_black(),
329 connector.bright_black(),
330 proc.pid.to_string().cyan()
331 );
332 } else {
333 let status_indicator = match proc.status {
334 crate::core::ProcessStatus::Running => "●".green(),
335 crate::core::ProcessStatus::Sleeping => "○".blue(),
336 crate::core::ProcessStatus::Stopped => "◐".yellow(),
337 crate::core::ProcessStatus::Zombie => "✗".red(),
338 _ => "?".white(),
339 };
340
341 println!(
342 "{}{}{} {} [{}] {:.1}% {}",
343 prefix.bright_black(),
344 connector.bright_black(),
345 status_indicator,
346 proc.name.white().bold(),
347 proc.pid.to_string().cyan(),
348 proc.cpu_percent,
349 format_memory(proc.memory_mb)
350 );
351 }
352
353 let child_prefix = if is_last {
354 format!("{} ", prefix)
355 } else {
356 format!("{}│ ", prefix)
357 };
358
359 if let Some(children) = children_map.get(&proc.pid) {
360 let mut sorted_children: Vec<&&Process> = children.iter().collect();
361 sorted_children.sort_by_key(|p| p.pid);
362
363 for (i, child) in sorted_children.iter().enumerate() {
364 let child_is_last = i == sorted_children.len() - 1;
365 self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
366 }
367 }
368 }
369
370 fn build_tree_node(
371 &self,
372 proc: &Process,
373 children_map: &HashMap<u32, Vec<&Process>>,
374 depth: usize,
375 ) -> TreeNode {
376 let children = if depth < self.depth {
377 children_map
378 .get(&proc.pid)
379 .map(|kids| {
380 kids.iter()
381 .map(|p| self.build_tree_node(p, children_map, depth + 1))
382 .collect()
383 })
384 .unwrap_or_default()
385 } else {
386 Vec::new()
387 };
388
389 TreeNode {
390 pid: proc.pid,
391 name: proc.name.clone(),
392 cpu_percent: proc.cpu_percent,
393 memory_mb: proc.memory_mb,
394 status: format!("{:?}", proc.status),
395 children,
396 }
397 }
398
399 fn show_ancestors(&self, printer: &Printer, pid_map: &HashMap<u32, &Process>) -> Result<()> {
401 use crate::core::{parse_target, resolve_target, TargetType};
402
403 let target = match &self.target {
404 Some(t) => t,
405 None => {
406 printer.warning("--ancestors requires a target (PID, :port, or name)");
407 return Ok(());
408 }
409 };
410
411 let target_processes = match parse_target(target) {
413 TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target)?,
414 TargetType::Name(ref pattern) => {
415 let pattern_lower = pattern.to_lowercase();
416 let self_pid = std::process::id();
417 let parent_pid = pid_map.get(&self_pid).and_then(|p| p.parent_pid);
418 pid_map
419 .values()
420 .filter(|p| {
421 p.pid != self_pid
422 && Some(p.pid) != parent_pid
423 && (p.name.to_lowercase().contains(&pattern_lower)
424 || p.command
425 .as_ref()
426 .map(|c| c.to_lowercase().contains(&pattern_lower))
427 .unwrap_or(false))
428 })
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: "ancestry",
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}