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;
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)]
26    json: bool,
27
28    /// Show extra details
29    #[arg(long, short)]
30    verbose: bool,
31}
32
33impl InfoCommand {
34    /// Executes the info command, displaying detailed process information.
35    pub fn execute(&self) -> Result<()> {
36        let format = if self.json {
37            OutputFormat::Json
38        } else {
39            OutputFormat::Human
40        };
41        let printer = Printer::new(format, self.verbose);
42
43        // Flatten targets - support both space-separated and comma-separated
44        let all_targets: Vec<String> = self.targets.iter().flat_map(|t| parse_targets(t)).collect();
45
46        let mut found = Vec::new();
47        let mut not_found = Vec::new();
48        let mut seen_pids = std::collections::HashSet::new();
49
50        for target in &all_targets {
51            match resolve_target(target) {
52                Ok(processes) => {
53                    if processes.is_empty() {
54                        not_found.push(target.clone());
55                    } else {
56                        for proc in processes {
57                            // Deduplicate by PID
58                            if seen_pids.insert(proc.pid) {
59                                found.push(proc);
60                            }
61                        }
62                    }
63                }
64                Err(_) => not_found.push(target.clone()),
65            }
66        }
67
68        if self.json {
69            printer.print_json(&InfoOutput {
70                action: "info",
71                success: !found.is_empty(),
72                found_count: found.len(),
73                not_found_count: not_found.len(),
74                processes: &found,
75                not_found: &not_found,
76            });
77        } else {
78            for proc in &found {
79                self.print_process_info(proc);
80            }
81
82            if !not_found.is_empty() {
83                for target in &not_found {
84                    printer.warning(&format!("Target '{}' not found", target));
85                }
86            }
87        }
88
89        Ok(())
90    }
91
92    fn print_process_info(&self, proc: &Process) {
93        println!(
94            "{} Process {}",
95            "✓".green().bold(),
96            proc.pid.to_string().cyan().bold()
97        );
98        println!();
99        println!("  {} {}", "Name:".bright_black(), proc.name.white().bold());
100        println!(
101            "  {} {}",
102            "PID:".bright_black(),
103            proc.pid.to_string().cyan()
104        );
105
106        if let Some(ref path) = proc.exe_path {
107            println!("  {} {}", "Path:".bright_black(), path);
108        }
109
110        if let Some(ref user) = proc.user {
111            println!("  {} {}", "User:".bright_black(), user);
112        }
113
114        if let Some(ppid) = proc.parent_pid {
115            println!(
116                "  {} {}",
117                "Parent PID:".bright_black(),
118                ppid.to_string().cyan()
119            );
120        }
121
122        let status_str = format!("{:?}", proc.status);
123        let status_colored = match proc.status {
124            ProcessStatus::Running => status_str.green(),
125            ProcessStatus::Sleeping => status_str.blue(),
126            ProcessStatus::Stopped => status_str.yellow(),
127            ProcessStatus::Zombie => status_str.red(),
128            _ => status_str.white(),
129        };
130        println!("  {} {}", "Status:".bright_black(), status_colored);
131
132        println!("  {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
133        println!("  {} {:.1} MB", "Memory:".bright_black(), proc.memory_mb);
134
135        if let Some(start_time) = proc.start_time {
136            let duration = std::time::SystemTime::now()
137                .duration_since(std::time::UNIX_EPOCH)
138                .map(|d| d.as_secs().saturating_sub(start_time))
139                .unwrap_or(0);
140
141            let uptime = format_duration(duration);
142            println!("  {} {}", "Uptime:".bright_black(), uptime);
143        }
144
145        if self.verbose {
146            if let Some(ref cmd) = proc.command {
147                println!("  {} {}", "Command:".bright_black(), cmd.bright_black());
148            }
149        }
150
151        println!();
152    }
153}
154
155fn format_duration(secs: u64) -> String {
156    if secs < 60 {
157        format!("{}s", secs)
158    } else if secs < 3600 {
159        format!("{}m {}s", secs / 60, secs % 60)
160    } else if secs < 86400 {
161        format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
162    } else {
163        format!("{}d {}h", secs / 86400, (secs % 86400) / 3600)
164    }
165}
166
167#[derive(Serialize)]
168struct InfoOutput<'a> {
169    action: &'static str,
170    success: bool,
171    found_count: usize,
172    not_found_count: usize,
173    processes: &'a [Process],
174    not_found: &'a [String],
175}