Skip to main content

lean_ctx/core/patterns/
pip.rs

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