lean_ctx_patterns/
just.rs1use std::collections::HashMap;
4
5pub fn compress(command: &str, output: &str) -> Option<String> {
6 if command.contains("--list") || command.contains("-l") {
7 return Some(compress_list(output));
8 }
9 if command.contains("--summary") {
10 return Some(compress_summary(output));
11 }
12 if command.contains("--evaluate") {
13 return Some(compress_evaluate(output));
14 }
15 Some(compress_run(output))
16}
17
18fn compress_list(output: &str) -> String {
19 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
20
21 let header = lines.first().filter(|l| l.contains("Available recipes"));
22
23 let recipes: Vec<&str> = lines
24 .iter()
25 .filter(|l| {
26 let t = l.trim();
27 !t.is_empty() && !t.starts_with("Available") && !t.starts_with("Wrote:")
28 })
29 .copied()
30 .collect();
31
32 let mut result = if let Some(h) = header {
33 format!("{}\n", h.trim())
34 } else {
35 format!("{} recipes:\n", recipes.len())
36 };
37
38 for r in recipes.iter().take(30) {
39 result.push_str(&format!(" {}\n", r.trim()));
40 }
41 if recipes.len() > 30 {
42 result.push_str(&format!(" ... +{} more\n", recipes.len() - 30));
43 }
44
45 result.trim_end().to_string()
46}
47
48fn compress_summary(output: &str) -> String {
49 let recipes: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
50 format!("{} recipes: {}", recipes.len(), recipes.join(", "))
51}
52
53fn compress_evaluate(output: &str) -> String {
54 let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
55 if lines.len() <= 10 {
56 return lines.join("\n");
57 }
58 format!(
59 "{}\n{}",
60 lines[..10].join("\n"),
61 super::elision_marker(lines.len() - 10)
62 )
63}
64
65fn compress_run(output: &str) -> String {
66 let lines: Vec<&str> = output.lines().collect();
67 let mut kept = Vec::new();
68 let mut echo_dedup: HashMap<String, u32> = HashMap::new();
69 let mut last_error: Option<String> = None;
70
71 for line in &lines {
72 let trimmed = line.trim();
73
74 if trimmed.starts_with("===>") || trimmed.starts_with("==>") {
75 kept.push(trimmed.to_string());
76 continue;
77 }
78
79 if trimmed.starts_with("error") || trimmed.contains("Error:") || trimmed.contains("FAILED")
80 {
81 last_error = Some(trimmed.to_string());
82 kept.push(trimmed.to_string());
83 continue;
84 }
85
86 if trimmed.starts_with("warning") || trimmed.contains("Warning:") {
87 kept.push(trimmed.to_string());
88 continue;
89 }
90
91 let echo_key = if trimmed.len() > 80 {
92 trimmed[..trimmed.floor_char_boundary(80)].to_string()
93 } else {
94 trimmed.to_string()
95 };
96 *echo_dedup.entry(echo_key).or_insert(0) += 1;
97 }
98
99 let dedup_count = echo_dedup.values().filter(|&&v| v > 1).count();
100 let total_lines = lines.len();
101
102 if kept.is_empty() && total_lines <= 20 {
103 return output.to_string();
104 }
105
106 if kept.is_empty() {
107 let shown = lines
108 .iter()
109 .take(15)
110 .copied()
111 .collect::<Vec<_>>()
112 .join("\n");
113 if total_lines > 15 {
114 return format!("{shown}\n{}", super::elision_marker(total_lines - 15));
115 }
116 return shown;
117 }
118
119 let mut result = kept.join("\n");
120 if dedup_count > 0 {
121 result.push_str(&format!(
122 "\n({dedup_count} repeated line groups suppressed)"
123 ));
124 }
125 if let Some(err) = last_error
126 && !result.contains(&err)
127 {
128 result.push_str(&format!("\nlast error: {err}"));
129 }
130 result
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn compresses_list() {
139 let output = "Available recipes:\n build\n test\n lint\n deploy\n clean\n";
140 let result = compress("just --list", output).unwrap();
141 assert!(result.contains("Available recipes"), "should keep header");
142 assert!(result.contains("build"), "should list recipes");
143 }
144
145 #[test]
146 fn compresses_summary() {
147 let output = "build\ntest\nlint\ndeploy\n";
148 let result = compress("just --summary", output).unwrap();
149 assert!(result.contains("4 recipes"), "should count recipes");
150 }
151
152 #[test]
153 fn compresses_run_with_errors() {
154 let output =
155 "===> Building project\nCompiling step 1\nCompiling step 2\nerror: build failed\n";
156 let result = compress("just build", output).unwrap();
157 assert!(result.contains("===> Building"), "should keep headers");
158 assert!(result.contains("error: build failed"), "should keep errors");
159 }
160
161 #[test]
162 fn short_output_passthrough() {
163 let output = "done\n";
164 let result = compress("just clean", output).unwrap();
165 assert_eq!(result.trim(), "done");
166 }
167}