diff --git a/crates/misaligned-terminal/src/input.rs b/crates/misaligned-terminal/src/input.rs index 7612d18e..7a9c9017 100644 --- a/crates/misaligned-terminal/src/input.rs +++ b/crates/misaligned-terminal/src/input.rs @@ -77,6 +77,27 @@ pub fn handle_key( ops_open: bool, ops_typing: bool, ) -> Option { + // Persistence is chord-local, not a modifier-owned mode. Route only a + // complete Ctrl/Command+S/L chord here; every other key continues into + // the surface that currently owns input. Crossterm reports Command as + // SUPER on macOS. + let persistence_modifier = key + .modifiers + .intersects(KeyModifiers::CONTROL | KeyModifiers::SUPER); + if persistence_modifier { + match key.code { + KeyCode::Char('s' | 'S') => return Some(Command::SaveGame), + KeyCode::Char('l' | 'L') => return Some(Command::LoadGame), + _ => {} + } + } + + // The representation flip is global and does not disturb an open menu or + // Operations text field (views.md criterion 1). + if key.code == KeyCode::F(3) { + return Some(Command::ToggleView); + } + // A field being written owns the keyboard: `j` is a letter, not a cursor // move, and Esc abandons the edit rather than the screen behind it. if ops_typing { @@ -90,19 +111,6 @@ pub fn handle_key( _ => None, }; } - if key.modifiers.contains(KeyModifiers::CONTROL) { - return match key.code { - KeyCode::Char('s') => Some(Command::SaveGame), - KeyCode::Char('l') => Some(Command::LoadGame), - _ => None, - }; - } - - // The representation flip is global and does not disturb an open menu's - // anchor or selected row (views.md criterion 1). - if key.code == KeyCode::F(3) { - return Some(Command::ToggleView); - } // While the Operations workspace is open it owns input // (operations-workspace.md): its printed keys navigate views, objects, @@ -221,6 +229,59 @@ mod tests { handle_key(f3, false, true, false), Some(Command::ToggleView) ); + assert_eq!( + handle_key(f3, false, true, true), + Some(Command::ToggleView), + "an Operations text field cannot swallow the global flip" + ); + let stale_control_f3 = KeyEvent::new(KeyCode::F(3), KeyModifiers::CONTROL); + assert_eq!( + handle_key(stale_control_f3, true, false, false), + Some(Command::ToggleView), + "a stale persistence modifier is not a modal state" + ); + } + + #[test] + fn persistence_chords_capture_only_save_and_load() { + for (modifiers, key, command) in [ + (KeyModifiers::CONTROL, 's', Command::SaveGame), + (KeyModifiers::CONTROL, 'L', Command::LoadGame), + (KeyModifiers::SUPER, 'S', Command::SaveGame), + (KeyModifiers::SUPER, 'l', Command::LoadGame), + ] { + assert_eq!( + handle_key( + KeyEvent::new(KeyCode::Char(key), modifiers), + false, + true, + true + ), + Some(command), + "a complete persistence chord stays global" + ); + } + + assert_eq!( + handle_key( + KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL), + false, + false, + false + ), + Some(Command::MoveUp), + "a held Control without S/L must not swallow movement" + ); + assert_eq!( + handle_key( + KeyEvent::new(KeyCode::Char('p'), KeyModifiers::SUPER), + false, + false, + false + ), + Some(Command::TogglePause), + "a held Command without S/L must not swallow time controls" + ); } #[test] diff --git a/wiki/interface/keymap.md b/wiki/interface/keymap.md index 68aa993b..db32d8ed 100644 --- a/wiki/interface/keymap.md +++ b/wiki/interface/keymap.md @@ -14,7 +14,10 @@ Status note: 2026-07-24 — created as the single binding table. Bindings were assigned `A` to movement: terminal now uses `e` / Enter for the menu and implements the same WASD plus semantic-jump navigation as Bevy. Amended 2026-07-29 for the Bevy command surface: `Tab` opens SYSTEMS, `N` opens - NOTICES, and `Esc` closes either before clearing world selection. + NOTICES, and `Esc` closes either before clearing world selection. Repaired + 2026-08-04 after the terminal still treated any held Control modifier as a + persistence mode: only a complete Ctrl/Command+S or +L chord now captures + input, while every unrelated modified key continues to its owning surface. Stage: B1 — The Basement Work order: keymap Work priority: 27 @@ -92,7 +95,7 @@ the rack it moved. | `I` (shift-i) | Open the Operations workspace | [operations-workspace.md](operations-workspace.md) | | `F3` | Flip DIGITAL / REAL | [views.md](views.md) | | `q` | Quit | [bevy.md](bevy.md) | -| `Ctrl`/`Cmd`+`S` / `+L` | Save / load. Chord-local: a bare modifier never suppresses other controls | [bevy.md](bevy.md) | +| `Ctrl`/`Cmd`+`S` / `+L` | Save / load. Chord-local and global: only the complete chord captures input; a bare or stale modifier never suppresses other controls | [action-vocabulary.md](action-vocabulary.md) | Dev-only, never a player surface: `F4` work light, `F8` two-pane. diff --git a/wiki/log/2026-08-04-terminal-persistence-chords.md b/wiki/log/2026-08-04-terminal-persistence-chords.md new file mode 100644 index 00000000..6a4608e7 --- /dev/null +++ b/wiki/log/2026-08-04-terminal-persistence-chords.md @@ -0,0 +1,41 @@ +# 2026-08-04 — Terminal persistence chords stay chord-local + +``` +Type: log +``` + +## Finding + +The shared input law says persistence is a complete-chord global control, not +a modifier-owned mode. Bevy already implemented that boundary, but terminal +returned from `handle_key` whenever Control was present, even when the key was +not `S` or `L`. A stale or bare Control state therefore swallowed F3, +movement, menus, and time controls. The terminal path also recognized Control +only, despite the binding table promising Command on macOS. + +## Changed + +- Terminal now captures only complete Ctrl/Command+S or +L chords. Crossterm's + `SUPER` modifier supplies the Command path. +- Complete persistence chords and F3 route before Operations text entry, so + the global controls keep their meaning under every modal surface. +- Every other modified key continues into the surface that currently owns + input instead of being consumed by persistence routing. +- The keymap's binding row now names the shared action-vocabulary owner rather + than treating this cross-frontend rule as Bevy-only. + +## Evidence + +`cargo test -p misaligned-terminal input::tests:: -- --nocapture` passed all +six focused input tests. The new regression covers Ctrl and Command save/load +in an Operations text field, F3 with a stale Control modifier, Control+W +movement, and Command+P time control. + +## Defense + +`wiki/interface/action-vocabulary.md` criterion 13 and the canonical +`wiki/interface/keymap.md` persistence row require a held modifier without +`S` or `L` not to block movement, menus, time controls, or other unrelated +commands. The terminal's early return violated that explicit chord-local +contract; this narrows capture to the two complete persistence chords without +changing any world action. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index 7d0aa40e..c2070323 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -36,6 +36,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-08-04-thought-fluid-lifecycle-reaudit.md](2026-08-04-thought-fluid-lifecycle-reaudit.md) +## 2026-08-04 - Terminal persistence chords stay chord-local + +- Intent: (see session log) +- Log: [wiki/log/2026-08-04-terminal-persistence-chords.md](2026-08-04-terminal-persistence-chords.md) + ## 2026-08-04 - Social contract re-audit - Intent: (see session log) diff --git a/wiki/process/tick-ledger.md b/wiki/process/tick-ledger.md index 6c281708..39417f35 100644 --- a/wiki/process/tick-ledger.md +++ b/wiki/process/tick-ledger.md @@ -35,7 +35,7 @@ Verdicts: **clean** (slice and code agree), **finding** (acted this tick), | `wiki/process/tick.md` + issue automation | 2026-07-27 | finding | the queued retired-client violation held across binding procedure, both skill mirrors, four active prompt templates, tick intake, project status, and doctor. `tools/tangled_issues.py` now keeps repository discovery and authenticated issue writes on canonical Go `tg`, reconstructs deterministic display numbers from public issue records, folds public label-op history, and provides fail-closed structured list/view/create/edit/comment/close/label operations. A same-day live harvest exposed one wrong record assumption: comments are cross-account `sh.tangled.feed.comment` records linked through `subject.uri`, not issue-local child records. The repaired helper follows that backlink into the author's PDS and returns the markup text; a fixture pins the exact shape, and live issue #15 now returns Cameron's choice `1` — [migration and repair log](../log/2026-07-27-tangled-issue-client-migration.md) | | `wiki/world/places/basement-map.md` room topology | 2026-07-27 | finding | the queued five non-hall corridor cuts remained, and an executable all-room perimeter audit exposed the same dead-door shape at the loading-dock entry plus fixed objects blocking the interior faces of the roll door, HVAC door, both storage doors, Janitor door, and stairwell. Every non-hall approach now reaches its authored door from outside the prefab, every other perimeter tile remains closed, doorway interiors are clear, and the roll, sealed, tier-2, and tier-3 boundaries retain their exact kinds — [room-approach log](../log/2026-07-27-room-approaches-meet-doors.md). Prior [west-approach](../log/2026-07-26-west-hall-approach.md) and [hall-density](../log/2026-07-26-foundation-hall-density.md) repairs stand. | | `wiki/mechanics/sensor-network.md` generated sensor population / criteria 1-5, 8 | 2026-07-28 | finding | criterion 8's standing subscription cost existed only in the global Thought-sink ledger, so a player choosing which individual camera to keep could not read that camera's exact burden. Every retained foreign subscription now names its exact `Thought/tick` upkeep and live/starved state on the device's shared inspect card, within both frontends' visible fact budget; UNTAP removes that addressed cost. The integrated several-subscription starvation/inbox-overflow experience remains pending, so criterion 8 is partial rather than overclaimed — [standing-upkeep log](../log/2026-07-28-subscription-upkeep-inspect.md). Prior generated-camera placement and badge-passage findings stand — [placement repair](../log/2026-07-27-room-camera-floor-placement.md), [passage implementation](../log/2026-07-27-badge-reader-passage-custody.md), [population foundation](../log/2026-07-27-sensor-population-foundation.md). | -| `wiki/interface/keymap.md` + terminal/Bevy input routes | 2026-07-26 | finding | the canonical table assigned `A` to left movement and only `e` / Enter to the context menu, but terminal still opened and closed menus with its older `a` alias and lacked the specified Shift+direction semantic jump. Terminal now implements WASD parity, `a` means left, `e` / Enter alone open the menu, and both frontends consume one renderer-neutral nearest-earned-anchor query without changing selection or opening a menu. README, action-vocabulary, terminal, context-menu, and pinned terminal hints now teach the same boundary — [log](../log/2026-07-26-terminal-keymap-a-reconciliation.md) | +| `wiki/interface/keymap.md` + terminal/Bevy input routes | 2026-08-04 | finding | the Bevy route still obeyed the shared chord-local persistence law, but terminal returned early on **every** Control-modified key before global, modal, or world routing: a stale/bare modifier made F3, movement, menus, and time controls look dead, and Command+S/L was not recognized. Terminal now captures only complete Ctrl/Command+S/L chords, routes those and F3 before modal text, and lets every unrelated modified key reach its current surface; focused regressions pin complete chords plus stale-modifier movement/time/global input — [log](../log/2026-08-04-terminal-persistence-chords.md). Prior A-key and semantic-jump reconciliation stands — [prior](../log/2026-07-26-terminal-keymap-a-reconciliation.md). | | `wiki/interface/action-vocabulary.md` + agent command registry | 2026-07-31 | finding | `PROPOSE SWITCH` still described declaration-only R2a after all four realization families and their exact agent route command had landed. The canonical inventory and agent-play spec now name current execution plus the honest remaining boundary—small-switch realization has no wire-run choice—and a paired corpus defense rejects regression to the stale support claim — [log](../log/2026-07-31-small-switch-action-vocabulary.md). Prior PLOT POLICY inventory repair stands — [log](../log/2026-07-26-action-vocabulary-plot-policy.md). | | `wiki/world/places/zplanes.md` + plane-stack substrate/API | 2026-07-26 | finding | criteria 1-2 remain implemented and criteria 3-6 honestly deferred, but the ratified plane-agnostic contract still left an unused `World::active()` simulation accessor plus active-plane comments on the B1 compatibility map path. The accessor is removed, map reads now say plane 0, the stale criterion/sensing comments are corrected, and a source-shape regression rejects restoration of simulation-owned floor selection — [log](../log/2026-07-26-zplanes-plane-agnostic-api-audit.md) | | `wiki/mechanics/markets.md` + `income.md` B1 contract/account/evidence mirror | 2026-08-04 | finding | the exact B1 contract/account/evidence chain still holds, but the 2026-07-28 Wager audit reopened optional analysis without updating all current mirrors: current-build still called the economy row wholly Live and said only that the Wager was “unchanged,” while implemented Operations law promised an analysis commitment no player action can author. Current-build, sim-mechanics, income, and Operations now distinguish the live base stake/timer/settlement route from the missing real-work analysis route — [re-audit](../log/2026-08-04-wager-current-mirror-reaudit.md). Prior custody reconciliation stands — [log](../log/2026-07-26-markets-contract-custody-reconciliation.md). | @@ -111,3 +111,5 @@ Format: `- YYYY-MM-DD · type · slice · one-line statement of the finding`. Types are the five from [tick.md](tick.md): violation, contradiction, question, bug, insecurity — plus `gate` for a checker owed to the recurrence-promotes-to-the-gate rule. + +- 2026-08-04 · violation · `wiki/interface/keymap.md` complete binding inventory · the binding table omits the live terminal `;` FOCUS EVENT and `Shift+t` SELECT MACHINES controls even though action-vocabulary, terminal law, README, runtime, and tests all name them.