proc_cli/commands/
find_in.rs1use crate::core::filters::resolve_dir;
11use crate::core::{sort_processes, NameMatch, Process, SortKey, StatusFilter};
12use crate::error::{ProcError, Result};
13use crate::ui::{OutputFormat, Printer};
14use clap::Args;
15use std::path::PathBuf;
16
17#[derive(Args, Debug)]
19pub struct InCommand {
20 pub path: String,
22
23 #[arg(long = "by", short = 'b')]
25 pub by_name: Option<String>,
26
27 #[arg(long)]
29 pub cmd: bool,
30
31 #[arg(long)]
33 pub min_cpu: Option<f32>,
34
35 #[arg(long)]
37 pub min_mem: Option<f64>,
38
39 #[arg(long, value_enum, ignore_case = true)]
41 pub status: Option<StatusFilter>,
42
43 #[arg(long)]
45 pub min_uptime: Option<u64>,
46
47 #[arg(long)]
49 pub parent: Option<u32>,
50
51 #[arg(long, short = 'j')]
53 pub json: bool,
54
55 #[arg(long, short = 'v')]
57 pub verbose: bool,
58
59 #[arg(long, short = 'w')]
61 pub wide: bool,
62
63 #[arg(long, short = 'n')]
65 pub limit: Option<usize>,
66
67 #[arg(long, short = 's', value_enum, default_value_t = SortKey::Cpu)]
69 pub sort: SortKey,
70}
71
72impl InCommand {
73 fn expand_tilde(path: &str) -> PathBuf {
75 if let Some(stripped) = path.strip_prefix("~/") {
76 if let Ok(home) = std::env::var("HOME") {
77 return PathBuf::from(home).join(stripped);
78 }
79 } else if path == "~" {
80 if let Ok(home) = std::env::var("HOME") {
81 return PathBuf::from(home);
82 }
83 }
84 PathBuf::from(path)
85 }
86
87 pub fn execute(&self) -> Result<()> {
89 let format = if self.json {
90 OutputFormat::Json
91 } else {
92 OutputFormat::Human
93 };
94 let printer = Printer::new(format, self.verbose).wide(self.wide);
95
96 let mut processes = if let Some(ref name) = self.by_name {
98 Process::find_by_name(name, NameMatch::from_flag(self.cmd))?
99 } else {
100 Process::find_all()?
101 };
102
103 Process::measure_cpu(&mut processes);
105
106 let dir_filter = resolve_dir(if self.path == "." {
108 PathBuf::from(".")
109 } else {
110 Self::expand_tilde(&self.path)
111 });
112
113 processes.retain(|p| {
115 p.cwd
116 .as_ref()
117 .is_some_and(|cwd| crate::core::filters::cwd_within(cwd, &dir_filter))
118 });
119
120 if processes.is_empty() && !dir_filter.is_dir() {
124 return Err(ProcError::InvalidInput(format!(
125 "Directory not found: {}",
126 self.path
127 )));
128 }
129
130 processes.retain(|p| self.keeps(p));
132
133 sort_processes(&mut processes, self.sort);
135
136 if let Some(limit) = self.limit {
138 processes.truncate(limit);
139 }
140
141 let mut context_parts = vec![format!("in {}", dir_filter.display())];
143 if let Some(ref name) = self.by_name {
144 context_parts.push(format!("by '{}'", name));
145 }
146 let context = Some(context_parts.join(" "));
147
148 printer.print_processes_as("in", &processes, context.as_deref());
149 Ok(())
150 }
151
152 fn keeps(&self, p: &Process) -> bool {
155 if let Some(min_cpu) = self.min_cpu {
157 if p.cpu_percent < min_cpu {
158 return false;
159 }
160 }
161
162 if let Some(min_mem) = self.min_mem {
164 if p.memory_mb < min_mem {
165 return false;
166 }
167 }
168
169 if let Some(status) = self.status {
171 if !status.matches(p.status) {
172 return false;
173 }
174 }
175
176 if let Some(min_uptime) = self.min_uptime {
178 if let Some(start_time) = p.start_time {
179 let now = std::time::SystemTime::now()
180 .duration_since(std::time::UNIX_EPOCH)
181 .map(|d| d.as_secs())
182 .unwrap_or(0);
183 if now.saturating_sub(start_time) < min_uptime {
184 return false;
185 }
186 } else {
187 return false;
188 }
189 }
190
191 if let Some(ppid) = self.parent {
193 if p.parent_pid != Some(ppid) {
194 return false;
195 }
196 }
197
198 true
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crate::core::ProcessStatus;
206
207 fn command() -> InCommand {
208 InCommand {
209 path: ".".to_string(),
210 by_name: None,
211 cmd: false,
212 min_cpu: None,
213 min_mem: None,
214 status: None,
215 min_uptime: None,
216 parent: None,
217 json: false,
218 verbose: false,
219 wide: false,
220 limit: None,
221 sort: SortKey::Cpu,
222 }
223 }
224
225 fn bare_process() -> Process {
227 Process {
228 pid: 1,
229 name: "x".to_string(),
230 exe_path: None,
231 cwd: None,
232 command: None,
233 argv0: None,
234 raw: None,
235 cpu_percent: 0.0,
236 memory_mb: 0.0,
237 status: ProcessStatus::Sleeping,
238 user: None,
239 parent_pid: None,
240 start_time: None,
241 uptime_secs: None,
242 }
243 }
244
245 #[test]
246 fn a_process_with_no_start_time_fails_min_uptime() {
247 let cmd = InCommand {
248 min_uptime: Some(0),
249 ..command()
250 };
251 assert!(!cmd.keeps(&bare_process()));
252 assert!(command().keeps(&bare_process()));
253 }
254
255 #[test]
256 fn a_process_that_passes_every_filter_is_kept() {
257 let mut process = bare_process();
258 process.start_time = Some(0);
259 process.parent_pid = Some(7);
260 let cmd = InCommand {
261 min_cpu: Some(0.0),
262 min_mem: Some(0.0),
263 status: Some(StatusFilter::Sleeping),
264 min_uptime: Some(1),
265 parent: Some(7),
266 ..command()
267 };
268 assert!(cmd.keeps(&process));
269 let other_parent = InCommand {
270 parent: Some(8),
271 ..command()
272 };
273 assert!(!other_parent.keeps(&process));
274 }
275}