Skip to main content

lean_ctx/core/patterns/
just.rs

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