docs(tui): config and keybinding docs

This commit is contained in:
Felipe Coury
2026-04-23 10:35:33 -03:00
parent 942fb20e00
commit 704225a300
3 changed files with 94 additions and 0 deletions
+43
View File
@@ -175,48 +175,91 @@ pub struct TuiEditorKeymap {
}
/// Vim normal-mode keybindings for modal editing inside text areas.
///
/// Actions that use uppercase letters (like `A` for append-line-end) should
/// be specified as `shift-a` in config; the runtime matcher handles
/// cross-terminal shift-reporting differences automatically.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct TuiVimNormalKeymap {
/// Enter insert mode at cursor (`i`).
pub enter_insert: Option<KeybindingsSpec>,
/// Enter insert mode after cursor (`a`).
pub append_after_cursor: Option<KeybindingsSpec>,
/// Enter insert mode at end of line (`A`).
pub append_line_end: Option<KeybindingsSpec>,
/// Enter insert mode at first non-blank of line (`I`).
pub insert_line_start: Option<KeybindingsSpec>,
/// Open a new line below and enter insert mode (`o`).
pub open_line_below: Option<KeybindingsSpec>,
/// Open a new line above and enter insert mode (`O`).
pub open_line_above: Option<KeybindingsSpec>,
/// Move cursor left (`h`).
pub move_left: Option<KeybindingsSpec>,
/// Move cursor right (`l`).
pub move_right: Option<KeybindingsSpec>,
/// Move cursor up (`k`).
pub move_up: Option<KeybindingsSpec>,
/// Move cursor down (`j`).
pub move_down: Option<KeybindingsSpec>,
/// Move cursor to start of next word (`w`).
pub move_word_forward: Option<KeybindingsSpec>,
/// Move cursor to start of previous word (`b`).
pub move_word_backward: Option<KeybindingsSpec>,
/// Move cursor to end of current/next word (`e`).
pub move_word_end: Option<KeybindingsSpec>,
/// Move cursor to start of line (`0`).
pub move_line_start: Option<KeybindingsSpec>,
/// Move cursor to end of line (`$`).
pub move_line_end: Option<KeybindingsSpec>,
/// Delete character under cursor (`x`).
pub delete_char: Option<KeybindingsSpec>,
/// Delete from cursor to end of line (`D`).
pub delete_to_line_end: Option<KeybindingsSpec>,
/// Yank the entire line (`Y`).
pub yank_line: Option<KeybindingsSpec>,
/// Paste after cursor (`p`).
pub paste_after: Option<KeybindingsSpec>,
/// Begin delete operator; next key selects motion (`d`).
pub start_delete_operator: Option<KeybindingsSpec>,
/// Begin yank operator; next key selects motion (`y`).
pub start_yank_operator: Option<KeybindingsSpec>,
/// Cancel a pending operator and return to normal mode.
pub cancel_operator: Option<KeybindingsSpec>,
}
/// Vim operator-pending keybindings for modal editing inside text areas.
///
/// This context is active only while waiting for a motion after `d` or `y`.
/// Repeating the operator key (`dd`, `yy`) targets the entire line. Pressing
/// `Esc` cancels the pending operator and returns to normal mode without
/// modifying text.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct TuiVimOperatorKeymap {
/// Repeat delete operator to delete the whole line (`dd`).
pub delete_line: Option<KeybindingsSpec>,
/// Repeat yank operator to yank the whole line (`yy`).
pub yank_line: Option<KeybindingsSpec>,
/// Motion: left (`h`).
pub motion_left: Option<KeybindingsSpec>,
/// Motion: right (`l`).
pub motion_right: Option<KeybindingsSpec>,
/// Motion: up one line (`k`).
pub motion_up: Option<KeybindingsSpec>,
/// Motion: down one line (`j`).
pub motion_down: Option<KeybindingsSpec>,
/// Motion: to start of next word (`w`).
pub motion_word_forward: Option<KeybindingsSpec>,
/// Motion: to start of previous word (`b`).
pub motion_word_backward: Option<KeybindingsSpec>,
/// Motion: to end of current/next word (`e`).
pub motion_word_end: Option<KeybindingsSpec>,
/// Motion: to start of line (`0`).
pub motion_line_start: Option<KeybindingsSpec>,
/// Motion: to end of line (`$`).
pub motion_line_end: Option<KeybindingsSpec>,
/// Cancel the pending operator and return to normal mode.
pub cancel: Option<KeybindingsSpec>,
}
+26
View File
@@ -1,3 +1,12 @@
//! Key binding primitives and input matching for the TUI.
//!
//! This module provides `KeyBinding`, the runtime representation of a single
//! keybinding (key code + modifier set), along with matching logic that handles
//! cross-terminal inconsistencies in how shifted letters are reported.
//!
//! It also supplies rendering helpers that convert bindings into styled
//! `ratatui::text::Span` values for UI hint display.
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
@@ -15,6 +24,12 @@ const ALT_PREFIX: &str = "alt + ";
const CTRL_PREFIX: &str = "ctrl + ";
const SHIFT_PREFIX: &str = "shift + ";
/// A single key chord (key code + modifier set) used for input matching.
///
/// Matching via `is_press` handles both exact equality and a shifted-letter
/// compatibility fallback for terminals that report uppercase letters without
/// the SHIFT modifier flag. This means a binding defined as `shift-a` will
/// match a terminal event of either `Shift+a` or plain `A`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct KeyBinding {
key: KeyCode,
@@ -38,6 +53,17 @@ impl KeyBinding {
self.matches_shifted_ascii_letter_compat(event)
}
/// Cross-terminal compatibility for shifted ASCII letter bindings.
///
/// Some terminals (e.g. certain Kitty/Alacritty builds) report `Shift+a` as
/// `KeyCode::Char('A')` with `KeyModifiers::NONE`, while others keep the
/// lowercase char and set the SHIFT flag. This method accepts both forms
/// when the binding specifies SHIFT + an ASCII lowercase letter, and
/// preserves any additional modifiers (e.g. Ctrl) so `ctrl-shift-i`
/// also matches `Ctrl+I`.
///
/// Only applies to ASCII lowercase letters — non-ASCII shift behavior
/// (accented characters on European layouts, for instance) is not handled.
fn matches_shifted_ascii_letter_compat(&self, event: KeyEvent) -> bool {
let (KeyCode::Char(bound), KeyCode::Char(observed)) = (self.key, event.code) else {
return false;
+25
View File
@@ -58,6 +58,12 @@ pub(crate) struct AppKeymap {
pub(crate) toggle_vim_mode: Vec<KeyBinding>,
}
/// Chat-level keybindings evaluated at the app event layer.
///
/// These participate in the first app-scope conflict validation pass alongside
/// `AppKeymap` actions because both are checked before input reaches the
/// composer. Dispatch gating (empty-composer guard for backtrack) happens in
/// handler code, not here.
#[derive(Clone, Debug)]
pub(crate) struct ChatKeymap {
/// Start/advance edit-previous flow when composer is empty.
@@ -66,6 +72,11 @@ pub(crate) struct ChatKeymap {
pub(crate) confirm_edit_previous_message: Vec<KeyBinding>,
}
/// Composer-level keybindings validated in the second app-scope conflict pass.
///
/// App-level handlers execute before the composer receives input, so any key
/// bound here that also appears in `AppKeymap` would be silently intercepted.
/// The conflict validator prevents this by checking app + composer uniqueness.
#[derive(Clone, Debug)]
pub(crate) struct ComposerKeymap {
/// Submit current draft.
@@ -100,6 +111,14 @@ pub(crate) struct EditorKeymap {
pub(crate) yank: Vec<KeyBinding>,
}
/// Vim normal-mode keybindings for modal editing in the composer textarea.
///
/// Normal mode is the resting state when Vim is enabled. Pressing a movement
/// or editing key here either moves the cursor, triggers an operator-pending
/// state (via `start_delete_operator` / `start_yank_operator`), or transitions
/// to insert mode. Default bindings include both `shift(letter)` and
/// `plain(UPPERCASE)` variants for uppercase commands like `A`, `I`, `O` to
/// handle cross-terminal shift-reporting inconsistencies.
#[derive(Clone, Debug)]
pub(crate) struct VimNormalKeymap {
pub(crate) enter_insert: Vec<KeyBinding>,
@@ -126,6 +145,12 @@ pub(crate) struct VimNormalKeymap {
pub(crate) cancel_operator: Vec<KeyBinding>,
}
/// Vim operator-pending keybindings active after `d` or `y` in normal mode.
///
/// When an operator (`start_delete_operator` or `start_yank_operator`) is
/// pressed, the next keypress is matched against this context to determine the
/// motion range. Repeating the operator key (`dd`, `yy`) acts on the whole
/// line. `Esc` cancels the pending operator and returns to normal mode.
#[derive(Clone, Debug)]
pub(crate) struct VimOperatorKeymap {
pub(crate) delete_line: Vec<KeyBinding>,