From ea9ddecc72d1f2996abd957f226cb63af29138f2 Mon Sep 17 00:00:00 2001 From: Dylan Socolobsky Date: Wed, 22 Apr 2026 11:14:36 -0300 Subject: [PATCH] fix(tui): route Ctrl+K and Ctrl+W through macOS readline fallback Makes Ctrl+K and Ctrl+W work in hermes --tui mode in macOS --- ui-tui/src/__tests__/platform.test.ts | 19 +++++++++++++++++++ ui-tui/src/components/textInput.tsx | 6 ++++-- ui-tui/src/lib/platform.ts | 13 ++++++++----- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/ui-tui/src/__tests__/platform.test.ts b/ui-tui/src/__tests__/platform.test.ts index 1d2f73fe4..dbb6f0fe6 100644 --- a/ui-tui/src/__tests__/platform.test.ts +++ b/ui-tui/src/__tests__/platform.test.ts @@ -30,3 +30,22 @@ describe('platform action modifier', () => { expect(isActionMod({ ctrl: false, meta: false, super: true })).toBe(false) }) }) + +describe('isMacActionFallback', () => { + it('routes raw Ctrl+K and Ctrl+W to readline kill-to-end / delete-word on macOS', async () => { + const { isMacActionFallback } = await importPlatform('darwin') + + expect(isMacActionFallback({ ctrl: true, meta: false, super: false }, 'k', 'k')).toBe(true) + expect(isMacActionFallback({ ctrl: true, meta: false, super: false }, 'w', 'w')).toBe(true) + // Must not fire when Cmd (meta/super) is held — those are distinct chords. + expect(isMacActionFallback({ ctrl: true, meta: true, super: false }, 'k', 'k')).toBe(false) + expect(isMacActionFallback({ ctrl: true, meta: false, super: true }, 'w', 'w')).toBe(false) + }) + + it('is a no-op on non-macOS (Linux routes Ctrl+K/W through isActionMod directly)', async () => { + const { isMacActionFallback } = await importPlatform('linux') + + expect(isMacActionFallback({ ctrl: true, meta: false, super: false }, 'k', 'k')).toBe(false) + expect(isMacActionFallback({ ctrl: true, meta: false, super: false }, 'w', 'w')).toBe(false) + }) +}) diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx index 12b228c1f..6c94674e4 100644 --- a/ui-tui/src/components/textInput.tsx +++ b/ui-tui/src/components/textInput.tsx @@ -634,6 +634,8 @@ export function TextInput({ const actionHome = k.home || (!isMac && mod && inp === 'a') || isMacActionFallback(k, inp, 'a') const actionEnd = k.end || (mod && inp === 'e') || isMacActionFallback(k, inp, 'e') const actionDeleteToStart = (mod && inp === 'u') || isMacActionFallback(k, inp, 'u') + const actionKillToEnd = (mod && inp === 'k') || isMacActionFallback(k, inp, 'k') + const actionDeleteWord = (mod && inp === 'w') || isMacActionFallback(k, inp, 'w') const range = selRange() const delFwd = k.delete || fwdDel.current @@ -697,7 +699,7 @@ export function TextInput({ } else { v = v.slice(0, c) + v.slice(nextPos(v, c)) } - } else if (mod && inp === 'w') { + } else if (actionDeleteWord) { if (range) { v = v.slice(0, range.start) + v.slice(range.end) c = range.start @@ -717,7 +719,7 @@ export function TextInput({ v = v.slice(c) c = 0 } - } else if (mod && inp === 'k') { + } else if (actionKillToEnd) { if (range) { v = v.slice(0, range.start) + v.slice(range.end) c = range.start diff --git a/ui-tui/src/lib/platform.ts b/ui-tui/src/lib/platform.ts index f4a524733..ab694baaf 100644 --- a/ui-tui/src/lib/platform.ts +++ b/ui-tui/src/lib/platform.ts @@ -16,15 +16,18 @@ export const isActionMod = (key: { ctrl: boolean; meta: boolean; super?: boolean isMac ? key.meta || key.super === true : key.ctrl /** - * Some macOS terminals rewrite Cmd navigation/deletion into readline control keys. - * Treat those as action shortcuts too, but only for the specific fallbacks we - * have observed from terminals: Cmd+Left → Ctrl+A, Cmd+Right → Ctrl+E, - * Cmd+Backspace → Ctrl+U. + * Accept raw Ctrl+ as an action shortcut on macOS, where `isActionMod` + * otherwise means Cmd. Two motivations: + * - Some macOS terminals rewrite Cmd navigation/deletion into readline control + * keys (Cmd+Left → Ctrl+A, Cmd+Right → Ctrl+E, Cmd+Backspace → Ctrl+U). + * - Ctrl+K (kill-to-end) and Ctrl+W (delete-word-back) are standard readline + * bindings that users expect to work regardless of platform, even though + * no terminal rewrites Cmd into them. */ export const isMacActionFallback = ( key: { ctrl: boolean; meta: boolean; super?: boolean }, ch: string, - target: 'a' | 'e' | 'u' + target: 'a' | 'e' | 'u' | 'k' | 'w' ): boolean => isMac && key.ctrl && !key.meta && key.super !== true && ch.toLowerCase() === target /** Match action-modifier + a single character (case-insensitive). */