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.
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 cycleexactly 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.mdis unaffected (an ADR-tracked UI refinement); the change is cross-referenced from the commit + this ADR.
Tests
- Tier-1 (
ui.rsunit):status_bar_bindingsreturns 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 settingAppfields; 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 oldstatus_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.