Skip to main content

mixtape_tools/process/
list_processes.rs

1use crate::prelude::*;
2use schemars::JsonSchema;
3use serde::Deserialize;
4use sysinfo::{ProcessesToUpdate, System};
5
6/// Input for listing processes (no parameters needed)
7#[derive(Debug, Deserialize, JsonSchema)]
8pub struct ListProcessesInput {}
9
10/// Tool for listing running processes
11pub struct ListProcessesTool;
12
13impl Tool for ListProcessesTool {
14    type Input = ListProcessesInput;
15
16    fn name(&self) -> &str {
17        "list_processes"
18    }
19
20    fn description(&self) -> &str {
21        "List all running processes on the system with their PID, name, CPU and memory usage."
22    }
23
24    async fn execute(&self, _input: Self::Input) -> std::result::Result<ToolResult, ToolError> {
25        let mut sys = System::new();
26        sys.refresh_processes(ProcessesToUpdate::All);
27
28        let mut processes: Vec<_> = sys.processes().iter().collect();
29        processes.sort_by_key(|(pid, _)| pid.as_u32());
30
31        let mut output = String::from("PID     | NAME                          | CPU%  | MEMORY\n");
32        output.push_str("--------|-------------------------------|-------|----------\n");
33
34        for (pid, process) in processes.iter().take(50) {
35            let name = process.name().to_string_lossy();
36            let cpu = process.cpu_usage();
37            let memory = process.memory();
38
39            let memory_str = if memory < 1024 * 1024 {
40                format!("{:.1} KB", memory as f64 / 1024.0)
41            } else if memory < 1024 * 1024 * 1024 {
42                format!("{:.1} MB", memory as f64 / (1024.0 * 1024.0))
43            } else {
44                format!("{:.1} GB", memory as f64 / (1024.0 * 1024.0 * 1024.0))
45            };
46
47            output.push_str(&format!(
48                "{:<7} | {:<29} | {:>5.1} | {:>8}\n",
49                pid.as_u32(),
50                if name.len() > 29 {
51                    format!("{}...", &name[..26])
52                } else {
53                    name.to_string()
54                },
55                cpu,
56                memory_str
57            ));
58        }
59
60        if processes.len() > 50 {
61            output.push_str(&format!(
62                "\n... and {} more processes\n",
63                processes.len() - 50
64            ));
65        }
66
67        Ok(output.into())
68    }
69
70    fn format_output_plain(&self, result: &ToolResult) -> String {
71        result.as_text().to_string()
72    }
73
74    fn format_output_ansi(&self, result: &ToolResult) -> String {
75        let output = result.as_text();
76        let lines: Vec<&str> = output.lines().collect();
77        if lines.len() < 3 {
78            return output.to_string();
79        }
80
81        let mut out = String::new();
82        out.push_str(&format!(
83            "\x1b[1m{:>7}  {:<25}  {:>6}  {:>10}  {}\x1b[0m\n",
84            "PID", "NAME", "CPU%", "MEMORY", "CPU"
85        ));
86        out.push_str(&format!("{}\n", "─".repeat(70)));
87
88        for line in lines.iter().skip(2) {
89            if line.starts_with("...") {
90                out.push_str(&format!("\x1b[2m{}\x1b[0m\n", line));
91                continue;
92            }
93
94            let parts: Vec<&str> = line.split('|').collect();
95            if parts.len() >= 4 {
96                let pid = parts[0].trim();
97                let name = parts[1].trim();
98                let cpu_str = parts[2].trim();
99                let memory = parts[3].trim();
100                let cpu: f32 = cpu_str.parse().unwrap_or(0.0);
101                let color = resource_color(cpu);
102                let bar = resource_bar(cpu.min(100.0), 10);
103
104                out.push_str(&format!(
105                    "\x1b[36m{:>7}\x1b[0m  {:<25}  {}{:>5.1}%\x1b[0m  {:>10}  {}{}\x1b[0m\n",
106                    pid,
107                    if name.len() > 25 { &name[..22] } else { name },
108                    color,
109                    cpu,
110                    memory,
111                    color,
112                    bar
113                ));
114            }
115        }
116        out
117    }
118
119    fn format_output_markdown(&self, result: &ToolResult) -> String {
120        let output = result.as_text();
121        let lines: Vec<&str> = output.lines().collect();
122        if lines.len() < 3 {
123            return format!("```\n{}\n```", output);
124        }
125
126        let mut out =
127            String::from("| PID | Name | CPU% | Memory |\n|-----|------|------|--------|\n");
128        for line in lines.iter().skip(2) {
129            if line.starts_with("...") {
130                out.push_str(&format!("\n*{}*\n", line));
131                continue;
132            }
133            let parts: Vec<&str> = line.split('|').collect();
134            if parts.len() >= 4 {
135                out.push_str(&format!(
136                    "| {} | `{}` | {} | {} |\n",
137                    parts[0].trim(),
138                    parts[1].trim(),
139                    parts[2].trim(),
140                    parts[3].trim()
141                ));
142            }
143        }
144        out
145    }
146}
147
148/// Create a visual bar for resource usage
149fn resource_bar(percent: f32, width: usize) -> String {
150    let filled = ((percent / 100.0) * width as f32).round() as usize;
151    let empty = width.saturating_sub(filled);
152    format!("[{}{}]", "█".repeat(filled), "░".repeat(empty))
153}
154
155/// Color code for resource usage (green → yellow → red)
156fn resource_color(percent: f32) -> &'static str {
157    if percent < 25.0 {
158        "\x1b[32m"
159    } else if percent < 50.0 {
160        "\x1b[33m"
161    } else if percent < 75.0 {
162        "\x1b[38;5;208m"
163    } else {
164        "\x1b[31m"
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use mixtape_core::ToolResult;
172
173    #[tokio::test]
174    async fn test_list_processes_basic() {
175        let tool = ListProcessesTool;
176        let input = ListProcessesInput {};
177
178        let result = tool.execute(input).await;
179        assert!(result.is_ok());
180
181        let output = result.unwrap().as_text();
182        // Should have headers
183        assert!(output.contains("PID"));
184        assert!(output.contains("NAME"));
185        assert!(output.contains("CPU"));
186        assert!(output.contains("MEMORY"));
187    }
188
189    #[tokio::test]
190    async fn test_list_processes_contains_processes() {
191        let tool = ListProcessesTool;
192        let input = ListProcessesInput {};
193
194        let result = tool.execute(input).await;
195        assert!(result.is_ok());
196
197        let output = result.unwrap().as_text();
198        // Should have at least one process (this test process)
199        let line_count = output.lines().count();
200        assert!(line_count > 2); // More than just headers
201    }
202
203    #[tokio::test]
204    async fn test_list_processes_memory_formatting() {
205        let tool = ListProcessesTool;
206        let input = ListProcessesInput {};
207
208        let result = tool.execute(input).await;
209        assert!(result.is_ok());
210
211        let output = result.unwrap().as_text();
212        // Should show memory units (KB, MB, or GB)
213        assert!(
214            output.contains("KB") || output.contains("MB") || output.contains("GB"),
215            "Expected memory units in output"
216        );
217    }
218
219    // ==================== resource_bar tests ====================
220
221    #[test]
222    fn test_resource_bar_zero() {
223        let bar = resource_bar(0.0, 10);
224        assert_eq!(bar, "[░░░░░░░░░░]");
225    }
226
227    #[test]
228    fn test_resource_bar_half() {
229        let bar = resource_bar(50.0, 10);
230        assert_eq!(bar, "[█████░░░░░]");
231    }
232
233    #[test]
234    fn test_resource_bar_full() {
235        let bar = resource_bar(100.0, 10);
236        assert_eq!(bar, "[██████████]");
237    }
238
239    #[test]
240    fn test_resource_bar_quarter() {
241        let bar = resource_bar(25.0, 10);
242        // 25% of 10 = 2.5, rounded = 3 (or 2 depending on rounding)
243        let filled_count = bar.chars().filter(|c| *c == '█').count();
244        assert!((2..=3).contains(&filled_count));
245    }
246
247    #[test]
248    fn test_resource_bar_different_width() {
249        let bar = resource_bar(50.0, 20);
250        let filled_count = bar.chars().filter(|c| *c == '█').count();
251        assert_eq!(filled_count, 10); // 50% of 20
252    }
253
254    #[test]
255    fn test_resource_bar_overflow() {
256        // Values over 100 are NOT capped internally - caller is responsible for capping
257        // (format_output_ansi calls resource_bar(cpu.min(100.0), 10))
258        let bar = resource_bar(150.0, 10);
259        let filled_count = bar.chars().filter(|c| *c == '█').count();
260        assert_eq!(filled_count, 15); // 150% of 10 = 15
261    }
262
263    // ==================== resource_color tests ====================
264
265    #[test]
266    fn test_resource_color_low() {
267        let color = resource_color(10.0);
268        assert_eq!(color, "\x1b[32m"); // green
269    }
270
271    #[test]
272    fn test_resource_color_medium_low() {
273        let color = resource_color(30.0);
274        assert_eq!(color, "\x1b[33m"); // yellow
275    }
276
277    #[test]
278    fn test_resource_color_medium_high() {
279        let color = resource_color(60.0);
280        assert_eq!(color, "\x1b[38;5;208m"); // orange
281    }
282
283    #[test]
284    fn test_resource_color_high() {
285        let color = resource_color(80.0);
286        assert_eq!(color, "\x1b[31m"); // red
287    }
288
289    #[test]
290    fn test_resource_color_boundaries() {
291        // Test boundary values
292        assert_eq!(resource_color(0.0), "\x1b[32m"); // green at 0
293        assert_eq!(resource_color(24.9), "\x1b[32m"); // green just under 25
294        assert_eq!(resource_color(25.0), "\x1b[33m"); // yellow at 25
295        assert_eq!(resource_color(49.9), "\x1b[33m"); // yellow just under 50
296        assert_eq!(resource_color(50.0), "\x1b[38;5;208m"); // orange at 50
297        assert_eq!(resource_color(74.9), "\x1b[38;5;208m"); // orange just under 75
298        assert_eq!(resource_color(75.0), "\x1b[31m"); // red at 75
299        assert_eq!(resource_color(100.0), "\x1b[31m"); // red at 100
300    }
301
302    // ==================== format_output tests ====================
303
304    #[test]
305    fn test_format_output_plain() {
306        let tool = ListProcessesTool;
307        let result: ToolResult = "PID     | NAME                          | CPU%  | MEMORY\n--------|-------------------------------|-------|----------\n1       | init                          |   0.0 |   10.0 MB".into();
308
309        let formatted = tool.format_output_plain(&result);
310
311        // Plain format should return the raw text
312        assert!(formatted.contains("PID"));
313        assert!(formatted.contains("init"));
314    }
315
316    #[test]
317    fn test_format_output_ansi_basic() {
318        let tool = ListProcessesTool;
319        let result: ToolResult = "PID     | NAME                          | CPU%  | MEMORY\n--------|-------------------------------|-------|----------\n1       | init                          |   0.0 |   10.0 MB".into();
320
321        let formatted = tool.format_output_ansi(&result);
322
323        // Should have ANSI codes
324        assert!(formatted.contains("\x1b["));
325        assert!(formatted.contains("\x1b[1m")); // bold header
326        assert!(formatted.contains("\x1b[36m")); // cyan for PID
327    }
328
329    #[test]
330    fn test_format_output_ansi_cpu_colors() {
331        let tool = ListProcessesTool;
332
333        // Low CPU (green)
334        let low_cpu: ToolResult = "PID     | NAME                          | CPU%  | MEMORY\n--------|-------------------------------|-------|----------\n1       | proc                          |   5.0 |   10.0 MB".into();
335        let formatted = tool.format_output_ansi(&low_cpu);
336        assert!(formatted.contains("\x1b[32m")); // green
337
338        // High CPU (red)
339        let high_cpu: ToolResult = "PID     | NAME                          | CPU%  | MEMORY\n--------|-------------------------------|-------|----------\n1       | proc                          |  80.0 |   10.0 MB".into();
340        let formatted = tool.format_output_ansi(&high_cpu);
341        assert!(formatted.contains("\x1b[31m")); // red
342    }
343
344    #[test]
345    fn test_format_output_ansi_with_overflow_indicator() {
346        let tool = ListProcessesTool;
347        let result: ToolResult = "PID     | NAME                          | CPU%  | MEMORY\n--------|-------------------------------|-------|----------\n1       | proc                          |   5.0 |   10.0 MB\n... and 100 more processes".into();
348
349        let formatted = tool.format_output_ansi(&result);
350
351        // Overflow indicator should be dimmed
352        assert!(formatted.contains("\x1b[2m")); // dim
353        assert!(formatted.contains("more processes"));
354    }
355
356    #[test]
357    fn test_format_output_markdown_basic() {
358        let tool = ListProcessesTool;
359        let result: ToolResult = "PID     | NAME                          | CPU%  | MEMORY\n--------|-------------------------------|-------|----------\n1       | init                          |   0.0 |   10.0 MB".into();
360
361        let formatted = tool.format_output_markdown(&result);
362
363        // Should have markdown table
364        assert!(formatted.contains("| PID |"));
365        assert!(formatted.contains("|-----|"));
366        assert!(formatted.contains("`init`")); // name in code style
367    }
368
369    #[test]
370    fn test_format_output_markdown_with_overflow() {
371        let tool = ListProcessesTool;
372        let result: ToolResult = "PID     | NAME                          | CPU%  | MEMORY\n--------|-------------------------------|-------|----------\n1       | proc                          |   5.0 |   10.0 MB\n... and 50 more processes".into();
373
374        let formatted = tool.format_output_markdown(&result);
375
376        // Overflow should be italicized
377        assert!(formatted.contains("*... and 50 more processes*"));
378    }
379
380    #[test]
381    fn test_format_output_markdown_short_input() {
382        let tool = ListProcessesTool;
383        let result: ToolResult = "short".into();
384
385        let formatted = tool.format_output_markdown(&result);
386
387        // Short input should be wrapped in code block
388        assert!(formatted.contains("```"));
389    }
390
391    // ==================== Tool metadata tests ====================
392
393    #[test]
394    fn test_tool_name() {
395        let tool = ListProcessesTool;
396        assert_eq!(tool.name(), "list_processes");
397    }
398
399    #[test]
400    fn test_tool_description() {
401        let tool = ListProcessesTool;
402        assert!(!tool.description().is_empty());
403        assert!(tool.description().contains("process"));
404    }
405}