1use crate::core::{
10 find_ports_for_pid, resolve_in_dir, sort_processes, PortInfo, Process, ProcessStatus, SortKey,
11};
12use crate::error::{ProcError, Result};
13use crate::ui::{format_memory, truncate_string, Printer};
14use clap::Args;
15use colored::*;
16use serde::Serialize;
17use std::collections::HashSet;
18use std::path::PathBuf;
19
20#[derive(Args, Debug)]
22pub struct ForCommand {
23 pub file: String,
25
26 #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
28 pub in_dir: Option<String>,
29
30 #[arg(long = "by", short = 'b')]
32 pub by_name: Option<String>,
33
34 #[arg(long)]
36 pub min_cpu: Option<f32>,
37
38 #[arg(long)]
40 pub min_mem: Option<f64>,
41
42 #[arg(long)]
44 pub status: Option<String>,
45
46 #[arg(long)]
48 pub min_uptime: Option<u64>,
49
50 #[arg(long, short = 'j')]
52 pub json: bool,
53
54 #[arg(long, short = 'v')]
56 pub verbose: bool,
57
58 #[arg(long, short = 's', value_enum, default_value_t = SortKey::Cpu)]
60 pub sort: SortKey,
61
62 #[arg(long, short = 'n')]
64 pub limit: Option<usize>,
65}
66
67impl ForCommand {
68 pub fn execute(&self) -> Result<()> {
70 let file_path = self.resolve_path(&self.file)?;
72
73 let exe_processes = Process::find_by_exe_path(&file_path)?;
75
76 let open_file_procs = Process::find_by_open_file(&file_path)?;
78
79 let mut seen_pids = HashSet::new();
81 let mut processes: Vec<Process> = Vec::new();
82
83 for proc in exe_processes {
84 if seen_pids.insert(proc.pid) {
85 processes.push(proc);
86 }
87 }
88
89 for proc in open_file_procs {
90 if seen_pids.insert(proc.pid) {
91 processes.push(proc);
92 }
93 }
94
95 self.apply_filters(&mut processes);
97
98 sort_processes(&mut processes, self.sort);
100
101 if let Some(limit) = self.limit {
103 processes.truncate(limit);
104 }
105
106 if processes.is_empty() {
108 return Err(ProcError::ProcessNotFound(format!(
109 "No processes found for file: {}",
110 self.file
111 )));
112 }
113
114 let mut results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
116 for proc in processes {
117 let ports = find_ports_for_pid(proc.pid)?;
118 results.push((proc, ports));
119 }
120
121 if self.json {
123 self.print_json(&results)?;
124 } else {
125 self.print_human(&results);
126 }
127
128 Ok(())
129 }
130
131 fn resolve_path(&self, path: &str) -> Result<PathBuf> {
132 let expanded = if let Some(stripped) = path.strip_prefix("~/") {
134 if let Ok(home) = std::env::var("HOME") {
135 PathBuf::from(home).join(stripped)
136 } else {
137 PathBuf::from(path)
138 }
139 } else if path == "~" {
140 PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
141 } else {
142 PathBuf::from(path)
143 };
144
145 let absolute = if expanded.is_relative() {
147 std::env::current_dir()?.join(expanded)
148 } else {
149 expanded
150 };
151
152 absolute
154 .canonicalize()
155 .map_err(|_| ProcError::InvalidInput(format!("File not found: {}", path)))
156 }
157
158 fn apply_filters(&self, processes: &mut Vec<Process>) {
159 let in_dir_filter = resolve_in_dir(&self.in_dir);
160
161 processes.retain(|p| {
162 if let Some(ref dir_path) = in_dir_filter {
164 if let Some(ref proc_cwd) = p.cwd {
165 let proc_path = PathBuf::from(proc_cwd);
166 if !proc_path.starts_with(dir_path) {
167 return false;
168 }
169 } else {
170 return false;
171 }
172 }
173
174 if let Some(ref name) = self.by_name {
176 if !crate::core::matches_by_filter(p, name) {
177 return false;
178 }
179 }
180
181 if let Some(min_cpu) = self.min_cpu {
183 if p.cpu_percent < min_cpu {
184 return false;
185 }
186 }
187
188 if let Some(min_mem) = self.min_mem {
190 if p.memory_mb < min_mem {
191 return false;
192 }
193 }
194
195 if let Some(ref status) = self.status {
197 let status_match = match status.to_lowercase().as_str() {
198 "running" => matches!(p.status, ProcessStatus::Running),
199 "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
200 "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
201 "zombie" => matches!(p.status, ProcessStatus::Zombie),
202 _ => true,
203 };
204 if !status_match {
205 return false;
206 }
207 }
208
209 if let Some(min_uptime) = self.min_uptime {
211 if let Some(start_time) = p.start_time {
212 let now = std::time::SystemTime::now()
213 .duration_since(std::time::UNIX_EPOCH)
214 .map(|d| d.as_secs())
215 .unwrap_or(0);
216 if now.saturating_sub(start_time) < min_uptime {
217 return false;
218 }
219 } else {
220 return false;
221 }
222 }
223
224 true
225 });
226 }
227
228 fn print_human(&self, results: &[(Process, Vec<PortInfo>)]) {
229 let count = results.len();
230 let file_display = &self.file;
231
232 if count == 1 {
233 let (proc, ports) = &results[0];
235
236 println!(
237 "{} Found 1 process for {}",
238 "✓".green().bold(),
239 file_display.cyan().bold()
240 );
241 println!();
242
243 println!(" {}", "Process:".bright_black());
244 println!(
245 " {} {} (PID {})",
246 "Name:".bright_black(),
247 proc.name.white().bold(),
248 proc.pid.to_string().cyan()
249 );
250 println!(" {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
251 println!(
252 " {} {}",
253 "MEM:".bright_black(),
254 format_memory(proc.memory_mb)
255 );
256
257 if let Some(ref path) = proc.exe_path {
258 println!(" {} {}", "Path:".bright_black(), path.bright_black());
259 }
260
261 if self.verbose {
262 if let Some(ref cwd) = proc.cwd {
263 println!(" {} {}", "CWD:".bright_black(), cwd.bright_black());
264 }
265 if let Some(ref cmd) = proc.command {
266 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
267 }
268 }
269
270 println!();
271
272 if ports.is_empty() {
273 println!(" {} No listening ports", "ℹ".blue());
274 } else {
275 println!(" {}", "Listening Ports:".bright_black());
276 for port_info in ports {
277 let addr = port_info.address.as_deref().unwrap_or("*");
278 println!(
279 " {} :{} ({} on {})",
280 "→".bright_black(),
281 port_info.port.to_string().cyan(),
282 format!("{:?}", port_info.protocol).to_uppercase(),
283 addr
284 );
285 }
286 }
287 } else {
288 println!(
290 "{} Found {} processes for {}",
291 "✓".green().bold(),
292 count.to_string().white().bold(),
293 file_display.cyan().bold()
294 );
295 println!();
296
297 println!(
299 " {:>7} {:<15} {:>5} {:>8} {}",
300 "PID".bright_black(),
301 "NAME".bright_black(),
302 "CPU%".bright_black(),
303 "MEM".bright_black(),
304 "PORTS".bright_black()
305 );
306 println!(" {}", "─".repeat(60).bright_black());
307
308 for (proc, ports) in results {
309 let ports_str = if ports.is_empty() {
310 "-".to_string()
311 } else {
312 ports
313 .iter()
314 .map(|p| format!(":{}", p.port))
315 .collect::<Vec<_>>()
316 .join(", ")
317 };
318
319 println!(
320 " {:>7} {:<15} {:>5.1} {:>8} {}",
321 proc.pid.to_string().cyan(),
322 truncate_string(&proc.name, 15).white(),
323 proc.cpu_percent,
324 format_memory(proc.memory_mb),
325 ports_str
326 );
327 }
328 }
329
330 println!();
331 }
332
333 fn print_json(&self, results: &[(Process, Vec<PortInfo>)]) -> Result<()> {
334 let printer = Printer::from_flags(true, self.verbose);
335 let items: Vec<ProcessForJson> = results
336 .iter()
337 .map(|(proc, ports)| ProcessForJson {
338 process: proc,
339 ports,
340 })
341 .collect();
342
343 printer.print_json(&ForOutput {
344 action: "for",
345 success: true,
346 count: items.len(),
347 results: &items,
348 });
349 Ok(())
350 }
351}
352
353#[derive(Serialize)]
354struct ProcessForJson<'a> {
355 process: &'a Process,
356 ports: &'a [PortInfo],
357}
358
359#[derive(Serialize)]
360struct ForOutput<'a> {
361 action: &'static str,
362 success: bool,
363 count: usize,
364 results: &'a [ProcessForJson<'a>],
365}