proc_cli/ui/
output.rs

1//! Output formatting for proc CLI
2//!
3//! Provides colored terminal output and JSON formatting.
4
5use crate::core::{PortInfo, Process};
6use colored::*;
7use serde::Serialize;
8
9/// Output format selection
10#[derive(Debug, Clone, Copy, Default)]
11pub enum OutputFormat {
12    #[default]
13    Human,
14    Json,
15}
16
17/// Main printer for CLI output
18pub struct Printer {
19    format: OutputFormat,
20    verbose: bool,
21}
22
23impl Printer {
24    pub fn new(format: OutputFormat, verbose: bool) -> Self {
25        Self { format, verbose }
26    }
27
28    /// Print a success message
29    pub fn success(&self, message: &str) {
30        match self.format {
31            OutputFormat::Human => {
32                println!("{} {}", "✓".green().bold(), message.green());
33            }
34            OutputFormat::Json => {
35                // JSON output handled separately
36            }
37        }
38    }
39
40    /// Print an error message
41    pub fn error(&self, message: &str) {
42        match self.format {
43            OutputFormat::Human => {
44                eprintln!("{} {}", "✗".red().bold(), message.red());
45            }
46            OutputFormat::Json => {
47                // JSON output handled separately
48            }
49        }
50    }
51
52    /// Print a warning message
53    pub fn warning(&self, message: &str) {
54        match self.format {
55            OutputFormat::Human => {
56                println!("{} {}", "⚠".yellow().bold(), message.yellow());
57            }
58            OutputFormat::Json => {
59                // JSON output handled separately
60            }
61        }
62    }
63
64    /// Print a list of processes with optional context (e.g., "in /path/to/dir")
65    pub fn print_processes_with_context(&self, processes: &[Process], context: Option<&str>) {
66        match self.format {
67            OutputFormat::Human => self.print_processes_human(processes, context),
68            OutputFormat::Json => self.print_json(&ProcessListOutput {
69                action: "find",
70                success: true,
71                count: processes.len(),
72                processes,
73            }),
74        }
75    }
76
77    /// Print a list of processes
78    pub fn print_processes(&self, processes: &[Process]) {
79        self.print_processes_with_context(processes, None)
80    }
81
82    fn print_processes_human(&self, processes: &[Process], context: Option<&str>) {
83        if processes.is_empty() {
84            let msg = match context {
85                Some(ctx) => format!("No processes found {}", ctx),
86                None => "No processes found".to_string(),
87            };
88            self.warning(&msg);
89            return;
90        }
91
92        let context_str = context.map(|c| format!(" {}", c)).unwrap_or_default();
93        println!(
94            "{} Found {} process{}{}",
95            "✓".green().bold(),
96            processes.len().to_string().cyan().bold(),
97            if processes.len() == 1 { "" } else { "es" },
98            context_str.bright_black()
99        );
100        println!();
101
102        if self.verbose {
103            // Verbose: full details, nothing truncated
104            for proc in processes {
105                let status_str = format!("{:?}", proc.status);
106                let status_colored = colorize_status(&proc.status, &status_str);
107
108                println!(
109                    "{} {} {}  {:.1}% CPU  {:.1} MB  {}",
110                    proc.pid.to_string().cyan().bold(),
111                    proc.name.white().bold(),
112                    format!("[{}]", status_colored).bright_black(),
113                    proc.cpu_percent,
114                    proc.memory_mb,
115                    proc.user.as_deref().unwrap_or("-").bright_black()
116                );
117
118                if let Some(ref cmd) = proc.command {
119                    println!("    {} {}", "cmd:".bright_black(), cmd);
120                }
121                if let Some(ref path) = proc.exe_path {
122                    println!("    {} {}", "exe:".bright_black(), path.bright_black());
123                }
124                if let Some(ref cwd) = proc.cwd {
125                    println!("    {} {}", "cwd:".bright_black(), cwd.bright_black());
126                }
127                if let Some(ppid) = proc.parent_pid {
128                    println!(
129                        "    {} {}",
130                        "parent:".bright_black(),
131                        ppid.to_string().bright_black()
132                    );
133                }
134                println!();
135            }
136        } else {
137            // Normal: compact table - PATH (directory) before NAME
138            println!(
139                "{:<8} {:<35} {:<24} {:>6} {:>8} {:>10}",
140                "PID".bright_blue().bold(),
141                "PATH".bright_blue().bold(),
142                "NAME".bright_blue().bold(),
143                "CPU%".bright_blue().bold(),
144                "MEM".bright_blue().bold(),
145                "STATUS".bright_blue().bold(),
146            );
147            println!("{}", "─".repeat(95).bright_black());
148
149            for proc in processes {
150                let name = truncate_string(&proc.name, 23);
151                let status_str = format!("{:?}", proc.status);
152                let status_colored = colorize_status(&proc.status, &status_str);
153
154                // Show directory only (not the executable name - that's redundant with NAME)
155                let dir_display = proc
156                    .exe_path
157                    .as_ref()
158                    .map(|p| {
159                        // Get parent directory
160                        std::path::Path::new(p)
161                            .parent()
162                            .map(|parent| truncate_path(&parent.to_string_lossy(), 34))
163                            .unwrap_or_else(|| "-".to_string())
164                    })
165                    .unwrap_or_else(|| "-".to_string());
166
167                println!(
168                    "{:<8} {:<35} {:<24} {:>6.1} {:>6.1}MB {:>10}",
169                    proc.pid.to_string().cyan(),
170                    dir_display.bright_black(),
171                    name.white(),
172                    proc.cpu_percent,
173                    proc.memory_mb,
174                    status_colored,
175                );
176            }
177        }
178        println!();
179    }
180
181    /// Print port information
182    pub fn print_ports(&self, ports: &[PortInfo]) {
183        match self.format {
184            OutputFormat::Human => self.print_ports_human(ports),
185            OutputFormat::Json => self.print_json(&PortListOutput {
186                action: "ports",
187                success: true,
188                count: ports.len(),
189                ports,
190            }),
191        }
192    }
193
194    fn print_ports_human(&self, ports: &[PortInfo]) {
195        if ports.is_empty() {
196            self.warning("No listening ports found");
197            return;
198        }
199
200        println!(
201            "{} Found {} listening port{}",
202            "✓".green().bold(),
203            ports.len().to_string().cyan().bold(),
204            if ports.len() == 1 { "" } else { "s" }
205        );
206        println!();
207
208        // Header
209        println!(
210            "{:<8} {:<10} {:<8} {:<20} {:<15}",
211            "PORT".bright_blue().bold(),
212            "PROTO".bright_blue().bold(),
213            "PID".bright_blue().bold(),
214            "PROCESS".bright_blue().bold(),
215            "ADDRESS".bright_blue().bold()
216        );
217        println!("{}", "─".repeat(65).bright_black());
218
219        for port in ports {
220            let addr = port.address.as_deref().unwrap_or("*");
221            let proto = format!("{:?}", port.protocol).to_uppercase();
222
223            println!(
224                "{:<8} {:<10} {:<8} {:<20} {:<15}",
225                port.port.to_string().cyan().bold(),
226                proto.white(),
227                port.pid.to_string().cyan(),
228                truncate_string(&port.process_name, 19).white(),
229                addr.bright_black()
230            );
231        }
232        println!();
233    }
234
235    /// Print a single port info (for `proc on :port`)
236    pub fn print_port_info(&self, port_info: &PortInfo) {
237        match self.format {
238            OutputFormat::Human => {
239                println!(
240                    "{} Process on port {}:",
241                    "✓".green().bold(),
242                    port_info.port.to_string().cyan().bold()
243                );
244                println!();
245                println!(
246                    "  {} {}",
247                    "Name:".bright_black(),
248                    port_info.process_name.white().bold()
249                );
250                println!(
251                    "  {} {}",
252                    "PID:".bright_black(),
253                    port_info.pid.to_string().cyan()
254                );
255                println!("  {} {:?}", "Protocol:".bright_black(), port_info.protocol);
256                if let Some(ref addr) = port_info.address {
257                    println!("  {} {}", "Address:".bright_black(), addr);
258                }
259                println!();
260            }
261            OutputFormat::Json => self.print_json(&SinglePortOutput {
262                action: "on",
263                success: true,
264                port: port_info,
265            }),
266        }
267    }
268
269    /// Print JSON output for any serializable type
270    pub fn print_json<T: Serialize>(&self, data: &T) {
271        match serde_json::to_string_pretty(data) {
272            Ok(json) => println!("{}", json),
273            Err(e) => eprintln!("Failed to serialize JSON: {}", e),
274        }
275    }
276
277    /// Print kill confirmation
278    pub fn print_kill_result(&self, killed: &[Process], failed: &[(Process, String)]) {
279        match self.format {
280            OutputFormat::Human => {
281                if !killed.is_empty() {
282                    println!(
283                        "{} Killed {} process{}",
284                        "✓".green().bold(),
285                        killed.len().to_string().cyan().bold(),
286                        if killed.len() == 1 { "" } else { "es" }
287                    );
288                    for proc in killed {
289                        println!(
290                            "  {} {} [PID {}]",
291                            "→".bright_black(),
292                            proc.name.white(),
293                            proc.pid.to_string().cyan()
294                        );
295                    }
296                }
297                if !failed.is_empty() {
298                    println!(
299                        "{} Failed to kill {} process{}",
300                        "✗".red().bold(),
301                        failed.len(),
302                        if failed.len() == 1 { "" } else { "es" }
303                    );
304                    for (proc, err) in failed {
305                        println!(
306                            "  {} {} [PID {}]: {}",
307                            "→".bright_black(),
308                            proc.name.white(),
309                            proc.pid.to_string().cyan(),
310                            err.red()
311                        );
312                    }
313                }
314            }
315            OutputFormat::Json => {
316                self.print_json(&KillOutput {
317                    action: "kill",
318                    success: failed.is_empty(),
319                    killed_count: killed.len(),
320                    failed_count: failed.len(),
321                    killed,
322                    failed: &failed
323                        .iter()
324                        .map(|(p, e)| FailedKill {
325                            process: p,
326                            error: e,
327                        })
328                        .collect::<Vec<_>>(),
329                });
330            }
331        }
332    }
333}
334
335/// Truncate a string to a maximum length
336fn truncate_string(s: &str, max_len: usize) -> String {
337    if s.len() <= max_len {
338        s.to_string()
339    } else {
340        format!("{}...", &s[..max_len.saturating_sub(3)])
341    }
342}
343
344/// Truncate a path intelligently - show the end (most relevant part)
345fn truncate_path(path: &str, max_len: usize) -> String {
346    if path.len() <= max_len {
347        path.to_string()
348    } else {
349        // Show ...ending of path
350        let start = path.len().saturating_sub(max_len.saturating_sub(3));
351        format!("...{}", &path[start..])
352    }
353}
354
355/// Colorize process status
356fn colorize_status(
357    status: &crate::core::ProcessStatus,
358    status_str: &str,
359) -> colored::ColoredString {
360    use colored::*;
361    match status {
362        crate::core::ProcessStatus::Running => status_str.green(),
363        crate::core::ProcessStatus::Sleeping => status_str.blue(),
364        crate::core::ProcessStatus::Stopped => status_str.yellow(),
365        crate::core::ProcessStatus::Zombie => status_str.red(),
366        _ => status_str.white(),
367    }
368}
369
370// JSON output structures
371#[derive(Serialize)]
372struct ProcessListOutput<'a> {
373    action: &'static str,
374    success: bool,
375    count: usize,
376    processes: &'a [Process],
377}
378
379#[derive(Serialize)]
380struct PortListOutput<'a> {
381    action: &'static str,
382    success: bool,
383    count: usize,
384    ports: &'a [PortInfo],
385}
386
387#[derive(Serialize)]
388struct SinglePortOutput<'a> {
389    action: &'static str,
390    success: bool,
391    port: &'a PortInfo,
392}
393
394#[derive(Serialize)]
395struct KillOutput<'a> {
396    action: &'static str,
397    success: bool,
398    killed_count: usize,
399    failed_count: usize,
400    killed: &'a [Process],
401    failed: &'a [FailedKill<'a>],
402}
403
404#[derive(Serialize)]
405struct FailedKill<'a> {
406    process: &'a Process,
407    error: &'a str,
408}
409
410impl Default for Printer {
411    fn default() -> Self {
412        Self::new(OutputFormat::Human, false)
413    }
414}