From ef292cb0696c2a0d416bb0183555e5a71e88e65b Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 27 Jun 2026 14:06:25 -0700 Subject: [PATCH] Map mobile special keys to terminal PTY bytes (#6515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand live input mapping to handle escape, tab, delete, insert, arrows, home, end, page navigation, and F1–F12 keys. - Safe Map lookup avoids collisions with object prototype methods. - Enter is excluded to prevent double-sent carriage returns. - Standardizes byte translation via buildTerminalShortcutKey. --- .../src/terminal/terminal-live-input.test.ts | 40 ++++++++++- mobile/src/terminal/terminal-live-input.ts | 67 ++++++++++++++++++- 2 files changed, 102 insertions(+), 5 deletions(-) diff --git a/mobile/src/terminal/terminal-live-input.test.ts b/mobile/src/terminal/terminal-live-input.test.ts index d5eb9f5d9..db3e96236 100644 --- a/mobile/src/terminal/terminal-live-input.test.ts +++ b/mobile/src/terminal/terminal-live-input.test.ts @@ -19,12 +19,48 @@ describe('terminal live input', () => { vi.useRealTimers() }) - it('maps phone keyboard special keys to PTY bytes', () => { - expect(getTerminalLiveSpecialKeyBytes('Backspace')).toBe('\x7f') + it.each([ + ['Escape', '\x1b'], + ['Esc', '\x1b'], + ['Tab', '\t'], + ['Backspace', '\x7f'], + ['Delete', '\x1b[3~'], + ['Insert', '\x1b[2~'], + ['ArrowUp', '\x1b[A'], + ['ArrowDown', '\x1b[B'], + ['ArrowLeft', '\x1b[D'], + ['ArrowRight', '\x1b[C'], + ['Home', '\x1b[H'], + ['End', '\x1b[F'], + ['PageUp', '\x1b[5~'], + ['PageDown', '\x1b[6~'], + ['F1', '\x1bOP'], + ['F2', '\x1bOQ'], + ['F3', '\x1bOR'], + ['F4', '\x1bOS'], + ['F5', '\x1b[15~'], + ['F6', '\x1b[17~'], + ['F7', '\x1b[18~'], + ['F8', '\x1b[19~'], + ['F9', '\x1b[20~'], + ['F10', '\x1b[21~'], + ['F11', '\x1b[23~'], + ['F12', '\x1b[24~'] + ])('maps %s to terminal PTY bytes', (key, bytes) => { + expect(getTerminalLiveSpecialKeyBytes(key)).toBe(bytes) + }) + + it('leaves submitted or printable keys on their existing input paths', () => { expect(getTerminalLiveSpecialKeyBytes('Enter')).toBeNull() expect(getTerminalLiveSpecialKeyBytes('a')).toBeNull() }) + it('ignores object prototype names from native key events', () => { + expect(getTerminalLiveSpecialKeyBytes('constructor')).toBeNull() + expect(getTerminalLiveSpecialKeyBytes('toString')).toBeNull() + expect(getTerminalLiveSpecialKeyBytes('hasOwnProperty')).toBeNull() + }) + it('enforces the paste-sized byte budget', () => { expect(isTerminalLiveInputWithinByteLimit('hello')).toBe(true) expect(isTerminalLiveInputWithinByteLimit('x'.repeat(TERMINAL_LIVE_INPUT_MAX_BYTES))).toBe(true) diff --git a/mobile/src/terminal/terminal-live-input.ts b/mobile/src/terminal/terminal-live-input.ts index 0cf72e8da..dd5f8e6a7 100644 --- a/mobile/src/terminal/terminal-live-input.ts +++ b/mobile/src/terminal/terminal-live-input.ts @@ -1,7 +1,67 @@ +import { buildTerminalShortcutKey } from './terminal-accessory-keys' + const TERMINAL_LIVE_INPUT_MAX_BYTES = 256 * 1024 const encoder = new TextEncoder() +type TerminalLiveSpecialKeyId = + | 'arrowDown' + | 'arrowLeft' + | 'arrowRight' + | 'arrowUp' + | 'backspace' + | 'delete' + | 'end' + | 'escape' + | 'f1' + | 'f2' + | 'f3' + | 'f4' + | 'f5' + | 'f6' + | 'f7' + | 'f8' + | 'f9' + | 'f10' + | 'f11' + | 'f12' + | 'home' + | 'insert' + | 'pageDown' + | 'pageUp' + | 'tab' + +// Why: Enter stays on onSubmitEditing; mapping it here can double-send carriage +// returns when native TextInput emits both submit and key events. +const TERMINAL_LIVE_SPECIAL_KEY_IDS = new Map([ + ['Escape', 'escape'], + ['Esc', 'escape'], + ['Tab', 'tab'], + ['Backspace', 'backspace'], + ['Delete', 'delete'], + ['Insert', 'insert'], + ['ArrowUp', 'arrowUp'], + ['ArrowDown', 'arrowDown'], + ['ArrowLeft', 'arrowLeft'], + ['ArrowRight', 'arrowRight'], + ['Home', 'home'], + ['End', 'end'], + ['PageUp', 'pageUp'], + ['PageDown', 'pageDown'], + ['F1', 'f1'], + ['F2', 'f2'], + ['F3', 'f3'], + ['F4', 'f4'], + ['F5', 'f5'], + ['F6', 'f6'], + ['F7', 'f7'], + ['F8', 'f8'], + ['F9', 'f9'], + ['F10', 'f10'], + ['F11', 'f11'], + ['F12', 'f12'] +]) + export type TerminalLiveInputFocusTimerRef = { current: ReturnType | null } @@ -15,10 +75,11 @@ export type TerminalLiveInputDefaultResult = { export type TerminalLiveInputPruneResult = TerminalLiveInputDefaultResult export function getTerminalLiveSpecialKeyBytes(key: string): string | null { - if (key === 'Backspace') { - return '\x7f' + const shortcutKey = TERMINAL_LIVE_SPECIAL_KEY_IDS.get(key) + if (!shortcutKey) { + return null } - return null + return buildTerminalShortcutKey({ key: shortcutKey, modifiers: [] })?.bytes ?? null } export function isTerminalLiveInputWithinByteLimit(