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};
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 let name_lower = name.to_lowercase();
177 if !p.name.to_lowercase().contains(&name_lower) {
178 return false;
179 }
180 }
181
182 if let Some(min_cpu) = self.min_cpu {
184 if p.cpu_percent < min_cpu {
185 return false;
186 }
187 }
188
189 if let Some(min_mem) = self.min_mem {
191 if p.memory_mb < min_mem {
192 return false;
193 }
194 }
195
196 if let Some(ref status) = self.status {
198 let status_match = match status.to_lowercase().as_str() {
199 "running" => matches!(p.status, ProcessStatus::Running),
200 "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
201 "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
202 "zombie" => matches!(p.status, ProcessStatus::Zombie),
203 _ => true,
204 };
205 if !status_match {
206 return false;
207 }
208 }
209
210 if let Some(min_uptime) = self.min_uptime {
212 if let Some(start_time) = p.start_time {
213 let now = std::time::SystemTime::now()
214 .duration_since(std::time::UNIX_EPOCH)
215 .map(|d| d.as_secs())
216 .unwrap_or(0);
217 if now.saturating_sub(start_time) < min_uptime {
218 return false;
219 }
220 } else {
221 return false;
222 }
223 }
224
225 true
226 });
227 }
228
229 fn print_human(&self, results: &[(Process, Vec<PortInfo>)]) {
230 let count = results.len();
231 let file_display = &self.file;
232
233 if count == 1 {
234 let (proc, ports) = &results[0];
236
237 println!(
238 "{} Found 1 process for {}",
239 "✓".green().bold(),
240 file_display.cyan().bold()
241 );
242 println!();
243
244 println!(" {}", "Process:".bright_black());
245 println!(
246 " {} {} (PID {})",
247 "Name:".bright_black(),
248 proc.name.white().bold(),
249 proc.pid.to_string().cyan()
250 );
251 println!(" {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
252 println!(
253 " {} {}",
254 "MEM:".bright_black(),
255 format_memory(proc.memory_mb)
256 );
257
258 if let Some(ref path) = proc.exe_path {
259 println!(" {} {}", "Path:".bright_black(), path.bright_black());
260 }
261
262 if self.verbose {
263 if let Some(ref cwd) = proc.cwd {
264 println!(" {} {}", "CWD:".bright_black(), cwd.bright_black());
265 }
266 if let Some(ref cmd) = proc.command {
267 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
268 }
269 }
270
271 println!();
272
273 if ports.is_empty() {
274 println!(" {} No listening ports", "ℹ".blue());
275 } else {
276 println!(" {}", "Listening Ports:".bright_black());
277 for port_info in ports {
278 let addr = port_info.address.as_deref().unwrap_or("*");
279 println!(
280 " {} :{} ({} on {})",
281 "→".bright_black(),
282 port_info.port.to_string().cyan(),
283 format!("{:?}", port_info.protocol).to_uppercase(),
284 addr
285 );
286 }
287 }
288 } else {
289 println!(
291 "{} Found {} processes for {}",
292 "✓".green().bold(),
293 count.to_string().white().bold(),
294 file_display.cyan().bold()
295 );
296 println!();
297
298 println!(
300 " {:>7} {:<15} {:>5} {:>8} {}",
301 "PID".bright_black(),
302 "NAME".bright_black(),
303 "CPU%".bright_black(),
304 "MEM".bright_black(),
305 "PORTS".bright_black()
306 );
307 println!(" {}", "─".repeat(60).bright_black());
308
309 for (proc, ports) in results {
310 let ports_str = if ports.is_empty() {
311 "-".to_string()
312 } else {
313 ports
314 .iter()
315 .map(|p| format!(":{}", p.port))
316 .collect::<Vec<_>>()
317 .join(", ")
318 };
319
320 println!(
321 " {:>7} {:<15} {:>5.1} {:>8} {}",
322 proc.pid.to_string().cyan(),
323 truncate_string(&proc.name, 15).white(),
324 proc.cpu_percent,
325 format_memory(proc.memory_mb),
326 ports_str
327 );
328 }
329 }
330
331 println!();
332 }
333
334 fn print_json(&self, results: &[(Process, Vec<PortInfo>)]) -> Result<()> {
335 let output: Vec<ProcessForJson> = results
336 .iter()
337 .map(|(proc, ports)| ProcessForJson {
338 process: proc,
339 ports,
340 })
341 .collect();
342
343 println!("{}", serde_json::to_string_pretty(&output)?);
344 Ok(())
345 }
346}
347
348#[derive(Serialize)]
349struct ProcessForJson<'a> {
350 process: &'a Process,
351 ports: &'a [PortInfo],
352}