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