Skip to main content

lean_ctx_patterns/
just.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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}