1use crate::core::{PortInfo, Process};
6use crate::error::{ExitCode, ProcError, 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(
469 &self,
470 action: &str,
471 succeeded: &[Process],
472 failed: &[(Process, String)],
473 ) -> Result<()> {
474 self.print_action_result_as(action, action, succeeded, failed)
475 }
476
477 pub fn print_action_result_as(
480 &self,
481 action: &str,
482 verb: &str,
483 succeeded: &[Process],
484 failed: &[(Process, String)],
485 ) -> Result<()> {
486 let past_tense = match verb {
488 "kill" => "Killed".to_string(),
489 "stop" => "Stopped".to_string(),
490 "freeze" => "Frozen".to_string(),
491 "thaw" => "Thawed".to_string(),
492 _ => format!("{}{}ed", verb[..1].to_uppercase(), &verb[1..]),
493 };
494
495 match self.format {
496 OutputFormat::Human => {
497 if !succeeded.is_empty() {
498 println!(
499 "{} {} {} process{}",
500 "✓".green().bold(),
501 past_tense,
502 succeeded.len().to_string().cyan().bold(),
503 plural(succeeded.len())
504 );
505 for proc in succeeded {
506 println!(
507 " {} {} [PID {}]",
508 "→".bright_black(),
509 proc.name.white(),
510 proc.pid.to_string().cyan()
511 );
512 }
513 }
514 if !failed.is_empty() {
515 println!(
516 "{} Failed to {} {} process{}",
517 "✗".red().bold(),
518 verb,
519 failed.len(),
520 plural(failed.len())
521 );
522 for (proc, err) in failed {
523 println!(
524 " {} {} [PID {}]: {}",
525 "→".bright_black(),
526 proc.name.white(),
527 proc.pid.to_string().cyan(),
528 err.red()
529 );
530 }
531 }
532 }
533 OutputFormat::Json => {
534 self.print_json(&ActionOutput {
535 action,
536 success: failed.is_empty(),
537 succeeded_count: succeeded.len(),
538 failed_count: failed.len(),
539 succeeded,
540 failed: &failed
541 .iter()
542 .map(|(p, e)| FailedAction {
543 process: p,
544 error: e,
545 })
546 .collect::<Vec<_>>(),
547 });
548 }
549 }
550
551 if failed.is_empty() {
552 Ok(())
553 } else {
554 Err(ProcError::reported(
555 ExitCode::GeneralError,
556 format!(
557 "Failed to {} {} process{}",
558 verb,
559 failed.len(),
560 plural(failed.len())
561 ),
562 ))
563 }
564 }
565
566 pub fn print_kill_result(
568 &self,
569 killed: &[Process],
570 failed: &[(Process, String)],
571 ) -> Result<()> {
572 self.print_action_result("kill", killed, failed)
573 }
574
575 pub fn print_dry_run(&self, action: &str, processes: &[Process]) {
580 self.print_dry_run_with(
581 action,
582 processes,
583 &format!(
584 "Dry run: would {} {} process{}",
585 action,
586 processes.len(),
587 plural(processes.len())
588 ),
589 );
590 }
591
592 pub fn print_dry_run_with(&self, action: &str, processes: &[Process], message: &str) {
596 match self.format {
597 OutputFormat::Human => {
598 self.print_processes_human(processes, None);
599 self.warning(message);
600 }
601 OutputFormat::Json => self.print_json(&DryRunOutput {
602 action,
603 success: true,
604 dry_run: true,
605 count: processes.len(),
606 processes,
607 }),
608 }
609 }
610
611 pub fn ask_confirm(&self, action: &str, processes: &[Process], yes: bool) -> Result<bool> {
616 if yes {
617 return Ok(true);
618 }
619 match self.format {
620 OutputFormat::Json => Ok(true),
621 OutputFormat::Human => {
622 self.print_confirmation(action, processes);
623 let prompt = format!(
624 "{}{} {} process{}?",
625 action[..1].to_uppercase(),
626 &action[1..],
627 processes.len(),
628 plural(processes.len())
629 );
630 let confirmed = Confirm::new()
631 .with_prompt(prompt)
632 .default(false)
633 .interact()?;
634 if !confirmed {
635 self.warning("Cancelled");
636 }
637 Ok(confirmed)
638 }
639 }
640 }
641
642 pub fn print_confirmation(&self, action: &str, processes: &[Process]) {
644 println!(
645 "\n{} Found {} process{} to {}:\n",
646 "⚠".yellow().bold(),
647 processes.len().to_string().cyan().bold(),
648 if processes.len() == 1 { "" } else { "es" },
649 action
650 );
651
652 for proc in processes {
653 println!(
654 " {} {} [PID {}] - CPU: {:.1}%, MEM: {}",
655 "→".bright_black(),
656 proc.name.white().bold(),
657 proc.pid.to_string().cyan(),
658 proc.cpu_percent,
659 format_memory(proc.memory_mb)
660 );
661 }
662 println!();
663 }
664}
665
666#[derive(Serialize)]
668struct EmptyResult<'a> {
669 action: &'a str,
670 success: bool,
671 count: usize,
672 message: &'a str,
673}
674
675#[derive(Serialize)]
676struct ProcessListOutput<'a> {
677 action: &'a str,
678 success: bool,
679 count: usize,
680 processes: &'a [Process],
681}
682
683#[derive(Serialize)]
684struct DryRunOutput<'a> {
685 action: &'a str,
686 success: bool,
687 dry_run: bool,
688 count: usize,
689 processes: &'a [Process],
690}
691
692#[derive(Serialize)]
693struct PortListOutput<'a> {
694 action: &'static str,
695 success: bool,
696 count: usize,
697 ports: &'a [PortInfo],
698}
699
700#[derive(Serialize)]
701struct SinglePortOutput<'a> {
702 action: &'static str,
703 success: bool,
704 port: &'a PortInfo,
705}
706
707#[derive(Serialize)]
708struct ActionOutput<'a> {
709 action: &'a str,
710 success: bool,
711 succeeded_count: usize,
712 failed_count: usize,
713 succeeded: &'a [Process],
714 failed: &'a [FailedAction<'a>],
715}
716
717#[derive(Serialize)]
718struct FailedAction<'a> {
719 process: &'a Process,
720 error: &'a str,
721}
722
723impl Default for Printer {
724 fn default() -> Self {
725 Self::new(OutputFormat::Human, false)
726 }
727}