lean_ctx_patterns/
systemd.rs1use 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}