diff --git a/docs/adr/0051-context-state-aware-keybinding-strip.md b/docs/adr/0051-context-state-aware-keybinding-strip.md new file mode 100644 index 0000000..bc50aab --- /dev/null +++ b/docs/adr/0051-context-state-aware-keybinding-strip.md @@ -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` +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. diff --git a/docs/adr/README.md b/docs/adr/README.md index 97253f2..2d81708 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -56,3 +56,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0048 — `seed` fake-data generation command](0048-seed-fake-data-generation.md) — **Accepted 2026-06-11; Phase 1 + Phase 2 implemented 2026-06-11** (Phase 1 commits `202e25a`→`fbd219b`; design settled with the user across an extended fork dialogue, hardened by a pre-build `/runda` pass (six blockers folded in), a post-implementation `/runda` pass (eight gaps closed — FK/shortid determinism so **D4 holds with no exceptions**, plus six untested ADR decisions), and a Phase-2 pre-build `/runda` pass (which caught the no-date-literal-token reality → the D2 quoted-dates amendment), and a post-implementation `/runda` pass (which added a friendly error for a bounded override on a UNIQUE column — see D2); **2400 tests pass, clippy clean**). Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1**. **Phase 1 shipped:** whole-row `seed [count] [--seed ]` with realistic name-aware generation (the `fake` crate + a type-gated heuristic catalogue, table-context name disambiguation, hand-rolled `product` generator, bounded dates), identifier + constraint uniqueness incl. junction distinct-combos, FK sampling from existing parent rows (empty-parent error), `IN`-CHECK derivation + complex-CHECK advisory, a required-column block guard, `--seed` reproducibility (serial/FK/shortid all deterministic), undo as one batch step, replay as a data write, a capped auto-show preview, the enum/CHECK advisory, and an O(N) single-transaction insert path. **Phase 2 shipped (2026-06-11):** the `set` override clause (D2 — fixed value / pick-list / `as ` / `between` range, **quoted** dates per the D2 amendment, type-aware, override drops the column from the advisory) and the `
.` column-fill form (D1 form 2 — an UPDATE over existing rows, refusing PK/autogen targets, empty-table no-op, FK/unique-respecting, one undo step), with the new `KNOWN_GENERATORS` vocabulary (D9), a range `Generator`, full completion/highlight (`HighlightClass::Function`)/validity (`IdentSource::Generators`)/help/pedagogy wiring, and the D13 advisory's Phase-2/3 wording. Further SD2 increments (custom generators, NULL injection, multi-locale, recursive auto-seed) out of scope. Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1** (the other being `hint`/**H2**). A dedicated `seed` command (own AST variant + `do_seed` executor, **both modes**) generating **realistic, name-aware** fake data. Two forms: **`seed
[count]`** (new rows, default **20**, capped) and **`seed
.`** (fill a column on existing rows, an UPDATE). Generation adds the **`fake` crate** (v5, English) driven by a **type-gated, token-matched name-heuristic catalogue** (~30 patterns, documented false-positive guards), with **table-context** disambiguating the `name`/`title` family (`products.name`→product, `users.name`→person, `vendors.name`→company), a **hand-rolled `product` generator** (`fake` has no commerce module), **bounded dates** (`date`/`timestamp`/`dob`/`*_at` recognised, recent windows — never "all of history"), the **identifier family** (`id`/`code`/`ref`/`number`, non-FK/non-PK) → **unique sequential**, and **enum-ish names** (`role`/`status`/`type`/…) left generic + a **post-seed Hint advisory** pointing at `set … in (…)`. A **`set` override clause** — `= value` / `in (a,b,c)` / `as ` / `between a and b` (numeric **and** date), reusing ADR-0026 operators — answers the heuristic-miss case. **`--seed `** makes runs reproducible (and enables exact-value tests). **FK** columns sampled uniformly from existing parent rows (**empty parent → friendly error**, no recursion v1); **junction/compound-PK** tables seeded with **distinct combinations**, capped + noted (SD1). A **required-column block guard** refuses rather than NULL-violate a `NOT NULL` column it can't fill (e.g. `NOT NULL blob`). Full ambient wiring (completion incl. a new generator-name vocabulary highlighted as `tok_function`, hints, `help seed`, ADR-0042 near-miss matrix, ADR-0027 validity); **no DSL→SQL teaching echo** (seed is a utility command, not a SQL twin). Honours **X5** — `do_seed` reuses insert/update *mechanics as helpers*, not by emitting `Command::Insert`. Implementation phased: (1) core whole-row seed → (2) `set` overrides → (3) column-fill. Deferred (future SD2): recursive auto-seed, NULL injection, multi-locale, user-defined custom generators, full per-column report. **Amendment 1, 2026-06-12** (issues #33/#34): two additive D7 catalogue rules — **year-as-int** (`year`/`*_year`/`published`/`founded` → a bounded `int` year, 1950–2025, or the `dob`-style birth window 1945–2007 for `birth`/`born`/`dob`; fixes nonsense like `9419`; `int`-gated, after the quantity rule so `year_count` stays a count; two new `YearRecent`/`YearBirth` generators, *not* added to the D9 vocabulary) and **conventional choice sets** (`priority`/`prio`, `severity`, `rating`/`stars` → type-gated built-in `PickFrom` value sets reusing the existing generator; `priority` leaves `ENUM_TOKENS`). `status` is **deliberately excluded** (user-confirmed — values too domain-specific; keeps the D12 "don't guess" + advisory); a user `IN`-CHECK still wins. Website `seed` cast re-record tracked on the `website` branch - [ADR-0049 — Input-field readline keymap: Esc-clear + Ctrl-A/E/W/K/U (I1b)](0049-input-field-readline-keymap.md) — **Accepted + implemented 2026-06-12 (issue #29)**, closes Gitea **#29** and the deferred **I1b** readline requirement. **Amends ADR-0046**, which listed "readline shortcuts (I1b)" as out-of-scope — that item is now in scope and decided here; orthogonal to ADR-0003's input-*mode* model and extends the I1a single-line cursor editing already shipped. Binds, in the input field (non-modal, non-nav, both modes): **`Esc`** clears a partly-typed command (empty buffer, cursor→0, scroll→0); **`Ctrl-A`/`Ctrl-E`** alias Home/End (line start/end); **`Ctrl-W`** deletes the previous word (readline-style — eats trailing whitespace then the preceding non-whitespace run, UTF-8-safe on char boundaries, only back to the cursor); **`Ctrl-K`** kills to end of line; **`Ctrl-U`** kills to start. **Esc precedence:** a live Tab-completion memo still wins (Esc undoes the completion first, ADR-0022; Esc clears only when no memo) — Esc-once backs out the completion, Esc-again clears. Forks all user-chosen: **single-Esc-clears** (not double-Esc — discoverable over accident-proof; an unsubmitted draft can be lost, a submitted line is always in history); the **full I1b set** (not just the issue's literal Ctrl-A/E + Esc); a **new ADR** (not an ADR-0046 amendment / no-ADR). Cursor-only keys (Ctrl-A/E) leave history navigation intact like Home/End; buffer-mutating keys (Esc-clear, Ctrl-W/K/U) end it like Backspace. Helpers `clear_input`/`delete_prev_word`/`kill_to_end`/`kill_to_start` in `src/app.rs`; **22 new Tier-1 tests, 2458 pass / 0 fail / 0 skip (1 ignored), clippy clean**. OOS: on-screen keybinding hints (issue #27 owns surfacing per-focus keybindings in the bottom status line — this ADR makes the keys *work*, #27 makes them *discoverable*); demo-mode badges for the new chords (ADR-0047 follow-up — Esc already badges `[ESC]`, the glyph-less Ctrl-chords are flagged but not added); multi-line input (I1); word-wise cursor motion (Alt-B/F) / transpose / yank - [ADR-0050 — Incidental-DDL confirmations omit relationship info (structure-only)](0050-incidental-ddl-confirmations-omit-relationships.md) — **Accepted + implemented 2026-06-12 (issue #28)**, closes Gitea **#28**. **Supersedes** the incidental-DDL clause of **ADR-0044 §1** and the relationship-block half of **ADR-0016 §5**. Incidental-DDL confirmation echoes (`create table`, `add`/`drop`/`rename`/`change column`, `add`/`drop index`) now render **structure only** — header + column box + `Indexes:` + constraints — with **no `References:` / `Referenced by:` block** (neither prose nor diagram), even when the table carries relationships the user did not touch. Rationale (owner): a confirmation echo reports the change just made, not untouched relationships; ADR-0044's terse prose was the lesser of "prose vs diagram", but the right answer for these surfaces is **neither**. **Relationship-subject surfaces are unchanged** — `show table`, `add`/`drop relationship`, `show relationship` still render ADR-0044 diagrams; relationships appear only when the user asks for (`show table`) or acts on (`add`/`drop relationship`) one, and are one `show table ` away — **no information lost**. Forks both user-chosen: **scope = all incidental DDL** (not just `add column` — the rationale is uniform, the mental model clean, and it's the simpler edit) and **delete the prose renderer** (not retain it dormant — no dead code). **Mechanism:** the `handle_dsl_success` `matches!` routing is unchanged (relationship-subject → diagrams; else → `render_structure`); the change is one line inside `render_structure` (`output_render.rs` — drop the relationship-block call) since all its callers are incidental DDL, plus deletion of the orphaned `relationship_prose_lines` + `cols_disp` helpers. The prose format survives in ADR-0016 §5 + git history for a future OOS-7 always-prose setting. **Tests:** the prose-presence unit test + its snapshot removed; a new unit test asserts `render_structure` on a description carrying **both** inbound and outbound relationships emits the box but no prose; the misnamed `add_relationship_flow_shows_inbound_section_on_parent` integration test (which sent an `AddColumn`) inverted + renamed to assert the add-column echo omits the prose; the diagram tests (`show table`, `add relationship`) unaffected. **2458 pass / 0 fail / 0 skip (1 ignored), clippy clean**. `requirements.md` unaffected (ADR-tracked refinement of a decided area, like ADR-0044 itself) +- [ADR-0051 — Bottom keybinding strip: context- and state-aware](0051-context-state-aware-keybinding-strip.md) — **Accepted + implemented 2026-06-13 (issue #27)**, closes Gitea **#27**. Repurposes the bottom status line into a **keystrokes-only, state-selected** strip (builds on ADR-0046 nav focus, ADR-0003 modes, ADR-0049 the #29 readline keys it now advertises, ADR-0022 the completion memo). A pure `status_bar_bindings(app) -> Vec<(key,label)>` chooses the strip by **priority, first match wins**: (1) **sidebar focus** → `Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input`; (2) **completion memo live** (`last_completion`) → `Tab/Shift-Tab cycle · Esc cancel · Enter run`; (3) **history navigation** (new `App::is_browsing_history()` exposing the private `history_cursor`) → `↑↓ browse · Esc clear · Enter run`; (4) **editing** (input non-empty) → `Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run` (surfaces the #29 keys, closing ADR-0049's deferred advertisement); (5) **default** (empty) → `Ctrl-O sidebar · Tab complete · ↑ history · Enter run`. Priority is correct because Up clears the completion memo and Tab cancels history nav, so states 2/3 never co-occur, and the five are exhaustive for Input focus. **Typed-command words leave the strip** (`mode advanced`/`mode simple` switch, `:` one-shot) and **mode discovery moves to the empty-input hint** (`resolve_hint_lines`), **simple mode only**: `\`mode advanced\` for SQL` (the verb "type" omitted — the prompt implies it; advanced mode shows **no** pointer per a post-trial user decision — a switcher knows how they got there and `help` covers the way back). The one-shot's old `Backspace cancel one-shot` label is subsumed by the editing state (behaviour intact). Forks all user-chosen: **editing state shows the #29 keys** (vs unadvertised); **`Ctrl-C quit` omitted** from the strip (vs always shown); **no width-drop machinery** — the longest strip (~65 cols) fits all supported widths, so a **width-budget unit test** keeps it lean by construction instead (the user's own observation). Catalog: 12 new `shortcut.*` labels + the `panel.hint_mode_advanced` string added to `en-US.yaml`+`keys.rs` (validator-checked 1:1), 5 now-dead strip strings removed. **Modal-aware strip is OOS** (pre-existing: a modal owns the keyboard and carries its own hints; the strip under it is unchanged-in-kind, not worsened). Tests: 9 Tier-1 unit (per-state key sets — completion/history driven through real key events; width budget; mode-pointer presence/absence), 1 Tier-3 rewritten (`status_bar_is_keystroke_only_and_state_aware`), 15 full-panel snapshots re-accepted (reviewed — strip/hint only). **2467 pass / 0 fail / 0 skip (1 ignored), clippy clean.** OOS: modal-aware strip; a full-key cheatsheet overlay; Ctrl-K/U advertisement (editing strip shows the highest-value subset within the width budget) diff --git a/src/app.rs b/src/app.rs index e414642..8efcb9f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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 /// hint, validity verdict, highlight overlays) should see, plus the /// byte offset stripped from the front and the cursor mapped into the diff --git a/src/friendly/keys.rs b/src/friendly/keys.rs index f5244b6..a6c6ae8 100644 --- a/src/friendly/keys.rs +++ b/src/friendly/keys.rs @@ -446,6 +446,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("undo.redo_failed", &["error"]), // ---- Status bar + panels ---- ("panel.hint_empty", &[]), + ("panel.hint_mode_advanced", &[]), ("panel.hint_title", &[]), ("panel.output_title", &[]), ("panel.relationships_empty", &[]), @@ -462,18 +463,25 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[ ("save.title_as", &[]), ("save.title_save", &[]), // ---- Shortcut hint labels ---- - ("shortcut.advanced_once", &[]), ("shortcut.back_to_list", &[]), + ("shortcut.browse", &[]), ("shortcut.browse_path", &[]), ("shortcut.cancel", &[]), - ("shortcut.cancel_one_shot", &[]), + ("shortcut.clear", &[]), + ("shortcut.complete", &[]), ("shortcut.confirm", &[]), + ("shortcut.cycle", &[]), + ("shortcut.del_word", &[]), + ("shortcut.history", &[]), + ("shortcut.home_end", &[]), ("shortcut.load", &[]), + ("shortcut.nav", &[]), + ("shortcut.next_pane", &[]), ("shortcut.no", &[]), - ("shortcut.quit", &[]), + ("shortcut.run", &[]), + ("shortcut.scroll", &[]), ("shortcut.select", &[]), - ("shortcut.submit", &[]), - ("shortcut.switch", &[]), + ("shortcut.to_input", &[]), ("shortcut.yes", &[]), // ---- mode / messages banners ---- ("messages.set_short", &[]), diff --git a/src/friendly/strings/en-US.yaml b/src/friendly/strings/en-US.yaml index 40930df..c8e45f1 100644 --- a/src/friendly/strings/en-US.yaml +++ b/src/friendly/strings/en-US.yaml @@ -883,14 +883,21 @@ panel: relationships_title: "Relationships" relationships_empty: "(none)" 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 # the rounded border, hence the leading/trailing space). output_title: "Output" hint_title: "Hint" # ---- 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: - submit: "submit" confirm: "confirm" cancel: "cancel" yes: "Yes" @@ -899,10 +906,19 @@ shortcut: select: "select" browse_path: "browse path" back_to_list: "back to list" - switch: "switch" - advanced_once: "advanced once" - cancel_one_shot: "cancel one-shot" - quit: "quit" + # Status-strip labels (ADR-0051, issue #27). + run: "run" + nav: "sidebar" + 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: diff --git a/src/snapshots/rdbms_playground__ui__tests__default_advanced_dark.snap b/src/snapshots/rdbms_playground__ui__tests__default_advanced_dark.snap index 46a7503..0505791 100644 --- a/src/snapshots/rdbms_playground__ui__tests__default_advanced_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__default_advanced_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2326 +assertion_line: 2836 expression: snapshot --- ╭ Output ──────────────────────────────────────────────────────────────────────╮ @@ -26,4 +26,4 @@ expression: snapshot │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ Project: Term Planner -Enter submit · mode simple switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__default_simple_dark.snap b/src/snapshots/rdbms_playground__ui__tests__default_simple_dark.snap index c49a798..e192380 100644 --- a/src/snapshots/rdbms_playground__ui__tests__default_simple_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__default_simple_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2309 +assertion_line: 2819 expression: snapshot --- ╭ Output ──────────────────────────────────────────────────────────────────────╮ @@ -22,8 +22,8 @@ expression: snapshot │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭ 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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__default_simple_light.snap b/src/snapshots/rdbms_playground__ui__tests__default_simple_light.snap index 4a41ef7..a86c7b3 100644 --- a/src/snapshots/rdbms_playground__ui__tests__default_simple_light.snap +++ b/src/snapshots/rdbms_playground__ui__tests__default_simple_light.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2317 +assertion_line: 2827 expression: snapshot --- ╭ Output ──────────────────────────────────────────────────────────────────────╮ @@ -22,8 +22,8 @@ expression: snapshot │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭ 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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_badge_and_caption_stacked_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_badge_and_caption_stacked_90x26.snap index 7f82289..f429374 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_badge_and_caption_stacked_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_badge_and_caption_stacked_90x26.snap @@ -1,5 +1,6 @@ --- source: src/ui.rs +assertion_line: 3442 expression: snapshot --- ╭ Output ────────────────────────────────────────────────────────────────────────────────╮ @@ -23,8 +24,8 @@ expression: snapshot │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ 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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_badge_enter_light_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_badge_enter_light_90x26.snap index 7120bbd..86f0ce5 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_badge_enter_light_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_badge_enter_light_90x26.snap @@ -1,5 +1,6 @@ --- source: src/ui.rs +assertion_line: 3388 expression: snapshot --- ╭ Output ────────────────────────────────────────────────────────────────────────────────╮ @@ -23,8 +24,8 @@ expression: snapshot │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ 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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_badge_tab_dark_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_badge_tab_dark_90x26.snap index d6358c1..e9b9e4a 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_badge_tab_dark_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_badge_tab_dark_90x26.snap @@ -1,5 +1,6 @@ --- source: src/ui.rs +assertion_line: 3378 expression: snapshot --- ╭ Output ────────────────────────────────────────────────────────────────────────────────╮ @@ -23,8 +24,8 @@ expression: snapshot │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ 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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_caption_dark_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_caption_dark_90x26.snap index b132bbd..1d2e68a 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_caption_dark_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_caption_dark_90x26.snap @@ -1,5 +1,6 @@ --- source: src/ui.rs +assertion_line: 3431 expression: snapshot --- ╭ Output ────────────────────────────────────────────────────────────────────────────────╮ @@ -23,8 +24,8 @@ expression: snapshot │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ 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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__demo_caption_wrapped_90x26.snap b/src/snapshots/rdbms_playground__ui__tests__demo_caption_wrapped_90x26.snap index 9d2184d..b3e064d 100644 --- a/src/snapshots/rdbms_playground__ui__tests__demo_caption_wrapped_90x26.snap +++ b/src/snapshots/rdbms_playground__ui__tests__demo_caption_wrapped_90x26.snap @@ -1,5 +1,6 @@ --- source: src/ui.rs +assertion_line: 3457 expression: snapshot --- ╭ Output ────────────────────────────────────────────────────────────────────────────────╮ @@ -23,8 +24,8 @@ expression: snapshot │ │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ 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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__highlighted_input_all_token_classes_dark.snap b/src/snapshots/rdbms_playground__ui__tests__highlighted_input_all_token_classes_dark.snap index 34a6f6a..0c1353e 100644 --- a/src/snapshots/rdbms_playground__ui__tests__highlighted_input_all_token_classes_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__highlighted_input_all_token_classes_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2369 +assertion_line: 2880 expression: snapshot --- ╭ Output ──────────────────────────────────────────────────────────────────────╮ @@ -26,4 +26,4 @@ expression: snapshot │insert into
[([, ...])] [values] ([, ...]) │ ╰──────────────────────────────────────────────────────────────────────────────╯ 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 diff --git a/src/snapshots/rdbms_playground__ui__tests__nav_overlay_relationships_focused_dark.snap b/src/snapshots/rdbms_playground__ui__tests__nav_overlay_relationships_focused_dark.snap index 57a76f8..feeda07 100644 --- a/src/snapshots/rdbms_playground__ui__tests__nav_overlay_relationships_focused_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__nav_overlay_relationships_focused_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2967 +assertion_line: 3347 expression: snapshot --- ╭ Tables ───────────────────────────────────╮ ─────────────────────────────────╮ @@ -22,8 +22,8 @@ expression: snapshot ╰───────────────────────────────────────────╯ │ ╭ Relationships ────────────────────────────╮ ─────────────────────────────────╯ │Customers_Orders │ ─────────────────────────────────╮ -│ Customers.id -> │ ` for a list │ +│ Customers.id -> │ ` for a list · `mode advanced` │ │ Orders.customer_id │ │ ╰───────────────────────────────────────────╯ ─────────────────────────────────╯ Project: Term Planner -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input diff --git a/src/snapshots/rdbms_playground__ui__tests__one_shot_advanced_dark.snap b/src/snapshots/rdbms_playground__ui__tests__one_shot_advanced_dark.snap index 5afcd79..99c972e 100644 --- a/src/snapshots/rdbms_playground__ui__tests__one_shot_advanced_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__one_shot_advanced_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2385 +assertion_line: 2896 expression: snapshot --- ╭ Output ──────────────────────────────────────────────────────────────────────╮ @@ -26,4 +26,4 @@ expression: snapshot │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ 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 diff --git a/src/snapshots/rdbms_playground__ui__tests__populated_with_table_dark.snap b/src/snapshots/rdbms_playground__ui__tests__populated_with_table_dark.snap index 012b295..bebe44f 100644 --- a/src/snapshots/rdbms_playground__ui__tests__populated_with_table_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__populated_with_table_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2679 +assertion_line: 3099 expression: snapshot --- ╭ Tables ──────────────────╮╭ Output ────────────────────────────────────────────────────────────────────────╮ @@ -22,8 +22,8 @@ expression: snapshot ╰──────────────────────────╯│ │ ╭ Relationships ───────────╮╰────────────────────────────────────────────────────────────────────────────────╯ │(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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__rebuild_confirm_modal_dark.snap b/src/snapshots/rdbms_playground__ui__tests__rebuild_confirm_modal_dark.snap index 2b36e30..f396dff 100644 --- a/src/snapshots/rdbms_playground__ui__tests__rebuild_confirm_modal_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__rebuild_confirm_modal_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2399 +assertion_line: 2909 expression: snapshot --- ╭ Output ──────────────────────────────────────────────────────────────────────╮ @@ -22,8 +22,8 @@ expression: snapshot │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭ 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 -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__relationships_panel_dark.snap b/src/snapshots/rdbms_playground__ui__tests__relationships_panel_dark.snap index 3840ae1..87afd3b 100644 --- a/src/snapshots/rdbms_playground__ui__tests__relationships_panel_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__relationships_panel_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2789 +assertion_line: 3209 expression: snapshot --- ╭ Tables ──────────────────╮╭ Output ────────────────────────────────────────────────────────────────────────╮ @@ -22,8 +22,8 @@ expression: snapshot ╰──────────────────────────╯│ │ ╭ Relationships ───────────╮╰────────────────────────────────────────────────────────────────────────────────╯ │Customers_Orders │╭ Hint ──────────────────────────────────────────────────────────────────────────╮ -│ Customers.id -> ││Type a command — press Tab for options, `help` for a list │ -│ Orders.customer_id ││ │ +│ Customers.id -> ││Type a command — press Tab for options, `help` for a list · `mode advanced` │ +│ Orders.customer_id ││for SQL │ ╰──────────────────────────╯╰────────────────────────────────────────────────────────────────────────────────╯ Project: Term Planner -Enter submit · : advanced once · mode advanced switch · Ctrl-C quit +Ctrl-O sidebar · Tab complete · ↑ history · Enter run diff --git a/src/snapshots/rdbms_playground__ui__tests__two_row_input_dark.snap b/src/snapshots/rdbms_playground__ui__tests__two_row_input_dark.snap index 9168780..88166eb 100644 --- a/src/snapshots/rdbms_playground__ui__tests__two_row_input_dark.snap +++ b/src/snapshots/rdbms_playground__ui__tests__two_row_input_dark.snap @@ -1,6 +1,6 @@ --- source: src/ui.rs -assertion_line: 2265 +assertion_line: 2616 expression: snapshot --- ╭ Output ──────────────────────────────────────────────────╮ @@ -46,4 +46,4 @@ expression: snapshot │with `mode advanced`, or prefix the line with `:` to run… │ ╰──────────────────────────────────────────────────────────╯ Project: Term Planner -Enter submit · : advanced once · mode advanced switch · +Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Ente diff --git a/src/ui.rs b/src/ui.rs index 16ac859..c50e95d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1694,7 +1694,19 @@ fn resolve_hint_lines( (None, Some(crate::input_render::AmbientHint::Candidates { items, selected })) => { 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) } +/// 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) { let key_style = Style::default() .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 mut spans: Vec> = Vec::new(); - - let push_shortcut = |spans: &mut Vec>, key: &'static str, label: &str| { + for (key, label) in status_bar_bindings(app) { if !spans.is_empty() { spans.push(separator.clone()); } spans.push(Span::styled(key, key_style)); spans.push(Span::raw(" ")); - spans.push(Span::styled(label.to_string(), 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); - } + spans.push(Span::styled(label, label_style)); } - push_shortcut(&mut spans, "Ctrl-C", &quit); let paragraph = Paragraph::new(Line::from(spans)).style(bar_style); frame.render_widget(paragraph, area); @@ -2582,6 +2630,168 @@ mod tests { .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::>() + .join(" · ") + } + + fn hint_text(lines: &[Line<'_>]) -> String { + lines + .iter() + .map(|l| l.spans.iter().map(|s| s.content.clone()).collect::()) + .collect::>() + .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] fn clamp_wrapped_truncates_with_ellipsis_past_max() { // ≤ max rows: untouched. diff --git a/tests/it/walking_skeleton.rs b/tests/it/walking_skeleton.rs index 5793393..375a8b3 100644 --- a/tests/it/walking_skeleton.rs +++ b/tests/it/walking_skeleton.rs @@ -223,21 +223,30 @@ fn typing_colon_in_simple_mode_flips_prompt_to_advanced() { } #[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 theme = Theme::dark(); - let simple = rendered_text(&mut app, &theme, 80, 24); - assert!(simple.contains("Enter"), "status bar lists Enter"); - assert!(simple.contains("Ctrl-C"), "status bar lists Ctrl-C"); - assert!(simple.contains("mode advanced")); + // Default (empty input): nav / complete / history / run keystrokes. + let default_view = rendered_text(&mut app, &theme, 80, 24); + assert!(default_view.contains("Ctrl-O sidebar"), "strip lists sidebar:\n{default_view}"); + 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"); - submit(&mut app); - let advanced = rendered_text(&mut app, &theme, 80, 24); - assert!(advanced.contains("Enter")); - assert!(advanced.contains("Ctrl-C")); - assert!(advanced.contains("mode simple")); + // Editing (input has text): the #29 readline edit keys appear. + type_str(&mut app, "create"); + let editing = rendered_text(&mut app, &theme, 80, 24); + assert!(editing.contains("Esc clear"), "editing strip lists clear:\n{editing}"); + assert!(editing.contains("Ctrl-W del word"), "editing strip lists del word:\n{editing}"); } // ---------------------------------------------------------------