1use crate::core::{PortInfo, Process};
6use colored::*;
7use serde::Serialize;
8
9#[derive(Debug, Clone, Copy, Default)]
11pub enum OutputFormat {
12 #[default]
13 Human,
14 Json,
15}
16
17pub struct Printer {
19 format: OutputFormat,
20 verbose: bool,
21}
22
23impl Printer {
24 pub fn new(format: OutputFormat, verbose: bool) -> Self {
25 Self { format, verbose }
26 }
27
28 pub fn success(&self, message: &str) {
30 match self.format {
31 OutputFormat::Human => {
32 println!("{} {}", "✓".green().bold(), message.green());
33 }
34 OutputFormat::Json => {
35 }
37 }
38 }
39
40 pub fn error(&self, message: &str) {
42 match self.format {
43 OutputFormat::Human => {
44 eprintln!("{} {}", "✗".red().bold(), message.red());
45 }
46 OutputFormat::Json => {
47 }
49 }
50 }
51
52 pub fn warning(&self, message: &str) {
54 match self.format {
55 OutputFormat::Human => {
56 println!("{} {}", "⚠".yellow().bold(), message.yellow());
57 }
58 OutputFormat::Json => {
59 }
61 }
62 }
63
64 pub fn print_processes_with_context(&self, processes: &[Process], context: Option<&str>) {
66 match self.format {
67 OutputFormat::Human => self.print_processes_human(processes, context),
68 OutputFormat::Json => self.print_json(&ProcessListOutput {
69 action: "ps",
70 success: true,
71 count: processes.len(),
72 processes,
73 }),
74 }
75 }
76
77 pub fn print_processes(&self, processes: &[Process]) {
79 self.print_processes_with_context(processes, None)
80 }
81
82 fn print_processes_human(&self, processes: &[Process], context: Option<&str>) {
83 if processes.is_empty() {
84 let msg = match context {
85 Some(ctx) => format!("No processes found {}", ctx),
86 None => "No processes found".to_string(),
87 };
88 self.warning(&msg);
89 return;
90 }
91
92 let context_str = context.map(|c| format!(" {}", c)).unwrap_or_default();
93 println!(
94 "{} Found {} process{}{}",
95 "✓".green().bold(),
96 processes.len().to_string().cyan().bold(),
97 if processes.len() == 1 { "" } else { "es" },
98 context_str.bright_black()
99 );
100 println!();
101
102 if self.verbose {
103 for proc in processes {
105 let status_str = format!("{:?}", proc.status);
106 let status_colored = colorize_status(&proc.status, &status_str);
107
108 println!(
109 "{} {} {} {:.1}% CPU {:.1} MB {}",
110 proc.pid.to_string().cyan().bold(),
111 proc.name.white().bold(),
112 format!("[{}]", status_colored).bright_black(),
113 proc.cpu_percent,
114 proc.memory_mb,
115 proc.user.as_deref().unwrap_or("-").bright_black()
116 );
117
118 if let Some(ref cmd) = proc.command {
119 println!(" {} {}", "cmd:".bright_black(), cmd);
120 }
121 if let Some(ref path) = proc.exe_path {
122 println!(" {} {}", "exe:".bright_black(), path.bright_black());
123 }
124 if let Some(ref cwd) = proc.cwd {
125 println!(" {} {}", "cwd:".bright_black(), cwd.bright_black());
126 }
127 if let Some(ppid) = proc.parent_pid {
128 println!(
129 " {} {}",
130 "parent:".bright_black(),
131 ppid.to_string().bright_black()
132 );
133 }
134 println!();
135 }
136 } else {
137 println!(
139 "{:<8} {:<35} {:<24} {:>6} {:>8} {:>10}",
140 "PID".bright_blue().bold(),
141 "PATH".bright_blue().bold(),
142 "NAME".bright_blue().bold(),
143 "CPU%".bright_blue().bold(),
144 "MEM".bright_blue().bold(),
145 "STATUS".bright_blue().bold(),
146 );
147 println!("{}", "─".repeat(95).bright_black());
148
149 for proc in processes {
150 let name = truncate_string(&proc.name, 23);
151 let status_str = format!("{:?}", proc.status);
152 let status_colored = colorize_status(&proc.status, &status_str);
153
154 let dir_display = proc
156 .exe_path
157 .as_ref()
158 .map(|p| {
159 std::path::Path::new(p)
161 .parent()
162 .map(|parent| truncate_path(&parent.to_string_lossy(), 34))
163 .unwrap_or_else(|| "-".to_string())
164 })
165 .unwrap_or_else(|| "-".to_string());
166
167 println!(
168 "{:<8} {:<35} {:<24} {:>6.1} {:>6.1}MB {:>10}",
169 proc.pid.to_string().cyan(),
170 dir_display.bright_black(),
171 name.white(),
172 proc.cpu_percent,
173 proc.memory_mb,
174 status_colored,
175 );
176 }
177 }
178 println!();
179 }
180
181 pub fn print_ports(&self, ports: &[PortInfo]) {
183 match self.format {
184 OutputFormat::Human => self.print_ports_human(ports),
185 OutputFormat::Json => self.print_json(&PortListOutput {
186 action: "ports",
187 success: true,
188 count: ports.len(),
189 ports,
190 }),
191 }
192 }
193
194 fn print_ports_human(&self, ports: &[PortInfo]) {
195 if ports.is_empty() {
196 self.warning("No listening ports found");
197 return;
198 }
199
200 println!(
201 "{} Found {} listening port{}",
202 "✓".green().bold(),
203 ports.len().to_string().cyan().bold(),
204 if ports.len() == 1 { "" } else { "s" }
205 );
206 println!();
207
208 println!(
210 "{:<8} {:<10} {:<8} {:<20} {:<15}",
211 "PORT".bright_blue().bold(),
212 "PROTO".bright_blue().bold(),
213 "PID".bright_blue().bold(),
214 "PROCESS".bright_blue().bold(),
215 "ADDRESS".bright_blue().bold()
216 );
217 println!("{}", "─".repeat(65).bright_black());
218
219 for port in ports {
220 let addr = port.address.as_deref().unwrap_or("*");
221 let proto = format!("{:?}", port.protocol).to_uppercase();
222
223 println!(
224 "{:<8} {:<10} {:<8} {:<20} {:<15}",
225 port.port.to_string().cyan().bold(),
226 proto.white(),
227 port.pid.to_string().cyan(),
228 truncate_string(&port.process_name, 19).white(),
229 addr.bright_black()
230 );
231 }
232 println!();
233 }
234
235 pub fn print_port_info(&self, port_info: &PortInfo) {
237 match self.format {
238 OutputFormat::Human => {
239 println!(
240 "{} Process on port {}:",
241 "✓".green().bold(),
242 port_info.port.to_string().cyan().bold()
243 );
244 println!();
245 println!(
246 " {} {}",
247 "Name:".bright_black(),
248 port_info.process_name.white().bold()
249 );
250 println!(
251 " {} {}",
252 "PID:".bright_black(),
253 port_info.pid.to_string().cyan()
254 );
255 println!(" {} {:?}", "Protocol:".bright_black(), port_info.protocol);
256 if let Some(ref addr) = port_info.address {
257 println!(" {} {}", "Address:".bright_black(), addr);
258 }
259 println!();
260 }
261 OutputFormat::Json => self.print_json(&SinglePortOutput {
262 action: "on",
263 success: true,
264 port: port_info,
265 }),
266 }
267 }
268
269 pub fn print_json<T: Serialize>(&self, data: &T) {
271 match serde_json::to_string_pretty(data) {
272 Ok(json) => println!("{}", json),
273 Err(e) => eprintln!("Failed to serialize JSON: {}", e),
274 }
275 }
276
277 pub fn print_kill_result(&self, killed: &[Process], failed: &[(Process, String)]) {
279 match self.format {
280 OutputFormat::Human => {
281 if !killed.is_empty() {
282 println!(
283 "{} Killed {} process{}",
284 "✓".green().bold(),
285 killed.len().to_string().cyan().bold(),
286 if killed.len() == 1 { "" } else { "es" }
287 );
288 for proc in killed {
289 println!(
290 " {} {} [PID {}]",
291 "→".bright_black(),
292 proc.name.white(),
293 proc.pid.to_string().cyan()
294 );
295 }
296 }
297 if !failed.is_empty() {
298 println!(
299 "{} Failed to kill {} process{}",
300 "✗".red().bold(),
301 failed.len(),
302 if failed.len() == 1 { "" } else { "es" }
303 );
304 for (proc, err) in failed {
305 println!(
306 " {} {} [PID {}]: {}",
307 "→".bright_black(),
308 proc.name.white(),
309 proc.pid.to_string().cyan(),
310 err.red()
311 );
312 }
313 }
314 }
315 OutputFormat::Json => {
316 self.print_json(&KillOutput {
317 action: "kill",
318 success: failed.is_empty(),
319 killed_count: killed.len(),
320 failed_count: failed.len(),
321 killed,
322 failed: &failed
323 .iter()
324 .map(|(p, e)| FailedKill {
325 process: p,
326 error: e,
327 })
328 .collect::<Vec<_>>(),
329 });
330 }
331 }
332 }
333}
334
335fn truncate_string(s: &str, max_len: usize) -> String {
337 if s.len() <= max_len {
338 s.to_string()
339 } else {
340 format!("{}...", &s[..max_len.saturating_sub(3)])
341 }
342}
343
344fn truncate_path(path: &str, max_len: usize) -> String {
346 if path.len() <= max_len {
347 path.to_string()
348 } else {
349 let start = path.len().saturating_sub(max_len.saturating_sub(3));
351 format!("...{}", &path[start..])
352 }
353}
354
355fn colorize_status(
357 status: &crate::core::ProcessStatus,
358 status_str: &str,
359) -> colored::ColoredString {
360 use colored::*;
361 match status {
362 crate::core::ProcessStatus::Running => status_str.green(),
363 crate::core::ProcessStatus::Sleeping => status_str.blue(),
364 crate::core::ProcessStatus::Stopped => status_str.yellow(),
365 crate::core::ProcessStatus::Zombie => status_str.red(),
366 _ => status_str.white(),
367 }
368}
369
370#[derive(Serialize)]
372struct ProcessListOutput<'a> {
373 action: &'static str,
374 success: bool,
375 count: usize,
376 processes: &'a [Process],
377}
378
379#[derive(Serialize)]
380struct PortListOutput<'a> {
381 action: &'static str,
382 success: bool,
383 count: usize,
384 ports: &'a [PortInfo],
385}
386
387#[derive(Serialize)]
388struct SinglePortOutput<'a> {
389 action: &'static str,
390 success: bool,
391 port: &'a PortInfo,
392}
393
394#[derive(Serialize)]
395struct KillOutput<'a> {
396 action: &'static str,
397 success: bool,
398 killed_count: usize,
399 failed_count: usize,
400 killed: &'a [Process],
401 failed: &'a [FailedKill<'a>],
402}
403
404#[derive(Serialize)]
405struct FailedKill<'a> {
406 process: &'a Process,
407 error: &'a str,
408}
409
410impl Default for Printer {
411 fn default() -> Self {
412 Self::new(OutputFormat::Human, false)
413 }
414}