Skip to content

feat(shortcuts): recordable recently-used-tabs gesture and a Search everywhere chord - #398

Merged
simion merged 18 commits into
simion:mainfrom
AppsOfUse:feat/recordable-shortcuts
Oct 13, 2026
Merged

simion merged 18 commits into
simion:mainfrom
AppsOfUse:feat/recordable-shortcuts

Conversation

@AppsOfUse

@AppsOfUse AppsOfUse commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

PR398-1-recently-used-tabs-neu-belegt PR398-2-konflikt-geste-und-kuerzel PR398-3-search-everywhere-kuerzel-aufgenommen

Recently used tabs and Search everywhere were fixed gestures with only a mode select.
Both are now the user's to set in Settings > Shortcuts.

Recently used tabs records a hold gesture: one modifier to hold (Ctrl, Option/Alt, or Cmd on macOS) and a key to tap, stored in the new synced pref recentTabsGesture.
The tap key is recorded by KeyboardEvent.code, because Option turns ` into a dead key and letters into other characters, so key would never match its own next press.
Absent means Ctrl+Tab, so there is no migration.
The recorder refuses, and says why, naming keys that do work with that modifier:
two or no modifiers; the OS's own chords (Cmd+Tab, Cmd+Space, Cmd+, Ctrl+Space; Alt+Tab, Alt+F4, Alt+ and Win/Super elsewhere);
Ctrl with a letter, digit, or a key that makes a control character ([ \ ] / -, readline's and the terminal's);
Cmd with a letter or digit (copy, paste, quit...);
Option with a letter or digit on macOS (it types), and Alt with any character key elsewhere (Meta to the terminal);
arrows, Home, End and Page keys; Enter, Space and Backspace.
Option with punctuation is accepted on macOS, matching the chord recorder; Option+then takes the à/è dead key while the gesture is on, whichdocs/shortcuts.md` states as the user's trade-off.
A synced gesture this platform cannot use falls back to Ctrl+Tab without rewriting the stored value, and the row says so with a reset.

Search everywhere becomes a normal shortcut entry with a chord that ships unbound, and keeps its double-Shift select on the same row.
It is in PASS_TO_APP, so on macOS the chord works from inside a terminal (elsewhere only with Shift or Alt, as before), and the command palette shows it.

Conflicts move into a pure conflicts(): chord vs chord as before, plus gesture vs chord while the gesture is on, flagged on both rows.
The gesture's mode select now reads Off / On, since the keys sit on the recorder.

Docs: docs/shortcuts.md.

Review follow-ups

A review found defects in the new code; all are fixed here, each with a unit test that failed before its fix:

  • On macOS a Cmd chord in PASS_TO_APP also swallowed the same Ctrl key in a terminal (bind Cmd+R, and Ctrl+R was dead). terminalPassesToApp now requires Cmd on macOS. This also fixes Ctrl+P in a terminal, which was dead on main because of Cmd+P.
  • The refusals above (Cmd+letter would have killed copy app-wide; control-character and cursor keys; Alt+F4).
  • An auto-repeat as the first keydown of a walk left the gesture pinned and recent-places recording suspended.
  • A gesture also stores the character it typed, so it displays and conflicts correctly on non-US layouts.
  • conflicts() is platform-aware (on macOS only a Cmd hold conflicts with a Cmd chord); the logo key is Super on Linux.
  • Docs and comments corrected, docs/shortcuts.md wrapped.

Testing

Manually tested by the author in the dev app: rebinding Recently used tabs and recording a Search everywhere chord. Both behaved as expected.
That pass was before the review follow-ups above, which have not been exercised by hand yet.

  • npm test (3233 passed), npx tsc -b, npm run typecheck:e2e
  • e2e: settings.e2e.ts 123 passing on three consecutive runs, codenav.e2e.ts 31, tabs-layout.e2e.ts 45, app.e2e.ts 40.
  • Controls run on macOS: removing search-everywhere from PASS_TO_APP turns "opens Search Everywhere on a recorded chord, from a focused terminal" red; restoring the old predicate in terminalPassesToApp turns "leaves ⌃R to the shell when ⌘R is bound" red. Both green again with the code as committed.
  • New: "sends nothing to the PTY while it walks on an Option hold" (tabs-layout.e2e.ts).

Pre-existing on main, found during review, not addressed here

  • A corrupt shortcutBindings blob falls into the outer catch {} of loadShortcuts, and the next save wipes every customisation.
  • The hint lines for Previous tab, Next tab and New tab show the raw key (shortcuts.defs.tabPrev.hint). shortcutHint compares against the key with its settings: prefix, which i18next does not return on a miss, so a missing hint is never detected (since feat(i18n): the shortcut rows speak Chinese #391).

Note: #397 also edits tabs-layout.e2e.ts; whichever merges second needs the fixedWaits.test.ts count rechecked.

🤖 Generated with Claude Code

@AppsOfUse
AppsOfUse force-pushed the feat/recordable-shortcuts branch from 755838f to ed53bbb Compare October 12, 2026 12:43
@AppsOfUse
AppsOfUse marked this pull request as ready for review October 13, 2026 07:44

@simion simion left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, nice addition. Appreciate you jumping in with two PRs already 🧡

@simion

simion commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Approved, but this now has merge conflicts (likely from #400 landing). Needs a rebase before merge.

Jonas Utke and others added 18 commits October 13, 2026 15:02
…hord

Recently used tabs and Search everywhere were fixed gestures with only a
mode select. Both are now the user's to set.

Recently used tabs records a HoldGesture: one modifier to hold (Ctrl,
Option/Alt, or Cmd on macOS) and a key to tap, stored as JSON in the new
synced pref `recentTabsGesture`. The tap key is recorded by
KeyboardEvent.code, because Option turns ` into a dead key and letters
into other characters, so `key` would never match its own next press.
Absent means Ctrl+Tab, so no migration; a stored value this platform
cannot use (Cmd synced to Linux) reads as the default without being
rewritten. The recorder refuses two or no modifiers, the OS's own
chords (Cmd+Tab, Cmd+Space, Ctrl+Space; Alt+Tab and the Win/Super key
elsewhere), Ctrl+letter (readline's) and Option+letter (it types). The
walk keeps its window capture listener and matches on code, lands on the
held modifier's release, and remembers the gesture it started with.

Search everywhere becomes a SHORTCUT_DEFS entry with a chord that ships
unbound (defaultBinding: null, BindingMap values nullable), keeps its
double-Shift select on the same row, is in PASS_TO_APP so the chord
works from a terminal, and shows its chord in the command palette.

Conflicts move into a pure `conflicts()`: chord vs chord as before, and
gesture vs chord while the gesture is on, flagged on both rows. The
mode select reads Off / On, since the keys now sit on the recorder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h chord

The earlier code-navigation cases leave navme.ts in front, and an editor
tab hides the agent's terminal, so the case found no visible terminal to
focus. It now waits for the agent, selects its tab, waits for the
terminal to lay out, and puts the previous tab back afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run showed the command palette over the recently-used-tabs snaps.
Why it was open is not established: the settings-navigation case that
opens it already closes it through the store, and the Tasks snap taken
just before these describes (task-port-range-settings.png) shows no
palette. So this is a guard, not a fix for a known cause: both
describes now close the palette and wait for it to be gone before
they draw anything, and the Search everywhere row is snapped with its
recorded chord.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…minal

On macOS PASS_TO_APP handed a key to the app whenever it matched a
binding, and bindingMatches folds Ctrl into cmd. With Search everywhere
bound to Cmd+R, Ctrl+R in a terminal was dropped by xterm, then
useShortcuts bailed on the Mac Ctrl chord, so reverse search reached
neither the shell nor the dialog. Ctrl+P had the same hole with the
shipped Cmd+P.

The predicate moves to terminalPassesToApp in lib/shortcuts.ts and on
macOS now requires metaKey. Off macOS the rule is unchanged (Shift or
Alt on the chord).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Hold Cmd, tap C" was recorded, and the walk's capture listener then
preventDefaulted every Cmd+C: copy was dead app-wide, and the conflict
check stayed silent because Cmd+C and Cmd+V are not SHORTCUT_DEFS. The
same went for V, X, Z, Q, W and the rest.

holdGestureError now refuses Cmd with any letter or digit (holdAppKey).
The key refusals carry the held modifier and the keys that do record
with it (holdAllowedKeys), so the message leads somewhere that works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The refusals covered letters and digits only. Ctrl+[ (ESC), Ctrl+\
(SIGQUIT), Ctrl+] and Ctrl+/ were accepted, so were Option+punctuation
(it types, and Option+` is the dead key for accented letters), every
arrow (word and line navigation, Mission Control), Alt+F4, Alt+` and
Alt+punctuation off macOS (Meta to the terminal), and Cmd+`.

holdGestureError now refuses, per modifier and platform:
- Ctrl: letters, digits and [ \ ] / - ` everywhere
- Option (macOS): every key with a character
- Alt (off macOS): every key with a character, and Tab, F4, Space, `,
  F1, F2, F7, F8, F10 as the desktop's
- Cmd (macOS): letters, digits, Tab, Space, `
- any: arrows, Home, End, Page Up / Down
Enter, Space and Backspace get a message of their own instead of "hold
exactly one modifier".

Every key refusal names the keys that do record with that modifier
(holdAllowedKeys), and a test records each of them, so the advice
cannot lead to another refusal. holdTypes used to say "Pick Tab or a
symbol key", which was false off macOS and is false now everywhere.

Option + ` is refused on macOS too. preventDefault during a walk would
stop the dead-key composition, but claiming the key takes accented
typing away from every editor, which is the harm the rule is about. A
stored Option + ` now reads as the default; the settings e2e case that
records it is updated in a later commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The keydown handler pinned the gesture (held.current) and suspended
recent-places recording before stepCtrlTab, which returns an idle walk
for a repeat. Only a landing undoes either, and a landing needs a live
walk. Holding Ctrl+Tab while a dialog closed therefore left the gesture
pinned (a rebind in Settings was ignored) and recording suspended for
the rest of the session.

Both now happen only once the step leaves the walk active. The three
listeners move out of the hook into recentTabsWalkHandlers so the key
handling is testable without rendering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gesture is stored by KeyboardEvent.code, which is right for
matching, but it was also displayed and checked for conflicts through
the US label of that code. On a German layout Cmd+ö read as Cmd+; and
was compared with a Cmd+; chord, a key that is Shift+comma there.

The recorder now also keeps the character the key typed (HoldGesture
.key, when it is one plain character and Shift or Option did not change
it). holdKeyGlyph prints it, and conflicts compares it with the chords,
which bindingFromEvent records by character too. A stored value without
the field still loads and falls back to the US label; a bad field is
dropped rather than discarding the gesture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Cmd chord

conflicts() folded Ctrl into Cmd for the gesture check, so on macOS a
Ctrl+; walk flagged a Cmd+; chord on both rows. The walk takes only the
Ctrl press there, and the chord's own Cmd press keeps working, so the
warning described nothing. The check now uses the modifier a cmd chord
is pressed with on the platform: Cmd on macOS, Ctrl elsewhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
holdModifierName said "Win" off macOS, so a Linux user pressing
Super+Tab in the recorder was told "Win+Tab belongs to the system". It
now says Super on Linux, Win on Windows, matching the terminal note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A synced gesture this platform cannot use (Cmd from a Mac on Linux, or
one recorded before a rule refused it) fell back to Ctrl+Tab without
being rewritten, which is right, but Settings showed Ctrl+Tab as the
untouched default with no reset and no word about why.

readHoldGesture now returns the refused gesture beside the one in use;
prefs keeps it as recentTabsGestureRefused. The row prints a short note
naming both and shows the reset button, and the reset writes the
default over the stored value even though it is the gesture already
running.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loadShortcuts reads a stored null as "no override", which is what keeps
search-everywhere unbound across a restart (the whole map is written on
every change, so its null is always there) and lets a null on a bound
command fall back to its default. Nothing pinned it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd stale

- docs/shortcuts.md said both non-chord triggers are now the user's to
  set, and later that double-Shift cannot be rebound. It now says which
  part of each is the user's.
- The useShortcuts header says where a recorded search chord works from
  a terminal (macOS; elsewhere only with Shift or Alt).
- prefs.ctrlTabMode's doc still described a fixed Ctrl+Tab.
- ShortcutsHelpDialog's Row.fixed held the double-Shift mode label, and
  its comment explained a recorder that no longer is missing. Renamed to
  doubleShiftLabel.
- docs/shortcuts.md wrapped to 78 columns (tables excepted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
settings.e2e.ts: the gesture recorder refuses Ctrl+R, Ctrl+Enter,
Ctrl+[, Cmd+C and Option/Alt+` with the new messages (each naming the
keys that do work), records Cmd+o-umlaut (Ctrl off macOS) by code and
prints the character, flags a chord on that character and not on the
US label, and shows the note and reset for a saved gesture that cannot
be used here. The Option+` recording case is gone: that gesture is now
refused. The closePalette comment no longer claims a cause nobody
established.

codenav.e2e.ts: the search chord case uses a pure Cmd+Enter on macOS.
xterm sends Enter for Cmd and cancels the event, so the case fails if
search-everywhere is dropped from PASS_TO_APP; a Cmd+letter would
bubble past xterm and pass either way. New macOS case: with Cmd+R
bound, Ctrl+R reaches the PTY as 0x12 and Cmd+R opens the dialog.

tabs-layout.e2e.ts: the rebound walk uses an Option hold the recorder
accepts (Option+Tab on macOS, Alt+F9 elsewhere), and a new case checks
that walking on it writes no byte to the PTY, after a typed-x control
proves the rig sees writes. The settle before each "the walk did not
move" assertion is written at the call site instead of inside
expectStillOn, and fixedWaits counts all four (it said one while the
helper ran four times).

app.e2e.ts: the sheet prints a gesture recorded on a German layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f waiting for it

waitGone on the palette input timed out in some full-file runs (14
cases failed from there on), so the guard now closes it via
dismissOverlays and hides leftover nodes; cause not proven.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chord recorder accepts Option with any key, so the hold gesture
should not be stricter. On macOS the recorder again refuses Option with
a letter or digit only (holdTypes), and accepts Option + punctuation,
Option + ` (the dead key) included. Off macOS the Alt rules stay as
they were.

Option + punctuation types a character, and Option + ` is the dead key
for accented letters: while such a gesture is on, the walk claims the
key and the character is gone from every editor and prompt. That is a
trade-off the user makes by recording it, and docs/shortcuts.md now
says so plainly.

holdAllowedKeys names Tab ` ; ' , . = for Option, the holdTypes message
says "a letter or digit", and a stored Option + ` loads on macOS again.
The settings e2e case records Option + ` on macOS (refuses Alt + `
elsewhere), and the unusable-gesture case seeds Option / Alt + E, which
every platform refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI failed both on other platforms:

- The Option-hold PTY case got ESC [ O / ESC [ I on Windows. Those are
  xterm's focus reports from switching tasks, not the tapped key; CI's
  Windows window has focus, the occluded local macOS one does not. The
  assertion now drops focus reports before checking for bytes.
- The search-everywhere recorder read "Not set\n" on Linux and macOS
  CI (innerText), so the two Not set checks compare trimmed text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e rebase

main's matchesTerminalAppBinding took Binding | undefined, and this
branch lets a stored binding be null (unbound). Widen it to match
bindingMatches, and drop the FIXED_SHORTCUTS import the branch renamed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AppsOfUse
AppsOfUse force-pushed the feat/recordable-shortcuts branch from 814a962 to 06f3ebc Compare October 13, 2026 13:07
@simion
simion merged commit 8a0ed98 into simion:main Oct 13, 2026
4 of 7 checks passed
@AppsOfUse
AppsOfUse deleted the feat/recordable-shortcuts branch October 13, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants