Skip to main content

lean_ctx/core/patterns/
sysinfo.rs

1pub 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}