From a34a6bec7e428ef836e660944f969a87696df296 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Sun, 31 May 2026 23:35:31 -0400 Subject: [PATCH] Add mobile terminal Space shortcut (#4335) Co-authored-by: Orca --- .../terminal/terminal-accessory-keys.test.ts | 33 ++++++++++++ .../src/terminal/terminal-accessory-keys.ts | 1 + .../terminal-accessory-layout.test.ts | 53 +++++++++++++++++-- 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/mobile/src/terminal/terminal-accessory-keys.test.ts b/mobile/src/terminal/terminal-accessory-keys.test.ts index 13a918248..b46f3ee8e 100644 --- a/mobile/src/terminal/terminal-accessory-keys.test.ts +++ b/mobile/src/terminal/terminal-accessory-keys.test.ts @@ -23,6 +23,21 @@ describe('TERMINAL_ACCESSORY_KEYS', () => { }) }) + it('includes a non-repeatable Space default key near the primary editing keys', () => { + const ids = TERMINAL_ACCESSORY_KEYS.map((key) => key.id) + + expect(TERMINAL_ACCESSORY_KEYS.find((candidate) => candidate.id === 'space')).toEqual({ + id: 'space', + label: 'Space', + bytes: ' ', + accessibilityLabel: 'Space' + }) + expect(ids.indexOf('space')).toBeGreaterThan(ids.indexOf('shiftTab')) + expect(ids.indexOf('space')).toBeLessThan(ids.indexOf('backspace')) + expect(ids.indexOf('space')).toBeLessThan(ids.indexOf('delete')) + expect(ids.indexOf('space')).toBeLessThan(ids.indexOf('arrowUp')) + }) + it('has unique non-empty built-in ids', () => { const ids = TERMINAL_ACCESSORY_KEYS.map((key) => key.id) @@ -63,6 +78,24 @@ describe('TERMINAL_ACCESSORY_KEYS', () => { }) }) + it('builds custom Space shortcut bytes with terminal modifiers', () => { + expect(buildTerminalShortcutKey({ key: 'space', modifiers: [] })).toEqual({ + label: 'Space', + bytes: ' ', + accessibilityLabel: 'Space' + }) + expect(buildTerminalShortcutKey({ key: 'space', modifiers: ['ctrl'] })).toEqual({ + label: 'Ctrl+Space', + bytes: '\x00', + accessibilityLabel: 'Ctrl Space' + }) + expect(buildTerminalShortcutKey({ key: 'space', modifiers: ['alt'] })).toEqual({ + label: 'Alt+Space', + bytes: '\x1b ', + accessibilityLabel: 'Alt Space' + }) + }) + it('builds modified special-key terminal sequences', () => { expect(buildTerminalShortcutKey({ key: 'tab', modifiers: ['shift'] })).toEqual({ label: 'Shift+Tab', diff --git a/mobile/src/terminal/terminal-accessory-keys.ts b/mobile/src/terminal/terminal-accessory-keys.ts index 2923c2543..cac5731f3 100644 --- a/mobile/src/terminal/terminal-accessory-keys.ts +++ b/mobile/src/terminal/terminal-accessory-keys.ts @@ -205,6 +205,7 @@ export const TERMINAL_ACCESSORY_KEYS: TerminalAccessoryKey[] = [ { id: 'enter', label: 'Enter', bytes: '\r', accessibilityLabel: 'Enter' }, // Why: terminal apps recognize ESC [ Z as the reverse-tab sequence. { id: 'shiftTab', label: 'Shift+Tab', bytes: '\x1b[Z', accessibilityLabel: 'Shift Tab' }, + { id: 'space', label: 'Space', bytes: ' ', accessibilityLabel: 'Space' }, { id: 'backspace', label: '⌫', bytes: '\x7f', accessibilityLabel: 'Backspace', repeatable: true }, { id: 'delete', diff --git a/mobile/src/terminal/terminal-accessory-layout.test.ts b/mobile/src/terminal/terminal-accessory-layout.test.ts index 57e1d7f03..bf00b6193 100644 --- a/mobile/src/terminal/terminal-accessory-layout.test.ts +++ b/mobile/src/terminal/terminal-accessory-layout.test.ts @@ -21,16 +21,27 @@ vi.mock('@react-native-async-storage/async-storage', () => ({ default: asyncStorageMock })) +function oldBuiltInIdsBeforeSpace(): string[] { + return getDefaultTerminalAccessoryBuiltInIds().filter((id) => id !== 'space') +} + describe('terminal accessory layout', () => { beforeEach(() => { asyncStorageMock.getItem.mockReset() asyncStorageMock.setItem.mockReset() }) - it('defaults include enter', () => { - expect(getDefaultTerminalAccessoryBuiltInIds()).toContain('enter') - expect(getVisibleTerminalAccessoryKeys(getDefaultTerminalAccessoryBuiltInIds())).toContainEqual( - expect.objectContaining({ id: 'enter', bytes: '\r' }) + it('defaults include Space near Enter, Tab, and Shift+Tab', () => { + const ids = getDefaultTerminalAccessoryBuiltInIds() + + expect(ids).toContain('enter') + expect(ids).toContain('space') + expect(ids.indexOf('space')).toBeGreaterThan(ids.indexOf('shiftTab')) + expect(ids.indexOf('space')).toBeLessThan(ids.indexOf('backspace')) + expect(ids.indexOf('space')).toBeLessThan(ids.indexOf('delete')) + expect(ids.indexOf('space')).toBeLessThan(ids.indexOf('arrowUp')) + expect(getVisibleTerminalAccessoryKeys(ids)).toContainEqual( + expect.objectContaining({ id: 'space', bytes: ' ', accessibilityLabel: 'Space' }) ) }) @@ -94,6 +105,40 @@ describe('terminal accessory layout', () => { ).toEqual(['escape']) }) + it('migrates Space into old personalized layouts without reordering existing visible keys', () => { + const oldBuiltInIds = oldBuiltInIdsBeforeSpace() + + expect( + normalizeTerminalAccessoryLayoutPreference({ + version: 1, + visibleBuiltInIds: ['tab', 'enter', 'shiftTab'], + knownBuiltInIds: oldBuiltInIds + }).visibleBuiltInIds + ).toEqual(['tab', 'enter', 'shiftTab', 'space']) + }) + + it('shows Space once for an all-hidden old layout', () => { + const oldBuiltInIds = oldBuiltInIdsBeforeSpace() + + expect( + normalizeTerminalAccessoryLayoutPreference({ + version: 1, + visibleBuiltInIds: [], + knownBuiltInIds: oldBuiltInIds + }).visibleBuiltInIds + ).toEqual(['space']) + }) + + it('keeps Space hidden after that choice is persisted with current known ids', () => { + const visibleBuiltInIds = getDefaultTerminalAccessoryBuiltInIds().filter((id) => id !== 'space') + const persisted = createTerminalAccessoryLayoutPreference(visibleBuiltInIds) + + expect(persisted.knownBuiltInIds).toContain('space') + expect(normalizeTerminalAccessoryLayoutPreference(persisted).visibleBuiltInIds).not.toContain( + 'space' + ) + }) + it('keeps hidden known defaults hidden, including an all-hidden layout', () => { const current = ['escape', 'tab', 'enter']