Skip to content

fix(tui): ignore letter shortcuts when ctrl or alt is held - #669

Open
eric-engberg wants to merge 3 commits into
sirmalloc:mainfrom
eric-engberg:fix/hotkey-modifiers
Open

eric-engberg wants to merge 3 commits into
sirmalloc:mainfrom
eric-engberg:fix/hotkey-modifiers

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

Problem

Letter shortcuts compared the typed character without checking modifiers, so a ctrl or alt combo ran the plain-letter action:

  • ctrl+r toggled raw value in the line editor and reset styling in Edit Colors.
  • ctrl+d deleted a widget, and ctrl+a opened the add-widget picker. That last one matters for anyone whose tmux prefix is ctrl+a: pressing ctrl+a twice sends a literal ctrl+a through.
  • On macOS, alt/option+←/→ is commonly sent as ESC b / ESC f, which Ink reads as alt+b / alt+f. Word-jumping with option-arrow could therefore toggle bold, background mode, and so on.
  • A few handlers had already patched ctrl+s one key at a time with && !key.ctrl. Widget keybinds checked ctrl but not alt.

Fix

getPlainInput(input, key) in utils/input-guards.ts returns the typed character only for a bare keypress, and '' while ctrl or alt/option is held. Every letter shortcut now compares against it:

  • the line editor's built-in shortcuts and the widget-specific keybind matching
  • Edit Colors, Global Overrides, and Powerline setup, themes and separators
  • the line selector and the hide-state checklist

These are unchanged:

  • arrows, Enter and ESC, so option+arrow still navigates
  • shift, so A/a handlers keep working
  • text entry, which was already guarded by shouldInsertInput
  • App's real combos, ctrl+c and ctrl+s

The ad-hoc !key.ctrl patches are gone, and modifier combos are free for bindings of their own.

Modifiers on macOS and Windows/Linux

Ink 6.2 reports ctrl, meta (alt/option) and shift:

  • Guarding ctrl and meta covers Ctrl, PC Alt, and Mac Option when "Option as Meta/Esc+" is on.
  • Mac Option in its default mode types a different character (∫, ®, …), which never matches a shortcut letter.
  • Cmd and the Windows key normally aren't passed to terminal programs.
  • AltGr arrives as the resulting character, so it's unaffected.

Testing

  • getPlainInput unit tests.
  • Line-editor handler tests: ctrl and alt held with each of a i d k c r m do nothing, the bare letter still works, and widget keybinds ignore alt as well as ctrl.
  • hotkey-modifiers.test.ts: a table-driven Ink test that sends the real terminal bytes. Ctrl is the control character, e.g. \x12 for ctrl+r; alt is ESC + key. For one shortcut in each of the seven components, it checks that the bare key fires and ctrl and alt don't. ctrl+i isn't used because it's the same byte as Tab.
  • On today's main (with docs: explain an empty status line in one folder (workspace trust) #606 and feat(git-is-fork): render forks as an editable glyph #617 merged in locally): bun test gives 2809 pass, 0 fail (Bun 1.4.2), and bun run lint is clean. Under Node 26 vitest, the 3 touched test files pass (70).
  • Built CLI under Bun 1.4.2 and Node 26.10.0: all runtimes agree with each other and with main in plain and Powerline modes, and the TUI opens and exits under both.
  • Manual run in tmux, which sends genuine M- and C- sequences: alt+b, ctrl+r and alt+d do nothing in the line editor, while plain b and r work.

Letter shortcuts compared the typed character without checking
modifiers, so ctrl+letter and alt+letter ran the plain-letter action:
ctrl+r toggled raw value, ctrl+d deleted a widget, and on macOS alt+arrow
(sent as ESC + letter) fired whatever letter followed. A few handlers
patched ctrl+s one key at a time with `&& !key.ctrl`; widget keybinds
checked ctrl but not alt.

Shortcuts now compare against getPlainInput(), which is empty while ctrl
or alt/option is held, across the line editor, widget keybinds, Edit
Colors, Global Overrides, Powerline setup/themes/separators, the line
selector and the hide-state checklist. Arrows, Enter, ESC, shift and text
entry are unchanged, and modifier combos are free for bindings of their
own.
The shortcut tests sent keys and checked the result after a fixed 25 ms. On a
busy machine Ink can take longer, so the bare-key cases could fail even though
the shortcut works. They now wait until the shortcut's effect shows (up to
3 s), and a follow-up key waits for the previous key's redraw. Checks that a
modified key does nothing give it 150 ms to act.

After each wait the test also gives React two setImmediate turns: React draws
a frame, then attaches the new input listener in a scheduled effect, so a key
sent the moment the frame shows could reach no listener.
# Conflicts:
#	src/tui/components/items-editor/input-handlers.ts
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.

1 participant