Skip to main content

lean_ctx_patterns/
systemd.rs

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