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