1use crate::core::{PortInfo, Process};
6use crate::error::Result;
7use crate::ui::format::{colorize_status, format_memory, plural, truncate_string};
8use colored::*;
9use comfy_table::presets::NOTHING;
10use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table};
11use dialoguer::Confirm;
12use serde::Serialize;
13
14#[derive(Debug, Clone, Copy, Default)]
16pub enum OutputFormat {
17 #[default]
19 Human,
20 Json,
22}
23
24pub struct Printer {
26 format: OutputFormat,
27 verbose: bool,
28}
29
30fn terminal_width() -> u16 {
32 crossterm::terminal::size().map(|(w, _)| w).unwrap_or(120)
33}
34
35impl Printer {
36 pub fn new(format: OutputFormat, verbose: bool) -> Self {
38 Self { format, verbose }
39 }
40
41 pub fn from_flags(json: bool, verbose: bool) -> Self {
43 Self::new(
44 if json {
45 OutputFormat::Json
46 } else {
47 OutputFormat::Human
48 },
49 verbose,
50 )
51 }
52
53 pub fn success(&self, message: &str) {
55 match self.format {
56 OutputFormat::Human => {
57 println!("{} {}", "✓".green().bold(), message.green());
58 }
59 OutputFormat::Json => {
60 }
62 }
63 }
64
65 pub fn print_empty_result(&self, action: &str, message: &str) {
70 match self.format {
71 OutputFormat::Human => self.success(message),
72 OutputFormat::Json => self.print_json(&EmptyResult {
73 action,
74 success: true,
75 count: 0,
76 message,
77 }),
78 }
79 }
80
81 pub fn error(&self, message: &str) {
83 match self.format {
84 OutputFormat::Human => {
85 eprintln!("{} {}", "✗".red().bold(), message.red());
86 }
87 OutputFormat::Json => {
88 }
90 }
91 }
92
93 pub fn warning(&self, message: &str) {
95 match self.format {
96 OutputFormat::Human => {
97 println!("{} {}", "⚠".yellow().bold(), message.yellow());
98 }
99 OutputFormat::Json => {
100 }
102 }
103 }
104
105 pub fn print_processes_as(&self, action: &str, processes: &[Process], context: Option<&str>) {
109 match self.format {
110 OutputFormat::Human => self.print_processes_human(processes, context),
111 OutputFormat::Json => self.print_json(&ProcessListOutput {
112 action,
113 success: true,
114 count: processes.len(),
115 processes,
116 }),
117 }
118 }
119
120 pub fn print_processes_with_context(&self, processes: &[Process], context: Option<&str>) {
123 self.print_processes_as("list", processes, context)
124 }
125
126 pub fn print_processes(&self, processes: &[Process]) {
128 self.print_processes_with_context(processes, None)
129 }
130
131 fn print_processes_human(&self, processes: &[Process], context: Option<&str>) {
132 if processes.is_empty() {
133 let msg = match context {
134 Some(ctx) => format!("No processes found {}", ctx),
135 None => "No processes found".to_string(),
136 };
137 self.warning(&msg);
138 return;
139 }
140
141 let context_str = context.map(|c| format!(" {}", c)).unwrap_or_default();
142 println!(
143 "{} Found {} process{}{}",
144 "✓".green().bold(),
145 processes.len().to_string().cyan().bold(),
146 if processes.len() == 1 { "" } else { "es" },
147 context_str.bright_black()
148 );
149 println!();
150
151 if self.verbose {
152 for proc in processes {
154 let status_str = format!("{:?}", proc.status);
155 let status_colored = colorize_status(&proc.status, &status_str);
156
157 println!(
158 "{} {} {} {:.1}% CPU {} {}",
159 proc.pid.to_string().cyan().bold(),
160 proc.name.white().bold(),
161 format!("[{}]", status_colored).bright_black(),
162 proc.cpu_percent,
163 format_memory(proc.memory_mb),
164 proc.user.as_deref().unwrap_or("-").bright_black()
165 );
166
167 if let Some(ref cmd) = proc.command {
168 println!(" {} {}", "cmd:".bright_black(), cmd);
169 }
170 if let Some(ref path) = proc.exe_path {
171 println!(" {} {}", "exe:".bright_black(), path.bright_black());
172 }
173 if let Some(ref cwd) = proc.cwd {
174 println!(" {} {}", "cwd:".bright_black(), cwd.bright_black());
175 }
176 if let Some(ppid) = proc.parent_pid {
177 println!(
178 " {} {}",
179 "parent:".bright_black(),
180 ppid.to_string().bright_black()
181 );
182 }
183 println!();
184 }
185 } else {
186 let width = terminal_width();
187
188 let mut table = Table::new();
189 table
190 .load_preset(NOTHING)
191 .set_content_arrangement(ContentArrangement::Dynamic)
192 .set_width(width);
193
194 table.set_header(vec![
196 Cell::new("PID")
197 .fg(Color::Blue)
198 .add_attribute(Attribute::Bold),
199 Cell::new("DIR")
200 .fg(Color::Blue)
201 .add_attribute(Attribute::Bold),
202 Cell::new("NAME")
203 .fg(Color::Blue)
204 .add_attribute(Attribute::Bold),
205 Cell::new("ARGS")
206 .fg(Color::Blue)
207 .add_attribute(Attribute::Bold),
208 Cell::new("CPU%")
209 .fg(Color::Blue)
210 .add_attribute(Attribute::Bold)
211 .set_alignment(CellAlignment::Right),
212 Cell::new("MEM")
213 .fg(Color::Blue)
214 .add_attribute(Attribute::Bold)
215 .set_alignment(CellAlignment::Right),
216 Cell::new("STATUS")
217 .fg(Color::Blue)
218 .add_attribute(Attribute::Bold)
219 .set_alignment(CellAlignment::Right),
220 ]);
221
222 use comfy_table::ColumnConstraint::*;
224 use comfy_table::Width::*;
225 table
228 .column_mut(0)
229 .expect("PID column")
230 .set_constraint(Absolute(Fixed(8))); table
232 .column_mut(1)
233 .expect("DIR column")
234 .set_constraint(LowerBoundary(Fixed(20)));
235 table
236 .column_mut(2)
237 .expect("NAME column")
238 .set_constraint(LowerBoundary(Fixed(10)));
239 let args_max = (width / 2).max(30);
241 table
242 .column_mut(3)
243 .expect("ARGS column")
244 .set_constraint(UpperBoundary(Fixed(args_max)));
245 table
246 .column_mut(4)
247 .expect("CPU% column")
248 .set_constraint(Absolute(Fixed(8))); table
250 .column_mut(5)
251 .expect("MEM column")
252 .set_constraint(Absolute(Fixed(11))); table
254 .column_mut(6)
255 .expect("STATUS column")
256 .set_constraint(Absolute(Fixed(12))); for proc in processes {
259 let status_str = format!("{:?}", proc.status);
260
261 let path_display = proc.cwd.as_deref().unwrap_or("-").to_string();
263
264 let cmd_display = proc
266 .command
267 .as_ref()
268 .map(|c| {
269 let parts: Vec<&str> = c.split_whitespace().collect();
270 if parts.len() > 1 {
271 let args: Vec<String> = parts[1..]
272 .iter()
273 .map(|arg| {
274 if arg.contains('/') && !arg.starts_with('-') {
275 std::path::Path::new(arg)
276 .file_name()
277 .map(|f| f.to_string_lossy().to_string())
278 .unwrap_or_else(|| arg.to_string())
279 } else {
280 arg.to_string()
281 }
282 })
283 .collect();
284 let result = args.join(" ");
285 if result.is_empty() {
286 "-".to_string()
287 } else {
288 truncate_string(&result, (args_max as usize).saturating_sub(2))
289 }
290 } else {
291 "-".to_string()
293 }
294 })
295 .unwrap_or_else(|| "-".to_string());
296
297 let mem_display = format_memory(proc.memory_mb);
298
299 let status_color = match proc.status {
300 crate::core::ProcessStatus::Running => Color::Green,
301 crate::core::ProcessStatus::Sleeping => Color::Blue,
302 crate::core::ProcessStatus::Stopped => Color::Yellow,
303 crate::core::ProcessStatus::Zombie => Color::Red,
304 _ => Color::White,
305 };
306
307 table.add_row(vec![
308 Cell::new(proc.pid).fg(Color::Cyan),
309 Cell::new(&path_display).fg(Color::DarkGrey),
310 Cell::new(&proc.name).fg(Color::White),
311 Cell::new(&cmd_display).fg(Color::DarkGrey),
312 Cell::new(format!("{:.1}", proc.cpu_percent))
313 .set_alignment(CellAlignment::Right),
314 Cell::new(&mem_display).set_alignment(CellAlignment::Right),
315 Cell::new(&status_str)
316 .fg(status_color)
317 .set_alignment(CellAlignment::Right),
318 ]);
319 }
320
321 println!("{table}");
322 }
323 println!();
324 }
325
326 pub fn print_ports(&self, ports: &[PortInfo]) {
328 match self.format {
329 OutputFormat::Human => self.print_ports_human(ports),
330 OutputFormat::Json => self.print_json(&PortListOutput {
331 action: "ports",
332 success: true,
333 count: ports.len(),
334 ports,
335 }),
336 }
337 }
338
339 fn print_ports_human(&self, ports: &[PortInfo]) {
340 if ports.is_empty() {
341 self.warning("No listening ports found");
342 return;
343 }
344
345 println!(
346 "{} Found {} listening port{}",
347 "✓".green().bold(),
348 ports.len().to_string().cyan().bold(),
349 if ports.len() == 1 { "" } else { "s" }
350 );
351 println!();
352
353 let width = terminal_width();
354
355 let mut table = Table::new();
356 table
357 .load_preset(NOTHING)
358 .set_content_arrangement(ContentArrangement::Dynamic)
359 .set_width(width);
360
361 table.set_header(vec![
362 Cell::new("PORT")
363 .fg(Color::Blue)
364 .add_attribute(Attribute::Bold),
365 Cell::new("PROTO")
366 .fg(Color::Blue)
367 .add_attribute(Attribute::Bold),
368 Cell::new("PID")
369 .fg(Color::Blue)
370 .add_attribute(Attribute::Bold),
371 Cell::new("PROCESS")
372 .fg(Color::Blue)
373 .add_attribute(Attribute::Bold),
374 Cell::new("ADDRESS")
375 .fg(Color::Blue)
376 .add_attribute(Attribute::Bold),
377 ]);
378
379 use comfy_table::ColumnConstraint::*;
380 use comfy_table::Width::*;
381 table
382 .column_mut(0)
383 .expect("PORT column")
384 .set_constraint(Absolute(Fixed(8)));
385 table
386 .column_mut(1)
387 .expect("PROTO column")
388 .set_constraint(Absolute(Fixed(6)));
389 table
390 .column_mut(2)
391 .expect("PID column")
392 .set_constraint(Absolute(Fixed(8)));
393 table
394 .column_mut(3)
395 .expect("PROCESS column")
396 .set_constraint(LowerBoundary(Fixed(12)));
397 table
398 .column_mut(4)
399 .expect("ADDRESS column")
400 .set_constraint(LowerBoundary(Fixed(10)));
401
402 for port in ports {
403 let addr = port.address.as_deref().unwrap_or("*");
404 let proto = format!("{:?}", port.protocol).to_uppercase();
405
406 table.add_row(vec![
407 Cell::new(port.port).fg(Color::Cyan),
408 Cell::new(&proto).fg(Color::White),
409 Cell::new(port.pid).fg(Color::Cyan),
410 Cell::new(truncate_string(&port.process_name, 19)).fg(Color::White),
411 Cell::new(addr).fg(Color::DarkGrey),
412 ]);
413 }
414
415 println!("{table}");
416 println!();
417 }
418
419 pub fn print_port_info(&self, port_info: &PortInfo) {
421 match self.format {
422 OutputFormat::Human => {
423 println!(
424 "{} Process on port {}:",
425 "✓".green().bold(),
426 port_info.port.to_string().cyan().bold()
427 );
428 println!();
429 println!(
430 " {} {}",
431 "Name:".bright_black(),
432 port_info.process_name.white().bold()
433 );
434 println!(
435 " {} {}",
436 "PID:".bright_black(),
437 port_info.pid.to_string().cyan()
438 );
439 println!(" {} {:?}", "Protocol:".bright_black(), port_info.protocol);
440 if let Some(ref addr) = port_info.address {
441 println!(" {} {}", "Address:".bright_black(), addr);
442 }
443 println!();
444 }
445 OutputFormat::Json => self.print_json(&SinglePortOutput {
446 action: "on",
447 success: true,
448 port: port_info,
449 }),
450 }
451 }
452
453 pub fn print_json<T: Serialize>(&self, data: &T) {
455 match serde_json::to_string_pretty(data) {
456 Ok(json) => println!("{}", json),
457 Err(e) => eprintln!("Failed to serialize JSON: {}", e),
458 }
459 }
460
461 pub fn print_action_result(
466 &self,
467 action: &str,
468 succeeded: &[Process],
469 failed: &[(Process, String)],
470 ) {
471 let past_tense = match action {
473 "kill" => "Killed".to_string(),
474 "freeze" => "Frozen".to_string(),
475 "resume" => "Resumed".to_string(),
476 _ => format!("{}{}ed", action[..1].to_uppercase(), &action[1..]),
477 };
478
479 match self.format {
480 OutputFormat::Human => {
481 if !succeeded.is_empty() {
482 println!(
483 "{} {} {} process{}",
484 "✓".green().bold(),
485 past_tense,
486 succeeded.len().to_string().cyan().bold(),
487 plural(succeeded.len())
488 );
489 for proc in succeeded {
490 println!(
491 " {} {} [PID {}]",
492 "→".bright_black(),
493 proc.name.white(),
494 proc.pid.to_string().cyan()
495 );
496 }
497 }
498 if !failed.is_empty() {
499 println!(
500 "{} Failed to {} {} process{}",
501 "✗".red().bold(),
502 action,
503 failed.len(),
504 plural(failed.len())
505 );
506 for (proc, err) in failed {
507 println!(
508 " {} {} [PID {}]: {}",
509 "→".bright_black(),
510 proc.name.white(),
511 proc.pid.to_string().cyan(),
512 err.red()
513 );
514 }
515 }
516 }
517 OutputFormat::Json => {
518 self.print_json(&ActionOutput {
519 action,
520 success: failed.is_empty(),
521 succeeded_count: succeeded.len(),
522 failed_count: failed.len(),
523 succeeded,
524 failed: &failed
525 .iter()
526 .map(|(p, e)| FailedAction {
527 process: p,
528 error: e,
529 })
530 .collect::<Vec<_>>(),
531 });
532 }
533 }
534 }
535
536 pub fn print_kill_result(&self, killed: &[Process], failed: &[(Process, String)]) {
538 self.print_action_result("kill", killed, failed);
539 }
540
541 pub fn print_dry_run(&self, verb: &str, processes: &[Process]) {
545 self.print_processes(processes);
546 self.warning(&format!(
547 "Dry run: would {} {} process{}",
548 verb,
549 processes.len(),
550 plural(processes.len())
551 ));
552 }
553
554 pub fn ask_confirm(&self, action: &str, processes: &[Process], yes: bool) -> Result<bool> {
559 if yes {
560 return Ok(true);
561 }
562 match self.format {
563 OutputFormat::Json => Ok(true),
564 OutputFormat::Human => {
565 self.print_confirmation(action, processes);
566 let prompt = format!(
567 "{}{} {} process{}?",
568 action[..1].to_uppercase(),
569 &action[1..],
570 processes.len(),
571 plural(processes.len())
572 );
573 let confirmed = Confirm::new()
574 .with_prompt(prompt)
575 .default(false)
576 .interact()?;
577 if !confirmed {
578 self.warning("Cancelled");
579 }
580 Ok(confirmed)
581 }
582 }
583 }
584
585 pub fn print_confirmation(&self, action: &str, processes: &[Process]) {
587 println!(
588 "\n{} Found {} process{} to {}:\n",
589 "⚠".yellow().bold(),
590 processes.len().to_string().cyan().bold(),
591 if processes.len() == 1 { "" } else { "es" },
592 action
593 );
594
595 for proc in processes {
596 println!(
597 " {} {} [PID {}] - CPU: {:.1}%, MEM: {}",
598 "→".bright_black(),
599 proc.name.white().bold(),
600 proc.pid.to_string().cyan(),
601 proc.cpu_percent,
602 format_memory(proc.memory_mb)
603 );
604 }
605 println!();
606 }
607}
608
609#[derive(Serialize)]
611struct EmptyResult<'a> {
612 action: &'a str,
613 success: bool,
614 count: usize,
615 message: &'a str,
616}
617
618#[derive(Serialize)]
619struct ProcessListOutput<'a> {
620 action: &'a str,
621 success: bool,
622 count: usize,
623 processes: &'a [Process],
624}
625
626#[derive(Serialize)]
627struct PortListOutput<'a> {
628 action: &'static str,
629 success: bool,
630 count: usize,
631 ports: &'a [PortInfo],
632}
633
634#[derive(Serialize)]
635struct SinglePortOutput<'a> {
636 action: &'static str,
637 success: bool,
638 port: &'a PortInfo,
639}
640
641#[derive(Serialize)]
642struct ActionOutput<'a> {
643 action: &'a str,
644 success: bool,
645 succeeded_count: usize,
646 failed_count: usize,
647 succeeded: &'a [Process],
648 failed: &'a [FailedAction<'a>],
649}
650
651#[derive(Serialize)]
652struct FailedAction<'a> {
653 process: &'a Process,
654 error: &'a str,
655}
656
657impl Default for Printer {
658 fn default() -> Self {
659 Self::new(OutputFormat::Human, false)
660 }
661}