mixtape_tools/process/
list_processes.rs1use crate::prelude::*;
2use schemars::JsonSchema;
3use serde::Deserialize;
4use sysinfo::{ProcessesToUpdate, System};
5
6#[derive(Debug, Deserialize, JsonSchema)]
8pub struct ListProcessesInput {}
9
10pub 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
148fn 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
155fn 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 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 let line_count = output.lines().count();
200 assert!(line_count > 2); }
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 assert!(
214 output.contains("KB") || output.contains("MB") || output.contains("GB"),
215 "Expected memory units in output"
216 );
217 }
218
219 #[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 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); }
253
254 #[test]
255 fn test_resource_bar_overflow() {
256 let bar = resource_bar(150.0, 10);
259 let filled_count = bar.chars().filter(|c| *c == '█').count();
260 assert_eq!(filled_count, 15); }
262
263 #[test]
266 fn test_resource_color_low() {
267 let color = resource_color(10.0);
268 assert_eq!(color, "\x1b[32m"); }
270
271 #[test]
272 fn test_resource_color_medium_low() {
273 let color = resource_color(30.0);
274 assert_eq!(color, "\x1b[33m"); }
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"); }
282
283 #[test]
284 fn test_resource_color_high() {
285 let color = resource_color(80.0);
286 assert_eq!(color, "\x1b[31m"); }
288
289 #[test]
290 fn test_resource_color_boundaries() {
291 assert_eq!(resource_color(0.0), "\x1b[32m"); assert_eq!(resource_color(24.9), "\x1b[32m"); assert_eq!(resource_color(25.0), "\x1b[33m"); assert_eq!(resource_color(49.9), "\x1b[33m"); assert_eq!(resource_color(50.0), "\x1b[38;5;208m"); assert_eq!(resource_color(74.9), "\x1b[38;5;208m"); assert_eq!(resource_color(75.0), "\x1b[31m"); assert_eq!(resource_color(100.0), "\x1b[31m"); }
301
302 #[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 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 assert!(formatted.contains("\x1b["));
325 assert!(formatted.contains("\x1b[1m")); assert!(formatted.contains("\x1b[36m")); }
328
329 #[test]
330 fn test_format_output_ansi_cpu_colors() {
331 let tool = ListProcessesTool;
332
333 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")); 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")); }
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 assert!(formatted.contains("\x1b[2m")); 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 assert!(formatted.contains("| PID |"));
365 assert!(formatted.contains("|-----|"));
366 assert!(formatted.contains("`init`")); }
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 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 assert!(formatted.contains("```"));
389 }
390
391 #[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}