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