1use crate::core::{find_ports_for_pid, PortInfo, Process, ProcessStatus};
10use crate::error::{ProcError, Result};
11use clap::Args;
12use colored::*;
13use serde::Serialize;
14use std::collections::HashSet;
15use std::path::PathBuf;
16
17#[derive(Args, Debug)]
19pub struct ForCommand {
20 pub file: String,
22
23 #[arg(long = "in", short = 'i')]
25 pub in_dir: Option<String>,
26
27 #[arg(long = "by", short = 'b')]
29 pub by_name: Option<String>,
30
31 #[arg(long)]
33 pub min_cpu: Option<f32>,
34
35 #[arg(long)]
37 pub min_mem: Option<f64>,
38
39 #[arg(long)]
41 pub status: Option<String>,
42
43 #[arg(long, short = 'j')]
45 pub json: bool,
46
47 #[arg(long, short = 'v')]
49 pub verbose: bool,
50}
51
52impl ForCommand {
53 pub fn execute(&self) -> Result<()> {
55 let file_path = self.resolve_path(&self.file)?;
57
58 let exe_processes = Process::find_by_exe_path(&file_path)?;
60
61 let open_file_procs = Process::find_by_open_file(&file_path)?;
63
64 let mut seen_pids = HashSet::new();
66 let mut processes: Vec<Process> = Vec::new();
67
68 for proc in exe_processes {
69 if seen_pids.insert(proc.pid) {
70 processes.push(proc);
71 }
72 }
73
74 for proc in open_file_procs {
75 if seen_pids.insert(proc.pid) {
76 processes.push(proc);
77 }
78 }
79
80 self.apply_filters(&mut processes);
82
83 if processes.is_empty() {
85 return Err(ProcError::ProcessNotFound(format!(
86 "No processes found for file: {}",
87 self.file
88 )));
89 }
90
91 let mut results: Vec<(Process, Vec<PortInfo>)> = Vec::new();
93 for proc in processes {
94 let ports = find_ports_for_pid(proc.pid)?;
95 results.push((proc, ports));
96 }
97
98 if self.json {
100 self.print_json(&results)?;
101 } else {
102 self.print_human(&results);
103 }
104
105 Ok(())
106 }
107
108 fn resolve_path(&self, path: &str) -> Result<PathBuf> {
109 let expanded = if let Some(stripped) = path.strip_prefix("~/") {
111 if let Ok(home) = std::env::var("HOME") {
112 PathBuf::from(home).join(stripped)
113 } else {
114 PathBuf::from(path)
115 }
116 } else if path == "~" {
117 PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
118 } else {
119 PathBuf::from(path)
120 };
121
122 let absolute = if expanded.is_relative() {
124 std::env::current_dir()?.join(expanded)
125 } else {
126 expanded
127 };
128
129 absolute
131 .canonicalize()
132 .map_err(|_| ProcError::InvalidInput(format!("File not found: {}", path)))
133 }
134
135 fn apply_filters(&self, processes: &mut Vec<Process>) {
136 let in_dir_filter: Option<PathBuf> = self.in_dir.as_ref().map(|p| {
138 if p == "." {
139 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
140 } else {
141 let path = Self::expand_tilde(p);
142 if path.is_relative() {
143 std::env::current_dir()
144 .unwrap_or_else(|_| PathBuf::from("."))
145 .join(path)
146 } else {
147 path
148 }
149 }
150 });
151
152 processes.retain(|p| {
153 if let Some(ref dir_path) = in_dir_filter {
155 if let Some(ref proc_cwd) = p.cwd {
156 let proc_path = PathBuf::from(proc_cwd);
157 if !proc_path.starts_with(dir_path) {
158 return false;
159 }
160 } else {
161 return false;
162 }
163 }
164
165 if let Some(ref name) = self.by_name {
167 let name_lower = name.to_lowercase();
168 if !p.name.to_lowercase().contains(&name_lower) {
169 return false;
170 }
171 }
172
173 if let Some(min_cpu) = self.min_cpu {
175 if p.cpu_percent < min_cpu {
176 return false;
177 }
178 }
179
180 if let Some(min_mem) = self.min_mem {
182 if p.memory_mb < min_mem {
183 return false;
184 }
185 }
186
187 if let Some(ref status) = self.status {
189 let status_match = match status.to_lowercase().as_str() {
190 "running" => matches!(p.status, ProcessStatus::Running),
191 "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
192 "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
193 "zombie" => matches!(p.status, ProcessStatus::Zombie),
194 _ => true,
195 };
196 if !status_match {
197 return false;
198 }
199 }
200
201 true
202 });
203 }
204
205 fn expand_tilde(path: &str) -> PathBuf {
206 if let Some(stripped) = path.strip_prefix("~/") {
207 if let Ok(home) = std::env::var("HOME") {
208 return PathBuf::from(home).join(stripped);
209 }
210 } else if path == "~" {
211 if let Ok(home) = std::env::var("HOME") {
212 return PathBuf::from(home);
213 }
214 }
215 PathBuf::from(path)
216 }
217
218 fn print_human(&self, results: &[(Process, Vec<PortInfo>)]) {
219 let count = results.len();
220 let file_display = &self.file;
221
222 if count == 1 {
223 let (proc, ports) = &results[0];
225
226 println!(
227 "{} Found 1 process for {}",
228 "✓".green().bold(),
229 file_display.cyan().bold()
230 );
231 println!();
232
233 println!(" {}", "Process:".bright_black());
234 println!(
235 " {} {} (PID {})",
236 "Name:".bright_black(),
237 proc.name.white().bold(),
238 proc.pid.to_string().cyan()
239 );
240 println!(" {} {:.1}%", "CPU:".bright_black(), proc.cpu_percent);
241 println!(" {} {:.1} MB", "MEM:".bright_black(), proc.memory_mb);
242
243 if let Some(ref path) = proc.exe_path {
244 println!(" {} {}", "Path:".bright_black(), path.bright_black());
245 }
246
247 if self.verbose {
248 if let Some(ref cwd) = proc.cwd {
249 println!(" {} {}", "CWD:".bright_black(), cwd.bright_black());
250 }
251 if let Some(ref cmd) = proc.command {
252 println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
253 }
254 }
255
256 println!();
257
258 if ports.is_empty() {
259 println!(" {} No listening ports", "ℹ".blue());
260 } else {
261 println!(" {}", "Listening Ports:".bright_black());
262 for port_info in ports {
263 let addr = port_info.address.as_deref().unwrap_or("*");
264 println!(
265 " {} :{} ({} on {})",
266 "→".bright_black(),
267 port_info.port.to_string().cyan(),
268 format!("{:?}", port_info.protocol).to_uppercase(),
269 addr
270 );
271 }
272 }
273 } else {
274 println!(
276 "{} Found {} processes for {}",
277 "✓".green().bold(),
278 count.to_string().white().bold(),
279 file_display.cyan().bold()
280 );
281 println!();
282
283 println!(
285 " {:>7} {:<15} {:>5} {:>8} {}",
286 "PID".bright_black(),
287 "NAME".bright_black(),
288 "CPU%".bright_black(),
289 "MEM".bright_black(),
290 "PORTS".bright_black()
291 );
292 println!(" {}", "─".repeat(60).bright_black());
293
294 for (proc, ports) in results {
295 let ports_str = if ports.is_empty() {
296 "-".to_string()
297 } else {
298 ports
299 .iter()
300 .map(|p| format!(":{}", p.port))
301 .collect::<Vec<_>>()
302 .join(", ")
303 };
304
305 println!(
306 " {:>7} {:<15} {:>5.1} {:>6.1}MB {}",
307 proc.pid.to_string().cyan(),
308 truncate(&proc.name, 15).white(),
309 proc.cpu_percent,
310 proc.memory_mb,
311 ports_str
312 );
313 }
314 }
315
316 println!();
317 }
318
319 fn print_json(&self, results: &[(Process, Vec<PortInfo>)]) -> Result<()> {
320 let output: Vec<ProcessForJson> = results
321 .iter()
322 .map(|(proc, ports)| ProcessForJson {
323 process: proc,
324 ports,
325 })
326 .collect();
327
328 println!("{}", serde_json::to_string_pretty(&output)?);
329 Ok(())
330 }
331}
332
333fn truncate(s: &str, max_len: usize) -> String {
334 if s.len() <= max_len {
335 s.to_string()
336 } else {
337 format!("{}…", &s[..max_len - 1])
338 }
339}
340
341#[derive(Serialize)]
342struct ProcessForJson<'a> {
343 process: &'a Process,
344 ports: &'a [PortInfo],
345}