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