use super::app::View;
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 and t are refused while a stage, an edit or a training \
start runs in this TUI; an overbrainer command in another \
terminal is not locked out.";
pub(super) const GLOBAL: &[KeyHelp] = &[
row("1 2 3 4, 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("r", "run a pipeline stage, or run (asks which)"),
row(
"y, n Esc Enter",
"in a dialog: confirm, cancel (the default, n)",
),
];
const DATASET: &[KeyHelp] = &[
row("k j, Up Down", "move"),
row("l h, Right Left, Enter", "expand, collapse"),
row("PgUp PgDn", "scroll the detail pane"),
row("/", "filter the tree (Enter keeps, Esc clears)"),
row("s", "stats pane"),
row("e", "edit in $EDITOR (question, answer, subtopic)"),
row("d", "delete, with what depends on it (asks first)"),
];
const TRAINING: &[KeyHelp] = &[
row("k j, Up Down", "select a run"),
row("a", "attach: follow the selected run again"),
row("c", "cancel the selected run's job (asks first)"),
row("c, Runpod run starting", "abandon it instead (asks first)"),
row("t", "start a training run (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"),
];
#[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,
Dialog {
yes: &'static str,
no: &'static str,
},
Help,
Menu,
}
const FOOTER_DATASET: &[Hint] = &[
hint("j/k", "move"),
hint("l", "open"),
hint("/", "filter"),
hint("s", "stats"),
locking("e", "edit"),
locking("d", "delete"),
];
const FOOTER_PIPELINE: &[Hint] = &[
locking("r", "run a stage"),
hint("1-4", "views"),
hint("q", "quit"),
];
const FOOTER_TRAINING: &[Hint] = &[
hint("j/k", "select"),
hint("a", "attach"),
hint("c", "cancel"),
locking("t", "start"),
];
const FOOTER_ABANDON: &[Hint] = &[
hint("j/k", "select"),
hint("a", "attach"),
hint("c", "abandon"),
locking("t", "start"),
];
const FOOTER_LOGS: &[Hint] = &[
hint("j/k", "scroll"),
hint("G", "follow"),
hint("f", "level"),
];
const FOOTER_FILTER: &[Hint] = &[hint("Enter", "keep"), hint("Esc", "clear")];
const FOOTER_HELP: &[Hint] = &[hint("Esc", "close")];
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::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::Abandon => FOOTER_ABANDON.to_vec(),
Context::Filter => FOOTER_FILTER.to_vec(),
Context::Dialog { yes, no } => vec![hint("y", yes), hint("n, Esc or Enter", no)],
Context::Help => FOOTER_HELP.to_vec(),
Context::Menu => FOOTER_MENU.to_vec(),
}
}
pub(super) fn of(view: View) -> &'static [KeyHelp] {
match view {
View::Dataset => DATASET,
View::Training => TRAINING,
View::Logs => LOGS,
View::Pipeline => &[],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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,
dialog,
Context::Help,
Context::Menu,
];
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}");
}
}
}