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