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("DIR")
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("DIR 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.cwd.as_deref().unwrap_or("-").to_string();
225
226 let cmd_display = proc
228 .command
229 .as_ref()
230 .map(|c| {
231 let parts: Vec<&str> = c.split_whitespace().collect();
232 if parts.len() > 1 {
233 let args: Vec<String> = parts[1..]
234 .iter()
235 .map(|arg| {
236 if arg.contains('/') && !arg.starts_with('-') {
237 std::path::Path::new(arg)
238 .file_name()
239 .map(|f| f.to_string_lossy().to_string())
240 .unwrap_or_else(|| arg.to_string())
241 } else {
242 arg.to_string()
243 }
244 })
245 .collect();
246 let result = args.join(" ");
247 if result.is_empty() {
248 "-".to_string()
249 } else {
250 truncate_string(&result, (args_max as usize).saturating_sub(2))
251 }
252 } else {
253 "-".to_string()
255 }
256 })
257 .unwrap_or_else(|| "-".to_string());
258
259 let mem_display = format_memory(proc.memory_mb);
260
261 let status_color = match proc.status {
262 crate::core::ProcessStatus::Running => Color::Green,
263 crate::core::ProcessStatus::Sleeping => Color::Blue,
264 crate::core::ProcessStatus::Stopped => Color::Yellow,
265 crate::core::ProcessStatus::Zombie => Color::Red,
266 _ => Color::White,
267 };
268
269 table.add_row(vec![
270 Cell::new(proc.pid).fg(Color::Cyan),
271 Cell::new(&path_display).fg(Color::DarkGrey),
272 Cell::new(&proc.name).fg(Color::White),
273 Cell::new(&cmd_display).fg(Color::DarkGrey),
274 Cell::new(format!("{:.1}", proc.cpu_percent))
275 .set_alignment(CellAlignment::Right),
276 Cell::new(&mem_display).set_alignment(CellAlignment::Right),
277 Cell::new(&status_str)
278 .fg(status_color)
279 .set_alignment(CellAlignment::Right),
280 ]);
281 }
282
283 println!("{table}");
284 }
285 println!();
286 }
287
288 pub fn print_ports(&self, ports: &[PortInfo]) {
290 match self.format {
291 OutputFormat::Human => self.print_ports_human(ports),
292 OutputFormat::Json => self.print_json(&PortListOutput {
293 action: "ports",
294 success: true,
295 count: ports.len(),
296 ports,
297 }),
298 }
299 }
300
301 fn print_ports_human(&self, ports: &[PortInfo]) {
302 if ports.is_empty() {
303 self.warning("No listening ports found");
304 return;
305 }
306
307 println!(
308 "{} Found {} listening port{}",
309 "✓".green().bold(),
310 ports.len().to_string().cyan().bold(),
311 if ports.len() == 1 { "" } else { "s" }
312 );
313 println!();
314
315 let width = terminal_width();
316
317 let mut table = Table::new();
318 table
319 .load_preset(NOTHING)
320 .set_content_arrangement(ContentArrangement::Dynamic)
321 .set_width(width);
322
323 table.set_header(vec![
324 Cell::new("PORT")
325 .fg(Color::Blue)
326 .add_attribute(Attribute::Bold),
327 Cell::new("PROTO")
328 .fg(Color::Blue)
329 .add_attribute(Attribute::Bold),
330 Cell::new("PID")
331 .fg(Color::Blue)
332 .add_attribute(Attribute::Bold),
333 Cell::new("PROCESS")
334 .fg(Color::Blue)
335 .add_attribute(Attribute::Bold),
336 Cell::new("ADDRESS")
337 .fg(Color::Blue)
338 .add_attribute(Attribute::Bold),
339 ]);
340
341 use comfy_table::ColumnConstraint::*;
342 use comfy_table::Width::*;
343 table
344 .column_mut(0)
345 .expect("PORT column")
346 .set_constraint(Absolute(Fixed(8)));
347 table
348 .column_mut(1)
349 .expect("PROTO column")
350 .set_constraint(Absolute(Fixed(6)));
351 table
352 .column_mut(2)
353 .expect("PID column")
354 .set_constraint(Absolute(Fixed(8)));
355 table
356 .column_mut(3)
357 .expect("PROCESS column")
358 .set_constraint(LowerBoundary(Fixed(12)));
359 table
360 .column_mut(4)
361 .expect("ADDRESS column")
362 .set_constraint(LowerBoundary(Fixed(10)));
363
364 for port in ports {
365 let addr = port.address.as_deref().unwrap_or("*");
366 let proto = format!("{:?}", port.protocol).to_uppercase();
367
368 table.add_row(vec![
369 Cell::new(port.port).fg(Color::Cyan),
370 Cell::new(&proto).fg(Color::White),
371 Cell::new(port.pid).fg(Color::Cyan),
372 Cell::new(truncate_string(&port.process_name, 19)).fg(Color::White),
373 Cell::new(addr).fg(Color::DarkGrey),
374 ]);
375 }
376
377 println!("{table}");
378 println!();
379 }
380
381 pub fn print_port_info(&self, port_info: &PortInfo) {
383 match self.format {
384 OutputFormat::Human => {
385 println!(
386 "{} Process on port {}:",
387 "✓".green().bold(),
388 port_info.port.to_string().cyan().bold()
389 );
390 println!();
391 println!(
392 " {} {}",
393 "Name:".bright_black(),
394 port_info.process_name.white().bold()
395 );
396 println!(
397 " {} {}",
398 "PID:".bright_black(),
399 port_info.pid.to_string().cyan()
400 );
401 println!(" {} {:?}", "Protocol:".bright_black(), port_info.protocol);
402 if let Some(ref addr) = port_info.address {
403 println!(" {} {}", "Address:".bright_black(), addr);
404 }
405 println!();
406 }
407 OutputFormat::Json => self.print_json(&SinglePortOutput {
408 action: "on",
409 success: true,
410 port: port_info,
411 }),
412 }
413 }
414
415 pub fn print_json<T: Serialize>(&self, data: &T) {
417 match serde_json::to_string_pretty(data) {
418 Ok(json) => println!("{}", json),
419 Err(e) => eprintln!("Failed to serialize JSON: {}", e),
420 }
421 }
422
423 pub fn print_action_result(
425 &self,
426 action: &str,
427 succeeded: &[Process],
428 failed: &[(Process, String)],
429 ) {
430 match self.format {
431 OutputFormat::Human => {
432 if !succeeded.is_empty() {
433 println!(
434 "{} {} {} process{}",
435 "✓".green().bold(),
436 action,
437 succeeded.len().to_string().cyan().bold(),
438 if succeeded.len() == 1 { "" } else { "es" }
439 );
440 for proc in succeeded {
441 println!(
442 " {} {} [PID {}]",
443 "→".bright_black(),
444 proc.name.white(),
445 proc.pid.to_string().cyan()
446 );
447 }
448 }
449 if !failed.is_empty() {
450 println!(
451 "{} Failed to {} {} process{}",
452 "✗".red().bold(),
453 action.to_lowercase(),
454 failed.len(),
455 if failed.len() == 1 { "" } else { "es" }
456 );
457 for (proc, err) in failed {
458 println!(
459 " {} {} [PID {}]: {}",
460 "→".bright_black(),
461 proc.name.white(),
462 proc.pid.to_string().cyan(),
463 err.red()
464 );
465 }
466 }
467 }
468 OutputFormat::Json => {
469 self.print_json(&ActionOutput {
470 action,
471 success: failed.is_empty(),
472 succeeded_count: succeeded.len(),
473 failed_count: failed.len(),
474 succeeded,
475 failed: &failed
476 .iter()
477 .map(|(p, e)| FailedAction {
478 process: p,
479 error: e,
480 })
481 .collect::<Vec<_>>(),
482 });
483 }
484 }
485 }
486
487 pub fn print_kill_result(&self, killed: &[Process], failed: &[(Process, String)]) {
489 self.print_action_result("Killed", killed, failed);
490 }
491
492 pub fn print_confirmation(&self, action: &str, processes: &[Process]) {
494 println!(
495 "\n{} Found {} process{} to {}:\n",
496 "⚠".yellow().bold(),
497 processes.len().to_string().cyan().bold(),
498 if processes.len() == 1 { "" } else { "es" },
499 action
500 );
501
502 for proc in processes {
503 println!(
504 " {} {} [PID {}] - CPU: {:.1}%, MEM: {}",
505 "→".bright_black(),
506 proc.name.white().bold(),
507 proc.pid.to_string().cyan(),
508 proc.cpu_percent,
509 format_memory(proc.memory_mb)
510 );
511 }
512 println!();
513 }
514}
515
516#[derive(Serialize)]
518struct ProcessListOutput<'a> {
519 action: &'static str,
520 success: bool,
521 count: usize,
522 processes: &'a [Process],
523}
524
525#[derive(Serialize)]
526struct PortListOutput<'a> {
527 action: &'static str,
528 success: bool,
529 count: usize,
530 ports: &'a [PortInfo],
531}
532
533#[derive(Serialize)]
534struct SinglePortOutput<'a> {
535 action: &'static str,
536 success: bool,
537 port: &'a PortInfo,
538}
539
540#[derive(Serialize)]
541struct ActionOutput<'a> {
542 action: &'a str,
543 success: bool,
544 succeeded_count: usize,
545 failed_count: usize,
546 succeeded: &'a [Process],
547 failed: &'a [FailedAction<'a>],
548}
549
550#[derive(Serialize)]
551struct FailedAction<'a> {
552 process: &'a Process,
553 error: &'a str,
554}
555
556impl Default for Printer {
557 fn default() -> Self {
558 Self::new(OutputFormat::Human, false)
559 }
560}