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