Skip to main content

proc_cli/commands/
list.rs

1//! `proc list` - List processes
2//!
3//! Examples:
4//!   proc list                  # List all processes
5//!   proc list node             # Filter by name
6//!   proc list --in             # Processes in current directory
7//!   proc list --in /project    # Processes in /project
8//!   proc list --min-cpu 10     # Processes using >10% CPU
9
10use crate::core::{resolve_in_dir, Process, ProcessStatus};
11use crate::error::Result;
12use crate::ui::{OutputFormat, Printer};
13use clap::Args;
14use std::path::PathBuf;
15
16/// List processes
17#[derive(Args, Debug)]
18pub struct ListCommand {
19    /// Process name or pattern to filter by
20    pub name: Option<String>,
21
22    /// Filter by directory (defaults to current directory if no path given)
23    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
24    pub in_dir: Option<String>,
25
26    /// Filter by executable path
27    #[arg(long, short = 'p')]
28    pub path: Option<String>,
29
30    /// Only show processes using more than this CPU %
31    #[arg(long)]
32    pub min_cpu: Option<f32>,
33
34    /// Only show processes using more than this memory (MB)
35    #[arg(long)]
36    pub min_mem: Option<f64>,
37
38    /// Filter by status: running, sleeping, stopped, zombie
39    #[arg(long)]
40    pub status: Option<String>,
41
42    /// Only show processes running longer than this (seconds)
43    #[arg(long)]
44    pub min_uptime: Option<u64>,
45
46    /// Only show children of this parent PID
47    #[arg(long)]
48    pub parent: Option<u32>,
49
50    /// Output as JSON
51    #[arg(long, short = 'j')]
52    pub json: bool,
53
54    /// Show verbose output with command line, cwd, and parent PID
55    #[arg(long, short = 'v')]
56    pub verbose: bool,
57
58    /// Limit the number of results
59    #[arg(long, short = 'n')]
60    pub limit: Option<usize>,
61
62    /// Sort by: cpu, mem, pid, name
63    #[arg(long, short = 's', default_value = "cpu")]
64    pub sort: String,
65}
66
67impl ListCommand {
68    /// Executes the list command, displaying processes matching the filters.
69    pub fn execute(&self) -> Result<()> {
70        let format = if self.json {
71            OutputFormat::Json
72        } else {
73            OutputFormat::Human
74        };
75        let printer = Printer::new(format, self.verbose);
76
77        // Get base process list (supports comma-separated names)
78        let mut processes = if let Some(ref name) = self.name {
79            let names: Vec<String> = name
80                .split(',')
81                .map(|s| s.trim().to_string())
82                .filter(|s| !s.is_empty())
83                .collect();
84            let mut all = Vec::new();
85            let mut seen = std::collections::HashSet::new();
86            for n in &names {
87                if let Ok(found) = Process::find_by_name(n) {
88                    for p in found {
89                        if seen.insert(p.pid) {
90                            all.push(p);
91                        }
92                    }
93                }
94            }
95            all
96        } else {
97            Process::find_all()?
98        };
99
100        // Resolve --in filter path
101        let in_dir_filter = resolve_in_dir(&self.in_dir);
102
103        // Resolve path filter
104        let path_filter: Option<PathBuf> = self.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 (--in)
118            if let Some(ref dir_path) = in_dir_filter {
119                if let Some(ref proc_cwd) = p.cwd {
120                    let proc_path = PathBuf::from(proc_cwd);
121                    if !proc_path.starts_with(dir_path) {
122                        return false;
123                    }
124                } else {
125                    return false;
126                }
127            }
128
129            // Path filter (executable path)
130            if let Some(ref exe_path) = path_filter {
131                if let Some(ref proc_exe) = p.exe_path {
132                    let proc_path = PathBuf::from(proc_exe);
133                    if !proc_path.starts_with(exe_path) {
134                        return false;
135                    }
136                } else {
137                    return false;
138                }
139            }
140
141            // CPU filter
142            if let Some(min_cpu) = self.min_cpu {
143                if p.cpu_percent < min_cpu {
144                    return false;
145                }
146            }
147
148            // Memory filter
149            if let Some(min_mem) = self.min_mem {
150                if p.memory_mb < min_mem {
151                    return false;
152                }
153            }
154
155            // Status filter
156            if let Some(ref status) = self.status {
157                let status_match = match status.to_lowercase().as_str() {
158                    "running" => matches!(p.status, ProcessStatus::Running),
159                    "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
160                    "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
161                    "zombie" => matches!(p.status, ProcessStatus::Zombie),
162                    _ => true,
163                };
164                if !status_match {
165                    return false;
166                }
167            }
168
169            // Uptime filter
170            if let Some(min_uptime) = self.min_uptime {
171                if let Some(start_time) = p.start_time {
172                    let now = std::time::SystemTime::now()
173                        .duration_since(std::time::UNIX_EPOCH)
174                        .map(|d| d.as_secs())
175                        .unwrap_or(0);
176                    if now.saturating_sub(start_time) < min_uptime {
177                        return false;
178                    }
179                } else {
180                    return false;
181                }
182            }
183
184            // Parent PID filter
185            if let Some(ppid) = self.parent {
186                if p.parent_pid != Some(ppid) {
187                    return false;
188                }
189            }
190
191            true
192        });
193
194        // Sort processes
195        match self.sort.to_lowercase().as_str() {
196            "cpu" => processes.sort_by(|a, b| {
197                b.cpu_percent
198                    .partial_cmp(&a.cpu_percent)
199                    .unwrap_or(std::cmp::Ordering::Equal)
200            }),
201            "mem" | "memory" => processes.sort_by(|a, b| {
202                b.memory_mb
203                    .partial_cmp(&a.memory_mb)
204                    .unwrap_or(std::cmp::Ordering::Equal)
205            }),
206            "pid" => processes.sort_by_key(|p| p.pid),
207            "name" => processes.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())),
208            _ => {} // Keep default order
209        }
210
211        // Apply limit if specified
212        if let Some(limit) = self.limit {
213            processes.truncate(limit);
214        }
215
216        // Build context string for output (e.g., "in /path/to/dir")
217        let context = in_dir_filter
218            .as_ref()
219            .map(|p| format!("in {}", p.display()));
220
221        printer.print_processes_with_context(&processes, context.as_deref());
222        Ok(())
223    }
224}