use crate::api::{Action, Query, Selection};
use crate::report_runs::By;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Tab {
Next,
Matrix,
Runs,
Jobs,
Agents,
Workflows,
Campaigns,
Report,
Notes,
Status,
Project,
}
impl Tab {
pub const ALL: [Tab; 11] = [
Tab::Next,
Tab::Matrix,
Tab::Runs,
Tab::Jobs,
Tab::Agents,
Tab::Workflows,
Tab::Campaigns,
Tab::Report,
Tab::Notes,
Tab::Status,
Tab::Project,
];
pub fn title(self) -> &'static str {
match self {
Tab::Next => "next",
Tab::Matrix => "matrix",
Tab::Runs => "runs",
Tab::Jobs => "jobs",
Tab::Agents => "agents",
Tab::Workflows => "workflows",
Tab::Campaigns => "campaigns",
Tab::Report => "report",
Tab::Notes => "notes",
Tab::Status => "status",
Tab::Project => "project",
}
}
pub fn query(self, by: By, project: &str) -> Query {
let of = Selection::default();
match self {
Tab::Next => Query::Next { of, all: true },
Tab::Matrix => Query::Matrix {
of,
quadrant: None,
all: true,
},
Tab::Runs => Query::Runs,
Tab::Jobs => Query::Jobs,
Tab::Agents => Query::Agents,
Tab::Workflows => Query::Workflows,
Tab::Campaigns => Query::Campaigns,
Tab::Report => Query::Report { by },
Tab::Notes => Query::Notes,
Tab::Status => Query::Status {
of,
explain: false,
all: true,
},
Tab::Project => Query::Project {
name: project.to_string(),
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Act {
ApproveRun,
ApproveRuns,
RejectRun,
AddReviewTime,
SettlePrs,
StopJob,
ApproveJob,
StopWorkflow,
AddNote,
EditNote,
RemoveNote,
Scan,
ScanProject,
Dispatch,
ResumeWorkflow,
StartWorkflow,
ReworkRun,
Pr,
Push,
RunCampaign,
Sync,
Verify,
}
impl Act {
pub const ALL: [Act; 22] = [
Act::ApproveRun,
Act::ApproveRuns,
Act::RejectRun,
Act::AddReviewTime,
Act::SettlePrs,
Act::StopJob,
Act::ApproveJob,
Act::StopWorkflow,
Act::AddNote,
Act::EditNote,
Act::RemoveNote,
Act::Scan,
Act::ScanProject,
Act::Dispatch,
Act::ResumeWorkflow,
Act::StartWorkflow,
Act::ReworkRun,
Act::Pr,
Act::Push,
Act::RunCampaign,
Act::Sync,
Act::Verify,
];
pub fn name(self) -> String {
format!("{self:?}")
}
pub fn on_row(self) -> bool {
!matches!(
self,
Act::Scan
| Act::ScanProject
| Act::Sync
| Act::AddNote
| Act::SettlePrs
| Act::ApproveRuns
| Act::StartWorkflow
)
}
pub fn weight(self) -> &'static str {
match self {
Act::ApproveRun | Act::ApproveJob | Act::Dispatch => "primary",
Act::RejectRun | Act::StopJob | Act::StopWorkflow | Act::RemoveNote | Act::Push => {
"danger"
}
_ => "default",
}
}
pub fn of(a: &Action) -> &'static [Act] {
match a {
Action::ApproveRun { .. } => &[Act::ApproveRun],
Action::ApproveRuns { .. } => &[Act::ApproveRuns],
Action::RejectRun { .. } => &[Act::RejectRun],
Action::AddReviewTime { .. } => &[Act::AddReviewTime],
Action::SettlePrs => &[Act::SettlePrs],
Action::StopJob { .. } => &[Act::StopJob],
Action::ApproveJob { .. } => &[Act::ApproveJob],
Action::StopWorkflow { .. } => &[Act::StopWorkflow],
Action::AddNote { .. } => &[Act::AddNote],
Action::EditNote { .. } => &[Act::EditNote],
Action::RemoveNote { .. } => &[Act::RemoveNote],
Action::Scan { .. } => &[Act::Scan, Act::ScanProject],
Action::Dispatch { .. } => &[Act::Dispatch],
Action::WorkflowRun { .. } => &[Act::ResumeWorkflow, Act::StartWorkflow],
Action::ReworkRun { .. } => &[Act::ReworkRun],
Action::Publish { .. } => &[Act::Pr, Act::Push],
Action::RunCampaign { .. } => &[Act::RunCampaign],
Action::Sync { .. } => &[Act::Sync],
Action::Verify { .. } => &[Act::Verify],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Command {
Tab(Tab),
Act(Act),
Down,
Up,
First,
Last,
NextPane,
PrevPane,
Open,
Back,
ReportBy,
OpenProject,
Help,
Quit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ask {
Nothing,
Confirm,
Text(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Binding {
pub key: &'static str,
pub command: Command,
pub tabs: &'static [Tab],
pub ask: Ask,
pub label: &'static str,
}
const fn bind(key: &'static str, command: Command, label: &'static str) -> Binding {
Binding {
key,
command,
tabs: &[],
ask: Ask::Nothing,
label,
}
}
const fn on(
key: &'static str,
tabs: &'static [Tab],
act: Act,
ask: Ask,
label: &'static str,
) -> Binding {
Binding {
key,
command: Command::Act(act),
tabs,
ask,
label,
}
}
const TASKS: &[Tab] = &[Tab::Next, Tab::Matrix, Tab::Project];
const PANES: &[Tab] = &[
Tab::Next,
Tab::Matrix,
Tab::Agents,
Tab::Workflows,
Tab::Project,
];
const RUNS: &[Tab] = &[Tab::Runs, Tab::Project];
pub const BINDINGS: &[Binding] = &[
bind("1", Command::Tab(Tab::Next), "next"),
bind("2", Command::Tab(Tab::Matrix), "matrix"),
bind("3", Command::Tab(Tab::Runs), "runs"),
bind("4", Command::Tab(Tab::Jobs), "jobs"),
bind("5", Command::Tab(Tab::Agents), "agents"),
bind("6", Command::Tab(Tab::Workflows), "workflows"),
bind("7", Command::Tab(Tab::Campaigns), "campaigns"),
bind("8", Command::Tab(Tab::Report), "report"),
bind("9", Command::Tab(Tab::Notes), "notes"),
bind("0", Command::Tab(Tab::Status), "status"),
bind("j", Command::Down, "down"),
bind("ArrowDown", Command::Down, "down"),
bind("k", Command::Up, "up"),
bind("ArrowUp", Command::Up, "up"),
bind("g", Command::First, "first"),
bind("G", Command::Last, "last"),
Binding {
tabs: PANES,
..bind("Tab", Command::NextPane, "next list")
},
Binding {
tabs: PANES,
..bind("l", Command::NextPane, "next list")
},
Binding {
tabs: PANES,
..bind("h", Command::PrevPane, "previous list")
},
Binding {
tabs: &[Tab::Runs, Tab::Jobs, Tab::Project],
..bind("Enter", Command::Open, "open")
},
Binding {
tabs: &[Tab::Next, Tab::Matrix, Tab::Runs, Tab::Agents, Tab::Status],
..bind("o", Command::OpenProject, "open project")
},
bind("Escape", Command::Back, "back"),
Binding {
tabs: &[Tab::Report],
..bind("b", Command::ReportBy, "by")
},
bind("?", Command::Help, "keys"),
bind("q", Command::Quit, "quit"),
on("c", &[], Act::Scan, Ask::Nothing, "scan"),
on("i", &[], Act::Sync, Ask::Confirm, "sync issues"),
on("n", &[], Act::AddNote, Ask::Text("note"), "add note"),
on(
"r",
&[Tab::Project],
Act::ScanProject,
Ask::Nothing,
"rescan project",
),
on("d", TASKS, Act::Dispatch, Ask::Confirm, "dispatch"),
on("v", TASKS, Act::Verify, Ask::Nothing, "verify project"),
on("a", RUNS, Act::ApproveRun, Ask::Nothing, "approve"),
on("b", RUNS, Act::ApproveRuns, Ask::Confirm, "approve clean"),
on("x", RUNS, Act::RejectRun, Ask::Confirm, "reject"),
on("w", RUNS, Act::ReworkRun, Ask::Text("feedback"), "rework"),
on(
"t",
RUNS,
Act::AddReviewTime,
Ask::Text("minutes"),
"review time",
),
on("p", RUNS, Act::Pr, Ask::Confirm, "pull request"),
on("u", RUNS, Act::Push, Ask::Confirm, "push"),
on("y", RUNS, Act::SettlePrs, Ask::Nothing, "check PRs"),
on(
"a",
&[Tab::Jobs],
Act::ApproveJob,
Ask::Nothing,
"approve plan",
),
on("s", &[Tab::Jobs], Act::StopJob, Ask::Confirm, "stop"),
on(
"r",
&[Tab::Workflows],
Act::ResumeWorkflow,
Ask::Nothing,
"resume",
),
on(
"f",
&[Tab::Workflows],
Act::StartWorkflow,
Ask::Text("workflow target [--local] [--unattended]"),
"start",
),
on(
"s",
&[Tab::Workflows],
Act::StopWorkflow,
Ask::Confirm,
"stop",
),
on(
"r",
&[Tab::Campaigns],
Act::RunCampaign,
Ask::Confirm,
"run",
),
on("e", &[Tab::Notes], Act::EditNote, Ask::Text("note"), "edit"),
on("x", &[Tab::Notes], Act::RemoveNote, Ask::Confirm, "remove"),
];
pub fn lookup(key: &str, tab: Tab) -> Option<&'static Binding> {
let fits = |b: &&Binding| b.key == key;
BINDINGS
.iter()
.filter(fits)
.find(|b| b.tabs.contains(&tab))
.or_else(|| BINDINGS.iter().filter(fits).find(|b| b.tabs.is_empty()))
}
pub fn on_tab(tab: Tab) -> Vec<&'static Binding> {
BINDINGS
.iter()
.filter(|b| b.tabs.is_empty() || b.tabs.contains(&tab))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_act_has_a_key() {
for act in Act::ALL {
assert!(
BINDINGS.iter().any(|b| b.command == Command::Act(act)),
"{act:?} has no key"
);
}
for tab in Tab::ALL.into_iter().filter(|t| *t != Tab::Project) {
assert!(
BINDINGS.iter().any(|b| b.command == Command::Tab(tab)),
"{tab:?} has no key"
);
}
assert!(
BINDINGS.iter().any(|b| b.command == Command::OpenProject),
"a project opens from a row"
);
}
#[test]
fn no_key_means_two_things_on_one_tab() {
for tab in Tab::ALL {
let keys: Vec<&str> = on_tab(tab).iter().map(|b| b.key).collect();
for k in &keys {
assert_eq!(
keys.iter().filter(|x| *x == k).count(),
1,
"`{k}` is bound twice on {tab:?}"
);
}
}
}
#[test]
fn keys_are_plain() {
for b in BINDINGS {
assert!(
b.key.chars().count() == 1 || b.key.chars().all(|c| c.is_ascii_alphabetic()),
"{}",
b.key
);
}
}
#[test]
fn lookup_prefers_the_tab() {
assert_eq!(
lookup("a", Tab::Jobs).map(|b| b.command),
Some(Command::Act(Act::ApproveJob))
);
assert_eq!(
lookup("a", Tab::Runs).map(|b| b.command),
Some(Command::Act(Act::ApproveRun))
);
assert!(lookup("a", Tab::Next).is_none());
assert_eq!(
lookup("c", Tab::Notes).map(|b| b.command),
Some(Command::Act(Act::Scan))
);
}
}