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