use super::app::View;
use super::widgets::picker::Mode;
pub(super) const HELP_WIDTH: u16 = 78;
pub(super) const HELP_PADDING: u16 = 1;
pub(super) const KEYS_WIDTH: u16 = 26;
pub(super) const ACTION_WIDTH: u16 = HELP_WIDTH - 2 - 2 * HELP_PADDING - KEYS_WIDTH - 1;
pub(super) struct KeyHelp {
pub(super) keys: &'static str,
pub(super) action: &'static str,
}
const fn row(keys: &'static str, action: &'static str) -> KeyHelp {
KeyHelp { keys, action }
}
pub(super) const NOTE: &str = "e, d, r, A, t, h, C and J are refused while a stage, an edit or \
a training start runs in this TUI; the TUI also holds the project lock, \
so no other overbrainer command writes to the project \
meanwhile.";
pub(super) const GLOBAL: &[KeyHelp] = &[
row("1-6, Tab, Shift-Tab", "switch view"),
row("?", "this help (Esc, ? or q closes it)"),
row("q, Ctrl-C", "quit"),
row("R", "reload the data files and runs"),
row("g", "open the overbrainer repository in a browser"),
row("r, A", "run auto or a stage (asks which); A: auto"),
row(
"y, n Esc Enter",
"in a dialog: confirm, cancel (the default, n)",
),
];
const PROJECT: &[KeyHelp] = &[
row(
"k j, PgUp PgDn, Home End",
"select a field, by page, the first or last",
),
row("Enter", "edit and save; toggles a bool, cycles a choice"),
row(
"t o, in a Runpod picker",
"type the value instead, sort another way",
),
row("a", "add a topic, a provider or a target"),
row("d", "delete the selected topic, provider or target"),
row("u", "undo the last write to overbrainer.toml"),
row("E", "open overbrainer.toml in $EDITOR"),
];
const DATASET: &[KeyHelp] = &[
row("k j, Up Down", "move"),
row("l h, Right Left, Enter", "expand, collapse (Enter toggles)"),
row(
"PgUp PgDn, [ ]",
"scroll the detail pane; [ ] jumps part to part",
),
row("/", "filter the tree (Enter keeps, Esc clears)"),
row("s", "stats pane"),
row("e, E", "edit in $EDITOR; E edits the question's answer"),
row("d", "delete, with what depends on it (asks first)"),
row("D", "delete the question's answer only (asks first)"),
];
const TRAINING: &[KeyHelp] = &[
row("k j, Up Down", "select a run"),
row("a, p", "attach the run again; p hides or shows its pod"),
row("c", "cancel the job; abandons a Runpod start (asks)"),
row("t", "start a training run (asks first)"),
row("s, T", "stop with a snapshot; T resumes it (asks first)"),
row("x", "hide the failed runs until restart (asks first)"),
row("h, C", "push to Hugging Face; compare it (both ask)"),
row(
"g c, starting on Runpod",
"choose GPU types, data centers (saved on y)",
),
];
const PIPELINE: &[KeyHelp] = &[row(
"c, auto mode running",
"cancel its stage and the rest (asks first)",
)];
const COMPARE: &[KeyHelp] = &[
row("k j, Up Down", "move in the focused list"),
row("h l, Left Right", "focus the compares or the questions"),
row(
"PgUp PgDn, [ ]",
"scroll the detail; [ ] jumps part to part",
),
row("f", "filter: all, losses, ties, wins, errors"),
row("C", "compare the newest run with a GGUF (asks first)"),
row("J", "judge the selected compare again (asks first)"),
row("c", "cancel the compare running (asks first)"),
];
const LOGS: &[KeyHelp] = &[
row("k j, Up Down, PgUp PgDn", "scroll"),
row("G, End", "follow the newest lines"),
row("f", "cycle level: error, warn, info, debug, trace"),
row("s", "toggle overbrainer / selected run's pod"),
row("x", "export the shown lines to .overbrainer/"),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Hint {
pub(super) key: &'static str,
pub(super) label: &'static str,
pub(super) locks: bool,
}
const fn hint(key: &'static str, label: &'static str) -> Hint {
Hint {
key,
label,
locks: false,
}
}
const fn locking(key: &'static str, label: &'static str) -> Hint {
Hint {
key,
label,
locks: true,
}
}
pub(super) const SEPARATOR: &str = " · ";
pub(super) const HELP_HINT: &str = "? help";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Context {
View(View),
Abandon,
Filter,
Form,
Pick,
Dialog {
yes: &'static str,
no: &'static str,
},
Help,
Menu,
Picker {
mode: Mode,
typed: bool,
},
Listing {
typed: bool,
},
Start,
}
const FOOTER_PROJECT: &[Hint] = &[
hint("j/k", "move"),
hint("Enter", "edit"),
hint("a", "add"),
hint("d", "delete"),
hint("u", "undo"),
hint("E", "$EDITOR"),
];
const FOOTER_DATASET: &[Hint] = &[
hint("j/k", "move"),
hint("l", "open"),
hint("/", "filter"),
hint("s", "stats"),
locking("e", "edit"),
locking("d", "delete"),
hint("[ ]", "parts"),
];
const FOOTER_PIPELINE: &[Hint] = &[
locking("r", "run a stage"),
locking("A", "auto"),
hint("q", "quit"),
hint("1-6", "views"),
];
const FOOTER_TRAINING: &[Hint] = &[
hint("j/k", "select"),
hint("a", "attach"),
hint("c", "cancel"),
locking("t", "start"),
locking("h", "push"),
hint("x", "clear"),
hint("p", "pod"),
];
const FOOTER_ABANDON: &[Hint] = &[
hint("j/k", "select"),
hint("a", "attach"),
hint("c", "abandon"),
locking("t", "start"),
locking("h", "push"),
hint("x", "clear"),
hint("p", "pod"),
];
const FOOTER_LOGS: &[Hint] = &[
hint("j/k", "scroll"),
hint("G", "follow"),
hint("f", "level"),
hint("s", "source"),
hint("x", "export"),
];
const FOOTER_COMPARE: &[Hint] = &[
hint("j/k", "move"),
hint("h/l", "focus"),
hint("f", "filter"),
locking("C", "compare"),
locking("J", "rejudge"),
hint("c", "cancel"),
];
const FOOTER_FILTER: &[Hint] = &[hint("Enter", "keep"), hint("Esc", "clear")];
const FOOTER_FORM: &[Hint] = &[hint("Enter", "save"), hint("Esc", "cancel")];
const FOOTER_PICK: &[Hint] = &[
hint("←/→", "choose"),
hint("Enter", "pick"),
hint("Esc", "cancel"),
];
const FOOTER_HELP: &[Hint] = &[hint("Esc", "close")];
const FOOTER_LISTING: &[Hint] = &[hint("t", "type"), hint("Esc", "close")];
const FOOTER_PICKER: &[Hint] = &[
hint("Space", "toggle"),
hint("J/K", "order"),
hint("/", "filter"),
hint("Enter", "keep"),
hint("t", "type"),
hint("Esc", "cancel"),
];
const FOOTER_PICK_ONE: &[Hint] = &[
hint("j/k", "move"),
hint("/", "filter"),
hint("Enter", "pick"),
hint("t", "type"),
hint("Esc", "cancel"),
];
const FOOTER_START: &[Hint] = &[
hint("y", "start"),
hint("g", "GPU types"),
hint("c", "data centers"),
hint("n, Esc or Enter", "cancel"),
];
const FOOTER_MENU: &[Hint] = &[
hint("j/k", "move"),
hint("Enter", "run"),
hint("Esc", "close"),
];
pub(super) fn footer(context: Context) -> Vec<Hint> {
match context {
Context::View(View::Project) => FOOTER_PROJECT.to_vec(),
Context::View(View::Dataset) => FOOTER_DATASET.to_vec(),
Context::View(View::Pipeline) => FOOTER_PIPELINE.to_vec(),
Context::View(View::Training) => FOOTER_TRAINING.to_vec(),
Context::View(View::Logs) => FOOTER_LOGS.to_vec(),
Context::View(View::Compare) => FOOTER_COMPARE.to_vec(),
Context::Abandon => FOOTER_ABANDON.to_vec(),
Context::Filter => FOOTER_FILTER.to_vec(),
Context::Form => FOOTER_FORM.to_vec(),
Context::Pick => FOOTER_PICK.to_vec(),
Context::Dialog { yes, no } => vec![hint("y", yes), hint("n, Esc or Enter", no)],
Context::Help => FOOTER_HELP.to_vec(),
Context::Listing { typed } => untyped(FOOTER_LISTING, typed),
Context::Menu => FOOTER_MENU.to_vec(),
Context::Picker {
mode: Mode::Multi,
typed,
} => untyped(FOOTER_PICKER, typed),
Context::Picker {
mode: Mode::Single,
typed,
} => untyped(FOOTER_PICK_ONE, typed),
Context::Start => FOOTER_START.to_vec(),
}
}
fn untyped(hints: &[Hint], typed: bool) -> Vec<Hint> {
hints
.iter()
.filter(|hint| typed || hint.key != "t")
.copied()
.collect()
}
pub(super) fn of(view: View) -> &'static [KeyHelp] {
match view {
View::Project => PROJECT,
View::Dataset => DATASET,
View::Training => TRAINING,
View::Logs => LOGS,
View::Pipeline => PIPELINE,
View::Compare => COMPARE,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn h_pushes_to_hugging_face() {
assert!(
TRAINING
.iter()
.any(|row| row.keys == "h, C" && row.action.starts_with("push to Hugging Face")),
"h in the help"
);
for context in [Context::View(View::Training), Context::Abandon] {
assert!(
footer(context).contains(&locking("h", "push")),
"{context:?}"
);
}
assert!(
NOTE.starts_with("e, d, r, A, t, h, C and J are refused"),
"{NOTE}"
);
}
#[test]
fn c_compares_and_j_judges_again() {
assert!(
TRAINING
.iter()
.any(|row| row.keys == "h, C" && row.action.ends_with("compare it (both ask)")),
"C in the Training help"
);
assert!(
COMPARE.iter().any(|row| row.keys == "J"),
"J in the Compare help"
);
let hints = footer(Context::View(View::Compare));
assert!(hints.contains(&locking("C", "compare")), "{hints:?}");
assert!(hints.contains(&locking("J", "rejudge")), "{hints:?}");
}
#[test]
fn every_row_fits_the_help_overlay() {
let views = View::ALL.into_iter().flat_map(of);
for KeyHelp { keys, action } in GLOBAL.iter().chain(views) {
assert!(keys.chars().count() <= usize::from(KEYS_WIDTH), "{keys}");
assert!(
action.chars().count() <= usize::from(ACTION_WIDTH),
"{action}"
);
}
}
#[test]
fn every_footer_fits_80_columns_with_help() {
let dialog = Context::Dialog {
yes: "cancel the run",
no: "keep it",
};
let others = [
Context::Abandon,
Context::Filter,
Context::Form,
Context::Pick,
dialog,
Context::Help,
Context::Menu,
Context::Picker {
mode: Mode::Multi,
typed: true,
},
Context::Picker {
mode: Mode::Single,
typed: true,
},
Context::Listing { typed: true },
Context::Start,
];
for context in View::ALL.map(Context::View).into_iter().chain(others) {
let hints = footer(context);
let text: Vec<String> = hints
.iter()
.map(|hint| format!("{} {}", hint.key, hint.label))
.collect();
let line = format!(" {} {HELP_HINT} ", text.join(SEPARATOR));
assert!(line.chars().count() <= 80, "{context:?}: {line}");
}
}
}