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> {
59 Some(output.to_string())
60}
61
62pub fn compress_du(output: &str) -> Option<String> {
63 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
64
65 if lines.len() <= 10 {
66 return None;
67 }
68
69 let mut parsed: Vec<(u64, &str)> = lines
70 .iter()
71 .filter_map(|line| {
72 let parts: Vec<&str> = line.splitn(2, |c: char| c.is_whitespace()).collect();
73 if parts.len() == 2 {
74 let size = parse_size_field(parts[0]);
75 Some((size, parts[1].trim()))
76 } else {
77 None
78 }
79 })
80 .collect();
81
82 parsed.sort_by_key(|b| std::cmp::Reverse(b.0));
83
84 let top: Vec<String> = parsed
85 .iter()
86 .take(15)
87 .map(|(size, path)| format!("{}\t{path}", format_size(*size)))
88 .collect();
89
90 Some(format!(
91 "du: {} entries (top 15 by size)\n{}",
92 lines.len(),
93 top.join("\n")
94 ))
95}
96
97fn parse_size_field(s: &str) -> u64 {
98 let s = s.trim();
99 if let Ok(v) = s.parse::<u64>() {
100 return v;
101 }
102 let (num_part, suffix) = s.split_at(s.len().saturating_sub(1));
103 let base: f64 = num_part.parse().unwrap_or(0.0);
104 match suffix.to_uppercase().as_str() {
105 "K" => (base * 1024.0) as u64,
106 "M" => (base * 1024.0 * 1024.0) as u64,
107 "G" => (base * 1024.0 * 1024.0 * 1024.0) as u64,
108 _ => s.parse().unwrap_or(0),
109 }
110}
111
112fn format_size(bytes: u64) -> String {
113 if bytes >= 1_073_741_824 {
114 format!("{:.1}G", bytes as f64 / 1_073_741_824.0)
115 } else if bytes >= 1_048_576 {
116 format!("{:.1}M", bytes as f64 / 1_048_576.0)
117 } else if bytes >= 1024 {
118 format!("{:.0}K", bytes as f64 / 1024.0)
119 } else {
120 format!("{bytes}")
121 }
122}
123
124pub fn compress_ping(output: &str) -> Option<String> {
125 let lines: Vec<&str> = output.lines().collect();
126 if lines.len() < 3 {
127 return None;
128 }
129
130 let mut host = "";
131 let mut stats_line = "";
132 let mut rtt_line = "";
133
134 for line in &lines {
135 if line.starts_with("PING ") || line.starts_with("ping ") {
136 host = line;
137 }
138 if line.contains("packets transmitted") || line.contains("packet loss") {
139 stats_line = line;
140 }
141 if line.contains("rtt ") || line.contains("round-trip") {
142 rtt_line = line;
143 }
144 }
145
146 if stats_line.is_empty() {
147 return None;
148 }
149
150 let mut out = String::new();
151 if !host.is_empty() {
152 out.push_str(host);
153 out.push('\n');
154 }
155 out.push_str(stats_line);
156 if !rtt_line.is_empty() {
157 out.push('\n');
158 out.push_str(rtt_line);
159 }
160 Some(out)
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn ps_compresses_large_output() {
169 let mut lines = vec![
170 "USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string(),
171 ];
172 for i in 0..50 {
173 lines.push(format!(
174 "user {:>5} 0.0 0.1 12345 1234 ? S 10:00 0:00 process_{i}",
175 1000 + i
176 ));
177 }
178 lines.push(
179 "root 9999 95.0 8.5 999999 99999 ? R 10:00 5:00 heavy_proc"
180 .to_string(),
181 );
182 let output = lines.join("\n");
183 let result = compress_ps(&output).expect("should compress");
184 assert!(result.contains("51 processes"));
185 assert!(result.contains("heavy_proc"));
186 }
187
188 #[test]
189 fn ps_skips_small_output() {
190 let output = "USER PID %CPU %MEM\nroot 1 0.0 0.1 init";
191 assert!(compress_ps(output).is_none());
192 }
193
194 #[test]
195 fn df_returns_verbatim() {
196 let mut lines =
197 vec!["Filesystem 1K-blocks Used Available Use% Mounted on".to_string()];
198 for i in 0..20 {
199 let pct = if i < 5 { 90 } else { 10 };
200 lines.push(format!(
201 "/dev/sda{i} 1000000 500000 500000 {pct}% /mnt/disk{i}"
202 ));
203 }
204 let output = lines.join("\n");
205 let result = compress_df(&output).expect("should return Some");
206 assert_eq!(result, output, "df must pass through verbatim");
207 }
208
209 #[test]
210 fn du_compresses_large_listing() {
211 let mut lines = Vec::new();
212 for i in 0..30 {
213 lines.push(format!("{}\t./dir_{i}", (i + 1) * 1024));
214 }
215 let output = lines.join("\n");
216 let result = compress_du(&output).expect("should compress");
217 assert!(result.contains("30 entries"));
218 assert!(result.contains("top 15"));
219 }
220
221 #[test]
222 fn ping_extracts_summary() {
223 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";
224 let result = compress_ping(output).expect("should compress");
225 assert!(result.contains("3 packets transmitted"));
226 assert!(result.contains("rtt"));
227 assert!(!result.contains("icmp_seq=1"));
228 }
229}