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_duration, 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 || crate::core::is_thread(proc) {
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 || crate::core::is_thread(proc) {
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 if let Some(uptime) = proc.uptime_secs {
396 output.push_str(&format!(" uptime: {}", format_duration(uptime)));
397 output.push('\n');
398 }
399 output.push('\n');
400 }
401 } else {
402 let mut table = Table::new();
404 table
405 .load_preset(NOTHING)
406 .set_content_arrangement(ContentArrangement::Dynamic)
407 .set_width(width);
408
409 let show_uptime = width >= 100;
412
413 let mut header = vec![
414 Cell::new("PID")
415 .fg(Color::Blue)
416 .add_attribute(Attribute::Bold),
417 Cell::new("DIR")
418 .fg(Color::Blue)
419 .add_attribute(Attribute::Bold),
420 Cell::new("NAME")
421 .fg(Color::Blue)
422 .add_attribute(Attribute::Bold),
423 Cell::new("ARGS")
424 .fg(Color::Blue)
425 .add_attribute(Attribute::Bold),
426 Cell::new("CPU%")
427 .fg(Color::Blue)
428 .add_attribute(Attribute::Bold)
429 .set_alignment(CellAlignment::Right),
430 Cell::new("MEM")
431 .fg(Color::Blue)
432 .add_attribute(Attribute::Bold)
433 .set_alignment(CellAlignment::Right),
434 ];
435 if show_uptime {
436 header.push(
437 Cell::new("UPTIME")
438 .fg(Color::Blue)
439 .add_attribute(Attribute::Bold)
440 .set_alignment(CellAlignment::Right),
441 );
442 }
443 header.push(
444 Cell::new("STATUS")
445 .fg(Color::Blue)
446 .add_attribute(Attribute::Bold)
447 .set_alignment(CellAlignment::Right),
448 );
449 table.set_header(header);
450
451 use comfy_table::ColumnConstraint::*;
452 use comfy_table::Width::*;
453
454 let args_max = (width / 2).max(30);
455
456 table
457 .column_mut(0)
458 .expect("PID column")
459 .set_constraint(Absolute(Fixed(8)));
460 table
461 .column_mut(1)
462 .expect("DIR column")
463 .set_constraint(LowerBoundary(Fixed(20)));
464 table
465 .column_mut(2)
466 .expect("NAME column")
467 .set_constraint(LowerBoundary(Fixed(10)));
468 table
469 .column_mut(3)
470 .expect("ARGS column")
471 .set_constraint(UpperBoundary(Fixed(args_max)));
472 table
473 .column_mut(4)
474 .expect("CPU% column")
475 .set_constraint(Absolute(Fixed(8)));
476 table
477 .column_mut(5)
478 .expect("MEM column")
479 .set_constraint(Absolute(Fixed(11)));
480 let status_column = if show_uptime {
481 table
482 .column_mut(6)
483 .expect("UPTIME column")
484 .set_constraint(Absolute(Fixed(10))); 7
486 } else {
487 6
488 };
489 table
490 .column_mut(status_column)
491 .expect("STATUS column")
492 .set_constraint(Absolute(Fixed(12)));
493
494 let display_count = processes.len().min(max_rows);
495 for proc in processes.iter().take(display_count) {
496 let status_str = format!("{:?}", proc.status);
497
498 let path_display = proc.cwd.as_deref().unwrap_or("-").to_string();
499
500 let cmd_display = proc
501 .command
502 .as_ref()
503 .map(|c| {
504 let parts: Vec<&str> = c.split_whitespace().collect();
505 if parts.len() > 1 {
506 let args: Vec<String> = parts[1..]
507 .iter()
508 .map(|arg| {
509 if arg.contains('/') && !arg.starts_with('-') {
510 std::path::Path::new(arg)
511 .file_name()
512 .map(|f| f.to_string_lossy().to_string())
513 .unwrap_or_else(|| arg.to_string())
514 } else {
515 arg.to_string()
516 }
517 })
518 .collect();
519 let result = args.join(" ");
520 if result.is_empty() {
521 "-".to_string()
522 } else {
523 truncate_string(&result, (args_max as usize).saturating_sub(2))
524 }
525 } else {
526 "-".to_string()
527 }
528 })
529 .unwrap_or_else(|| "-".to_string());
530
531 let mem_display = format_memory(proc.memory_mb);
532
533 let status_color = match proc.status {
534 ProcessStatus::Running => Color::Green,
535 ProcessStatus::Sleeping => Color::Blue,
536 ProcessStatus::Stopped => Color::Yellow,
537 ProcessStatus::Zombie => Color::Red,
538 _ => Color::White,
539 };
540
541 let mut row = vec![
542 Cell::new(proc.pid).fg(Color::Cyan),
543 Cell::new(&path_display).fg(Color::DarkGrey),
544 Cell::new(&proc.name).fg(Color::White),
545 Cell::new(&cmd_display).fg(Color::DarkGrey),
546 Cell::new(format!("{:.1}", proc.cpu_percent))
547 .set_alignment(CellAlignment::Right),
548 Cell::new(&mem_display).set_alignment(CellAlignment::Right),
549 ];
550 if show_uptime {
551 let uptime_display = proc
552 .uptime_secs
553 .map(format_duration)
554 .unwrap_or_else(|| "-".to_string());
555 row.push(Cell::new(uptime_display).set_alignment(CellAlignment::Right));
556 }
557 row.push(
558 Cell::new(&status_str)
559 .fg(status_color)
560 .set_alignment(CellAlignment::Right),
561 );
562 table.add_row(row);
563 }
564
565 output.push_str(&table.to_string());
566
567 if processes.len() > display_count {
568 output.push('\n');
569 output.push_str(&format!(
570 " ... and {} more (use --limit to control)",
571 processes.len() - display_count
572 ));
573 }
574 }
575
576 output
577 }
578
579 fn run_json_loop(&self) -> Result<()> {
581 let mut sys = System::new_all();
582 let interval = Duration::from_secs_f64(self.interval);
583
584 sys.refresh_all();
586 std::thread::sleep(Duration::from_millis(250));
587
588 loop {
589 sys.refresh_all();
590 let processes = self.collect_processes(&sys);
591
592 let output = WatchJsonOutput {
593 action: "watch",
594 success: true,
595 count: processes.len(),
596 processes,
597 };
598
599 match serde_json::to_string(&output) {
600 Ok(json) => {
601 if writeln!(io::stdout(), "{}", json).is_err() {
603 return Ok(()); }
605 if io::stdout().flush().is_err() {
606 return Ok(());
607 }
608 }
609 Err(e) => {
610 eprintln!("JSON serialization error: {}", e);
611 }
612 }
613
614 std::thread::sleep(interval);
615 }
616 }
617
618 fn run_snapshot(&self) -> Result<()> {
620 let mut sys = System::new_all();
621 sys.refresh_all();
622 std::thread::sleep(Duration::from_millis(250));
623 sys.refresh_all();
624
625 let processes = self.collect_processes(&sys);
626
627 if self.json {
628 let output = WatchJsonOutput {
629 action: "watch",
630 success: true,
631 count: processes.len(),
632 processes,
633 };
634 if let Ok(json) = serde_json::to_string_pretty(&output) {
635 println!("{}", json);
636 }
637 } else {
638 let printer = crate::ui::Printer::from_flags(false, self.verbose);
639 printer.print_processes(&processes);
640 }
641
642 Ok(())
643 }
644}