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::{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    /// Output as JSON
43    #[arg(long, short = 'j')]
44    pub json: bool,
45
46    /// Show verbose output with command line, cwd, and parent PID
47    #[arg(long, short = 'v')]
48    pub verbose: bool,
49
50    /// Limit the number of results
51    #[arg(long, short = 'n')]
52    pub limit: Option<usize>,
53
54    /// Sort by: cpu, mem, pid, name
55    #[arg(long, short = 's', default_value = "cpu")]
56    pub sort: String,
57}
58
59impl ListCommand {
60    /// Executes the list command, displaying processes matching the filters.
61    pub fn execute(&self) -> Result<()> {
62        let format = if self.json {
63            OutputFormat::Json
64        } else {
65            OutputFormat::Human
66        };
67        let printer = Printer::new(format, self.verbose);
68
69        // Get base process list
70        let mut processes = if let Some(ref name) = self.name {
71            Process::find_by_name(name)?
72        } else {
73            Process::find_all()?
74        };
75
76        // Resolve --in filter path
77        let in_dir_filter: Option<PathBuf> = self.in_dir.as_ref().map(|p| {
78            if p == "." {
79                std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
80            } else {
81                let path = PathBuf::from(p);
82                if path.is_relative() {
83                    std::env::current_dir()
84                        .unwrap_or_else(|_| PathBuf::from("."))
85                        .join(path)
86                } else {
87                    path
88                }
89            }
90        });
91
92        // Resolve path filter
93        let path_filter: Option<PathBuf> = self.path.as_ref().map(|p| {
94            let path = PathBuf::from(p);
95            if path.is_relative() {
96                std::env::current_dir()
97                    .unwrap_or_else(|_| PathBuf::from("."))
98                    .join(path)
99            } else {
100                path
101            }
102        });
103
104        // Apply filters
105        processes.retain(|p| {
106            // Directory filter (--in)
107            if let Some(ref dir_path) = in_dir_filter {
108                if let Some(ref proc_cwd) = p.cwd {
109                    let proc_path = PathBuf::from(proc_cwd);
110                    if !proc_path.starts_with(dir_path) {
111                        return false;
112                    }
113                } else {
114                    return false;
115                }
116            }
117
118            // Path filter (executable path)
119            if let Some(ref exe_path) = path_filter {
120                if let Some(ref proc_exe) = p.exe_path {
121                    let proc_path = PathBuf::from(proc_exe);
122                    if !proc_path.starts_with(exe_path) {
123                        return false;
124                    }
125                } else {
126                    return false;
127                }
128            }
129
130            // CPU filter
131            if let Some(min_cpu) = self.min_cpu {
132                if p.cpu_percent < min_cpu {
133                    return false;
134                }
135            }
136
137            // Memory filter
138            if let Some(min_mem) = self.min_mem {
139                if p.memory_mb < min_mem {
140                    return false;
141                }
142            }
143
144            // Status filter
145            if let Some(ref status) = self.status {
146                let status_match = match status.to_lowercase().as_str() {
147                    "running" => matches!(p.status, ProcessStatus::Running),
148                    "sleeping" | "sleep" => matches!(p.status, ProcessStatus::Sleeping),
149                    "stopped" | "stop" => matches!(p.status, ProcessStatus::Stopped),
150                    "zombie" => matches!(p.status, ProcessStatus::Zombie),
151                    _ => true,
152                };
153                if !status_match {
154                    return false;
155                }
156            }
157
158            true
159        });
160
161        // Sort processes
162        match self.sort.to_lowercase().as_str() {
163            "cpu" => processes.sort_by(|a, b| {
164                b.cpu_percent
165                    .partial_cmp(&a.cpu_percent)
166                    .unwrap_or(std::cmp::Ordering::Equal)
167            }),
168            "mem" | "memory" => processes.sort_by(|a, b| {
169                b.memory_mb
170                    .partial_cmp(&a.memory_mb)
171                    .unwrap_or(std::cmp::Ordering::Equal)
172            }),
173            "pid" => processes.sort_by_key(|p| p.pid),
174            "name" => processes.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())),
175            _ => {} // Keep default order
176        }
177
178        // Apply limit if specified
179        if let Some(limit) = self.limit {
180            processes.truncate(limit);
181        }
182
183        // Build context string for output (e.g., "in /path/to/dir")
184        let context = in_dir_filter
185            .as_ref()
186            .map(|p| format!("in {}", p.display()));
187
188        printer.print_processes_with_context(&processes, context.as_deref());
189        Ok(())
190    }
191}