Files
rdbms-playground/docs/adr/0051-context-state-aware-keybinding-strip.md
claude@clouddev1 eceedc19b7 feat(ui): context- and state-aware bottom keybinding strip (#27)
Per ADR-0051 (closing issue #27): the bottom status line is now a
keystrokes-only, state-selected strip. A pure status_bar_bindings()
picks the binding set by priority (first match wins):

  sidebar focus   → Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input
  completion live → Tab/Shift-Tab cycle · Esc cancel · Enter run
  history nav     → ↑↓ browse · Esc clear · Enter run
  editing         → Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run
  default (empty) → Ctrl-O sidebar · Tab complete · ↑ history · Enter run

The editing state surfaces the #29 readline keys (ADR-0049's deferred
advertisement). Typed-command words (mode advanced/simple, the ':'
one-shot) and Ctrl-C quit leave the strip; simple mode's empty-input
hint gains a '`mode advanced` for SQL' pointer (advanced mode shows
none — a switcher knows the way back, and help covers it).

Mechanism: status_bar_bindings + a thin renderer (unit-testable);
App::is_browsing_history() exposes the private history_cursor; the mode
pointer lives in resolve_hint_lines. Catalog: 12 new shortcut labels +
panel.hint_mode_advanced (en-US.yaml + keys.rs, validator 1:1), 5 dead
strip strings removed.

Forks user-chosen: editing state shows #29 keys; quit omitted; no
width-drop machinery (longest strip ~65 cols fits; a width-budget test
keeps it lean). Modal-aware strip is OOS (pre-existing). Tests: 9
Tier-1 unit (per-state key sets, width budget, mode pointer), 1 Tier-3
rewritten, 15 full-panel snapshots re-accepted (reviewed). 2467 pass /
0 fail / 0 skip, clippy clean.
2026-06-13 12:18:37 +00:00

7.3 KiB

ADR-0051: Bottom keybinding strip — context- and state-aware

Status

Accepted 2026-06-13 (issue #27). Closes Gitea #27. All forks below were escalated to the user and user-chosen before any code was written; to be implemented test-first. Builds on ADR-0046 (nav focus), ADR-0003 (input modes), ADR-0049 (the #29 readline keys this strip now advertises), and ADR-0022 (the Tab-completion memo).

Context

The bottom status line (render_status_bar, ui.rs) mixed keystrokes with typed-command words: Enter submit · : advanced once · mode advanced switch · Ctrl-C quit. That is redundant — the hint panel already teaches help and Enter when the input is empty — and it is static apart from a three-way mode branch, so it never reflects what the user can actually do right now (navigating the sidebar, cycling a completion, browsing history, editing a line).

Issue #27: repurpose the line as a keybindings-only strip that is context-sensitive to nav focus and state-aware of the current transient interaction, and move mode discovery into the empty-input hint.

Decision

1. The strip is keybindings-only and state-selected

A single pure function status_bar_bindings(app) -> Vec<Binding> computes the strip from app state; render_status_bar is a thin renderer over it (so the binding sets are unit-testable without a Frame). history_cursor is private to App, so a small pub fn is_browsing_history(&self) -> bool accessor exposes the history-navigation predicate; mode / nav_focus / last_completion are already pub and effective_mode() is a pub method. The state is chosen by priority — first match wins:

Priority State (predicate) Strip
1 Sidebar focus (nav_focus in a sidebar) Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input
2 Completion memo live (last_completion.is_some()) Tab/Shift-Tab cycle · Esc cancel · Enter run
3 History navigation (history_cursor.is_some()) ↑↓ browse · Esc clear · Enter run
4 Editing (Input focus, input non-empty) Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run
5 Default (Input focus, input empty) Ctrl-O sidebar · Tab complete · ↑ history · Enter run

Priority order matters: a completion memo or history navigation is a non-empty-input situation, so states 2 and 3 must precede state 4. The sidebar overlay occludes the input entirely (ADR-0046), so state 1 wins outright.

2. Mode discovery moves off the strip, into the empty-input hint

The typed-command advertisements (mode advanced / mode simple switch, the : one-shot) leave the strip — they are not keystrokes. Mode discovery moves to the empty-input hint (resolve_hint_lines's (None, None) arm), in simple mode only:

  • Simple: … · \mode advanced` for SQL`
  • Advanced (persistent): no pointer.

The pointer omits the verb "type" — the surrounding prompt already implies it (we don't say "type help" either). Advanced mode shows no pointer (user decision, post-trial): a user who switched into advanced mode knows how they got there, and help covers the way back — a "switch back" pointer only reads naturally in the moment right after switching, so it earns its space poorly.

The one-shot advanced state's old Backspace cancel one-shot label is subsumed by the editing state (the input is non-empty in one-shot; Esc-clear and Backspace both cancel it). No behaviour is lost — only the dedicated label.

3. Width: no drop machinery; a budget test instead

The longest strip (state 4, editing) is ≈ 65 display columns, which fits every supported width (90-col screencasts, 80-col terminals) with margin — so the priority-drop / abbreviation machinery considered would never trigger and is not built (user-confirmed). Ratatui's existing clip-at-edge is the trivial fallback for pathologically narrow (< 65-col) terminals. Instead, a width-budget unit test pins the longest rendered strip within an 80-col budget, keeping the strip lean by construction — a future over-long strip fails the test rather than silently clipping in a cast.

Forks (all user-chosen)

  • Editing state — yes: when the input has text, surface the #29 readline keys (Esc-clear, Ctrl-A/E, Ctrl-W); the strip stays lean (nav/complete/history) when empty. (vs not advertising the #29 keys.)
  • Ctrl-C quit — omitted from the strip (vs always shown): quit is a near-universal convention; omitting it keeps the strips lean and matches the issue's sketch.
  • Width — budget test, no drop logic (vs graceful priority-drop / abbreviation): the strips fit at supported widths, so the machinery would be dead weight (user's own observation).

Consequences

  • The strip now teaches the keys for the current situation; learners see Tab/Shift-Tab cycle exactly while cycling, the editing keys exactly while editing, etc.
  • The #29 readline keys (ADR-0049) gain their on-screen advertisement, closing that ADR's deferred item.
  • 15 existing full-panel insta snapshots churn (the bottom line — and, on empty-input views, the hint pointer — changes in every one, including the rebuild-confirm modal view, whose modal box is itself unchanged); each diff was reviewed, not blind-accepted.
  • requirements.md is unaffected (an ADR-tracked UI refinement); the change is cross-referenced from the commit + this ADR.

Tests

  • Tier-1 (ui.rs unit): status_bar_bindings returns the expected key set for each of the five states (sidebar, completion-live, history-nav, editing, default) — the completion/history states driven through real key events (update) so the predicate transitions are exercised, the others by setting App fields; plus the width-budget assertion across states. (Per-state coverage is these unit tests, not snapshots — a one-line strip is asserted more precisely by its exact key list than by a full-panel snapshot.)
  • Tier-1: the empty-input hint appends the correct mode pointer in Simple vs Advanced, and does not append it when an ambient hint is showing (non-empty input).
  • Tier-3 (walking_skeleton): the old status_bar_lists_quit_and_ submit_in_all_modes (which asserted the pre-ADR strip) is rewritten + renamed to assert the keystroke-only, state-aware strip end-to-end through the real render path (default → editing transition).
  • Tier-2 (insta): the 15 full-panel snapshots re-accepted (each diff reviewed — strip line and/or hint pointer only).

Out of scope

  • Modal-aware strip. While a modal is open (load picker, rebuild / undo confirm) it owns the keyboard and carries its own in-box key hints; the bottom strip under a modal computes from input state exactly as it does today (modals render over the status bar). This issue does not redesign the modal case — pre-existing behaviour, unchanged and not worsened.
  • A persistent/togglable help overlay listing all keys (the strip is a contextual subset, not a cheatsheet).
  • Per-key colour theming beyond the existing key/label/separator styles.
  • Localisation of the new label strings beyond adding catalog entries.
  • The remaining I1b kill keys' (Ctrl-K/Ctrl-U) advertisement — the editing strip shows the highest-value subset (Esc/Ctrl-A/E/Ctrl-W) to stay within the width budget; Ctrl-K/U remain unadvertised muscle memory.