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