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