lean_ctx/core/patterns/
gh.rs1macro_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 pr_line_re() -> &'static regex::Regex {
11 static_regex!(r"#(\d+)\s+(.+?)\s{2,}(\S+)\s+(\S+)")
12}
13fn issue_line_re() -> &'static regex::Regex {
14 static_regex!(r"#(\d+)\s+(.+?)\s{2,}")
15}
16fn pr_created_re() -> &'static regex::Regex {
17 static_regex!(r"https://github\.com/\S+/pull/(\d+)")
18}
19
20pub fn compress(command: &str, output: &str) -> Option<String> {
21 if command.contains("pr") {
22 if command.contains("list") {
23 return Some(compress_pr_list(output));
24 }
25 if command.contains("view") {
26 return Some(compress_pr_view(output));
27 }
28 if command.contains("create") {
29 return Some(compress_pr_create(output));
30 }
31 if command.contains("merge") {
32 return Some(compress_simple_action(output, "merged"));
33 }
34 if command.contains("close") {
35 return Some(compress_simple_action(output, "closed"));
36 }
37 if command.contains("checkout") || command.contains("co") {
38 return Some(compress_simple_action(output, "checked out"));
39 }
40 }
41 if command.contains("issue") {
42 if command.contains("list") {
43 return Some(compress_issue_list(output));
44 }
45 if command.contains("view") {
46 return Some(compress_issue_view(output));
47 }
48 if command.contains("create") {
49 return Some(compress_simple_action(output, "created"));
50 }
51 }
52 if command.contains("run") {
53 if command.contains("list") {
54 return Some(compress_run_list(output));
55 }
56 if command.contains("view") {
57 return Some(compress_run_view(output));
58 }
59 }
60 if command.contains("repo") {
61 return Some(compress_repo(output));
62 }
63 if command.contains("release") {
64 return Some(compress_release(output));
65 }
66
67 Some(compact_output(output, 10))
68}
69
70fn compress_pr_list(output: &str) -> String {
71 let trimmed = output.trim();
72 if trimmed.is_empty() || trimmed.contains("no pull requests") {
73 return "no PRs".to_string();
74 }
75
76 let mut prs = Vec::new();
77 for line in trimmed.lines() {
78 if let Some(caps) = pr_line_re().captures(line) {
79 let num = &caps[1];
80 let title = caps[2].trim();
81 let branch = &caps[3];
82 prs.push(format!("#{num} {title} ({branch})"));
83 } else {
84 let l = line.trim();
85 if !l.is_empty() && l.starts_with('#') {
86 prs.push(l.to_string());
87 }
88 }
89 }
90
91 if prs.is_empty() {
92 return compact_output(trimmed, 10);
93 }
94 format!("{} PRs:\n{}", prs.len(), prs.join("\n"))
95}
96
97fn compress_pr_view(output: &str) -> String {
98 let lines: Vec<&str> = output.lines().collect();
99 if lines.len() <= 5 {
100 return output.to_string();
101 }
102
103 let mut title = String::new();
104 let mut state = String::new();
105 let mut labels = Vec::new();
106 let mut checks = Vec::new();
107
108 for line in &lines {
109 let l = line.trim();
110 if l.starts_with("title:") || (title.is_empty() && l.starts_with('#')) {
111 title = l.replace("title:", "").replace('#', "").trim().to_string();
112 }
113 if l.starts_with("state:") {
114 state = l.replace("state:", "").trim().to_string();
115 }
116 if l.starts_with("labels:") {
117 labels = l
118 .replace("labels:", "")
119 .split(',')
120 .map(|s| s.trim().to_string())
121 .collect();
122 }
123 if l.contains("✓") || l.contains("✗") || l.contains("pass") || l.contains("fail") {
124 checks.push(l.to_string());
125 }
126 }
127
128 let mut parts = Vec::new();
129 if !title.is_empty() {
130 parts.push(title);
131 }
132 if !state.is_empty() {
133 parts.push(format!("state: {state}"));
134 }
135 if !labels.is_empty() {
136 parts.push(format!("labels: {}", labels.join(", ")));
137 }
138 if !checks.is_empty() && checks.len() <= 5 {
139 parts.push(format!("checks: {}", checks.join("; ")));
140 }
141
142 if parts.is_empty() {
143 return compact_output(output, 10);
144 }
145 parts.join("\n")
146}
147
148fn compress_pr_create(output: &str) -> String {
149 if let Some(caps) = pr_created_re().captures(output) {
150 return format!("#{} created", &caps[1]);
151 }
152 let trimmed = output.trim();
153 if trimmed.contains("http") {
154 for line in trimmed.lines() {
155 if line.contains("http") {
156 return format!("created: {}", line.trim());
157 }
158 }
159 }
160 compact_output(trimmed, 3)
161}
162
163fn compress_issue_list(output: &str) -> String {
164 let trimmed = output.trim();
165 if trimmed.is_empty() || trimmed.contains("no issues") {
166 return "no issues".to_string();
167 }
168
169 let mut issues = Vec::new();
170 for line in trimmed.lines() {
171 if let Some(caps) = issue_line_re().captures(line) {
172 let num = &caps[1];
173 let title = caps[2].trim();
174 issues.push(format!("#{num} {title}"));
175 } else {
176 let l = line.trim();
177 if !l.is_empty() && l.starts_with('#') {
178 issues.push(l.to_string());
179 }
180 }
181 }
182
183 if issues.is_empty() {
184 return compact_output(trimmed, 10);
185 }
186 format!("{} issues:\n{}", issues.len(), issues.join("\n"))
187}
188
189fn compress_issue_view(output: &str) -> String {
190 compact_output(output, 15)
191}
192
193fn compress_run_list(output: &str) -> String {
194 let trimmed = output.trim();
195 if trimmed.is_empty() {
196 return "no runs".to_string();
197 }
198
199 let mut runs = Vec::new();
200 for line in trimmed.lines() {
201 let l = line.trim();
202 if l.is_empty() || l.starts_with("STATUS") || l.starts_with("--") {
203 continue;
204 }
205 if l.contains("completed")
206 || l.contains("in_progress")
207 || l.contains("queued")
208 || l.contains("failure")
209 || l.contains("success")
210 {
211 runs.push(l.to_string());
212 }
213 }
214
215 if runs.is_empty() {
216 return compact_output(trimmed, 10);
217 }
218 format!("{} runs:\n{}", runs.len(), runs.join("\n"))
219}
220
221fn compress_run_view(output: &str) -> String {
222 compact_output(output, 15)
223}
224
225fn compress_repo(output: &str) -> String {
226 compact_output(output, 10)
227}
228
229fn compress_release(output: &str) -> String {
230 compact_output(output, 10)
231}
232
233fn compress_simple_action(output: &str, action: &str) -> String {
234 let trimmed = output.trim();
235 if trimmed.is_empty() {
236 return format!("ok ({action})");
237 }
238 for line in trimmed.lines() {
239 if line.contains("http") || line.contains('#') {
240 return format!("{action}: {}", line.trim());
241 }
242 }
243 action.to_string()
244}
245
246fn compact_output(text: &str, max: usize) -> String {
247 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
248 if lines.len() <= max {
249 return lines.join("\n");
250 }
251 format!(
252 "{}\n... ({} more lines)",
253 lines[..max].join("\n"),
254 lines.len() - max
255 )
256}