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 (biggest first); pid, name (ascending). Ties come by PID
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| crate::core::filters::matches_in_filter(p, &dir_filter));
115
116        // A missing path with nothing running there is a typo, not an empty
117        // directory (same as `proc for`). A deleted directory that processes
118        // still run in is kept, so its leftovers can be found.
119        if processes.is_empty() && !dir_filter.is_dir() {
120            return Err(ProcError::InvalidInput(format!(
121                "Directory not found: {}",
122                self.path
123            )));
124        }
125
126        // Apply filters
127        processes.retain(|p| self.keeps(p));
128
129        // Sort processes
130        sort_processes(&mut processes, self.sort);
131
132        // Apply limit if specified
133        if let Some(limit) = self.limit {
134            processes.truncate(limit);
135        }
136
137        // Build context string for output
138        let mut context_parts = vec![format!("in {}", dir_filter.display())];
139        if let Some(ref name) = self.by_name {
140            context_parts.push(format!("by '{}'", name));
141        }
142        let context = Some(context_parts.join(" "));
143
144        printer.print_processes_as("in", &processes, context.as_deref());
145        Ok(())
146    }
147
148    /// Whether a process passes every filter but the directory: `--min-cpu`,
149    /// `--min-mem`, `--status`, `--min-uptime` and `--parent`.
150    fn keeps(&self, p: &Process) -> bool {
151        // CPU filter
152        if let Some(min_cpu) = self.min_cpu {
153            if p.cpu_percent < min_cpu {
154                return false;
155            }
156        }
157
158        // Memory filter
159        if let Some(min_mem) = self.min_mem {
160            if p.memory_mb < min_mem {
161                return false;
162            }
163        }
164
165        // Status filter
166        if let Some(status) = self.status {
167            if !status.matches(p.status) {
168                return false;
169            }
170        }
171
172        // Uptime filter
173        if let Some(min_uptime) = self.min_uptime {
174            if let Some(start_time) = p.start_time {
175                let now = std::time::SystemTime::now()
176                    .duration_since(std::time::UNIX_EPOCH)
177                    .map(|d| d.as_secs())
178                    .unwrap_or(0);
179                if now.saturating_sub(start_time) < min_uptime {
180                    return false;
181                }
182            } else {
183                return false;
184            }
185        }
186
187        // Parent PID filter
188        if let Some(ppid) = self.parent {
189            if p.parent_pid != Some(ppid) {
190                return false;
191            }
192        }
193
194        true
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::core::ProcessStatus;
202
203    fn command() -> InCommand {
204        InCommand {
205            path: ".".to_string(),
206            by_name: None,
207            cmd: false,
208            min_cpu: None,
209            min_mem: None,
210            status: None,
211            min_uptime: None,
212            parent: None,
213            json: false,
214            verbose: false,
215            wide: false,
216            limit: None,
217            sort: SortKey::Cpu,
218        }
219    }
220
221    /// A process record with no start time, which some processes really are
222    fn bare_process() -> Process {
223        Process {
224            pid: 1,
225            name: "x".to_string(),
226            exe_path: None,
227            cwd: None,
228            command: None,
229            argv0: None,
230            raw: None,
231            cpu_percent: 0.0,
232            memory_mb: 0.0,
233            status: ProcessStatus::Sleeping,
234            user: None,
235            parent_pid: None,
236            start_time: None,
237            uptime_secs: None,
238        }
239    }
240
241    #[test]
242    fn a_process_with_no_start_time_fails_min_uptime() {
243        let cmd = InCommand {
244            min_uptime: Some(0),
245            ..command()
246        };
247        assert!(!cmd.keeps(&bare_process()));
248        assert!(command().keeps(&bare_process()));
249    }
250
251    #[test]
252    fn a_process_that_passes_every_filter_is_kept() {
253        let mut process = bare_process();
254        process.start_time = Some(0);
255        process.parent_pid = Some(7);
256        let cmd = InCommand {
257            min_cpu: Some(0.0),
258            min_mem: Some(0.0),
259            status: Some(StatusFilter::Sleeping),
260            min_uptime: Some(1),
261            parent: Some(7),
262            ..command()
263        };
264        assert!(cmd.keeps(&process));
265        let other_parent = InCommand {
266            parent: Some(8),
267            ..command()
268        };
269        assert!(!other_parent.keeps(&process));
270    }
271}