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