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:
claude@clouddev1
2026-06-13 12:18:37 +00:00
parent 8ac3537df0
commit eceedc19b7
22 changed files with 493 additions and 86 deletions
+11
View File
@@ -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
+13 -5
View File
@@ -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", &[]),
+21 -5
View File
@@ -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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 <Table> [(<col>[, ...])] [values] (<value>[, ...]) │
╰──────────────────────────────────────────────────────────────────────────────╯
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+234 -24
View File
@@ -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<Span<'_>> = Vec::new();
let push_shortcut = |spans: &mut Vec<Span<'_>>, 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::<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]
fn clamp_wrapped_truncates_with_ellipsis_past_max() {
// ≤ max rows: untouched.