1use crate::api::{Action, Query, Selection};
8use crate::report_runs::By;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum Tab {
13 Next,
14 Matrix,
15 Runs,
16 Jobs,
17 Agents,
18 Workflows,
19 Campaigns,
20 Report,
21 Notes,
22 Status,
23 Project,
26}
27
28impl Tab {
29 pub const ALL: [Tab; 11] = [
30 Tab::Next,
31 Tab::Matrix,
32 Tab::Runs,
33 Tab::Jobs,
34 Tab::Agents,
35 Tab::Workflows,
36 Tab::Campaigns,
37 Tab::Report,
38 Tab::Notes,
39 Tab::Status,
40 Tab::Project,
41 ];
42
43 pub fn title(self) -> &'static str {
44 match self {
45 Tab::Next => "next",
46 Tab::Matrix => "matrix",
47 Tab::Runs => "runs",
48 Tab::Jobs => "jobs",
49 Tab::Agents => "agents",
50 Tab::Workflows => "workflows",
51 Tab::Campaigns => "campaigns",
52 Tab::Report => "report",
53 Tab::Notes => "notes",
54 Tab::Status => "status",
55 Tab::Project => "project",
56 }
57 }
58
59 pub fn query(self, by: By, project: &str) -> Query {
62 let of = Selection::default();
63 match self {
64 Tab::Next => Query::Next { of, all: true },
65 Tab::Matrix => Query::Matrix {
66 of,
67 quadrant: None,
68 all: true,
69 },
70 Tab::Runs => Query::Runs,
71 Tab::Jobs => Query::Jobs,
72 Tab::Agents => Query::Agents,
73 Tab::Workflows => Query::Workflows,
74 Tab::Campaigns => Query::Campaigns,
75 Tab::Report => Query::Report { by },
76 Tab::Notes => Query::Notes,
77 Tab::Status => Query::Status {
80 of,
81 explain: false,
82 all: true,
83 },
84 Tab::Project => Query::Project {
85 name: project.to_string(),
86 },
87 }
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum Act {
94 ApproveRun,
95 ApproveRuns,
96 RejectRun,
97 AddReviewTime,
98 SettlePrs,
99 StopJob,
100 ApproveJob,
101 StopWorkflow,
102 AddNote,
103 EditNote,
104 RemoveNote,
105 Scan,
106 ScanProject,
108 Dispatch,
109 ResumeWorkflow,
111 StartWorkflow,
113 ReworkRun,
114 Pr,
115 Push,
116 RunCampaign,
117 Sync,
118 Verify,
119}
120
121impl Act {
122 pub const ALL: [Act; 22] = [
123 Act::ApproveRun,
124 Act::ApproveRuns,
125 Act::RejectRun,
126 Act::AddReviewTime,
127 Act::SettlePrs,
128 Act::StopJob,
129 Act::ApproveJob,
130 Act::StopWorkflow,
131 Act::AddNote,
132 Act::EditNote,
133 Act::RemoveNote,
134 Act::Scan,
135 Act::ScanProject,
136 Act::Dispatch,
137 Act::ResumeWorkflow,
138 Act::StartWorkflow,
139 Act::ReworkRun,
140 Act::Pr,
141 Act::Push,
142 Act::RunCampaign,
143 Act::Sync,
144 Act::Verify,
145 ];
146
147 pub fn name(self) -> String {
148 format!("{self:?}")
149 }
150
151 pub fn on_row(self) -> bool {
154 !matches!(
155 self,
156 Act::Scan
157 | Act::ScanProject
158 | Act::Sync
159 | Act::AddNote
160 | Act::SettlePrs
161 | Act::ApproveRuns
162 | Act::StartWorkflow
163 )
164 }
165
166 pub fn weight(self) -> &'static str {
169 match self {
170 Act::ApproveRun | Act::ApproveJob | Act::Dispatch => "primary",
171 Act::RejectRun | Act::StopJob | Act::StopWorkflow | Act::RemoveNote | Act::Push => {
172 "danger"
173 }
174 _ => "default",
175 }
176 }
177
178 pub fn of(a: &Action) -> &'static [Act] {
181 match a {
182 Action::ApproveRun { .. } => &[Act::ApproveRun],
183 Action::ApproveRuns { .. } => &[Act::ApproveRuns],
184 Action::RejectRun { .. } => &[Act::RejectRun],
185 Action::AddReviewTime { .. } => &[Act::AddReviewTime],
186 Action::SettlePrs => &[Act::SettlePrs],
187 Action::StopJob { .. } => &[Act::StopJob],
188 Action::ApproveJob { .. } => &[Act::ApproveJob],
189 Action::StopWorkflow { .. } => &[Act::StopWorkflow],
190 Action::AddNote { .. } => &[Act::AddNote],
191 Action::EditNote { .. } => &[Act::EditNote],
192 Action::RemoveNote { .. } => &[Act::RemoveNote],
193 Action::Scan { .. } => &[Act::Scan, Act::ScanProject],
194 Action::Dispatch { .. } => &[Act::Dispatch],
195 Action::WorkflowRun { .. } => &[Act::ResumeWorkflow, Act::StartWorkflow],
196 Action::ReworkRun { .. } => &[Act::ReworkRun],
197 Action::Publish { .. } => &[Act::Pr, Act::Push],
198 Action::RunCampaign { .. } => &[Act::RunCampaign],
199 Action::Sync { .. } => &[Act::Sync],
200 Action::Verify { .. } => &[Act::Verify],
201 }
202 }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum Command {
207 Tab(Tab),
208 Act(Act),
209 Down,
210 Up,
211 First,
212 Last,
213 NextPane,
215 PrevPane,
216 Open,
218 Back,
220 ReportBy,
222 OpenProject,
224 Help,
225 Quit,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum Ask {
232 Nothing,
233 Confirm,
235 Text(&'static str),
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct Binding {
241 pub key: &'static str,
242 pub command: Command,
243 pub tabs: &'static [Tab],
245 pub ask: Ask,
246 pub label: &'static str,
247}
248
249const fn bind(key: &'static str, command: Command, label: &'static str) -> Binding {
250 Binding {
251 key,
252 command,
253 tabs: &[],
254 ask: Ask::Nothing,
255 label,
256 }
257}
258
259const fn on(
260 key: &'static str,
261 tabs: &'static [Tab],
262 act: Act,
263 ask: Ask,
264 label: &'static str,
265) -> Binding {
266 Binding {
267 key,
268 command: Command::Act(act),
269 tabs,
270 ask,
271 label,
272 }
273}
274
275const TASKS: &[Tab] = &[Tab::Next, Tab::Matrix, Tab::Project];
276const PANES: &[Tab] = &[
277 Tab::Next,
278 Tab::Matrix,
279 Tab::Agents,
280 Tab::Workflows,
281 Tab::Project,
282];
283const RUNS: &[Tab] = &[Tab::Runs, Tab::Project];
284
285pub const BINDINGS: &[Binding] = &[
286 bind("1", Command::Tab(Tab::Next), "next"),
287 bind("2", Command::Tab(Tab::Matrix), "matrix"),
288 bind("3", Command::Tab(Tab::Runs), "runs"),
289 bind("4", Command::Tab(Tab::Jobs), "jobs"),
290 bind("5", Command::Tab(Tab::Agents), "agents"),
291 bind("6", Command::Tab(Tab::Workflows), "workflows"),
292 bind("7", Command::Tab(Tab::Campaigns), "campaigns"),
293 bind("8", Command::Tab(Tab::Report), "report"),
294 bind("9", Command::Tab(Tab::Notes), "notes"),
295 bind("0", Command::Tab(Tab::Status), "status"),
296 bind("j", Command::Down, "down"),
297 bind("ArrowDown", Command::Down, "down"),
298 bind("k", Command::Up, "up"),
299 bind("ArrowUp", Command::Up, "up"),
300 bind("g", Command::First, "first"),
301 bind("G", Command::Last, "last"),
302 Binding {
303 tabs: PANES,
304 ..bind("Tab", Command::NextPane, "next list")
305 },
306 Binding {
307 tabs: PANES,
308 ..bind("l", Command::NextPane, "next list")
309 },
310 Binding {
311 tabs: PANES,
312 ..bind("h", Command::PrevPane, "previous list")
313 },
314 Binding {
315 tabs: &[Tab::Runs, Tab::Jobs, Tab::Project],
316 ..bind("Enter", Command::Open, "open")
317 },
318 Binding {
319 tabs: &[Tab::Next, Tab::Matrix, Tab::Runs, Tab::Agents, Tab::Status],
320 ..bind("o", Command::OpenProject, "open project")
321 },
322 bind("Escape", Command::Back, "back"),
323 Binding {
324 tabs: &[Tab::Report],
325 ..bind("b", Command::ReportBy, "by")
326 },
327 bind("?", Command::Help, "keys"),
328 bind("q", Command::Quit, "quit"),
329 on("c", &[], Act::Scan, Ask::Nothing, "scan"),
331 on("i", &[], Act::Sync, Ask::Confirm, "sync issues"),
332 on("n", &[], Act::AddNote, Ask::Text("note"), "add note"),
333 on(
334 "r",
335 &[Tab::Project],
336 Act::ScanProject,
337 Ask::Nothing,
338 "rescan project",
339 ),
340 on("d", TASKS, Act::Dispatch, Ask::Confirm, "dispatch"),
342 on("v", TASKS, Act::Verify, Ask::Nothing, "verify project"),
343 on("a", RUNS, Act::ApproveRun, Ask::Nothing, "approve"),
345 on("b", RUNS, Act::ApproveRuns, Ask::Confirm, "approve clean"),
346 on("x", RUNS, Act::RejectRun, Ask::Confirm, "reject"),
347 on("w", RUNS, Act::ReworkRun, Ask::Text("feedback"), "rework"),
348 on(
349 "t",
350 RUNS,
351 Act::AddReviewTime,
352 Ask::Text("minutes"),
353 "review time",
354 ),
355 on("p", RUNS, Act::Pr, Ask::Confirm, "pull request"),
356 on("u", RUNS, Act::Push, Ask::Confirm, "push"),
357 on("y", RUNS, Act::SettlePrs, Ask::Nothing, "check PRs"),
358 on(
360 "a",
361 &[Tab::Jobs],
362 Act::ApproveJob,
363 Ask::Nothing,
364 "approve plan",
365 ),
366 on("s", &[Tab::Jobs], Act::StopJob, Ask::Confirm, "stop"),
367 on(
369 "r",
370 &[Tab::Workflows],
371 Act::ResumeWorkflow,
372 Ask::Nothing,
373 "resume",
374 ),
375 on(
376 "f",
377 &[Tab::Workflows],
378 Act::StartWorkflow,
379 Ask::Text("workflow target [--local] [--unattended]"),
380 "start",
381 ),
382 on(
383 "s",
384 &[Tab::Workflows],
385 Act::StopWorkflow,
386 Ask::Confirm,
387 "stop",
388 ),
389 on(
391 "r",
392 &[Tab::Campaigns],
393 Act::RunCampaign,
394 Ask::Confirm,
395 "run",
396 ),
397 on("e", &[Tab::Notes], Act::EditNote, Ask::Text("note"), "edit"),
399 on("x", &[Tab::Notes], Act::RemoveNote, Ask::Confirm, "remove"),
400];
401
402pub fn lookup(key: &str, tab: Tab) -> Option<&'static Binding> {
405 let fits = |b: &&Binding| b.key == key;
406 BINDINGS
407 .iter()
408 .filter(fits)
409 .find(|b| b.tabs.contains(&tab))
410 .or_else(|| BINDINGS.iter().filter(fits).find(|b| b.tabs.is_empty()))
411}
412
413pub fn on_tab(tab: Tab) -> Vec<&'static Binding> {
415 BINDINGS
416 .iter()
417 .filter(|b| b.tabs.is_empty() || b.tabs.contains(&tab))
418 .collect()
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
428 fn every_act_has_a_key() {
429 for act in Act::ALL {
430 assert!(
431 BINDINGS.iter().any(|b| b.command == Command::Act(act)),
432 "{act:?} has no key"
433 );
434 }
435 for tab in Tab::ALL.into_iter().filter(|t| *t != Tab::Project) {
436 assert!(
437 BINDINGS.iter().any(|b| b.command == Command::Tab(tab)),
438 "{tab:?} has no key"
439 );
440 }
441 assert!(
442 BINDINGS.iter().any(|b| b.command == Command::OpenProject),
443 "a project opens from a row"
444 );
445 }
446
447 #[test]
450 fn no_key_means_two_things_on_one_tab() {
451 for tab in Tab::ALL {
452 let keys: Vec<&str> = on_tab(tab).iter().map(|b| b.key).collect();
453 for k in &keys {
454 assert_eq!(
455 keys.iter().filter(|x| *x == k).count(),
456 1,
457 "`{k}` is bound twice on {tab:?}"
458 );
459 }
460 }
461 }
462
463 #[test]
464 fn keys_are_plain() {
465 for b in BINDINGS {
466 assert!(
467 b.key.chars().count() == 1 || b.key.chars().all(|c| c.is_ascii_alphabetic()),
468 "{}",
469 b.key
470 );
471 }
472 }
473
474 #[test]
475 fn lookup_prefers_the_tab() {
476 assert_eq!(
477 lookup("a", Tab::Jobs).map(|b| b.command),
478 Some(Command::Act(Act::ApproveJob))
479 );
480 assert_eq!(
481 lookup("a", Tab::Runs).map(|b| b.command),
482 Some(Command::Act(Act::ApproveRun))
483 );
484 assert!(lookup("a", Tab::Next).is_none());
485 assert_eq!(
486 lookup("c", Tab::Notes).map(|b| b.command),
487 Some(Command::Act(Act::Scan))
488 );
489 }
490}