1use crate::core::{
12 parse_target, resolve_in_dir, sort_processes, Process, ProcessStatus, SortKey, TargetType,
13};
14use crate::error::Result;
15use crate::ui::format::{format_memory, truncate_string};
16use clap::{Args, ValueEnum};
17use comfy_table::presets::NOTHING;
18use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table};
19use crossterm::{
20 cursor,
21 event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
22 execute, terminal,
23};
24use serde::Serialize;
25use std::collections::HashSet;
26use std::io::{self, IsTerminal, Write};
27use std::path::PathBuf;
28use std::time::{Duration, Instant};
29use sysinfo::{Pid, System};
30
31#[derive(Args, Debug)]
33pub struct WatchCommand {
34 pub target: Option<String>,
36
37 #[arg(long = "interval", short = 'n', default_value = "2")]
39 pub interval: f64,
40
41 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
43 pub in_dir: Option<String>,
44
45 #[arg(long = "by", short = 'b')]
47 pub by_name: Option<String>,
48
49 #[arg(long)]
51 pub min_cpu: Option<f32>,
52
53 #[arg(long)]
55 pub min_mem: Option<f64>,
56
57 #[arg(long, short = 'j')]
59 pub json: bool,
60
61 #[arg(long, short = 'v')]
63 pub verbose: bool,
64
65 #[arg(long, short = 'l')]
67 pub limit: Option<usize>,
68
69 #[arg(long, short = 's', value_enum, default_value_t = WatchSortKey::Cpu)]
71 pub sort: WatchSortKey,
72}
73
74#[derive(Debug, Clone, Copy, ValueEnum)]
76pub enum WatchSortKey {
77 Cpu,
79 Mem,
81 Pid,
83 Name,
85}
86
87impl From<WatchSortKey> for SortKey {
88 fn from(key: WatchSortKey) -> Self {
89 match key {
90 WatchSortKey::Cpu => SortKey::Cpu,
91 WatchSortKey::Mem => SortKey::Mem,
92 WatchSortKey::Pid => SortKey::Pid,
93 WatchSortKey::Name => SortKey::Name,
94 }
95 }
96}
97
98#[derive(Serialize)]
99struct WatchJsonOutput {
100 action: &'static str,
101 success: bool,
102 count: usize,
103 processes: Vec<Process>,
104}
105
106impl WatchCommand {
107 pub fn execute(&self) -> Result<()> {
109 let is_tty = io::stdout().is_terminal();
110
111 if self.json {
112 self.run_json_loop()
113 } else if is_tty {
114 self.run_tui_loop()
115 } else {
116 self.run_snapshot()
118 }
119 }
120
121 fn collect_processes(&self, sys: &System) -> Vec<Process> {
123 let self_pid = Pid::from_u32(std::process::id());
124 let in_dir_filter = resolve_in_dir(&self.in_dir);
125
126 let targets: Vec<TargetType> = self
128 .target
129 .as_ref()
130 .map(|t| {
131 t.split(',')
132 .map(|s| s.trim())
133 .filter(|s| !s.is_empty())
134 .map(parse_target)
135 .collect()
136 })
137 .unwrap_or_default();
138
139 let mut seen_pids = HashSet::new();
140 let mut processes = Vec::new();
141
142 if targets.is_empty() {
143 for (pid, proc) in sys.processes() {
145 if *pid == self_pid {
146 continue;
147 }
148 if seen_pids.insert(pid.as_u32()) {
149 processes.push(Process::from_sysinfo(*pid, proc));
150 }
151 }
152 } else {
153 for target in &targets {
154 match target {
155 TargetType::Port(port) => {
156 if let Ok(Some(port_info)) = crate::core::PortInfo::find_by_port(*port) {
157 let pid = Pid::from_u32(port_info.pid);
158 if let Some(proc) = sys.process(pid) {
159 if seen_pids.insert(port_info.pid) {
160 processes.push(Process::from_sysinfo(pid, proc));
161 }
162 }
163 }
164 }
165 TargetType::Pid(pid) => {
166 let sysinfo_pid = Pid::from_u32(*pid);
167 if let Some(proc) = sys.process(sysinfo_pid) {
168 if seen_pids.insert(*pid) {
169 processes.push(Process::from_sysinfo(sysinfo_pid, proc));
170 }
171 }
172 }
173 TargetType::Name(name) => {
174 let pattern_lower = name.to_lowercase();
175 for (pid, proc) in sys.processes() {
176 if *pid == self_pid {
177 continue;
178 }
179 let proc_name = proc.name().to_string_lossy().to_string();
180 let cmd: String = proc
181 .cmd()
182 .iter()
183 .map(|s| s.to_string_lossy())
184 .collect::<Vec<_>>()
185 .join(" ");
186
187 if (proc_name.to_lowercase().contains(&pattern_lower)
188 || cmd.to_lowercase().contains(&pattern_lower))
189 && seen_pids.insert(pid.as_u32())
190 {
191 processes.push(Process::from_sysinfo(*pid, proc));
192 }
193 }
194 }
195 }
196 }
197 }
198
199 if let Some(ref by_name) = self.by_name {
201 processes.retain(|p| crate::core::matches_by_filter(p, by_name));
202 }
203
204 if let Some(ref dir_path) = in_dir_filter {
206 processes.retain(|p| {
207 if let Some(ref proc_cwd) = p.cwd {
208 PathBuf::from(proc_cwd).starts_with(dir_path)
209 } else {
210 false
211 }
212 });
213 }
214
215 if let Some(min_cpu) = self.min_cpu {
217 processes.retain(|p| p.cpu_percent >= min_cpu);
218 }
219
220 if let Some(min_mem) = self.min_mem {
222 processes.retain(|p| p.memory_mb >= min_mem);
223 }
224
225 sort_processes(&mut processes, self.sort.into());
227
228 if let Some(limit) = self.limit {
230 processes.truncate(limit);
231 }
232
233 processes
234 }
235
236 fn run_tui_loop(&self) -> Result<()> {
238 let mut stdout = io::stdout();
239
240 let original_hook = std::panic::take_hook();
242 std::panic::set_hook(Box::new(move |panic_info| {
243 let _ = terminal::disable_raw_mode();
244 let _ = execute!(io::stdout(), cursor::Show, terminal::LeaveAlternateScreen);
245 original_hook(panic_info);
246 }));
247
248 execute!(stdout, terminal::EnterAlternateScreen, cursor::Hide)
250 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?;
251 terminal::enable_raw_mode()
252 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?;
253
254 let mut sys = System::new_all();
255 let interval = Duration::from_secs_f64(self.interval);
256
257 sys.refresh_all();
259 std::thread::sleep(Duration::from_millis(250));
260
261 let result = self.tui_event_loop(&mut sys, &mut stdout, interval);
262
263 let _ = terminal::disable_raw_mode();
265 let _ = execute!(stdout, cursor::Show, terminal::LeaveAlternateScreen);
266
267 result
268 }
269
270 fn tui_event_loop(
271 &self,
272 sys: &mut System,
273 stdout: &mut io::Stdout,
274 interval: Duration,
275 ) -> Result<()> {
276 loop {
277 sys.refresh_all();
278 let processes = self.collect_processes(sys);
279
280 let (width, height) = terminal::size().unwrap_or((120, 40));
282
283 let frame = self.render_frame(&processes, width, height);
285
286 execute!(stdout, cursor::MoveTo(0, 0))
288 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?;
289 execute!(stdout, terminal::Clear(terminal::ClearType::All))
291 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?;
292 execute!(stdout, cursor::MoveTo(0, 0))
293 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?;
294
295 for line in frame.lines() {
296 write!(stdout, "{}\r\n", line)
297 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?;
298 }
299 stdout
300 .flush()
301 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?;
302
303 let deadline = Instant::now() + interval;
305 loop {
306 let remaining = deadline.saturating_duration_since(Instant::now());
307 if remaining.is_zero() {
308 break;
309 }
310
311 if event::poll(remaining)
312 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?
313 {
314 if let Event::Key(KeyEvent {
315 code, modifiers, ..
316 }) = event::read()
317 .map_err(|e| crate::error::ProcError::SystemError(e.to_string()))?
318 {
319 match code {
320 KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
321 KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
322 return Ok(())
323 }
324 _ => {}
325 }
326 }
327 }
328 }
329 }
330 }
331
332 fn render_frame(&self, processes: &[Process], width: u16, height: u16) -> String {
334 let sort_label = match self.sort {
335 WatchSortKey::Cpu => "CPU",
336 WatchSortKey::Mem => "Memory",
337 WatchSortKey::Pid => "PID",
338 WatchSortKey::Name => "Name",
339 };
340
341 let target_label = self.target.as_deref().unwrap_or("all");
342
343 let header = format!(
345 " Watching {} | {} processes | Sort: {} | Refresh: {:.1}s | q to exit",
346 target_label,
347 processes.len(),
348 sort_label,
349 self.interval,
350 );
351
352 let mut output = String::new();
353 output.push_str(&header);
354 output.push('\n');
355 output.push('\n');
356
357 if processes.is_empty() {
358 output.push_str(" No matching processes found.");
359 return output;
360 }
361
362 let max_rows = (height as usize).saturating_sub(5);
364
365 if self.verbose {
366 for (i, proc) in processes.iter().enumerate() {
368 if i >= max_rows {
369 output.push_str(&format!(" ... and {} more", processes.len() - i));
370 break;
371 }
372 let status_str = format!("{:?}", proc.status);
373 output.push_str(&format!(
374 " {} {} [{}] {:.1}% CPU {} {}",
375 proc.pid,
376 proc.name,
377 status_str,
378 proc.cpu_percent,
379 format_memory(proc.memory_mb),
380 proc.user.as_deref().unwrap_or("-")
381 ));
382 output.push('\n');
383 if let Some(ref cmd) = proc.command {
384 output.push_str(&format!(" cmd: {}", cmd));
385 output.push('\n');
386 }
387 if let Some(ref path) = proc.exe_path {
388 output.push_str(&format!(" exe: {}", path));
389 output.push('\n');
390 }
391 if let Some(ref cwd) = proc.cwd {
392 output.push_str(&format!(" cwd: {}", cwd));
393 output.push('\n');
394 }
395 output.push('\n');
396 }
397 } else {
398 let mut table = Table::new();
400 table
401 .load_preset(NOTHING)
402 .set_content_arrangement(ContentArrangement::Dynamic)
403 .set_width(width);
404
405 table.set_header(vec![
406 Cell::new("PID")
407 .fg(Color::Blue)
408 .add_attribute(Attribute::Bold),
409 Cell::new("DIR")
410 .fg(Color::Blue)
411 .add_attribute(Attribute::Bold),
412 Cell::new("NAME")
413 .fg(Color::Blue)
414 .add_attribute(Attribute::Bold),
415 Cell::new("ARGS")
416 .fg(Color::Blue)
417 .add_attribute(Attribute::Bold),
418 Cell::new("CPU%")
419 .fg(Color::Blue)
420 .add_attribute(Attribute::Bold)
421 .set_alignment(CellAlignment::Right),
422 Cell::new("MEM")
423 .fg(Color::Blue)
424 .add_attribute(Attribute::Bold)
425 .set_alignment(CellAlignment::Right),
426 Cell::new("STATUS")
427 .fg(Color::Blue)
428 .add_attribute(Attribute::Bold)
429 .set_alignment(CellAlignment::Right),
430 ]);
431
432 use comfy_table::ColumnConstraint::*;
433 use comfy_table::Width::*;
434
435 let args_max = (width / 2).max(30);
436
437 table
438 .column_mut(0)
439 .expect("PID column")
440 .set_constraint(Absolute(Fixed(8)));
441 table
442 .column_mut(1)
443 .expect("DIR column")
444 .set_constraint(LowerBoundary(Fixed(20)));
445 table
446 .column_mut(2)
447 .expect("NAME column")
448 .set_constraint(LowerBoundary(Fixed(10)));
449 table
450 .column_mut(3)
451 .expect("ARGS column")
452 .set_constraint(UpperBoundary(Fixed(args_max)));
453 table
454 .column_mut(4)
455 .expect("CPU% column")
456 .set_constraint(Absolute(Fixed(8)));
457 table
458 .column_mut(5)
459 .expect("MEM column")
460 .set_constraint(Absolute(Fixed(11)));
461 table
462 .column_mut(6)
463 .expect("STATUS column")
464 .set_constraint(Absolute(Fixed(12)));
465
466 let display_count = processes.len().min(max_rows);
467 for proc in processes.iter().take(display_count) {
468 let status_str = format!("{:?}", proc.status);
469
470 let path_display = proc.cwd.as_deref().unwrap_or("-").to_string();
471
472 let cmd_display = proc
473 .command
474 .as_ref()
475 .map(|c| {
476 let parts: Vec<&str> = c.split_whitespace().collect();
477 if parts.len() > 1 {
478 let args: Vec<String> = parts[1..]
479 .iter()
480 .map(|arg| {
481 if arg.contains('/') && !arg.starts_with('-') {
482 std::path::Path::new(arg)
483 .file_name()
484 .map(|f| f.to_string_lossy().to_string())
485 .unwrap_or_else(|| arg.to_string())
486 } else {
487 arg.to_string()
488 }
489 })
490 .collect();
491 let result = args.join(" ");
492 if result.is_empty() {
493 "-".to_string()
494 } else {
495 truncate_string(&result, (args_max as usize).saturating_sub(2))
496 }
497 } else {
498 "-".to_string()
499 }
500 })
501 .unwrap_or_else(|| "-".to_string());
502
503 let mem_display = format_memory(proc.memory_mb);
504
505 let status_color = match proc.status {
506 ProcessStatus::Running => Color::Green,
507 ProcessStatus::Sleeping => Color::Blue,
508 ProcessStatus::Stopped => Color::Yellow,
509 ProcessStatus::Zombie => Color::Red,
510 _ => Color::White,
511 };
512
513 table.add_row(vec![
514 Cell::new(proc.pid).fg(Color::Cyan),
515 Cell::new(&path_display).fg(Color::DarkGrey),
516 Cell::new(&proc.name).fg(Color::White),
517 Cell::new(&cmd_display).fg(Color::DarkGrey),
518 Cell::new(format!("{:.1}", proc.cpu_percent))
519 .set_alignment(CellAlignment::Right),
520 Cell::new(&mem_display).set_alignment(CellAlignment::Right),
521 Cell::new(&status_str)
522 .fg(status_color)
523 .set_alignment(CellAlignment::Right),
524 ]);
525 }
526
527 output.push_str(&table.to_string());
528
529 if processes.len() > display_count {
530 output.push('\n');
531 output.push_str(&format!(
532 " ... and {} more (use --limit to control)",
533 processes.len() - display_count
534 ));
535 }
536 }
537
538 output
539 }
540
541 fn run_json_loop(&self) -> Result<()> {
543 let mut sys = System::new_all();
544 let interval = Duration::from_secs_f64(self.interval);
545
546 sys.refresh_all();
548 std::thread::sleep(Duration::from_millis(250));
549
550 loop {
551 sys.refresh_all();
552 let processes = self.collect_processes(&sys);
553
554 let output = WatchJsonOutput {
555 action: "watch",
556 success: true,
557 count: processes.len(),
558 processes,
559 };
560
561 match serde_json::to_string(&output) {
562 Ok(json) => {
563 if writeln!(io::stdout(), "{}", json).is_err() {
565 return Ok(()); }
567 if io::stdout().flush().is_err() {
568 return Ok(());
569 }
570 }
571 Err(e) => {
572 eprintln!("JSON serialization error: {}", e);
573 }
574 }
575
576 std::thread::sleep(interval);
577 }
578 }
579
580 fn run_snapshot(&self) -> Result<()> {
582 let mut sys = System::new_all();
583 sys.refresh_all();
584 std::thread::sleep(Duration::from_millis(250));
585 sys.refresh_all();
586
587 let processes = self.collect_processes(&sys);
588
589 if self.json {
590 let output = WatchJsonOutput {
591 action: "watch",
592 success: true,
593 count: processes.len(),
594 processes,
595 };
596 if let Ok(json) = serde_json::to_string_pretty(&output) {
597 println!("{}", json);
598 }
599 } else {
600 let printer = crate::ui::Printer::from_flags(false, self.verbose);
601 printer.print_processes(&processes);
602 }
603
604 Ok(())
605 }
606}