1use crate::core::{PortInfo, Process};
6use crate::ui::format::{colorize_status, format_memory, truncate_string};
7use colored::*;
8use comfy_table::presets::NOTHING;
9use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table};
10use serde::Serialize;
11
12#[derive(Debug, Clone, Copy, Default)]
14pub enum OutputFormat {
15 #[default]
17 Human,
18 Json,
20}
21
22pub struct Printer {
24 format: OutputFormat,
25 verbose: bool,
26}
27
28fn terminal_width() -> u16 {
30 crossterm::terminal::size().map(|(w, _)| w).unwrap_or(120)
31}
32
33impl Printer {
34 pub fn new(format: OutputFormat, verbose: bool) -> Self {
36 Self { format, verbose }
37 }
38
39 pub fn success(&self, message: &str) {
41 match self.format {
42 OutputFormat::Human => {
43 println!("{} {}", "✓".green().bold(), message.green());
44 }
45 OutputFormat::Json => {
46 }
48 }
49 }
50
51 pub fn error(&self, message: &str) {
53 match self.format {
54 OutputFormat::Human => {
55 eprintln!("{} {}", "✗".red().bold(), message.red());
56 }
57 OutputFormat::Json => {
58 }
60 }
61 }
62
63 pub fn warning(&self, message: &str) {
65 match self.format {
66 OutputFormat::Human => {
67 println!("{} {}", "⚠".yellow().bold(), message.yellow());
68 }
69 OutputFormat::Json => {
70 }
72 }
73 }
74
75 pub fn print_processes_with_context(&self, processes: &[Process], context: Option<&str>) {
77 match self.format {
78 OutputFormat::Human => self.print_processes_human(processes, context),
79 OutputFormat::Json => self.print_json(&ProcessListOutput {
80 action: "list",
81 success: true,
82 count: processes.len(),
83 processes,
84 }),
85 }
86 }
87
88 pub fn print_processes(&self, processes: &[Process]) {
90 self.print_processes_with_context(processes, None)
91 }
92
93 fn print_processes_human(&self, processes: &[Process], context: Option<&str>) {
94 if processes.is_empty() {
95 let msg = match context {
96 Some(ctx) => format!("No processes found {}", ctx),
97 None => "No processes found".to_string(),
98 };
99 self.warning(&msg);
100 return;
101 }
102
103 let context_str = context.map(|c| format!(" {}", c)).unwrap_or_default();
104 println!(
105 "{} Found {} process{}{}",
106 "✓".green().bold(),
107 processes.len().to_string().cyan().bold(),
108 if processes.len() == 1 { "" } else { "es" },
109 context_str.bright_black()
110 );
111 println!();
112
113 if self.verbose {
114 for proc in processes {
116 let status_str = format!("{:?}", proc.status);
117 let status_colored = colorize_status(&proc.status, &status_str);
118
119 println!(
120 "{} {} {} {:.1}% CPU {} {}",
121 proc.pid.to_string().cyan().bold(),
122 proc.name.white().bold(),
123 format!("[{}]", status_colored).bright_black(),
124 proc.cpu_percent,
125 format_memory(proc.memory_mb),
126 proc.user.as_deref().unwrap_or("-").bright_black()
127 );
128
129 if let Some(ref cmd) = proc.command {
130 println!(" {} {}", "cmd:".bright_black(), cmd);
131 }
132 if let Some(ref path) = proc.exe_path {
133 println!(" {} {}", "exe:".bright_black(), path.bright_black());
134 }
135 if let Some(ref cwd) = proc.cwd {
136 println!(" {} {}", "cwd:".bright_black(), cwd.bright_black());
137 }
138 if let Some(ppid) = proc.parent_pid {
139 println!(
140 " {} {}",
141 "parent:".bright_black(),
142 ppid.to_string().bright_black()
143 );
144 }
145 println!();
146 }
147 } else {
148 let width = terminal_width();
149
150 let mut table = Table::new();
151 table
152 .load_preset(NOTHING)
153 .set_content_arrangement(ContentArrangement::Dynamic)
154 .set_width(width);
155
156 table.set_header(vec![
158 Cell::new("PID")
159 .fg(Color::Blue)
160 .add_attribute(Attribute::Bold),
161 Cell::new("PATH")
162 .fg(Color::Blue)
163 .add_attribute(Attribute::Bold),
164 Cell::new("NAME")
165 .fg(Color::Blue)
166 .add_attribute(Attribute::Bold),
167 Cell::new("ARGS")
168 .fg(Color::Blue)
169 .add_attribute(Attribute::Bold),
170 Cell::new("CPU%")
171 .fg(Color::Blue)
172 .add_attribute(Attribute::Bold)
173 .set_alignment(CellAlignment::Right),
174 Cell::new("MEM")
175 .fg(Color::Blue)
176 .add_attribute(Attribute::Bold)
177 .set_alignment(CellAlignment::Right),
178 Cell::new("STATUS")
179 .fg(Color::Blue)
180 .add_attribute(Attribute::Bold)
181 .set_alignment(CellAlignment::Right),
182 ]);
183
184 use comfy_table::ColumnConstraint::*;
186 use comfy_table::Width::*;
187 table
190 .column_mut(0)
191 .expect("PID column")
192 .set_constraint(Absolute(Fixed(8))); table
194 .column_mut(1)
195 .expect("PATH column")
196 .set_constraint(LowerBoundary(Fixed(20)));
197 table
198 .column_mut(2)
199 .expect("NAME column")
200 .set_constraint(LowerBoundary(Fixed(10)));
201 let args_max = (width / 2).max(30);
203 table
204 .column_mut(3)
205 .expect("ARGS column")
206 .set_constraint(UpperBoundary(Fixed(args_max)));
207 table
208 .column_mut(4)
209 .expect("CPU% column")
210 .set_constraint(Absolute(Fixed(8))); table
212 .column_mut(5)
213 .expect("MEM column")
214 .set_constraint(Absolute(Fixed(11))); table
216 .column_mut(6)
217 .expect("STATUS column")
218 .set_constraint(Absolute(Fixed(12))); for proc in processes {
221 let status_str = format!("{:?}", proc.status);
222
223 let path_display = proc
225 .exe_path
226 .as_ref()
227 .map(|p| {
228 std::path::Path::new(p)
229 .parent()
230 .map(|parent| parent.to_string_lossy().to_string())
231 .unwrap_or_else(|| "-".to_string())
232 })
233 .unwrap_or_else(|| "-".to_string());
234
235 let cmd_display = proc
237 .command
238 .as_ref()
239 .map(|c| {
240 let parts: Vec<&str> = c.split_whitespace().collect();
241 if parts.len() > 1 {
242 let args: Vec<String> = parts[1..]
243 .iter()
244 .map(|arg| {
245 if arg.contains('/') && !arg.starts_with('-') {
246 std::path::Path::new(arg)
247 .file_name()
248 .map(|f| f.to_string_lossy().to_string())
249 .unwrap_or_else(|| arg.to_string())
250 } else {
251 arg.to_string()
252 }
253 })
254 .collect();
255 let result = args.join(" ");
256 if result.is_empty() {
257 "-".to_string()
258 } else {
259 truncate_string(&result, (args_max as usize).saturating_sub(2))
260 }
261 } else {
262 "-".to_string()
264 }
265 })
266 .unwrap_or_else(|| "-".to_string());
267
268 let mem_display = format_memory(proc.memory_mb);
269
270 let status_color = match proc.status {
271 crate::core::ProcessStatus::Running => Color::Green,
272 crate::core::ProcessStatus::Sleeping => Color::Blue,
273 crate::core::ProcessStatus::Stopped => Color::Yellow,
274 crate::core::ProcessStatus::Zombie => Color::Red,
275 _ => Color::White,
276 };
277
278 table.add_row(vec![
279 Cell::new(proc.pid).fg(Color::Cyan),
280 Cell::new(&path_display).fg(Color::DarkGrey),
281 Cell::new(&proc.name).fg(Color::White),
282 Cell::new(&cmd_display).fg(Color::DarkGrey),
283 Cell::new(format!("{:.1}", proc.cpu_percent))
284 .set_alignment(CellAlignment::Right),
285 Cell::new(&mem_display).set_alignment(CellAlignment::Right),
286 Cell::new(&status_str)
287 .fg(status_color)
288 .set_alignment(CellAlignment::Right),
289 ]);
290 }
291
292 println!("{table}");
293 }
294 println!();
295 }
296
297 pub fn print_ports(&self, ports: &[PortInfo]) {
299 match self.format {
300 OutputFormat::Human => self.print_ports_human(ports),
301 OutputFormat::Json => self.print_json(&PortListOutput {
302 action: "ports",
303 success: true,
304 count: ports.len(),
305 ports,
306 }),
307 }
308 }
309
310 fn print_ports_human(&self, ports: &[PortInfo]) {
311 if ports.is_empty() {
312 self.warning("No listening ports found");
313 return;
314 }
315
316 println!(
317 "{} Found {} listening port{}",
318 "✓".green().bold(),
319 ports.len().to_string().cyan().bold(),
320 if ports.len() == 1 { "" } else { "s" }
321 );
322 println!();
323
324 let width = terminal_width();
325
326 let mut table = Table::new();
327 table
328 .load_preset(NOTHING)
329 .set_content_arrangement(ContentArrangement::Dynamic)
330 .set_width(width);
331
332 table.set_header(vec![
333 Cell::new("PORT")
334 .fg(Color::Blue)
335 .add_attribute(Attribute::Bold),
336 Cell::new("PROTO")
337 .fg(Color::Blue)
338 .add_attribute(Attribute::Bold),
339 Cell::new("PID")
340 .fg(Color::Blue)
341 .add_attribute(Attribute::Bold),
342 Cell::new("PROCESS")
343 .fg(Color::Blue)
344 .add_attribute(Attribute::Bold),
345 Cell::new("ADDRESS")
346 .fg(Color::Blue)
347 .add_attribute(Attribute::Bold),
348 ]);
349
350 use comfy_table::ColumnConstraint::*;
351 use comfy_table::Width::*;
352 table
353 .column_mut(0)
354 .expect("PORT column")
355 .set_constraint(Absolute(Fixed(8)));
356 table
357 .column_mut(1)
358 .expect("PROTO column")
359 .set_constraint(Absolute(Fixed(6)));
360 table
361 .column_mut(2)
362 .expect("PID column")
363 .set_constraint(Absolute(Fixed(8)));
364 table
365 .column_mut(3)
366 .expect("PROCESS column")
367 .set_constraint(LowerBoundary(Fixed(12)));
368 table
369 .column_mut(4)
370 .expect("ADDRESS column")
371 .set_constraint(LowerBoundary(Fixed(10)));
372
373 for port in ports {
374 let addr = port.address.as_deref().unwrap_or("*");
375 let proto = format!("{:?}", port.protocol).to_uppercase();
376
377 table.add_row(vec![
378 Cell::new(port.port).fg(Color::Cyan),
379 Cell::new(&proto).fg(Color::White),
380 Cell::new(port.pid).fg(Color::Cyan),
381 Cell::new(truncate_string(&port.process_name, 19)).fg(Color::White),
382 Cell::new(addr).fg(Color::DarkGrey),
383 ]);
384 }
385
386 println!("{table}");
387 println!();
388 }
389
390 pub fn print_port_info(&self, port_info: &PortInfo) {
392 match self.format {
393 OutputFormat::Human => {
394 println!(
395 "{} Process on port {}:",
396 "✓".green().bold(),
397 port_info.port.to_string().cyan().bold()
398 );
399 println!();
400 println!(
401 " {} {}",
402 "Name:".bright_black(),
403 port_info.process_name.white().bold()
404 );
405 println!(
406 " {} {}",
407 "PID:".bright_black(),
408 port_info.pid.to_string().cyan()
409 );
410 println!(" {} {:?}", "Protocol:".bright_black(), port_info.protocol);
411 if let Some(ref addr) = port_info.address {
412 println!(" {} {}", "Address:".bright_black(), addr);
413 }
414 println!();
415 }
416 OutputFormat::Json => self.print_json(&SinglePortOutput {
417 action: "on",
418 success: true,
419 port: port_info,
420 }),
421 }
422 }
423
424 pub fn print_json<T: Serialize>(&self, data: &T) {
426 match serde_json::to_string_pretty(data) {
427 Ok(json) => println!("{}", json),
428 Err(e) => eprintln!("Failed to serialize JSON: {}", e),
429 }
430 }
431
432 pub fn print_action_result(
434 &self,
435 action: &str,
436 succeeded: &[Process],
437 failed: &[(Process, String)],
438 ) {
439 match self.format {
440 OutputFormat::Human => {
441 if !succeeded.is_empty() {
442 println!(
443 "{} {} {} process{}",
444 "✓".green().bold(),
445 action,
446 succeeded.len().to_string().cyan().bold(),
447 if succeeded.len() == 1 { "" } else { "es" }
448 );
449 for proc in succeeded {
450 println!(
451 " {} {} [PID {}]",
452 "→".bright_black(),
453 proc.name.white(),
454 proc.pid.to_string().cyan()
455 );
456 }
457 }
458 if !failed.is_empty() {
459 println!(
460 "{} Failed to {} {} process{}",
461 "✗".red().bold(),
462 action.to_lowercase(),
463 failed.len(),
464 if failed.len() == 1 { "" } else { "es" }
465 );
466 for (proc, err) in failed {
467 println!(
468 " {} {} [PID {}]: {}",
469 "→".bright_black(),
470 proc.name.white(),
471 proc.pid.to_string().cyan(),
472 err.red()
473 );
474 }
475 }
476 }
477 OutputFormat::Json => {
478 self.print_json(&ActionOutput {
479 action,
480 success: failed.is_empty(),
481 succeeded_count: succeeded.len(),
482 failed_count: failed.len(),
483 succeeded,
484 failed: &failed
485 .iter()
486 .map(|(p, e)| FailedAction {
487 process: p,
488 error: e,
489 })
490 .collect::<Vec<_>>(),
491 });
492 }
493 }
494 }
495
496 pub fn print_kill_result(&self, killed: &[Process], failed: &[(Process, String)]) {
498 self.print_action_result("Killed", killed, failed);
499 }
500
501 pub fn print_confirmation(&self, action: &str, processes: &[Process]) {
503 println!(
504 "\n{} Found {} process{} to {}:\n",
505 "⚠".yellow().bold(),
506 processes.len().to_string().cyan().bold(),
507 if processes.len() == 1 { "" } else { "es" },
508 action
509 );
510
511 for proc in processes {
512 println!(
513 " {} {} [PID {}] - CPU: {:.1}%, MEM: {}",
514 "→".bright_black(),
515 proc.name.white().bold(),
516 proc.pid.to_string().cyan(),
517 proc.cpu_percent,
518 format_memory(proc.memory_mb)
519 );
520 }
521 println!();
522 }
523}
524
525#[derive(Serialize)]
527struct ProcessListOutput<'a> {
528 action: &'static str,
529 success: bool,
530 count: usize,
531 processes: &'a [Process],
532}
533
534#[derive(Serialize)]
535struct PortListOutput<'a> {
536 action: &'static str,
537 success: bool,
538 count: usize,
539 ports: &'a [PortInfo],
540}
541
542#[derive(Serialize)]
543struct SinglePortOutput<'a> {
544 action: &'static str,
545 success: bool,
546 port: &'a PortInfo,
547}
548
549#[derive(Serialize)]
550struct ActionOutput<'a> {
551 action: &'a str,
552 success: bool,
553 succeeded_count: usize,
554 failed_count: usize,
555 succeeded: &'a [Process],
556 failed: &'a [FailedAction<'a>],
557}
558
559#[derive(Serialize)]
560struct FailedAction<'a> {
561 process: &'a Process,
562 error: &'a str,
563}
564
565impl Default for Printer {
566 fn default() -> Self {
567 Self::new(OutputFormat::Human, false)
568 }
569}