1use crate::core::{PortInfo, Process};
6use crate::error::{ExitCode, ProcError, Result};
7use crate::ui::format::{colorize_status, format_duration, 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 self.print_process_rows(processes);
152 }
153
154 pub fn print_processes_under_headline(&self, action: &str, processes: &[Process]) {
158 match self.format {
159 OutputFormat::Human => {
160 println!();
161 self.print_process_rows(processes);
162 }
163 OutputFormat::Json => self.print_processes_as(action, processes, None),
164 }
165 }
166
167 fn print_process_rows(&self, processes: &[Process]) {
169 if self.verbose {
170 for proc in processes {
172 let status_str = format!("{:?}", proc.status);
173 let status_colored = colorize_status(&proc.status, &status_str);
174
175 println!(
176 "{} {} {} {:.1}% CPU {} {}",
177 proc.pid.to_string().cyan().bold(),
178 proc.name.white().bold(),
179 format!("[{}]", status_colored).bright_black(),
180 proc.cpu_percent,
181 format_memory(proc.memory_mb),
182 proc.user.as_deref().unwrap_or("-").bright_black()
183 );
184
185 if let Some(ref cmd) = proc.command {
186 println!(" {} {}", "cmd:".bright_black(), cmd);
187 }
188 if let Some(ref path) = proc.exe_path {
189 println!(" {} {}", "exe:".bright_black(), path.bright_black());
190 }
191 if let Some(ref cwd) = proc.cwd {
192 println!(" {} {}", "cwd:".bright_black(), cwd.bright_black());
193 }
194 if let Some(ppid) = proc.parent_pid {
195 println!(
196 " {} {}",
197 "parent:".bright_black(),
198 ppid.to_string().bright_black()
199 );
200 }
201 if let Some(uptime) = proc.uptime_secs {
202 println!(
203 " {} {}",
204 "uptime:".bright_black(),
205 format_duration(uptime).bright_black()
206 );
207 }
208 println!();
209 }
210 } else {
211 let width = terminal_width();
212 let show_uptime = width >= 100;
214
215 let mut table = Table::new();
216 table
217 .load_preset(NOTHING)
218 .set_content_arrangement(ContentArrangement::Dynamic)
219 .set_width(width);
220
221 let mut header = vec![
223 Cell::new("PID")
224 .fg(Color::Blue)
225 .add_attribute(Attribute::Bold),
226 Cell::new("DIR")
227 .fg(Color::Blue)
228 .add_attribute(Attribute::Bold),
229 Cell::new("NAME")
230 .fg(Color::Blue)
231 .add_attribute(Attribute::Bold),
232 Cell::new("ARGS")
233 .fg(Color::Blue)
234 .add_attribute(Attribute::Bold),
235 Cell::new("CPU%")
236 .fg(Color::Blue)
237 .add_attribute(Attribute::Bold)
238 .set_alignment(CellAlignment::Right),
239 Cell::new("MEM")
240 .fg(Color::Blue)
241 .add_attribute(Attribute::Bold)
242 .set_alignment(CellAlignment::Right),
243 ];
244 if show_uptime {
245 header.push(
246 Cell::new("UPTIME")
247 .fg(Color::Blue)
248 .add_attribute(Attribute::Bold)
249 .set_alignment(CellAlignment::Right),
250 );
251 }
252 header.push(
253 Cell::new("STATUS")
254 .fg(Color::Blue)
255 .add_attribute(Attribute::Bold)
256 .set_alignment(CellAlignment::Right),
257 );
258 table.set_header(header);
259
260 use comfy_table::ColumnConstraint::*;
262 use comfy_table::Width::*;
263 table
266 .column_mut(0)
267 .expect("PID column")
268 .set_constraint(Absolute(Fixed(8))); table
270 .column_mut(1)
271 .expect("DIR column")
272 .set_constraint(LowerBoundary(Fixed(20)));
273 table
274 .column_mut(2)
275 .expect("NAME column")
276 .set_constraint(LowerBoundary(Fixed(10)));
277 let args_max = (width / 2).max(30);
279 table
280 .column_mut(3)
281 .expect("ARGS column")
282 .set_constraint(UpperBoundary(Fixed(args_max)));
283 table
284 .column_mut(4)
285 .expect("CPU% column")
286 .set_constraint(Absolute(Fixed(8))); table
288 .column_mut(5)
289 .expect("MEM column")
290 .set_constraint(Absolute(Fixed(11))); let status_column = if show_uptime {
292 table
293 .column_mut(6)
294 .expect("UPTIME column")
295 .set_constraint(Absolute(Fixed(10))); 7
297 } else {
298 6
299 };
300 table
301 .column_mut(status_column)
302 .expect("STATUS column")
303 .set_constraint(Absolute(Fixed(12))); for proc in processes {
306 let status_str = format!("{:?}", proc.status);
307
308 let path_display = proc.cwd.as_deref().unwrap_or("-").to_string();
310
311 let cmd_display = proc
313 .command
314 .as_ref()
315 .map(|c| {
316 let parts: Vec<&str> = c.split_whitespace().collect();
317 if parts.len() > 1 {
318 let args: Vec<String> = parts[1..]
319 .iter()
320 .map(|arg| {
321 if arg.contains('/') && !arg.starts_with('-') {
322 std::path::Path::new(arg)
323 .file_name()
324 .map(|f| f.to_string_lossy().to_string())
325 .unwrap_or_else(|| arg.to_string())
326 } else {
327 arg.to_string()
328 }
329 })
330 .collect();
331 let result = args.join(" ");
332 if result.is_empty() {
333 "-".to_string()
334 } else {
335 truncate_string(&result, (args_max as usize).saturating_sub(2))
336 }
337 } else {
338 "-".to_string()
340 }
341 })
342 .unwrap_or_else(|| "-".to_string());
343
344 let mem_display = format_memory(proc.memory_mb);
345
346 let status_color = match proc.status {
347 crate::core::ProcessStatus::Running => Color::Green,
348 crate::core::ProcessStatus::Sleeping => Color::Blue,
349 crate::core::ProcessStatus::Stopped => Color::Yellow,
350 crate::core::ProcessStatus::Zombie => Color::Red,
351 _ => Color::White,
352 };
353
354 let mut row = vec![
355 Cell::new(proc.pid).fg(Color::Cyan),
356 Cell::new(&path_display).fg(Color::DarkGrey),
357 Cell::new(&proc.name).fg(Color::White),
358 Cell::new(&cmd_display).fg(Color::DarkGrey),
359 Cell::new(format!("{:.1}", proc.cpu_percent))
360 .set_alignment(CellAlignment::Right),
361 Cell::new(&mem_display).set_alignment(CellAlignment::Right),
362 ];
363 if show_uptime {
364 let uptime_display = proc
365 .uptime_secs
366 .map(format_duration)
367 .unwrap_or_else(|| "-".to_string());
368 row.push(Cell::new(uptime_display).set_alignment(CellAlignment::Right));
369 }
370 row.push(
371 Cell::new(&status_str)
372 .fg(status_color)
373 .set_alignment(CellAlignment::Right),
374 );
375 table.add_row(row);
376 }
377
378 println!("{table}");
379 }
380 println!();
381 }
382
383 pub fn print_ports(&self, ports: &[PortInfo]) {
385 match self.format {
386 OutputFormat::Human => self.print_ports_human(ports),
387 OutputFormat::Json => self.print_json(&PortListOutput {
388 action: "ports",
389 success: true,
390 count: ports.len(),
391 ports,
392 }),
393 }
394 }
395
396 fn print_ports_human(&self, ports: &[PortInfo]) {
397 if ports.is_empty() {
398 self.warning("No listening ports found");
399 return;
400 }
401
402 println!(
403 "{} Found {} listening port{}",
404 "✓".green().bold(),
405 ports.len().to_string().cyan().bold(),
406 if ports.len() == 1 { "" } else { "s" }
407 );
408 println!();
409
410 let width = terminal_width();
411
412 let mut table = Table::new();
413 table
414 .load_preset(NOTHING)
415 .set_content_arrangement(ContentArrangement::Dynamic)
416 .set_width(width);
417
418 table.set_header(vec![
419 Cell::new("PORT")
420 .fg(Color::Blue)
421 .add_attribute(Attribute::Bold),
422 Cell::new("PROTO")
423 .fg(Color::Blue)
424 .add_attribute(Attribute::Bold),
425 Cell::new("PID")
426 .fg(Color::Blue)
427 .add_attribute(Attribute::Bold),
428 Cell::new("PROCESS")
429 .fg(Color::Blue)
430 .add_attribute(Attribute::Bold),
431 Cell::new("ADDRESS")
432 .fg(Color::Blue)
433 .add_attribute(Attribute::Bold),
434 ]);
435
436 use comfy_table::ColumnConstraint::*;
437 use comfy_table::Width::*;
438 table
439 .column_mut(0)
440 .expect("PORT column")
441 .set_constraint(Absolute(Fixed(8)));
442 table
443 .column_mut(1)
444 .expect("PROTO column")
445 .set_constraint(Absolute(Fixed(6)));
446 table
447 .column_mut(2)
448 .expect("PID column")
449 .set_constraint(Absolute(Fixed(8)));
450 table
451 .column_mut(3)
452 .expect("PROCESS column")
453 .set_constraint(LowerBoundary(Fixed(12)));
454 table
455 .column_mut(4)
456 .expect("ADDRESS column")
457 .set_constraint(LowerBoundary(Fixed(10)));
458
459 for port in ports {
460 let addr = port.address.as_deref().unwrap_or("*");
461 let proto = format!("{:?}", port.protocol).to_uppercase();
462
463 table.add_row(vec![
464 Cell::new(port.port).fg(Color::Cyan),
465 Cell::new(&proto).fg(Color::White),
466 Cell::new(port.pid).fg(Color::Cyan),
467 Cell::new(truncate_string(&port.process_name, 19)).fg(Color::White),
468 Cell::new(addr).fg(Color::DarkGrey),
469 ]);
470 }
471
472 println!("{table}");
473 println!();
474 }
475
476 pub fn print_port_info(&self, port_info: &PortInfo) {
478 match self.format {
479 OutputFormat::Human => {
480 println!(
481 "{} Process on port {}:",
482 "✓".green().bold(),
483 port_info.port.to_string().cyan().bold()
484 );
485 println!();
486 println!(
487 " {} {}",
488 "Name:".bright_black(),
489 port_info.process_name.white().bold()
490 );
491 println!(
492 " {} {}",
493 "PID:".bright_black(),
494 port_info.pid.to_string().cyan()
495 );
496 println!(" {} {:?}", "Protocol:".bright_black(), port_info.protocol);
497 if let Some(ref addr) = port_info.address {
498 println!(" {} {}", "Address:".bright_black(), addr);
499 }
500 println!();
501 }
502 OutputFormat::Json => self.print_json(&SinglePortOutput {
503 action: "on",
504 success: true,
505 port: port_info,
506 }),
507 }
508 }
509
510 pub fn print_json<T: Serialize>(&self, data: &T) {
512 match serde_json::to_string_pretty(data) {
513 Ok(json) => println!("{}", json),
514 Err(e) => eprintln!("Failed to serialize JSON: {}", e),
515 }
516 }
517
518 pub fn print_action_result(
526 &self,
527 action: &str,
528 succeeded: &[Process],
529 failed: &[(Process, String)],
530 ) -> Result<()> {
531 self.print_action_result_as(action, action, succeeded, failed)
532 }
533
534 pub fn print_action_result_as(
537 &self,
538 action: &str,
539 verb: &str,
540 succeeded: &[Process],
541 failed: &[(Process, String)],
542 ) -> Result<()> {
543 let past_tense = match verb {
545 "kill" => "Killed".to_string(),
546 "stop" => "Stopped".to_string(),
547 "freeze" => "Frozen".to_string(),
548 "thaw" => "Thawed".to_string(),
549 _ => format!("{}{}ed", verb[..1].to_uppercase(), &verb[1..]),
550 };
551
552 match self.format {
553 OutputFormat::Human => {
554 if !succeeded.is_empty() {
555 println!(
556 "{} {} {} process{}",
557 "✓".green().bold(),
558 past_tense,
559 succeeded.len().to_string().cyan().bold(),
560 plural(succeeded.len())
561 );
562 for proc in succeeded {
563 println!(
564 " {} {} [PID {}]",
565 "→".bright_black(),
566 proc.name.white(),
567 proc.pid.to_string().cyan()
568 );
569 }
570 }
571 if !failed.is_empty() {
572 println!(
573 "{} Failed to {} {} process{}",
574 "✗".red().bold(),
575 verb,
576 failed.len(),
577 plural(failed.len())
578 );
579 for (proc, err) in failed {
580 println!(
581 " {} {} [PID {}]: {}",
582 "→".bright_black(),
583 proc.name.white(),
584 proc.pid.to_string().cyan(),
585 err.red()
586 );
587 }
588 }
589 }
590 OutputFormat::Json => {
591 self.print_json(&ActionOutput {
592 action,
593 success: failed.is_empty(),
594 succeeded_count: succeeded.len(),
595 failed_count: failed.len(),
596 succeeded,
597 failed: &failed
598 .iter()
599 .map(|(p, e)| FailedAction {
600 process: p,
601 error: e,
602 })
603 .collect::<Vec<_>>(),
604 });
605 }
606 }
607
608 if failed.is_empty() {
609 Ok(())
610 } else {
611 Err(ProcError::reported(
612 ExitCode::GeneralError,
613 format!(
614 "Failed to {} {} process{}",
615 verb,
616 failed.len(),
617 plural(failed.len())
618 ),
619 ))
620 }
621 }
622
623 pub fn print_kill_result(
625 &self,
626 killed: &[Process],
627 failed: &[(Process, String)],
628 ) -> Result<()> {
629 self.print_action_result("kill", killed, failed)
630 }
631
632 pub fn print_dry_run(&self, action: &str, processes: &[Process]) {
637 self.print_dry_run_with(
638 action,
639 processes,
640 &format!(
641 "Dry run: would {} {} process{}",
642 action,
643 processes.len(),
644 plural(processes.len())
645 ),
646 );
647 }
648
649 pub fn print_dry_run_with(&self, action: &str, processes: &[Process], message: &str) {
653 match self.format {
654 OutputFormat::Human => {
655 self.print_processes_human(processes, None);
656 self.warning(message);
657 }
658 OutputFormat::Json => self.print_json(&DryRunOutput {
659 action,
660 success: true,
661 dry_run: true,
662 count: processes.len(),
663 processes,
664 }),
665 }
666 }
667
668 pub fn ask_confirm(&self, action: &str, processes: &[Process], yes: bool) -> Result<bool> {
673 if yes {
674 return Ok(true);
675 }
676 match self.format {
677 OutputFormat::Json => Ok(true),
678 OutputFormat::Human => {
679 self.print_confirmation(action, processes);
680 let prompt = format!(
681 "{}{} {} process{}?",
682 action[..1].to_uppercase(),
683 &action[1..],
684 processes.len(),
685 plural(processes.len())
686 );
687 let confirmed = Confirm::new()
688 .with_prompt(prompt)
689 .default(false)
690 .interact()?;
691 if !confirmed {
692 self.warning("Cancelled");
693 }
694 Ok(confirmed)
695 }
696 }
697 }
698
699 pub fn print_confirmation(&self, action: &str, processes: &[Process]) {
701 println!(
702 "\n{} Found {} process{} to {}:\n",
703 "⚠".yellow().bold(),
704 processes.len().to_string().cyan().bold(),
705 if processes.len() == 1 { "" } else { "es" },
706 action
707 );
708
709 for proc in processes {
710 println!(
711 " {} {} [PID {}] - CPU: {:.1}%, MEM: {}",
712 "→".bright_black(),
713 proc.name.white().bold(),
714 proc.pid.to_string().cyan(),
715 proc.cpu_percent,
716 format_memory(proc.memory_mb)
717 );
718 }
719 println!();
720 }
721}
722
723#[derive(Serialize)]
725struct EmptyResult<'a> {
726 action: &'a str,
727 success: bool,
728 count: usize,
729 message: &'a str,
730}
731
732#[derive(Serialize)]
733struct ProcessListOutput<'a> {
734 action: &'a str,
735 success: bool,
736 count: usize,
737 processes: &'a [Process],
738}
739
740#[derive(Serialize)]
741struct DryRunOutput<'a> {
742 action: &'a str,
743 success: bool,
744 dry_run: bool,
745 count: usize,
746 processes: &'a [Process],
747}
748
749#[derive(Serialize)]
750struct PortListOutput<'a> {
751 action: &'static str,
752 success: bool,
753 count: usize,
754 ports: &'a [PortInfo],
755}
756
757#[derive(Serialize)]
758struct SinglePortOutput<'a> {
759 action: &'static str,
760 success: bool,
761 port: &'a PortInfo,
762}
763
764#[derive(Serialize)]
765struct ActionOutput<'a> {
766 action: &'a str,
767 success: bool,
768 succeeded_count: usize,
769 failed_count: usize,
770 succeeded: &'a [Process],
771 failed: &'a [FailedAction<'a>],
772}
773
774#[derive(Serialize)]
775struct FailedAction<'a> {
776 process: &'a Process,
777 error: &'a str,
778}
779
780impl Default for Printer {
781 fn default() -> Self {
782 Self::new(OutputFormat::Human, false)
783 }
784}