feat(demo): Ctrl-G as a demo-mode F1 alias for casts (ADR-0047 Amendment 1)
ci / gate (push) Successful in 3m3s
ci / gate (push) Successful in 3m3s
The contextual hint overlay (ADR-0053) opens on F1, but F1 is an escape sequence the autocast recorder can't emit — so casts (and presenter / teacher sessions) couldn't trigger the most teaching-relevant overlay. In demo mode only, Ctrl-G now aliases F1: it runs the same hint logic and badges AS [F1], so a recording is visually identical to a real F1 press. Ctrl-G is the only fit — Ctrl+digit (e.g. Ctrl-1) isn't encodable in a legacy terminal (arrives as a bare `1`), and the kitty protocol that would encode it needs escape sequences autocast can't send (and the app doesn't enable keyboard-enhancement flags). Demo-gated, so the shipped keymap stays F1-only; outside demo mode Ctrl-G is inert. - app.rs: hint_key guard gains the demo-gated Ctrl-G disjunct; demo_badge_label maps Ctrl-G -> [F1]; 3 Tier-1 tests + badge assertion. - ADR-0047 Amendment 1 + README index; also removed two stray </content> / </invoke> lines accidentally committed in the ADR file. docs: drop three more stale "deferred" entries from CLAUDE.md — readline shortcuts (I1b, ADR-0049), tab completion (I3), and syntax highlighting (I4) are all implemented; only multi-line input (I1) remains open.
This commit is contained in:
@@ -347,11 +347,8 @@ not yet implemented:
|
|||||||
- **Session log + Markdown export** (V4): the bigger UX
|
- **Session log + Markdown export** (V4): the bigger UX
|
||||||
project — scrollable session journal, smart structure
|
project — scrollable session journal, smart structure
|
||||||
rendering, save-as-markdown.
|
rendering, save-as-markdown.
|
||||||
- **Readline shortcuts** (I1b): Ctrl-A/Ctrl-E, Ctrl-W/Ctrl-K/
|
|
||||||
Ctrl-U.
|
|
||||||
- **Multi-line input** (I1): Enter inserts newline,
|
- **Multi-line input** (I1): Enter inserts newline,
|
||||||
Ctrl-Enter submits.
|
Ctrl-Enter submits.
|
||||||
- **Tab completion** (I3), **syntax highlighting** (I4).
|
|
||||||
- **ER diagram export** (V3).
|
- **ER diagram export** (V3).
|
||||||
- **Full TT5** (CI): the pipeline is live (see the CI decision
|
- **Full TT5** (CI): the pipeline is live (see the CI decision
|
||||||
above / `docs/ci/adr/`), but "all tiers on all OSes" isn't
|
above / `docs/ci/adr/`), but "all tiers on all OSes" isn't
|
||||||
|
|||||||
@@ -414,5 +414,41 @@ time-boxed-`recv` path. We therefore test the **pure pieces**
|
|||||||
exhaustively (label fn, capture state machine, nearest-deadline helper)
|
exhaustively (label fn, capture state machine, nearest-deadline helper)
|
||||||
and assert plumbing via Tier-3, rather than over-claiming an integration
|
and assert plumbing via Tier-3, rather than over-claiming an integration
|
||||||
test of the `tokio` timeout itself.
|
test of the `tokio` timeout itself.
|
||||||
</content>
|
|
||||||
</invoke>
|
## Amendment 1 — `Ctrl-G` demo-mode alias for F1 (2026-06-15)
|
||||||
|
|
||||||
|
**Context.** The contextual `hint` overlay (ADR-0053 / H2) is opened with
|
||||||
|
**F1**. But F1 reaches the app only as an escape sequence (`\eOP` /
|
||||||
|
`\e[11~`), and the `autocast` recorder used for our screencasts **cannot
|
||||||
|
emit escape sequences** — so a cast can never trigger F1, and the single
|
||||||
|
most teaching-relevant overlay is unreachable in recordings. The same
|
||||||
|
wall already bit step-captions (which is why `Ctrl+]`, a single control
|
||||||
|
byte, was chosen over `Ctrl+!`).
|
||||||
|
|
||||||
|
**Decision.** In **demo mode only**, **`Ctrl-G`** is an alias for F1. It
|
||||||
|
runs the exact F1 hint logic (live-input → form hint; empty input →
|
||||||
|
recent-error / getting-started) and is **badged as `[F1]`** (not
|
||||||
|
`[CTRL-G]`) so a recorded cast is visually identical to a genuine F1
|
||||||
|
press. `Ctrl-G` is the only viable choice: it is a single legacy control
|
||||||
|
byte autocast can send, whereas `Ctrl`+digit (e.g. the mnemonic `Ctrl-1`)
|
||||||
|
is **not encodable in a legacy terminal at all** — digits have no control
|
||||||
|
byte, so `Ctrl-1` arrives as a bare `1`; the kitty protocol *would* encode
|
||||||
|
it but only as an escape sequence (the very thing autocast can't send),
|
||||||
|
and this app deliberately does not enable keyboard-enhancement flags.
|
||||||
|
|
||||||
|
**Why demo-gated.** The shipped keymap stays F1-only — a real user never
|
||||||
|
trips the alias, and demo mode is also the mode teachers/presenters run,
|
||||||
|
so the alias is available exactly where it's wanted. Outside demo mode
|
||||||
|
`Ctrl-G` falls through to the inert catch-all (the `Char(c)` insert arm
|
||||||
|
excludes CONTROL, so no `g` is typed).
|
||||||
|
|
||||||
|
**Scope.** `hint_key` guard in `App::handle_key` gains the demo-gated
|
||||||
|
`Ctrl-G` disjunct; `demo_badge_label` maps `Ctrl-G → [F1]` (consulted
|
||||||
|
only in demo mode). Test-first: three `app.rs` Tier-1 tests (alias fires
|
||||||
|
on input + on empty input; inert when demo off) + the badge-map
|
||||||
|
assertion. The keybinding strip (ADR-0051) is **not** changed — F1 stays
|
||||||
|
the advertised key; `Ctrl-G` is a recorder aid, and the badge already
|
||||||
|
reads `[F1]`.
|
||||||
|
|
||||||
|
*(Editorial: this amendment also removed two stray `</content>` /
|
||||||
|
`</invoke>` lines accidentally committed at the end of this file.)*
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+79
-6
@@ -520,10 +520,11 @@ const HISTORY_CAPACITY: usize = 1000;
|
|||||||
/// produces a glyph of its own (and so needs no badge) — ADR-0047 D2.
|
/// produces a glyph of its own (and so needs no badge) — ADR-0047 D2.
|
||||||
///
|
///
|
||||||
/// The set is exactly the *otherwise-invisible* keys: motion, editing,
|
/// The set is exactly the *otherwise-invisible* keys: motion, editing,
|
||||||
/// submission, and the `Ctrl-O` navigation toggle. Plain character keys
|
/// submission, the `Ctrl-O` navigation toggle, and the `Ctrl-G` F1-alias
|
||||||
/// already appear on the input line, and `Ctrl-C` (quit) / `Ctrl+]`
|
/// (ADR-0047 amendment). Plain character keys already appear on the input
|
||||||
/// (the caption toggle) are deliberately excluded. Pure and total, so
|
/// line, and `Ctrl-C` (quit) / `Ctrl+]` (the caption toggle) are
|
||||||
/// it is exhaustively unit-testable without a running app.
|
/// deliberately excluded. Pure and total, so it is exhaustively
|
||||||
|
/// unit-testable without a running app.
|
||||||
pub const fn demo_badge_label(key: &KeyEvent) -> Option<&'static str> {
|
pub const fn demo_badge_label(key: &KeyEvent) -> Option<&'static str> {
|
||||||
match (key.code, key.modifiers) {
|
match (key.code, key.modifiers) {
|
||||||
(KeyCode::Tab, _) => Some("[TAB]"),
|
(KeyCode::Tab, _) => Some("[TAB]"),
|
||||||
@@ -541,8 +542,12 @@ pub const fn demo_badge_label(key: &KeyEvent) -> Option<&'static str> {
|
|||||||
(KeyCode::PageDown, _) => Some("[PGDN]"),
|
(KeyCode::PageDown, _) => Some("[PGDN]"),
|
||||||
(KeyCode::Backspace, _) => Some("[BKSP]"),
|
(KeyCode::Backspace, _) => Some("[BKSP]"),
|
||||||
(KeyCode::Delete, _) => Some("[DEL]"),
|
(KeyCode::Delete, _) => Some("[DEL]"),
|
||||||
// The only badged control chord: the ADR-0046 navigation toggle.
|
// The ADR-0046 navigation toggle.
|
||||||
(KeyCode::Char('o'), m) if m.contains(KeyModifiers::CONTROL) => Some("[CTRL-O]"),
|
(KeyCode::Char('o'), m) if m.contains(KeyModifiers::CONTROL) => Some("[CTRL-O]"),
|
||||||
|
// ADR-0047 amendment: the Ctrl-G F1-alias badges AS [F1] so a
|
||||||
|
// cast recorded by a tool that can't send F1 looks identical to a
|
||||||
|
// real F1 press. (Only consulted in demo mode — the caller gates.)
|
||||||
|
(KeyCode::Char('g'), m) if m.contains(KeyModifiers::CONTROL) => Some("[F1]"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1223,7 +1228,18 @@ impl App {
|
|||||||
// the memo-clearing completion match below. Non-empty input →
|
// the memo-clearing completion match below. Non-empty input →
|
||||||
// a hint for the command being typed; empty input → expand on
|
// a hint for the command being typed; empty input → expand on
|
||||||
// the most recent error (or a getting-started pointer).
|
// the most recent error (or a getting-started pointer).
|
||||||
if key.code == KeyCode::F(1) {
|
//
|
||||||
|
// ADR-0047 amendment: in demo mode, Ctrl-G is an alias for F1.
|
||||||
|
// The cast recorder (autocast) can't emit F1 (an escape
|
||||||
|
// sequence) but can send the single control byte Ctrl-G; it
|
||||||
|
// badges AS [F1] (see `demo_badge_label`) so the cast is visually
|
||||||
|
// identical to a real F1 press. Demo-gated, so the shipped keymap
|
||||||
|
// stays F1-only.
|
||||||
|
let hint_key = key.code == KeyCode::F(1)
|
||||||
|
|| (self.demo_mode
|
||||||
|
&& (key.code, key.modifiers)
|
||||||
|
== (KeyCode::Char('g'), KeyModifiers::CONTROL));
|
||||||
|
if hint_key {
|
||||||
if self.input.trim().is_empty() {
|
if self.input.trim().is_empty() {
|
||||||
self.note_hint_for_recent_error();
|
self.note_hint_for_recent_error();
|
||||||
} else {
|
} else {
|
||||||
@@ -3459,6 +3475,13 @@ mod tests {
|
|||||||
demo_badge_label(&ke(KeyCode::Char('o'), KeyModifiers::CONTROL)),
|
demo_badge_label(&ke(KeyCode::Char('o'), KeyModifiers::CONTROL)),
|
||||||
Some("[CTRL-O]")
|
Some("[CTRL-O]")
|
||||||
);
|
);
|
||||||
|
// ADR-0047 amendment: the Ctrl-G F1-alias badges AS [F1], so a
|
||||||
|
// cast recorded with autocast (which can't send F1) is visually
|
||||||
|
// identical to a real F1 press.
|
||||||
|
assert_eq!(
|
||||||
|
demo_badge_label(&ke(KeyCode::Char('g'), KeyModifiers::CONTROL)),
|
||||||
|
Some("[F1]")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -5756,6 +5779,56 @@ mod tests {
|
|||||||
assert_eq!(app.input, input, "F1 must not change the buffer");
|
assert_eq!(app.input, input, "F1 must not change the buffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── ADR-0047 amendment: Ctrl-G is a demo-mode alias for F1 ──────
|
||||||
|
// The cast recorder (autocast) cannot emit F1 — an escape sequence —
|
||||||
|
// but Ctrl-G is a single legacy control byte it can send. Demo-gated
|
||||||
|
// so the real keymap stays F1-only, and badged as [F1] (see
|
||||||
|
// `demo_badge_label`) so a recorded cast looks identical to a genuine
|
||||||
|
// F1 press.
|
||||||
|
|
||||||
|
fn ctrl_g() -> AppEvent {
|
||||||
|
key_mod(KeyCode::Char('g'), KeyModifiers::CONTROL)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_g_in_demo_mode_aliases_f1_on_input() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.demo_mode = true;
|
||||||
|
type_str(&mut app, "insert into T");
|
||||||
|
let input = app.input.clone();
|
||||||
|
let before = app.output.len();
|
||||||
|
app.update(ctrl_g());
|
||||||
|
assert_eq!(app.input, input, "Ctrl-G must not change the buffer (no `g` typed)");
|
||||||
|
assert!(app.output.len() > before, "Ctrl-G must emit the same hint F1 does");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_g_in_demo_mode_aliases_f1_on_empty_input() {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.demo_mode = true;
|
||||||
|
let before = app.output.len();
|
||||||
|
app.update(ctrl_g());
|
||||||
|
assert!(
|
||||||
|
app.output.len() > before,
|
||||||
|
"Ctrl-G on empty input emits the getting-started hint"
|
||||||
|
);
|
||||||
|
assert!(output_contains(&app, "press F1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_g_outside_demo_mode_is_inert() {
|
||||||
|
// Not in demo mode: Ctrl-G is neither the hint alias nor a typed
|
||||||
|
// glyph (the `Char(c)` insert arm excludes CONTROL), so it falls
|
||||||
|
// through to the inert catch-all — no `g`, no hint.
|
||||||
|
let mut app = App::new();
|
||||||
|
type_str(&mut app, "insert");
|
||||||
|
let input = app.input.clone();
|
||||||
|
let before = app.output.len();
|
||||||
|
app.update(ctrl_g());
|
||||||
|
assert_eq!(app.input, input, "Ctrl-G must not insert a `g`");
|
||||||
|
assert_eq!(app.output.len(), before, "Ctrl-G does nothing when demo mode is off");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dsl_failure_sets_hint_class_and_a_later_dsl_command_clears_it() {
|
fn dsl_failure_sets_hint_class_and_a_later_dsl_command_clears_it() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user