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        # Node processes in cwd
7//!   proc in ~/projects         # Processes in ~/projects
8
9use crate::core::{Process, ProcessStatus};
10use crate::error::Result;
11use crate::ui::{OutputFormat, Printer};
12use clap::Args;
13use std::path::PathBuf;
14
15/// Filter processes by working directory
16#[derive(Args, Debug)]
17pub struct InCommand {
18    /// Directory path (absolute, relative, or . for cwd)
19    pub path: String,
20
21    /// Filter by process name
22    #[arg(long = "by", short = 'b')]
23    pub by_name: Option<String>,
24
25    /// Filter by executable path
26    #[arg(long, short = 'p')]
27    pub exe_path: Option<String>,
28
29    /// Only show processes using more than this CPU %
30    #[arg(long)]
31    pub min_cpu: Option<f32>,
32
33    /// Only show processes using more than this memory (MB)
34    #[arg(long)]
35    pub min_mem: Option<f64>,
36
37    /// Filter by status: running, sleeping, stopped, zombie
38    #[arg(long)]
39    pub status: Option<String>,
40
41    /// Output as JSON
42    #[arg(long, short = 'j')]
43    pub json: bool,
44
45    /// Show verbose output with command line, cwd, and parent PID
46    #[arg(long, short = 'v')]
47    pub verbose: bool,
48
49    /// Limit the number of results
50    #[arg(long, short = 'n')]
51    pub limit: Option<usize>,
52
53    /// Sort by: cpu, mem, pid, name
54    #[arg(long, short = 's', default_value = "cpu")]
55    pub sort: String,
56}
57
58impl InCommand {
59    /// Expand ~ to home directory
60    fn expand_tilde(path: &str) -> PathBuf {
61        if let Some(stripped) = path.strip_prefix("~/") {
62            if let Ok(home) = std::env::var("HOME") {
63                return PathBuf::from(home).join(stripped);
64            }
65        } else if path == "~" {
66            if let Ok(home) = std::env::var("HOME") {
67                return PathBuf::from(home);
68            }
69        }
70        PathBuf::from(path)
71    }
72
73    /// Executes the in command, listing processes in the specified directory.
74    pub fn execute(&self) -> Result<()> {
75        let format = if self.json {
76            OutputFormat::Json
77        } else {
78            OutputFormat::Human
79        };
80        let printer = Printer::new(format, self.verbose);
81
82        // Get base process list
83        let mut processes = if let Some(ref name) = self.by_name {
84            Process::find_by_name(name)?
85        } else {
86            Process::find_all()?
87        };
88
89        // Resolve directory path
90        let dir_filter = if self.path == "." {
91            std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
92        } else {
93            let expanded = Self::expand_tilde(&self.path);
94            if expanded.is_relative() {
95                std::env::current_dir()
96                    .unwrap_or_else(|_| PathBuf::from("."))
97                    .join(expanded)
98            } else {
99                expanded
100            }
101        };
102
103        // Resolve executable path filter
104        let exe_path_filter: Option<PathBuf> = self.exe_path.as_ref().map(|p| {
105            let path = PathBuf::from(p);
106            if path.is_relative() {
107                std::env::current_dir()
108                    .unwrap_or_else(|_| PathBuf::from("."))
109                    .join(path)
110            } else {
111                path
112            }
113        });
114
115        // Apply filters
116        processes.retain(|p| {
117            // Directory filter (required for this command)
118            if let Some(ref proc_cwd) = p.cwd {
119                let proc_path = PathBuf::from(proc_cwd);
120                if !proc_path.starts_with(&dir_filter) {
121                    return false;
122                }
123            } else {
124                return false;
125            }
126
127            // Executable path filter
128            if let Some(ref exe_path) = exe_path_filter {
129                if let Some(ref proc_exe) = p.exe_path {
130                    let proc_path = PathBuf::from(proc_exe);
131                    if !proc_path.starts_with(exe_path) {
132                        return false;
133                    }
134                } else {
135                    return false;
136                }
137            }
138
139            // CPU filter
140            if let Some(min_cpu) = self.min_cpu {
141                if p.cpu_percent < min_cpu {
142                    return false;
143                }
144            }
145
146            // Memory filter
147            if let Some(min_mem) = self.min_mem {
148                if p.memory_mb < min_mem {
149                    return false;
150                }
151            }
152
153            // Status filter
154            if let Some(ref status) = self.status {
155                let status_match = match status.to_lowercase().as_str() {
156                    "running" => matches!(p.status, ProcessStatus::Running),
157                    "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
158                    "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
159                    "zombie" => matches!(p.status, ProcessStatus::Zombie),
160                    _ => true,
161                };
162                if !status_match {
163                    return false;
164                }
165            }
166
167            true
168        });
169
170        // Sort processes
171        match self.sort.to_lowercase().as_str() {
172            "cpu" => processes.sort_by(|a, b| {
173                b.cpu_percent
174                    .partial_cmp(&a.cpu_percent)
175                    .unwrap_or(std::cmp::Ordering::Equal)
176            }),
177            "mem" | "memory" => processes.sort_by(|a, b| {
178                b.memory_mb
179                    .partial_cmp(&a.memory_mb)
180                    .unwrap_or(std::cmp::Ordering::Equal)
181            }),
182            "pid" => processes.sort_by_key(|p| p.pid),
183            "name" => processes.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())),
184            _ => {} // Keep default order
185        }
186
187        // Apply limit if specified
188        if let Some(limit) = self.limit {
189            processes.truncate(limit);
190        }
191
192        // Build context string for output
193        let mut context_parts = vec![format!("in {}", dir_filter.display())];
194        if let Some(ref name) = self.by_name {
195            context_parts.push(format!("by '{}'", name));
196        }
197        let context = Some(context_parts.join(" "));
198
199        printer.print_processes_with_context(&processes, context.as_deref());
200        Ok(())
201    }
202}