lean_ctx/core/patterns/
systemd.rs1use 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}