1use crate::core::{PortInfo, Process};
6use colored::*;
7use serde::Serialize;
8
9#[derive(Debug, Clone, Copy, Default)]
11pub enum OutputFormat {
12 #[default]
13 Human,
14 Json,
15}
16
17pub 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 pub fn success(&self, message: &str) {
30 match self.format {
31 OutputFormat::Human => {
32 println!("{} {}", "✓".green().bold(), message.green());
33 }
34 OutputFormat::Json => {
35 }
37 }
38 }
39
40 pub fn error(&self, message: &str) {
42 match self.format {
43 OutputFormat::Human => {
44 eprintln!("{} {}", "✗".red().bold(), message.red());
45 }
46 OutputFormat::Json => {
47 }
49 }
50 }
51
52 pub fn warning(&self, message: &str) {
54 match self.format {
55 OutputFormat::Human => {
56 println!("{} {}", "⚠".yellow().bold(), message.yellow());
57 }
58 OutputFormat::Json => {
59 }
61 }
62 }
63
64 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: "list",
70 success: true,
71 count: processes.len(),
72 processes,
73 }),
74 }
75 }
76
77 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 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 println!(
139 "{:<7} {:<20} {:<12} {:<35} {:>5} {:>8} {:>8}",
140 "PID".bright_blue().bold(),
141 "PATH".bright_blue().bold(),
142 "NAME".bright_blue().bold(),
143 "ARGS".bright_blue().bold(),
144 "CPU%".bright_blue().bold(),
145 "MEM".bright_blue().bold(),
146 "STATUS".bright_blue().bold(),
147 );
148 println!("{}", "─".repeat(100).bright_black());
149
150 for proc in processes {
151 let name = truncate_string(&proc.name, 11);
152 let status_str = format!("{:?}", proc.status);
153 let status_colored = colorize_status(&proc.status, &status_str);
154
155 let path_display = proc
157 .exe_path
158 .as_ref()
159 .map(|p| {
160 std::path::Path::new(p)
161 .parent()
162 .map(|parent| truncate_path(&parent.to_string_lossy(), 19))
163 .unwrap_or_else(|| "-".to_string())
164 })
165 .unwrap_or_else(|| "-".to_string());
166
167 let cmd_display = proc
169 .command
170 .as_ref()
171 .map(|c| {
172 let parts: Vec<&str> = c.split_whitespace().collect();
174 if parts.len() > 1 {
175 let args: Vec<String> = parts[1..]
177 .iter()
178 .map(|arg| {
179 if arg.contains('/') && !arg.starts_with('-') {
180 std::path::Path::new(arg)
182 .file_name()
183 .map(|f| f.to_string_lossy().to_string())
184 .unwrap_or_else(|| arg.to_string())
185 } else {
186 arg.to_string()
187 }
188 })
189 .collect();
190 truncate_string(&args.join(" "), 34)
191 } else {
192 truncate_string(c, 34)
193 }
194 })
195 .unwrap_or_else(|| "-".to_string());
196
197 println!(
198 "{:<7} {:<20} {:<12} {:<35} {:>5.1} {:>6.1}MB {:>8}",
199 proc.pid.to_string().cyan(),
200 path_display.bright_black(),
201 name.white(),
202 cmd_display.bright_black(),
203 proc.cpu_percent,
204 proc.memory_mb,
205 status_colored,
206 );
207 }
208 }
209 println!();
210 }
211
212 pub fn print_ports(&self, ports: &[PortInfo]) {
214 match self.format {
215 OutputFormat::Human => self.print_ports_human(ports),
216 OutputFormat::Json => self.print_json(&PortListOutput {
217 action: "ports",
218 success: true,
219 count: ports.len(),
220 ports,
221 }),
222 }
223 }
224
225 fn print_ports_human(&self, ports: &[PortInfo]) {
226 if ports.is_empty() {
227 self.warning("No listening ports found");
228 return;
229 }
230
231 println!(
232 "{} Found {} listening port{}",
233 "✓".green().bold(),
234 ports.len().to_string().cyan().bold(),
235 if ports.len() == 1 { "" } else { "s" }
236 );
237 println!();
238
239 println!(
241 "{:<8} {:<10} {:<8} {:<20} {:<15}",
242 "PORT".bright_blue().bold(),
243 "PROTO".bright_blue().bold(),
244 "PID".bright_blue().bold(),
245 "PROCESS".bright_blue().bold(),
246 "ADDRESS".bright_blue().bold()
247 );
248 println!("{}", "─".repeat(65).bright_black());
249
250 for port in ports {
251 let addr = port.address.as_deref().unwrap_or("*");
252 let proto = format!("{:?}", port.protocol).to_uppercase();
253
254 println!(
255 "{:<8} {:<10} {:<8} {:<20} {:<15}",
256 port.port.to_string().cyan().bold(),
257 proto.white(),
258 port.pid.to_string().cyan(),
259 truncate_string(&port.process_name, 19).white(),
260 addr.bright_black()
261 );
262 }
263 println!();
264 }
265
266 pub fn print_port_info(&self, port_info: &PortInfo) {
268 match self.format {
269 OutputFormat::Human => {
270 println!(
271 "{} Process on port {}:",
272 "✓".green().bold(),
273 port_info.port.to_string().cyan().bold()
274 );
275 println!();
276 println!(
277 " {} {}",
278 "Name:".bright_black(),
279 port_info.process_name.white().bold()
280 );
281 println!(
282 " {} {}",
283 "PID:".bright_black(),
284 port_info.pid.to_string().cyan()
285 );
286 println!(" {} {:?}", "Protocol:".bright_black(), port_info.protocol);
287 if let Some(ref addr) = port_info.address {
288 println!(" {} {}", "Address:".bright_black(), addr);
289 }
290 println!();
291 }
292 OutputFormat::Json => self.print_json(&SinglePortOutput {
293 action: "on",
294 success: true,
295 port: port_info,
296 }),
297 }
298 }
299
300 pub fn print_json<T: Serialize>(&self, data: &T) {
302 match serde_json::to_string_pretty(data) {
303 Ok(json) => println!("{}", json),
304 Err(e) => eprintln!("Failed to serialize JSON: {}", e),
305 }
306 }
307
308 pub fn print_kill_result(&self, killed: &[Process], failed: &[(Process, String)]) {
310 match self.format {
311 OutputFormat::Human => {
312 if !killed.is_empty() {
313 println!(
314 "{} Killed {} process{}",
315 "✓".green().bold(),
316 killed.len().to_string().cyan().bold(),
317 if killed.len() == 1 { "" } else { "es" }
318 );
319 for proc in killed {
320 println!(
321 " {} {} [PID {}]",
322 "→".bright_black(),
323 proc.name.white(),
324 proc.pid.to_string().cyan()
325 );
326 }
327 }
328 if !failed.is_empty() {
329 println!(
330 "{} Failed to kill {} process{}",
331 "✗".red().bold(),
332 failed.len(),
333 if failed.len() == 1 { "" } else { "es" }
334 );
335 for (proc, err) in failed {
336 println!(
337 " {} {} [PID {}]: {}",
338 "→".bright_black(),
339 proc.name.white(),
340 proc.pid.to_string().cyan(),
341 err.red()
342 );
343 }
344 }
345 }
346 OutputFormat::Json => {
347 self.print_json(&KillOutput {
348 action: "kill",
349 success: failed.is_empty(),
350 killed_count: killed.len(),
351 failed_count: failed.len(),
352 killed,
353 failed: &failed
354 .iter()
355 .map(|(p, e)| FailedKill {
356 process: p,
357 error: e,
358 })
359 .collect::<Vec<_>>(),
360 });
361 }
362 }
363 }
364}
365
366fn truncate_string(s: &str, max_len: usize) -> String {
368 if s.len() <= max_len {
369 s.to_string()
370 } else {
371 format!("{}...", &s[..max_len.saturating_sub(3)])
372 }
373}
374
375fn truncate_path(path: &str, max_len: usize) -> String {
377 if path.len() <= max_len {
378 path.to_string()
379 } else {
380 let start = path.len().saturating_sub(max_len.saturating_sub(3));
382 format!("...{}", &path[start..])
383 }
384}
385
386fn colorize_status(
388 status: &crate::core::ProcessStatus,
389 status_str: &str,
390) -> colored::ColoredString {
391 use colored::*;
392 match status {
393 crate::core::ProcessStatus::Running => status_str.green(),
394 crate::core::ProcessStatus::Sleeping => status_str.blue(),
395 crate::core::ProcessStatus::Stopped => status_str.yellow(),
396 crate::core::ProcessStatus::Zombie => status_str.red(),
397 _ => status_str.white(),
398 }
399}
400
401#[derive(Serialize)]
403struct ProcessListOutput<'a> {
404 action: &'static str,
405 success: bool,
406 count: usize,
407 processes: &'a [Process],
408}
409
410#[derive(Serialize)]
411struct PortListOutput<'a> {
412 action: &'static str,
413 success: bool,
414 count: usize,
415 ports: &'a [PortInfo],
416}
417
418#[derive(Serialize)]
419struct SinglePortOutput<'a> {
420 action: &'static str,
421 success: bool,
422 port: &'a PortInfo,
423}
424
425#[derive(Serialize)]
426struct KillOutput<'a> {
427 action: &'static str,
428 success: bool,
429 killed_count: usize,
430 failed_count: usize,
431 killed: &'a [Process],
432 failed: &'a [FailedKill<'a>],
433}
434
435#[derive(Serialize)]
436struct FailedKill<'a> {
437 process: &'a Process,
438 error: &'a str,
439}
440
441impl Default for Printer {
442 fn default() -> Self {
443 Self::new(OutputFormat::Human, false)
444 }
445}