use std::fmt::Write;
use crossterm::event::KeyCode;
use crate::{
message::{GlobalAction, ModeAction, WorkerSelectorAction},
navigation::Direction,
};
#[derive(Clone, Copy)]
pub struct KeyAction<A: 'static> {
pub key: KeyCode,
pub action: A,
}
pub struct KeyBinding<A: 'static> {
pub label: &'static str,
pub help_description: &'static str,
pub status_description: Option<&'static str>,
pub key_actions: &'static [KeyAction<A>],
}
pub fn find_key_action<A: Copy>(bindings: &[KeyBinding<A>], key: KeyCode) -> Option<A> {
bindings
.iter()
.flat_map(|binding| binding.key_actions)
.find(|key_action| key_action.key == key)
.map(|key_action| key_action.action)
}
const fn global(key: char, action: GlobalAction) -> KeyAction<GlobalAction> {
KeyAction {
key: KeyCode::Char(key),
action,
}
}
pub const GLOBAL_KEYBINDINGS: &[KeyBinding<GlobalAction>] = &[
KeyBinding {
label: "?",
help_description: "toggle help",
status_description: Some("help"),
key_actions: &[global('?', GlobalAction::ToggleHelp)],
},
KeyBinding {
label: "[/]",
help_description: "cycle display mode",
status_description: Some("style"),
key_actions: &[
global(']', GlobalAction::NextDisplayMode),
global('[', GlobalAction::PreviousDisplayMode),
],
},
KeyBinding {
label: "i/o",
help_description: "toggle input/output panes",
status_description: Some("in/out"),
key_actions: &[
global('i', GlobalAction::ToggleInputPane),
global('o', GlobalAction::ToggleOutputPane),
],
},
KeyBinding {
label: "m",
help_description: "cycle timing metrics",
status_description: Some("metrics"),
key_actions: &[global('m', GlobalAction::CycleTimingDisplay)],
},
KeyBinding {
label: "g",
help_description: "goto origin",
status_description: Some("goto"),
key_actions: &[
global('g', GlobalAction::GotoSelection),
KeyAction {
key: KeyCode::Enter,
action: GlobalAction::GotoSelection,
},
],
},
KeyBinding {
label: "y",
help_description: "copy to clipboard",
status_description: Some("copy"),
key_actions: &[global('y', GlobalAction::CopySelection)],
},
KeyBinding {
label: "f",
help_description: "select file",
status_description: None,
key_actions: &[global('f', GlobalAction::OpenWorkerSelector)],
},
KeyBinding {
label: "F",
help_description: "toggle follow mode",
status_description: None,
key_actions: &[global('F', GlobalAction::ToggleFollowMode)],
},
KeyBinding {
label: "L",
help_description: "toggle legend",
status_description: None,
key_actions: &[global('L', GlobalAction::ToggleLegend)],
},
KeyBinding {
label: "Esc",
help_description: "close popup",
status_description: None,
key_actions: &[KeyAction {
key: KeyCode::Esc,
action: GlobalAction::CloseOverlay,
}],
},
KeyBinding {
label: "q",
help_description: "quit",
status_description: Some("quit"),
key_actions: &[global('q', GlobalAction::Quit)],
},
];
const fn move_on(key: KeyCode, direction: Direction) -> KeyAction<ModeAction> {
KeyAction {
key,
action: ModeAction::Move { direction },
}
}
const VERTICAL_MOVES: [KeyAction<ModeAction>; 4] = [
move_on(KeyCode::Char('j'), Direction::Down),
move_on(KeyCode::Down, Direction::Down),
move_on(KeyCode::Char('k'), Direction::Up),
move_on(KeyCode::Up, Direction::Up),
];
const HORIZONTAL_MOVES: [KeyAction<ModeAction>; 4] = [
move_on(KeyCode::Char('h'), Direction::Left),
move_on(KeyCode::Left, Direction::Left),
move_on(KeyCode::Char('l'), Direction::Right),
move_on(KeyCode::Right, Direction::Right),
];
const ALL_MOVES: [KeyAction<ModeAction>; 8] = [
VERTICAL_MOVES[0],
VERTICAL_MOVES[1],
VERTICAL_MOVES[2],
VERTICAL_MOVES[3],
HORIZONTAL_MOVES[0],
HORIZONTAL_MOVES[1],
HORIZONTAL_MOVES[2],
HORIZONTAL_MOVES[3],
];
const fn mode(key: char, action: ModeAction) -> KeyAction<ModeAction> {
KeyAction {
key: KeyCode::Char(key),
action,
}
}
const GRAPH_NAVIGATION: KeyBinding<ModeAction> = KeyBinding {
label: "hjkl",
help_description: "navigate",
status_description: Some("nav"),
key_actions: &ALL_MOVES,
};
const FLIP_ORIENTATION: KeyBinding<ModeAction> = KeyBinding {
label: "t",
help_description: "flip tree orientation",
status_description: Some("flip"),
key_actions: &[mode('t', ModeAction::FlipOrientation)],
};
pub const SEMANTIC_TABLEAU_KEYBINDINGS: &[KeyBinding<ModeAction>] =
&[GRAPH_NAVIGATION, FLIP_ORIENTATION];
pub const DATA_FLOW_KEYBINDINGS: &[KeyBinding<ModeAction>] = &[GRAPH_NAVIGATION, FLIP_ORIENTATION];
pub const EFFECT_FLOW_KEYBINDINGS: &[KeyBinding<ModeAction>] = &[
GRAPH_NAVIGATION,
KeyBinding {
label: "e",
help_description: "expand pure steps",
status_description: Some("expand"),
key_actions: &[mode('e', ModeAction::ToggleExpanded)],
},
];
const LIST_SELECTION: KeyBinding<ModeAction> = KeyBinding {
label: "jk",
help_description: "select hypothesis or goal",
status_description: Some("select"),
key_actions: &VERTICAL_MOVES,
};
pub const COMPACT_TREE_KEYBINDINGS: &[KeyBinding<ModeAction>] = &[
LIST_SELECTION,
KeyBinding {
label: "hl",
help_description: "scroll steps",
status_description: Some("scroll"),
key_actions: &HORIZONTAL_MOVES,
},
];
const fn selector(key: KeyCode, action: WorkerSelectorAction) -> KeyAction<WorkerSelectorAction> {
KeyAction { key, action }
}
pub const WORKER_SELECTOR_KEYBINDINGS: &[KeyBinding<WorkerSelectorAction>] = &[
KeyBinding {
label: "↑/↓",
help_description: "select",
status_description: None,
key_actions: &[
selector(KeyCode::Up, WorkerSelectorAction::HighlightPrevious),
selector(KeyCode::Char('k'), WorkerSelectorAction::HighlightPrevious),
selector(KeyCode::Down, WorkerSelectorAction::HighlightNext),
selector(KeyCode::Char('j'), WorkerSelectorAction::HighlightNext),
],
},
KeyBinding {
label: "Enter",
help_description: "connect",
status_description: None,
key_actions: &[selector(KeyCode::Enter, WorkerSelectorAction::Connect)],
},
KeyBinding {
label: "r",
help_description: "rescan",
status_description: None,
key_actions: &[selector(KeyCode::Char('r'), WorkerSelectorAction::Rescan)],
},
KeyBinding {
label: "Esc",
help_description: "cancel",
status_description: None,
key_actions: &[selector(KeyCode::Esc, WorkerSelectorAction::Cancel)],
},
];
pub const BEFORE_AFTER_KEYBINDINGS: &[KeyBinding<ModeAction>] = &[
LIST_SELECTION,
KeyBinding {
label: "p",
help_description: "toggle previous step",
status_description: Some("prev"),
key_actions: &[mode('p', ModeAction::TogglePreviousPane)],
},
KeyBinding {
label: "n",
help_description: "toggle next step",
status_description: Some("next"),
key_actions: &[mode('n', ModeAction::ToggleNextPane)],
},
];
fn markdown_keybinding_section<A>(title: &str, bindings: &[KeyBinding<A>]) -> String {
bindings.iter().fold(
format!("## {title}\n\n| Key | Action |\n| --- | --- |\n"),
|mut table, binding| {
let _ = writeln!(
table,
"| `{}` | {} |",
binding.label, binding.help_description
);
table
},
)
}
#[must_use]
pub fn keybinding_reference_markdown() -> String {
let sections = [
markdown_keybinding_section("Global", GLOBAL_KEYBINDINGS),
markdown_keybinding_section("Compact", BEFORE_AFTER_KEYBINDINGS),
markdown_keybinding_section("Compact tree", COMPACT_TREE_KEYBINDINGS),
markdown_keybinding_section("Semantic tableau", SEMANTIC_TABLEAU_KEYBINDINGS),
markdown_keybinding_section("Data flow", DATA_FLOW_KEYBINDINGS),
markdown_keybinding_section("Effect flow", EFFECT_FLOW_KEYBINDINGS),
markdown_keybinding_section("File picker", WORKER_SELECTOR_KEYBINDINGS),
];
format!(
"# Keybindings\n\n<!-- Generated by `cargo xtask keybinding-docs` from \
src/keybindings.rs. -->\n\nPress `?` in lean-tui to see the bindings of the current \
view.\n\n{}",
sections.join("\n")
)
}