Skip to main content

proc_cli/commands/
find_in.rs

1//! `proc in` - Filter processes by working directory
2//!
3//! Examples:
4//!   proc in .                  # Processes in current directory
5//!   proc in /path/to/project   # Processes in specific directory
6//!   proc in . --by node        # Processes named node in cwd
7//!   proc in . --by node --cmd  # ... or with node anywhere in their command line
8//!   proc in ~/projects         # Processes in ~/projects
9
10use 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/// Filter processes by working directory
18#[derive(Args, Debug)]
19pub struct InCommand {
20    /// Directory path (absolute, relative, or . for cwd)
21    pub path: String,
22
23    /// Filter by process name
24    #[arg(long = "by", short = 'b')]
25    pub by_name: Option<String>,
26
27    /// Match --by as a substring of the process name or full command line (like pkill -f)
28    #[arg(long)]
29    pub cmd: bool,
30
31    /// Only show processes using more than this CPU %
32    #[arg(long)]
33    pub min_cpu: Option<f32>,
34
35    /// Only show processes using more than this memory (MB)
36    #[arg(long)]
37    pub min_mem: Option<f64>,
38
39    /// Filter by status: running, sleeping, stopped, zombie
40    #[arg(long, value_enum, ignore_case = true)]
41    pub status: Option<StatusFilter>,
42
43    /// Only show processes running longer than this (seconds)
44    #[arg(long)]
45    pub min_uptime: Option<u64>,
46
47    /// Only show children of this parent PID
48    #[arg(long)]
49    pub parent: Option<u32>,
50
51    /// Output as JSON
52    #[arg(long, short = 'j')]
53    pub json: bool,
54
55    /// Show verbose output with command line, cwd, parent PID, and uptime
56    #[arg(long, short = 'v')]
57    pub verbose: bool,
58
59    /// Print full directories and arguments instead of fitting the terminal width
60    #[arg(long, short = 'w')]
61    pub wide: bool,
62
63    /// Limit the number of results
64    #[arg(long, short = 'n')]
65    pub limit: Option<usize>,
66
67    /// Sort by: cpu, mem, pid, name
68    #[arg(long, short = 's', value_enum, default_value_t = SortKey::Cpu)]
69    pub sort: SortKey,
70}
71
72impl InCommand {
73    /// Expand ~ to home directory
74    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    /// Executes the in command, listing processes in the specified directory.
88    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        // Get base process list
97        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        // Real CPU readings for the table, --min-cpu and --sort cpu
104        Process::measure_cpu(&mut processes);
105
106        // Resolve directory path
107        let dir_filter = resolve_dir(if self.path == "." {
108            PathBuf::from(".")
109        } else {
110            Self::expand_tilde(&self.path)
111        });
112
113        // Directory filter (required for this command)
114        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        // A missing path with nothing running there is a typo, not an empty
121        // directory (same as `proc for`). A deleted directory that processes
122        // still run in is kept, so its leftovers can be found.
123        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        // Apply filters
131        processes.retain(|p| self.keeps(p));
132
133        // Sort processes
134        sort_processes(&mut processes, self.sort);
135
136        // Apply limit if specified
137        if let Some(limit) = self.limit {
138            processes.truncate(limit);
139        }
140
141        // Build context string for output
142        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    /// Whether a process passes every filter but the directory: `--min-cpu`,
153    /// `--min-mem`, `--status`, `--min-uptime` and `--parent`.
154    fn keeps(&self, p: &Process) -> bool {
155        // CPU filter
156        if let Some(min_cpu) = self.min_cpu {
157            if p.cpu_percent < min_cpu {
158                return false;
159            }
160        }
161
162        // Memory filter
163        if let Some(min_mem) = self.min_mem {
164            if p.memory_mb < min_mem {
165                return false;
166            }
167        }
168
169        // Status filter
170        if let Some(status) = self.status {
171            if !status.matches(p.status) {
172                return false;
173            }
174        }
175
176        // Uptime filter
177        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        // Parent PID filter
192        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    /// A process record with no start time, which some processes really are
226    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}