Skip to main content

lean_ctx/core/patterns/
pip.rs

1use regex::Regex;
2use std::sync::OnceLock;
3
4static PIP_INSTALLED_RE: OnceLock<Regex> = OnceLock::new();
5static PIP_OUTDATED_RE: OnceLock<Regex> = OnceLock::new();
6
7fn pip_installed_re() -> &'static Regex {
8    PIP_INSTALLED_RE.get_or_init(|| Regex::new(r"Successfully installed\s+(.+)").unwrap())
9}
10fn pip_outdated_re() -> &'static Regex {
11    PIP_OUTDATED_RE.get_or_init(|| Regex::new(r"^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)").unwrap())
12}
13
14pub fn compress(command: &str, output: &str) -> Option<String> {
15    if command.contains("uninstall") {
16        return Some(compress_uninstall(output));
17    }
18    if command.contains("install") {
19        return Some(compress_install(output));
20    }
21    if command.contains("list") || command.contains("freeze") {
22        if command.contains("outdated") || command.contains("--outdated") {
23            return Some(compress_outdated(output));
24        }
25        return Some(compress_list(output));
26    }
27    if command.contains("show") {
28        return Some(compress_show(output));
29    }
30    if command.contains("check") {
31        return Some(compress_check(output));
32    }
33    None
34}
35
36fn compress_install(output: &str) -> String {
37    let trimmed = output.trim();
38    if trimmed.is_empty() {
39        return "ok".to_string();
40    }
41
42    if let Some(caps) = pip_installed_re().captures(trimmed) {
43        let packages: Vec<&str> = caps[1].split_whitespace().collect();
44        return format!("ok (+{} packages): {}", packages.len(), packages.join(", "));
45    }
46
47    if trimmed.contains("already satisfied") {
48        return "ok (already satisfied)".to_string();
49    }
50
51    compact_output(trimmed, 5)
52}
53
54fn compress_list(output: &str) -> String {
55    let lines: Vec<&str> = output.lines().collect();
56    if lines.len() <= 2 {
57        return output.to_string();
58    }
59
60    let skip = if lines[0].starts_with("Package") || lines[0].starts_with("---") {
61        2
62    } else {
63        0
64    };
65    let packages: Vec<String> = lines[skip..]
66        .iter()
67        .filter_map(|l| {
68            let parts: Vec<&str> = l.split_whitespace().collect();
69            if parts.len() >= 2 {
70                Some(format!("{}=={}", parts[0], parts[1]))
71            } else {
72                None
73            }
74        })
75        .collect();
76
77    if packages.is_empty() {
78        return output.to_string();
79    }
80    format!("{} packages:\n{}", packages.len(), packages.join("\n"))
81}
82
83fn compress_outdated(output: &str) -> String {
84    let lines: Vec<&str> = output.lines().collect();
85    let skip = if lines
86        .first()
87        .map(|l| l.starts_with("Package"))
88        .unwrap_or(false)
89    {
90        2
91    } else {
92        0
93    };
94
95    let mut outdated = Vec::new();
96    for line in lines.iter().skip(skip) {
97        if let Some(caps) = pip_outdated_re().captures(line) {
98            let name = &caps[1];
99            let current = &caps[2];
100            let latest = &caps[3];
101            outdated.push(format!("{name}: {current} → {latest}"));
102        }
103    }
104
105    if outdated.is_empty() {
106        return "all up-to-date".to_string();
107    }
108    format!("{} outdated:\n{}", outdated.len(), outdated.join("\n"))
109}
110
111fn compress_uninstall(output: &str) -> String {
112    let trimmed = output.trim();
113    if trimmed.is_empty() {
114        return "ok".to_string();
115    }
116
117    let removed: Vec<&str> = trimmed
118        .lines()
119        .filter(|l| l.contains("Successfully uninstalled"))
120        .collect();
121
122    if removed.is_empty() {
123        return compact_output(trimmed, 3);
124    }
125
126    let names: Vec<String> = removed
127        .iter()
128        .take(30)
129        .map(|l| {
130            l.trim()
131                .strip_prefix("Successfully uninstalled ")
132                .unwrap_or(l.trim())
133                .to_string()
134        })
135        .collect();
136
137    let total = removed.len();
138    if names.is_empty() {
139        return format!("ok (removed {total} packages)");
140    }
141    format!("ok (removed {total} packages): {}", names.join(", "))
142}
143
144fn compress_show(output: &str) -> String {
145    compact_output(output, 10)
146}
147
148fn compress_check(output: &str) -> String {
149    let trimmed = output.trim();
150    if trimmed.is_empty() || trimmed.contains("No broken requirements") {
151        return "ok (no broken dependencies)".to_string();
152    }
153
154    let broken: Vec<&str> = trimmed
155        .lines()
156        .filter(|l| l.contains("requires") || l.contains("has requirement"))
157        .collect();
158
159    if broken.is_empty() {
160        return compact_output(trimmed, 5);
161    }
162    format!(
163        "{} broken dependencies:\n{}",
164        broken.len(),
165        broken.join("\n")
166    )
167}
168
169fn compact_output(text: &str, max: usize) -> String {
170    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
171    if lines.len() <= max {
172        return lines.join("\n");
173    }
174    format!(
175        "{}\n... ({} more lines)",
176        lines[..max].join("\n"),
177        lines.len() - max
178    )
179}