Skip to main content

lean_ctx/core/patterns/
systemd.rs

1use std::collections::HashMap;
2
3pub fn compress(cmd: &str, output: &str) -> Option<String> {
4    let trimmed = output.trim();
5    if trimmed.is_empty() {
6        return Some("ok".to_string());
7    }
8
9    if cmd.starts_with("systemctl") {
10        return Some(compress_systemctl(cmd, trimmed));
11    }
12    if cmd.starts_with("journalctl") {
13        return Some(compress_journal(trimmed));
14    }
15
16    Some(compact_lines(trimmed, 15))
17}
18
19fn compress_systemctl(cmd: &str, output: &str) -> String {
20    if cmd.contains("status") {
21        return compress_status(output);
22    }
23    if cmd.contains("list-units")
24        || cmd.contains("list-unit-files")
25        || (!cmd.contains("start")
26            && !cmd.contains("stop")
27            && !cmd.contains("restart")
28            && !cmd.contains("enable")
29            && !cmd.contains("disable"))
30    {
31        return compress_list(output);
32    }
33    compact_lines(output, 10)
34}
35
36fn compress_status(output: &str) -> String {
37    let mut parts = Vec::new();
38    for line in output.lines() {
39        let trimmed = line.trim();
40        if trimmed.starts_with("Active:")
41            || trimmed.starts_with("Loaded:")
42            || trimmed.starts_with("Main PID:")
43            || trimmed.starts_with("Memory:")
44            || trimmed.starts_with("CPU:")
45            || trimmed.starts_with("Tasks:")
46        {
47            parts.push(trimmed.to_string());
48        }
49        if trimmed.contains(".service") && trimmed.contains("-") && parts.is_empty() {
50            parts.insert(0, trimmed.to_string());
51        }
52    }
53    if parts.is_empty() {
54        return compact_lines(output, 10);
55    }
56    parts.join("\n")
57}
58
59fn compress_list(output: &str) -> String {
60    let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
61    if lines.len() <= 20 {
62        return lines.join("\n");
63    }
64
65    let mut by_state: HashMap<String, u32> = HashMap::new();
66    for line in &lines[1..] {
67        let parts: Vec<&str> = line.split_whitespace().collect();
68        if parts.len() >= 3 {
69            let state = parts[2].to_string();
70            *by_state.entry(state).or_insert(0) += 1;
71        }
72    }
73
74    let header = lines.first().unwrap_or(&"");
75    let mut result = format!("{header}\n{} units:", lines.len() - 1);
76    for (state, count) in &by_state {
77        result.push_str(&format!("\n  {state}: {count}"));
78    }
79    result
80}
81
82fn compress_journal(output: &str) -> String {
83    let lines: Vec<&str> = output.lines().collect();
84    if lines.len() <= 30 {
85        return lines.join("\n");
86    }
87
88    let mut deduped: HashMap<String, u32> = HashMap::new();
89    for line in &lines {
90        let parts: Vec<&str> = line.splitn(4, ' ').collect();
91        let key = if parts.len() >= 4 {
92            parts[3].to_string()
93        } else {
94            line.to_string()
95        };
96        *deduped.entry(key).or_insert(0) += 1;
97    }
98
99    let mut sorted: Vec<_> = deduped.into_iter().collect();
100    sorted.sort_by(|a, b| b.1.cmp(&a.1));
101
102    let top: Vec<String> = sorted
103        .iter()
104        .take(20)
105        .map(|(msg, count)| {
106            if *count > 1 {
107                format!("  ({count}x) {msg}")
108            } else {
109                format!("  {msg}")
110            }
111        })
112        .collect();
113
114    format!(
115        "{} log lines (deduped to {}):\n{}",
116        lines.len(),
117        top.len(),
118        top.join("\n")
119    )
120}
121
122fn compact_lines(text: &str, max: usize) -> String {
123    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
124    if lines.len() <= max {
125        return lines.join("\n");
126    }
127    format!(
128        "{}\n... ({} more lines)",
129        lines[..max].join("\n"),
130        lines.len() - max
131    )
132}