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.
This commit is contained in:
@@ -0,0 +1,147 @@
|
|||||||
|
# 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.
|
||||||
File diff suppressed because one or more lines are too long
+11
@@ -646,6 +646,17 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the user is currently browsing a recalled history entry
|
||||||
|
/// (Up/Down navigation, unedited). Exposes the private
|
||||||
|
/// `history_cursor` predicate so the context-aware status strip
|
||||||
|
/// (ADR-0051) can select its history-navigation state. Editing the
|
||||||
|
/// recalled line ends navigation (`cancel_history_navigation`), so
|
||||||
|
/// this is `false` again the moment the user types.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_browsing_history(&self) -> bool {
|
||||||
|
self.history_cursor.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
/// The input view the **live-feedback** walkers (completion, ambient
|
/// The input view the **live-feedback** walkers (completion, ambient
|
||||||
/// hint, validity verdict, highlight overlays) should see, plus the
|
/// hint, validity verdict, highlight overlays) should see, plus the
|
||||||
/// byte offset stripped from the front and the cursor mapped into the
|
/// byte offset stripped from the front and the cursor mapped into the
|
||||||
|
|||||||
+13
-5
@@ -446,6 +446,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("undo.redo_failed", &["error"]),
|
("undo.redo_failed", &["error"]),
|
||||||
// ---- Status bar + panels ----
|
// ---- Status bar + panels ----
|
||||||
("panel.hint_empty", &[]),
|
("panel.hint_empty", &[]),
|
||||||
|
("panel.hint_mode_advanced", &[]),
|
||||||
("panel.hint_title", &[]),
|
("panel.hint_title", &[]),
|
||||||
("panel.output_title", &[]),
|
("panel.output_title", &[]),
|
||||||
("panel.relationships_empty", &[]),
|
("panel.relationships_empty", &[]),
|
||||||
@@ -462,18 +463,25 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("save.title_as", &[]),
|
("save.title_as", &[]),
|
||||||
("save.title_save", &[]),
|
("save.title_save", &[]),
|
||||||
// ---- Shortcut hint labels ----
|
// ---- Shortcut hint labels ----
|
||||||
("shortcut.advanced_once", &[]),
|
|
||||||
("shortcut.back_to_list", &[]),
|
("shortcut.back_to_list", &[]),
|
||||||
|
("shortcut.browse", &[]),
|
||||||
("shortcut.browse_path", &[]),
|
("shortcut.browse_path", &[]),
|
||||||
("shortcut.cancel", &[]),
|
("shortcut.cancel", &[]),
|
||||||
("shortcut.cancel_one_shot", &[]),
|
("shortcut.clear", &[]),
|
||||||
|
("shortcut.complete", &[]),
|
||||||
("shortcut.confirm", &[]),
|
("shortcut.confirm", &[]),
|
||||||
|
("shortcut.cycle", &[]),
|
||||||
|
("shortcut.del_word", &[]),
|
||||||
|
("shortcut.history", &[]),
|
||||||
|
("shortcut.home_end", &[]),
|
||||||
("shortcut.load", &[]),
|
("shortcut.load", &[]),
|
||||||
|
("shortcut.nav", &[]),
|
||||||
|
("shortcut.next_pane", &[]),
|
||||||
("shortcut.no", &[]),
|
("shortcut.no", &[]),
|
||||||
("shortcut.quit", &[]),
|
("shortcut.run", &[]),
|
||||||
|
("shortcut.scroll", &[]),
|
||||||
("shortcut.select", &[]),
|
("shortcut.select", &[]),
|
||||||
("shortcut.submit", &[]),
|
("shortcut.to_input", &[]),
|
||||||
("shortcut.switch", &[]),
|
|
||||||
("shortcut.yes", &[]),
|
("shortcut.yes", &[]),
|
||||||
// ---- mode / messages banners ----
|
// ---- mode / messages banners ----
|
||||||
("messages.set_short", &[]),
|
("messages.set_short", &[]),
|
||||||
|
|||||||
@@ -883,14 +883,21 @@ panel:
|
|||||||
relationships_title: "Relationships"
|
relationships_title: "Relationships"
|
||||||
relationships_empty: "(none)"
|
relationships_empty: "(none)"
|
||||||
hint_empty: "Type a command — press Tab for options, `help` for a list"
|
hint_empty: "Type a command — press Tab for options, `help` for a list"
|
||||||
|
# Mode-discovery pointer appended to the empty-input hint in SIMPLE
|
||||||
|
# mode (ADR-0051): the `mode advanced` switch left the keybinding
|
||||||
|
# strip, so the hint advertises it. Leading separator continues the
|
||||||
|
# prompt line. Advanced mode shows no pointer — users know how they
|
||||||
|
# got there, and `help` covers the way back.
|
||||||
|
hint_mode_advanced: " · `mode advanced` for SQL"
|
||||||
# Panel titles for the output and hint panels (rendered inside
|
# Panel titles for the output and hint panels (rendered inside
|
||||||
# the rounded border, hence the leading/trailing space).
|
# the rounded border, hence the leading/trailing space).
|
||||||
output_title: "Output"
|
output_title: "Output"
|
||||||
hint_title: "Hint"
|
hint_title: "Hint"
|
||||||
|
|
||||||
# ---- Shortcut hints (paired with key names in the bottom bar) -------
|
# ---- Shortcut hints (paired with key names in the bottom bar) -------
|
||||||
|
# The bottom strip is keystrokes-only and state-aware (ADR-0051). Labels
|
||||||
|
# pair with a key name in the renderer (e.g. `Enter` + `run`).
|
||||||
shortcut:
|
shortcut:
|
||||||
submit: "submit"
|
|
||||||
confirm: "confirm"
|
confirm: "confirm"
|
||||||
cancel: "cancel"
|
cancel: "cancel"
|
||||||
yes: "Yes"
|
yes: "Yes"
|
||||||
@@ -899,10 +906,19 @@ shortcut:
|
|||||||
select: "select"
|
select: "select"
|
||||||
browse_path: "browse path"
|
browse_path: "browse path"
|
||||||
back_to_list: "back to list"
|
back_to_list: "back to list"
|
||||||
switch: "switch"
|
# Status-strip labels (ADR-0051, issue #27).
|
||||||
advanced_once: "advanced once"
|
run: "run"
|
||||||
cancel_one_shot: "cancel one-shot"
|
nav: "sidebar"
|
||||||
quit: "quit"
|
next_pane: "next pane"
|
||||||
|
scroll: "scroll"
|
||||||
|
to_input: "input"
|
||||||
|
cycle: "cycle"
|
||||||
|
browse: "browse"
|
||||||
|
clear: "clear"
|
||||||
|
complete: "complete"
|
||||||
|
history: "history"
|
||||||
|
home_end: "home/end"
|
||||||
|
del_word: "del word"
|
||||||
|
|
||||||
# ---- mode / messages banners (app-level commands) -------------------
|
# ---- mode / messages banners (app-level commands) -------------------
|
||||||
mode:
|
mode:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2326
|
assertion_line: 2836
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -26,4 +26,4 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · mode simple switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2309
|
assertion_line: 2819
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -22,8 +22,8 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ Hint ────────────────────────────────────────────────────────────────────────╮
|
╭ Hint ────────────────────────────────────────────────────────────────────────╮
|
||||||
│Type a command — press Tab for options, `help` for a list │
|
│Type a command — press Tab for options, `help` for a list · `mode advanced` │
|
||||||
│ │
|
│for SQL │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2317
|
assertion_line: 2827
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -22,8 +22,8 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ Hint ────────────────────────────────────────────────────────────────────────╮
|
╭ Hint ────────────────────────────────────────────────────────────────────────╮
|
||||||
│Type a command — press Tab for options, `help` for a list │
|
│Type a command — press Tab for options, `help` for a list · `mode advanced` │
|
||||||
│ │
|
│for SQL │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 3442
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -23,8 +24,8 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Type a command — press Tab for options, `help` for a list │
|
│Type a command — press Tab for options, `help` for a list · `mode advanced` for SQL │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 3388
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -23,8 +24,8 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Type a command — press Tab for options, `help` for a list │
|
│Type a command — press Tab for options, `help` for a list · `mode advanced` for SQL │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 3378
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -23,8 +24,8 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Type a command — press Tab for options, `help` for a list │
|
│Type a command — press Tab for options, `help` for a list · `mode advanced` for SQL │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 3431
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -23,8 +24,8 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Type a command — press Tab for options, `help` for a list │
|
│Type a command — press Tab for options, `help` for a list · `mode advanced` for SQL │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
|
assertion_line: 3457
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
╭ Output ────────────────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -23,8 +24,8 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
╭ Hint ──────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│Type a command — press Tab for options, `help` for a list │
|
│Type a command — press Tab for options, `help` for a list · `mode advanced` for SQL │
|
||||||
│ │
|
│ │
|
||||||
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
╰────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2369
|
assertion_line: 2880
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -26,4 +26,4 @@ expression: snapshot
|
|||||||
│insert into <Table> [(<col>[, ...])] [values] (<value>[, ...]) │
|
│insert into <Table> [(<col>[, ...])] [values] (<value>[, ...]) │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2967
|
assertion_line: 3347
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ───────────────────────────────────╮ ─────────────────────────────────╮
|
╭ Tables ───────────────────────────────────╮ ─────────────────────────────────╮
|
||||||
@@ -22,8 +22,8 @@ expression: snapshot
|
|||||||
╰───────────────────────────────────────────╯ │
|
╰───────────────────────────────────────────╯ │
|
||||||
╭ Relationships ────────────────────────────╮ ─────────────────────────────────╯
|
╭ Relationships ────────────────────────────╮ ─────────────────────────────────╯
|
||||||
│Customers_Orders │ ─────────────────────────────────╮
|
│Customers_Orders │ ─────────────────────────────────╮
|
||||||
│ Customers.id -> │ ` for a list │
|
│ Customers.id -> │ ` for a list · `mode advanced` │
|
||||||
│ Orders.customer_id │ │
|
│ Orders.customer_id │ │
|
||||||
╰───────────────────────────────────────────╯ ─────────────────────────────────╯
|
╰───────────────────────────────────────────╯ ─────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2385
|
assertion_line: 2896
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -26,4 +26,4 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · Backspace cancel one-shot · Ctrl-C quit
|
Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2679
|
assertion_line: 3099
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ────────────────────────────────────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ────────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -22,8 +22,8 @@ expression: snapshot
|
|||||||
╰──────────────────────────╯│ │
|
╰──────────────────────────╯│ │
|
||||||
╭ Relationships ───────────╮╰────────────────────────────────────────────────────────────────────────────────╯
|
╭ Relationships ───────────╮╰────────────────────────────────────────────────────────────────────────────────╯
|
||||||
│(none) │╭ Hint ──────────────────────────────────────────────────────────────────────────╮
|
│(none) │╭ Hint ──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ ││Type a command — press Tab for options, `help` for a list │
|
│ ││Type a command — press Tab for options, `help` for a list · `mode advanced` │
|
||||||
│ ││ │
|
│ ││for SQL │
|
||||||
╰──────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2399
|
assertion_line: 2909
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
╭ Output ──────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -22,8 +22,8 @@ expression: snapshot
|
|||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
╭ Hint ────────────────────────────────────────────────────────────────────────╮
|
╭ Hint ────────────────────────────────────────────────────────────────────────╮
|
||||||
│Type a command — press Tab for options, `help` for a list │
|
│Type a command — press Tab for options, `help` for a list · `mode advanced` │
|
||||||
│ │
|
│for SQL │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2789
|
assertion_line: 3209
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Tables ──────────────────╮╭ Output ────────────────────────────────────────────────────────────────────────╮
|
╭ Tables ──────────────────╮╭ Output ────────────────────────────────────────────────────────────────────────╮
|
||||||
@@ -22,8 +22,8 @@ expression: snapshot
|
|||||||
╰──────────────────────────╯│ │
|
╰──────────────────────────╯│ │
|
||||||
╭ Relationships ───────────╮╰────────────────────────────────────────────────────────────────────────────────╯
|
╭ Relationships ───────────╮╰────────────────────────────────────────────────────────────────────────────────╯
|
||||||
│Customers_Orders │╭ Hint ──────────────────────────────────────────────────────────────────────────╮
|
│Customers_Orders │╭ Hint ──────────────────────────────────────────────────────────────────────────╮
|
||||||
│ Customers.id -> ││Type a command — press Tab for options, `help` for a list │
|
│ Customers.id -> ││Type a command — press Tab for options, `help` for a list · `mode advanced` │
|
||||||
│ Orders.customer_id ││ │
|
│ Orders.customer_id ││for SQL │
|
||||||
╰──────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch · Ctrl-C quit
|
Ctrl-O sidebar · Tab complete · ↑ history · Enter run
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
source: src/ui.rs
|
source: src/ui.rs
|
||||||
assertion_line: 2265
|
assertion_line: 2616
|
||||||
expression: snapshot
|
expression: snapshot
|
||||||
---
|
---
|
||||||
╭ Output ──────────────────────────────────────────────────╮
|
╭ Output ──────────────────────────────────────────────────╮
|
||||||
@@ -46,4 +46,4 @@ expression: snapshot
|
|||||||
│with `mode advanced`, or prefix the line with `:` to run… │
|
│with `mode advanced`, or prefix the line with `:` to run… │
|
||||||
╰──────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────╯
|
||||||
Project: Term Planner
|
Project: Term Planner
|
||||||
Enter submit · : advanced once · mode advanced switch ·
|
Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Ente
|
||||||
|
|||||||
@@ -1694,7 +1694,19 @@ fn resolve_hint_lines(
|
|||||||
(None, Some(crate::input_render::AmbientHint::Candidates { items, selected })) => {
|
(None, Some(crate::input_render::AmbientHint::Candidates { items, selected })) => {
|
||||||
vec![render_candidate_line(&items, selected, inner, theme)]
|
vec![render_candidate_line(&items, selected, inner, theme)]
|
||||||
}
|
}
|
||||||
(None, None) => prose(&crate::t!("panel.hint_empty")),
|
// Empty input: the base prompt, plus — in simple mode only — a
|
||||||
|
// pointer to advanced mode (ADR-0051, issue #27), since the
|
||||||
|
// `mode advanced` switch left the keybinding strip. Advanced
|
||||||
|
// mode shows no pointer: users know how they reached it, and
|
||||||
|
// `help` covers the way back. (One-shot never reaches here — its
|
||||||
|
// `:` makes the input non-empty → ambient path.)
|
||||||
|
(None, None) => {
|
||||||
|
let mut text = crate::t!("panel.hint_empty");
|
||||||
|
if matches!(app.effective_mode(), EffectiveMode::Simple) {
|
||||||
|
text.push_str(&crate::t!("panel.hint_mode_advanced"));
|
||||||
|
}
|
||||||
|
prose(&text)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1845,6 +1857,63 @@ fn render_candidate_line(
|
|||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The keybinding strip is keystrokes-only and **state-selected**
|
||||||
|
/// (ADR-0051, issue #27): it advertises the keys for the user's *current*
|
||||||
|
/// interaction, chosen by priority — first matching state wins.
|
||||||
|
///
|
||||||
|
/// Returns `(key, label)` pairs (label localised via `t!`); the renderer
|
||||||
|
/// is a thin span builder over this list, so the binding sets are
|
||||||
|
/// unit-testable without a `Frame`. Mode-switch / `:` advertisements
|
||||||
|
/// deliberately leave the strip — they are typed commands, not
|
||||||
|
/// keystrokes — and move to the empty-input hint (`resolve_hint_lines`).
|
||||||
|
fn status_bar_bindings(app: &App) -> Vec<(&'static str, String)> {
|
||||||
|
// 1. Sidebar focus (Ctrl-O): the input is occluded by the overlay,
|
||||||
|
// so the panel-scroll keys win outright (ADR-0046).
|
||||||
|
if app.nav_focus.in_sidebar() {
|
||||||
|
return vec![
|
||||||
|
("Ctrl-O", crate::t!("shortcut.next_pane")),
|
||||||
|
("↑↓/PgUp/PgDn", crate::t!("shortcut.scroll")),
|
||||||
|
("Esc", crate::t!("shortcut.to_input")),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// 2. A live Tab-completion memo (ADR-0022): cycling keys. Pressing
|
||||||
|
// Up clears the memo, so this never co-occurs with state 3.
|
||||||
|
if app.last_completion.is_some() {
|
||||||
|
return vec![
|
||||||
|
("Tab/Shift-Tab", crate::t!("shortcut.cycle")),
|
||||||
|
("Esc", crate::t!("shortcut.cancel")),
|
||||||
|
("Enter", crate::t!("shortcut.run")),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// 3. Browsing recalled history (unedited): browse keys. Editing the
|
||||||
|
// recalled line ends navigation, dropping to state 4.
|
||||||
|
if app.is_browsing_history() {
|
||||||
|
return vec![
|
||||||
|
("↑↓", crate::t!("shortcut.browse")),
|
||||||
|
("Esc", crate::t!("shortcut.clear")),
|
||||||
|
("Enter", crate::t!("shortcut.run")),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// 4. Editing — the input has text: surface the readline edit keys
|
||||||
|
// (ADR-0049). The highest-value subset stays within the width
|
||||||
|
// budget; Ctrl-K/U remain unadvertised muscle memory.
|
||||||
|
if !app.input.is_empty() {
|
||||||
|
return vec![
|
||||||
|
("Esc", crate::t!("shortcut.clear")),
|
||||||
|
("Ctrl-A/E", crate::t!("shortcut.home_end")),
|
||||||
|
("Ctrl-W", crate::t!("shortcut.del_word")),
|
||||||
|
("Enter", crate::t!("shortcut.run")),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// 5. Default — empty input, Input focus.
|
||||||
|
vec![
|
||||||
|
("Ctrl-O", crate::t!("shortcut.nav")),
|
||||||
|
("Tab", crate::t!("shortcut.complete")),
|
||||||
|
("↑", crate::t!("shortcut.history")),
|
||||||
|
("Enter", crate::t!("shortcut.run")),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn render_status_bar(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
fn render_status_bar(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect) {
|
||||||
let key_style = Style::default()
|
let key_style = Style::default()
|
||||||
.fg(theme.fg)
|
.fg(theme.fg)
|
||||||
@@ -1855,35 +1924,14 @@ fn render_status_bar(app: &App, theme: &Theme, frame: &mut Frame<'_>, area: Rect
|
|||||||
|
|
||||||
let separator = Span::styled(" · ", sep_style);
|
let separator = Span::styled(" · ", sep_style);
|
||||||
let mut spans: Vec<Span<'_>> = Vec::new();
|
let mut spans: Vec<Span<'_>> = Vec::new();
|
||||||
|
for (key, label) in status_bar_bindings(app) {
|
||||||
let push_shortcut = |spans: &mut Vec<Span<'_>>, key: &'static str, label: &str| {
|
|
||||||
if !spans.is_empty() {
|
if !spans.is_empty() {
|
||||||
spans.push(separator.clone());
|
spans.push(separator.clone());
|
||||||
}
|
}
|
||||||
spans.push(Span::styled(key, key_style));
|
spans.push(Span::styled(key, key_style));
|
||||||
spans.push(Span::raw(" "));
|
spans.push(Span::raw(" "));
|
||||||
spans.push(Span::styled(label.to_string(), label_style));
|
spans.push(Span::styled(label, label_style));
|
||||||
};
|
|
||||||
|
|
||||||
let submit = crate::t!("shortcut.submit");
|
|
||||||
push_shortcut(&mut spans, "Enter", &submit);
|
|
||||||
let switch = crate::t!("shortcut.switch");
|
|
||||||
let advanced_once = crate::t!("shortcut.advanced_once");
|
|
||||||
let cancel_one_shot = crate::t!("shortcut.cancel_one_shot");
|
|
||||||
let quit = crate::t!("shortcut.quit");
|
|
||||||
match app.effective_mode() {
|
|
||||||
EffectiveMode::Simple => {
|
|
||||||
push_shortcut(&mut spans, ":", &advanced_once);
|
|
||||||
push_shortcut(&mut spans, "mode advanced", &switch);
|
|
||||||
}
|
}
|
||||||
EffectiveMode::AdvancedPersistent => {
|
|
||||||
push_shortcut(&mut spans, "mode simple", &switch);
|
|
||||||
}
|
|
||||||
EffectiveMode::AdvancedOneShot => {
|
|
||||||
push_shortcut(&mut spans, "Backspace", &cancel_one_shot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
push_shortcut(&mut spans, "Ctrl-C", &quit);
|
|
||||||
|
|
||||||
let paragraph = Paragraph::new(Line::from(spans)).style(bar_style);
|
let paragraph = Paragraph::new(Line::from(spans)).style(bar_style);
|
||||||
frame.render_widget(paragraph, area);
|
frame.render_widget(paragraph, area);
|
||||||
@@ -2582,6 +2630,168 @@ mod tests {
|
|||||||
.expect("hint bottom border present")
|
.expect("hint bottom border present")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- ADR-0051 (issue #27): context- and state-aware strip ----
|
||||||
|
|
||||||
|
fn key_event(code: crossterm::event::KeyCode) -> crate::event::AppEvent {
|
||||||
|
crate::event::AppEvent::Key(crossterm::event::KeyEvent::new(
|
||||||
|
code,
|
||||||
|
crossterm::event::KeyModifiers::NONE,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `key` column of the strip's bindings, in order.
|
||||||
|
fn strip_keys(app: &App) -> Vec<&'static str> {
|
||||||
|
status_bar_bindings(app).into_iter().map(|(k, _)| k).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full rendered strip text (keys + labels + separators).
|
||||||
|
fn strip_text(app: &App) -> String {
|
||||||
|
status_bar_bindings(app)
|
||||||
|
.iter()
|
||||||
|
.map(|(k, l)| format!("{k} {l}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" · ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hint_text(lines: &[Line<'_>]) -> String {
|
||||||
|
lines
|
||||||
|
.iter()
|
||||||
|
.map(|l| l.spans.iter().map(|s| s.content.clone()).collect::<String>())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_default_state_is_nav_complete_history_run() {
|
||||||
|
let app = App::new();
|
||||||
|
assert_eq!(strip_keys(&app), vec!["Ctrl-O", "Tab", "↑", "Enter"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_editing_state_surfaces_readline_keys() {
|
||||||
|
// Input has text (no completion/history transient) → the #29
|
||||||
|
// editing keys (ADR-0049).
|
||||||
|
let mut app = App::new();
|
||||||
|
app.input.push_str("create ta");
|
||||||
|
assert_eq!(
|
||||||
|
strip_keys(&app),
|
||||||
|
vec!["Esc", "Ctrl-A/E", "Ctrl-W", "Enter"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_sidebar_focus_state_is_pane_scroll_input() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.nav_focus = NavFocus::SidebarTables;
|
||||||
|
assert_eq!(
|
||||||
|
strip_keys(&app),
|
||||||
|
vec!["Ctrl-O", "↑↓/PgUp/PgDn", "Esc"],
|
||||||
|
);
|
||||||
|
// ...and the relationships sidebar is the same state.
|
||||||
|
app.nav_focus = NavFocus::SidebarRelationships;
|
||||||
|
assert_eq!(strip_keys(&app), vec!["Ctrl-O", "↑↓/PgUp/PgDn", "Esc"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_completion_memo_state_is_cycle_cancel_run() {
|
||||||
|
// Drive the real flow: `show ` + Tab leaves a multi-candidate
|
||||||
|
// memo (ADR-0022). The strip must win over the editing state.
|
||||||
|
let mut app = App::new();
|
||||||
|
for c in "show ".chars() {
|
||||||
|
app.update(key_event(crossterm::event::KeyCode::Char(c)));
|
||||||
|
}
|
||||||
|
app.update(key_event(crossterm::event::KeyCode::Tab));
|
||||||
|
assert!(app.last_completion.is_some(), "memo set by Tab");
|
||||||
|
assert!(!app.input.is_empty(), "input non-empty — would be editing");
|
||||||
|
assert_eq!(
|
||||||
|
strip_keys(&app),
|
||||||
|
vec!["Tab/Shift-Tab", "Esc", "Enter"],
|
||||||
|
"completion state wins over editing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strip_history_navigation_state_is_browse_clear_run() {
|
||||||
|
// Submit a command, then Up to recall it — `history_cursor` is
|
||||||
|
// set, input is the (non-empty) recalled line, no memo.
|
||||||
|
let mut app = App::new();
|
||||||
|
for c in "drop table T".chars() {
|
||||||
|
app.update(key_event(crossterm::event::KeyCode::Char(c)));
|
||||||
|
}
|
||||||
|
app.update(key_event(crossterm::event::KeyCode::Enter)); // submit
|
||||||
|
app.update(key_event(crossterm::event::KeyCode::Up)); // recall
|
||||||
|
assert!(app.is_browsing_history(), "browsing recalled history");
|
||||||
|
assert!(app.last_completion.is_none(), "no completion memo");
|
||||||
|
assert_eq!(
|
||||||
|
strip_keys(&app),
|
||||||
|
vec!["↑↓", "Esc", "Enter"],
|
||||||
|
"history state wins over editing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_strip_state_fits_the_eighty_column_budget() {
|
||||||
|
// ADR-0051 §3: the strips are kept lean by construction — the
|
||||||
|
// longest must fit an 80-col status line, so no graceful-drop
|
||||||
|
// machinery is needed. A future over-long strip fails here.
|
||||||
|
let sidebar = {
|
||||||
|
let mut a = App::new();
|
||||||
|
a.nav_focus = NavFocus::SidebarTables;
|
||||||
|
a
|
||||||
|
};
|
||||||
|
let editing = {
|
||||||
|
let mut a = App::new();
|
||||||
|
a.input.push('x');
|
||||||
|
a
|
||||||
|
};
|
||||||
|
for app in [&App::new(), &sidebar, &editing] {
|
||||||
|
let text = strip_text(app);
|
||||||
|
assert!(
|
||||||
|
text.chars().count() <= 80,
|
||||||
|
"strip {} cols > 80: {text:?}",
|
||||||
|
text.chars().count(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_hint_advertises_advanced_mode_in_simple() {
|
||||||
|
let app = App::new();
|
||||||
|
// Wide width so the pointer never wrap-splits.
|
||||||
|
let text = hint_text(&resolve_hint_lines(&app, &Theme::dark(), 200, 3));
|
||||||
|
assert!(
|
||||||
|
text.contains("`mode advanced` for SQL"),
|
||||||
|
"simple empty hint carries the advanced pointer:\n{text}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn advanced_mode_empty_hint_has_no_mode_pointer() {
|
||||||
|
// ADR-0051: advanced mode shows no mode pointer (users know how
|
||||||
|
// they got there; `help` covers the way back).
|
||||||
|
let mut app = App::new();
|
||||||
|
app.mode = Mode::Advanced;
|
||||||
|
let text = hint_text(&resolve_hint_lines(&app, &Theme::dark(), 200, 3));
|
||||||
|
assert!(
|
||||||
|
!text.contains("mode simple") && !text.contains("mode advanced"),
|
||||||
|
"advanced empty hint carries no mode pointer:\n{text}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typing_replaces_the_empty_hint_mode_pointer() {
|
||||||
|
// Non-empty input → ambient hint path, not the empty-hint
|
||||||
|
// mode pointer.
|
||||||
|
let mut app = App::new();
|
||||||
|
app.input.push_str("create table");
|
||||||
|
app.input_cursor = app.input.len();
|
||||||
|
let text = hint_text(&resolve_hint_lines(&app, &Theme::dark(), 200, 3));
|
||||||
|
assert!(
|
||||||
|
!text.contains("for SQL"),
|
||||||
|
"no mode pointer once typing:\n{text}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn clamp_wrapped_truncates_with_ellipsis_past_max() {
|
fn clamp_wrapped_truncates_with_ellipsis_past_max() {
|
||||||
// ≤ max rows: untouched.
|
// ≤ max rows: untouched.
|
||||||
|
|||||||
@@ -223,21 +223,30 @@ fn typing_colon_in_simple_mode_flips_prompt_to_advanced() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn status_bar_lists_quit_and_submit_in_all_modes() {
|
fn status_bar_is_keystroke_only_and_state_aware() {
|
||||||
|
// ADR-0051 (issue #27): the bottom strip is keystrokes-only and
|
||||||
|
// tracks the interaction state. Typed-command words (`:` advanced
|
||||||
|
// once, `mode advanced`/`mode simple` switch) and `Ctrl-C quit`
|
||||||
|
// leave the strip; mode discovery moves to the hint (locked by the
|
||||||
|
// ui.rs unit tests). This test exercises the real render path.
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
let theme = Theme::dark();
|
let theme = Theme::dark();
|
||||||
|
|
||||||
let simple = rendered_text(&mut app, &theme, 80, 24);
|
// Default (empty input): nav / complete / history / run keystrokes.
|
||||||
assert!(simple.contains("Enter"), "status bar lists Enter");
|
let default_view = rendered_text(&mut app, &theme, 80, 24);
|
||||||
assert!(simple.contains("Ctrl-C"), "status bar lists Ctrl-C");
|
assert!(default_view.contains("Ctrl-O sidebar"), "strip lists sidebar:\n{default_view}");
|
||||||
assert!(simple.contains("mode advanced"));
|
assert!(default_view.contains("Enter run"), "strip lists run:\n{default_view}");
|
||||||
|
assert!(!default_view.contains("Ctrl-C"), "quit dropped from the strip:\n{default_view}");
|
||||||
|
assert!(
|
||||||
|
!default_view.contains("advanced once"),
|
||||||
|
"`:` command word dropped from the strip:\n{default_view}",
|
||||||
|
);
|
||||||
|
|
||||||
type_str(&mut app, "mode advanced");
|
// Editing (input has text): the #29 readline edit keys appear.
|
||||||
submit(&mut app);
|
type_str(&mut app, "create");
|
||||||
let advanced = rendered_text(&mut app, &theme, 80, 24);
|
let editing = rendered_text(&mut app, &theme, 80, 24);
|
||||||
assert!(advanced.contains("Enter"));
|
assert!(editing.contains("Esc clear"), "editing strip lists clear:\n{editing}");
|
||||||
assert!(advanced.contains("Ctrl-C"));
|
assert!(editing.contains("Ctrl-W del word"), "editing strip lists del word:\n{editing}");
|
||||||
assert!(advanced.contains("mode simple"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user