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