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("install") {
16        return Some(compress_install(output));
17    }
18    if command.contains("list") || command.contains("freeze") {
19        if command.contains("outdated") || command.contains("--outdated") {
20            return Some(compress_outdated(output));
21        }
22        return Some(compress_list(output));
23    }
24    if command.contains("uninstall") {
25        return Some(compress_uninstall(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    format!("ok (removed {} packages)", removed.len())
126}
127
128fn compress_show(output: &str) -> String {
129    compact_output(output, 10)
130}
131
132fn compress_check(output: &str) -> String {
133    let trimmed = output.trim();
134    if trimmed.is_empty() || trimmed.contains("No broken requirements") {
135        return "ok (no broken dependencies)".to_string();
136    }
137
138    let broken: Vec<&str> = trimmed
139        .lines()
140        .filter(|l| l.contains("requires") || l.contains("has requirement"))
141        .collect();
142
143    if broken.is_empty() {
144        return compact_output(trimmed, 5);
145    }
146    format!(
147        "{} broken dependencies:\n{}",
148        broken.len(),
149        broken.join("\n")
150    )
151}
152
153fn compact_output(text: &str, max: usize) -> String {
154    let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
155    if lines.len() <= max {
156        return lines.join("\n");
157    }
158    format!(
159        "{}\n... ({} more lines)",
160        lines[..max].join("\n"),
161        lines.len() - max
162    )
163}