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::{parse_targets, resolve_target, Process, ProcessStatus};
11use crate::error::Result;
12use crate::ui::{OutputFormat, Printer};
13use clap::Args;
14use colored::*;
15use serde::Serialize;
16use std::path::PathBuf;
17
18/// Show detailed process information
19#[derive(Args, Debug)]
20pub struct InfoCommand {
21    /// Target(s): PID, :port, or name (comma-separated for multiple)
22    #[arg(required = true)]
23    targets: Vec<String>,
24
25    /// Output as JSON
26    #[arg(long, short)]
27    json: bool,
28
29    /// Show extra details
30    #[arg(long, short)]
31    verbose: bool,
32
33    /// Filter by directory (defaults to current directory if no path given)
34    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
35    pub in_dir: Option<String>,
36
37    /// Filter by process name
38    #[arg(long = "by", short = 'b')]
39    pub by_name: Option<String>,
40}
41
42impl InfoCommand {
43    /// Executes the info command, displaying detailed process information.
44    pub fn execute(&self) -> Result<()> {
45        let format = if self.json {
46            OutputFormat::Json
47        } else {
48            OutputFormat::Human
49        };
50        let printer = Printer::new(format, self.verbose);
51
52        // Flatten targets - support both space-separated and comma-separated
53        let all_targets: Vec<String> = self.targets.iter().flat_map(|t| parse_targets(t)).collect();
54
55        let mut found = Vec::new();
56        let mut not_found = Vec::new();
57        let mut seen_pids = std::collections::HashSet::new();
58
59        for target in &all_targets {
60            match resolve_target(target) {
61                Ok(processes) => {
62                    if processes.is_empty() {
63                        not_found.push(target.clone());
64                    } else {
65                        for proc in processes {
66                            // Deduplicate by PID
67                            if seen_pids.insert(proc.pid) {
68                                found.push(proc);
69                            }
70                        }
71                    }
72                }
73                Err(_) => not_found.push(target.clone()),
74            }
75        }
76
77        // Apply --in and --by filters
78        let in_dir_filter = resolve_in_dir(&self.in_dir);
79        found.retain(|p| {
80            if let Some(ref dir_path) = in_dir_filter {
81                if let Some(ref cwd) = p.cwd {
82                    if !PathBuf::from(cwd).starts_with(dir_path) {
83                        return false;
84                    }
85                } else {
86                    return false;
87                }
88            }
89            if let Some(ref name) = self.by_name {
90                if !p.name.to_lowercase().contains(&name.to_lowercase()) {
91                    return false;
92                }
93            }
94            true
95        });
96
97        if self.json {
98            printer.print_json(&InfoOutput {
99                action: "info",
100                success: !found.is_empty(),
101                found_count: found.len(),
102                not_found_count: not_found.len(),
103                processes: &found,
104                not_found: &not_found,
105            });
106        } else {
107            for proc in &found {
108                self.print_process_info(proc);
109            }
110
111            if !not_found.is_empty() {
112                for target in &not_found {
113                    printer.warning(&format!("Target '{}' not found", target));
114                }
115            }
116        }
117
118        Ok(())
119    }
120
121    fn print_process_info(&self, proc: &Process) {
122        println!(
123            "{} Process {}",
124            "✓".green().bold(),
125            proc.pid.to_string().cyan().bold()
126        );
127        println!();
128        println!("  {} {}", "Name:".bright_black(), proc.name.white().bold());
129        println!(
130            "  {} {}",
131            "PID:".bright_black(),
132            proc.pid.to_string().cyan()
133        );
134
135        if let Some(ref path) = proc.exe_path {
136            println!("  {} {}", "Path:".bright_black(), path);
137        }
138
139        if let Some(ref user) = proc.user {
140            println!("  {} {}", "User:".bright_black(), user);
141        }
142
143        if let Some(ppid) = proc.parent_pid {
144            println!(
145                "  {} {}",
146                "Parent PID:".bright_black(),
147                ppid.to_string().cyan()
148            );
149        }
150
151        let status_str = format!("{:?}", proc.status);
152        let status_colored = match proc.status {
153            ProcessStatus::Running => status_str.green(),
154            ProcessStatus::Sleeping => status_str.blue(),
155            ProcessStatus::Stopped => status_str.yellow(),
156            ProcessStatus::Zombie => status_str.red(),
157            _ => status_str.white(),
158        };
159        println!("  {} {}", "Status:".bright_black(), status_colored);
160
161        println!("  {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
162        println!("  {} {:.1} MB", "Memory:".bright_black(), proc.memory_mb);
163
164        if let Some(start_time) = proc.start_time {
165            let duration = std::time::SystemTime::now()
166                .duration_since(std::time::UNIX_EPOCH)
167                .map(|d| d.as_secs().saturating_sub(start_time))
168                .unwrap_or(0);
169
170            let uptime = format_duration(duration);
171            println!("  {} {}", "Uptime:".bright_black(), uptime);
172        }
173
174        if self.verbose {
175            if let Some(ref cmd) = proc.command {
176                println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
177            }
178        }
179
180        println!();
181    }
182}
183
184fn format_duration(secs: u64) -> String {
185    if secs < 60 {
186        format!("{}s", secs)
187    } else if secs < 3600 {
188        format!("{}m {}s", secs / 60, secs % 60)
189    } else if secs < 86400 {
190        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
191    } else {
192        format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
193    }
194}
195
196#[derive(Serialize)]
197struct InfoOutput<'a> {
198    action: &'static str,
199    success: bool,
200    found_count: usize,
201    not_found_count: usize,
202    processes: &'a [Process],
203    not_found: &'a [String],
204}
205
206fn resolve_in_dir(in_dir: &Option<String>) -> Option<PathBuf> {
207    in_dir.as_ref().map(|p| {
208        if p == "." {
209            std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
210        } else {
211            let path = PathBuf::from(p);
212            if path.is_relative() {
213                std::env::current_dir()
214                    .unwrap_or_else(|_| PathBuf::from("."))
215                    .join(path)
216            } else {
217                path
218            }
219        }
220    })
221}