Skip to main content

proc_cli/commands/
info.rs

1//! Info command - Get detailed process information
2//!
3//! Usage:
4//!   proc info 1234              # Info for PID
5//!   proc info :3000             # Info for process on port 3000
6//!   proc info node              # Info for processes named node
7//!   proc info :3000,:8080       # Info for multiple targets
8//!   proc info :3000,1234,node   # Mixed targets (port + PID + name)
9
10use crate::core::{apply_filters, parse_targets, resolve_target, Process};
11use crate::error::{ExitCode, ProcError, Result};
12use crate::ui::{colorize_status, format_duration, format_memory, Printer};
13use clap::Args;
14use colored::*;
15use serde::Serialize;
16
17/// Show detailed process information
18#[derive(Args, Debug)]
19pub struct InfoCommand {
20    /// Target(s): PID, :port, or name (comma-separated for multiple)
21    #[arg(required = true)]
22    targets: Vec<String>,
23
24    /// Output as JSON
25    #[arg(long, short = 'j')]
26    json: bool,
27
28    /// Show extra details
29    #[arg(long, short = 'v')]
30    verbose: bool,
31
32    /// Filter by directory (defaults to current directory if no path given)
33    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
34    pub in_dir: Option<String>,
35
36    /// Filter by process name
37    #[arg(long = "by", short = 'b')]
38    pub by_name: Option<String>,
39}
40
41impl InfoCommand {
42    /// Executes the info command, displaying detailed process information.
43    pub fn execute(&self) -> Result<()> {
44        let printer = Printer::from_flags(self.json, self.verbose);
45
46        // Flatten targets - support both space-separated and comma-separated
47        let all_targets: Vec<String> = self.targets.iter().flat_map(|t| parse_targets(t)).collect();
48
49        let mut found = Vec::new();
50        let mut not_found = Vec::new();
51        let mut seen_pids = std::collections::HashSet::new();
52
53        for target in &all_targets {
54            match resolve_target(target) {
55                Ok(processes) => {
56                    if processes.is_empty() {
57                        not_found.push(target.clone());
58                    } else {
59                        for proc in processes {
60                            // Deduplicate by PID
61                            if seen_pids.insert(proc.pid) {
62                                found.push(proc);
63                            }
64                        }
65                    }
66                }
67                Err(_) => not_found.push(target.clone()),
68            }
69        }
70
71        // Apply --in and --by filters
72        apply_filters(&mut found, &self.in_dir, &self.by_name);
73
74        if self.json {
75            printer.print_json(&InfoOutput {
76                action: "info",
77                success: !found.is_empty(),
78                found_count: found.len(),
79                not_found_count: not_found.len(),
80                processes: &found,
81                not_found: &not_found,
82            });
83        } else {
84            for proc in &found {
85                self.print_process_info(proc);
86            }
87
88            if !not_found.is_empty() {
89                printer.warning(&format!("Not found: {}", not_found.join(", ")));
90            }
91        }
92
93        if found.is_empty() {
94            return Err(ProcError::reported(
95                ExitCode::NotFound,
96                format!("No process found matching '{}'", self.targets.join(" ")),
97            ));
98        }
99
100        Ok(())
101    }
102
103    fn print_process_info(&self, proc: &Process) {
104        println!(
105            "{} Process {}",
106            "✓".green().bold(),
107            proc.pid.to_string().cyan().bold()
108        );
109        println!();
110        println!("  {} {}", "Name:".bright_black(), proc.name.white().bold());
111        println!(
112            "  {} {}",
113            "PID:".bright_black(),
114            proc.pid.to_string().cyan()
115        );
116
117        if let Some(ref cwd) = proc.cwd {
118            println!("  {} {}", "Directory:".bright_black(), cwd);
119        }
120
121        if let Some(ref path) = proc.exe_path {
122            println!("  {} {}", "Path:".bright_black(), path);
123        }
124
125        if let Some(ref user) = proc.user {
126            println!("  {} {}", "User:".bright_black(), user);
127        }
128
129        if let Some(ppid) = proc.parent_pid {
130            println!(
131                "  {} {}",
132                "Parent PID:".bright_black(),
133                ppid.to_string().cyan()
134            );
135        }
136
137        let status_str = format!("{:?}", proc.status);
138        let status_colored = colorize_status(&proc.status, &status_str);
139        println!("  {} {}", "Status:".bright_black(), status_colored);
140
141        println!("  {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
142        println!(
143            "  {} {}",
144            "Memory:".bright_black(),
145            format_memory(proc.memory_mb)
146        );
147
148        if let Some(start_time) = proc.start_time {
149            let duration = std::time::SystemTime::now()
150                .duration_since(std::time::UNIX_EPOCH)
151                .map(|d| d.as_secs().saturating_sub(start_time))
152                .unwrap_or(0);
153
154            let uptime = format_duration(duration);
155            println!("  {} {}", "Uptime:".bright_black(), uptime);
156        }
157
158        if self.verbose {
159            if let Some(ref cmd) = proc.command {
160                println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
161            }
162        }
163
164        println!();
165    }
166}
167
168#[derive(Serialize)]
169struct InfoOutput<'a> {
170    action: &'static str,
171    success: bool,
172    found_count: usize,
173    not_found_count: usize,
174    processes: &'a [Process],
175    not_found: &'a [String],
176}