lean_ctx/core/patterns/
sysinfo.rs1pub fn compress_ps(output: &str) -> Option<String> {
2 let lines: Vec<&str> = output.lines().collect();
3 if lines.len() < 2 {
4 return None;
5 }
6
7 let header = lines[0];
8 let procs: Vec<&str> = lines[1..]
9 .iter()
10 .filter(|l| !l.trim().is_empty())
11 .copied()
12 .collect();
13 let total = procs.len();
14
15 if total <= 10 {
16 return None;
17 }
18
19 let mut high_cpu: Vec<&str> = Vec::new();
20 let mut high_mem: Vec<&str> = Vec::new();
21
22 for &line in &procs {
23 let cols: Vec<&str> = line.split_whitespace().collect();
24 if cols.len() >= 4 {
25 let cpu: f64 = cols.get(2).and_then(|s| s.parse().ok()).unwrap_or(0.0);
26 let mem: f64 = cols.get(3).and_then(|s| s.parse().ok()).unwrap_or(0.0);
27 if cpu > 1.0 {
28 high_cpu.push(line);
29 }
30 if mem > 1.0 {
31 high_mem.push(line);
32 }
33 }
34 }
35
36 let mut out = format!("ps: {total} processes\n{header}\n");
37
38 if !high_cpu.is_empty() {
39 out.push_str(&format!("--- high CPU ({}) ---\n", high_cpu.len()));
40 for l in high_cpu.iter().take(15) {
41 out.push_str(l);
42 out.push('\n');
43 }
44 }
45 if !high_mem.is_empty() {
46 out.push_str(&format!("--- high MEM ({}) ---\n", high_mem.len()));
47 for l in high_mem.iter().take(15) {
48 out.push_str(l);
49 out.push('\n');
50 }
51 }
52
53 Some(out.trim_end().to_string())
54}
55
56pub fn compress_df(output: &str) -> Option<String> {
57 let lines: Vec<&str> = output.lines().collect();
58 if lines.len() < 2 {
59 return None;
60 }
61
62 let header = lines[0];
63 let entries: Vec<&str> = lines[1..]
64 .iter()
65 .filter(|l| !l.trim().is_empty())
66 .copied()
67 .collect();
68
69 if entries.len() <= 5 {
70 return None;
71 }
72
73 let mut relevant: Vec<&str> = Vec::new();
74 for &line in &entries {
75 let cols: Vec<&str> = line.split_whitespace().collect();
76 if let Some(pct_str) = cols.iter().find(|s| s.ends_with('%')) {
77 let pct: u32 = pct_str.trim_end_matches('%').parse().unwrap_or(0);
78 if pct >= 50 {
79 relevant.push(line);
80 }
81 }
82 }
83
84 if relevant.is_empty() {
85 relevant = entries.iter().take(5).copied().collect();
86 }
87
88 let mut out = format!(
89 "df: {} filesystems ({} shown)\n{header}\n",
90 entries.len(),
91 relevant.len()
92 );
93 for l in &relevant {
94 out.push_str(l);
95 out.push('\n');
96 }
97 Some(out.trim_end().to_string())
98}
99
100pub fn compress_du(output: &str) -> Option<String> {
101 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
102
103 if lines.len() <= 10 {
104 return None;
105 }
106
107 let mut parsed: Vec<(u64, &str)> = lines
108 .iter()
109 .filter_map(|line| {
110 let parts: Vec<&str> = line.splitn(2, |c: char| c.is_whitespace()).collect();
111 if parts.len() == 2 {
112 let size = parse_size_field(parts[0]);
113 Some((size, parts[1].trim()))
114 } else {
115 None
116 }
117 })
118 .collect();
119
120 parsed.sort_by_key(|b| std::cmp::Reverse(b.0));
121
122 let top: Vec<String> = parsed
123 .iter()
124 .take(15)
125 .map(|(size, path)| format!("{}\t{path}", format_size(*size)))
126 .collect();
127
128 Some(format!(
129 "du: {} entries (top 15 by size)\n{}",
130 lines.len(),
131 top.join("\n")
132 ))
133}
134
135fn parse_size_field(s: &str) -> u64 {
136 let s = s.trim();
137 if let Ok(v) = s.parse::<u64>() {
138 return v;
139 }
140 let (num_part, suffix) = s.split_at(s.len().saturating_sub(1));
141 let base: f64 = num_part.parse().unwrap_or(0.0);
142 match suffix.to_uppercase().as_str() {
143 "K" => (base * 1024.0) as u64,
144 "M" => (base * 1024.0 * 1024.0) as u64,
145 "G" => (base * 1024.0 * 1024.0 * 1024.0) as u64,
146 _ => s.parse().unwrap_or(0),
147 }
148}
149
150fn format_size(bytes: u64) -> String {
151 if bytes >= 1_073_741_824 {
152 format!("{:.1}G", bytes as f64 / 1_073_741_824.0)
153 } else if bytes >= 1_048_576 {
154 format!("{:.1}M", bytes as f64 / 1_048_576.0)
155 } else if bytes >= 1024 {
156 format!("{:.0}K", bytes as f64 / 1024.0)
157 } else {
158 format!("{bytes}")
159 }
160}
161
162pub fn compress_ping(output: &str) -> Option<String> {
163 let lines: Vec<&str> = output.lines().collect();
164 if lines.len() < 3 {
165 return None;
166 }
167
168 let mut host = "";
169 let mut stats_line = "";
170 let mut rtt_line = "";
171
172 for line in &lines {
173 if line.starts_with("PING ") || line.starts_with("ping ") {
174 host = line;
175 }
176 if line.contains("packets transmitted") || line.contains("packet loss") {
177 stats_line = line;
178 }
179 if line.contains("rtt ") || line.contains("round-trip") {
180 rtt_line = line;
181 }
182 }
183
184 if stats_line.is_empty() {
185 return None;
186 }
187
188 let mut out = String::new();
189 if !host.is_empty() {
190 out.push_str(host);
191 out.push('\n');
192 }
193 out.push_str(stats_line);
194 if !rtt_line.is_empty() {
195 out.push('\n');
196 out.push_str(rtt_line);
197 }
198 Some(out)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn ps_compresses_large_output() {
207 let mut lines = vec![
208 "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string(),
209 ];
210 for i in 0..50 {
211 lines.push(format!(
212 "user {:>5} 0.0 0.1 12345 1234 ? S 10:00 0:00 process_{i}",
213 1000 + i
214 ));
215 }
216 lines.push(
217 "root 9999 95.0 8.5 999999 99999 ? R 10:00 5:00 heavy_proc"
218 .to_string(),
219 );
220 let output = lines.join("\n");
221 let result = compress_ps(&output).expect("should compress");
222 assert!(result.contains("51 processes"));
223 assert!(result.contains("heavy_proc"));
224 }
225
226 #[test]
227 fn ps_skips_small_output() {
228 let output = "USER PID %CPU %MEM\nroot 1 0.0 0.1 init";
229 assert!(compress_ps(output).is_none());
230 }
231
232 #[test]
233 fn df_compresses_many_filesystems() {
234 let mut lines =
235 vec!["Filesystem 1K-blocks Used Available Use% Mounted on".to_string()];
236 for i in 0..20 {
237 let pct = if i < 5 { 90 } else { 10 };
238 lines.push(format!(
239 "/dev/sda{i} 1000000 500000 500000 {pct}% /mnt/disk{i}"
240 ));
241 }
242 let output = lines.join("\n");
243 let result = compress_df(&output).expect("should compress");
244 assert!(result.contains("20 filesystems"));
245 assert!(result.contains("90%"));
246 }
247
248 #[test]
249 fn du_compresses_large_listing() {
250 let mut lines = Vec::new();
251 for i in 0..30 {
252 lines.push(format!("{}\t./dir_{i}", (i + 1) * 1024));
253 }
254 let output = lines.join("\n");
255 let result = compress_du(&output).expect("should compress");
256 assert!(result.contains("30 entries"));
257 assert!(result.contains("top 15"));
258 }
259
260 #[test]
261 fn ping_extracts_summary() {
262 let output = "PING google.com (142.250.80.46): 56 data bytes\n64 bytes from 142.250.80.46: icmp_seq=0 ttl=116 time=12.3 ms\n64 bytes from 142.250.80.46: icmp_seq=1 ttl=116 time=11.8 ms\n64 bytes from 142.250.80.46: icmp_seq=2 ttl=116 time=12.1 ms\n\n--- google.com ping statistics ---\n3 packets transmitted, 3 packets received, 0.0% packet loss\nrtt min/avg/max/stddev = 11.8/12.1/12.3/0.2 ms";
263 let result = compress_ping(output).expect("should compress");
264 assert!(result.contains("3 packets transmitted"));
265 assert!(result.contains("rtt"));
266 assert!(!result.contains("icmp_seq=1"));
267 }
268}