This commit is contained in:
Brennan Benson 2026-04-21 10:44:37 +00:00 committed by GitHub
commit 41ff0304aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 107 additions and 8 deletions

View file

@ -0,0 +1,79 @@
import { describe, expect, it } from 'vitest'
import {
resolveTerminalShortcutAction,
type TerminalShortcutEvent
} from './terminal-shortcut-policy'
function event(overrides: Partial<TerminalShortcutEvent>): TerminalShortcutEvent {
return {
key: '',
code: '',
metaKey: false,
ctrlKey: false,
altKey: false,
shiftKey: false,
repeat: false,
...overrides
}
}
describe('non-mac Ctrl+Left/Right word-nav', () => {
// Windows Terminal, GNOME Terminal, and Konsole all bind Ctrl+←/→ to
// word-nav. xterm.js emits \e[1;5D / \e[1;5C which default readline doesn't
// map, so translate to \eb / \ef (same bytes as our Alt+Arrow rule).
it('translates Ctrl+←/→ on Windows/Linux to readline \\eb / \\ef', () => {
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true }),
false
)
).toEqual({ type: 'sendInput', data: '\x1bb' })
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowRight', code: 'ArrowRight', ctrlKey: true }),
false
)
).toEqual({ type: 'sendInput', data: '\x1bf' })
})
it('does not translate Ctrl+Arrow on macOS (reserved by OS)', () => {
// Mac uses Cmd+Arrow for line-nav and Option+Arrow for word-nav.
// Ctrl+Arrow is the macOS Mission Control / Spaces chord.
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true }),
true
)
).toBeNull()
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowRight', code: 'ArrowRight', ctrlKey: true }),
true
)
).toBeNull()
})
it('does not intercept Ctrl+Shift+Arrow (selection passthrough)', () => {
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true, shiftKey: true }),
false
)
).toBeNull()
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowRight', code: 'ArrowRight', ctrlKey: true, shiftKey: true }),
false
)
).toBeNull()
})
it('does not intercept Ctrl+Alt+Arrow (different chord)', () => {
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true, altKey: true }),
false
)
).toBeNull()
})
})

View file

@ -110,14 +110,6 @@ describe('resolveTerminalShortcutAction', () => {
true
)
).toBeNull()
// Non-Mac Ctrl+Arrow must pass through unchanged (readline's word-nav there).
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true }),
false
)
).toBeNull()
})
it('uses ctrl as the non-mac pane modifier but still requires shift for tab-safe chords', () => {
@ -217,6 +209,14 @@ describe('resolveTerminalShortcutAction', () => {
)
).toBeNull()
// Ctrl+Alt+Arrow (Linux workspace switching on some desktops) must pass through on non-Mac.
expect(
resolveTerminalShortcutAction(
event({ key: 'ArrowLeft', code: 'ArrowLeft', ctrlKey: true, altKey: true }),
false
)
).toBeNull()
// Regression guard: plain ArrowLeft must still pass through untouched.
expect(
resolveTerminalShortcutAction(event({ key: 'ArrowLeft', code: 'ArrowLeft' }), true)

View file

@ -176,6 +176,26 @@ export function resolveTerminalShortcutAction(
return { type: 'sendInput', data: event.key === 'ArrowLeft' ? '\x1bb' : '\x1bf' }
}
if (
!isMac &&
!event.metaKey &&
event.ctrlKey &&
!event.altKey &&
!event.shiftKey &&
(event.key === 'ArrowLeft' || event.key === 'ArrowRight')
) {
// Why: Windows Terminal, GNOME Terminal, and Konsole all bind Ctrl+←/→ for
// word navigation on Linux/Windows — but xterm.js emits \e[1;5D / \e[1;5C,
// which default readline (bash, zsh) does not bind to backward-word /
// forward-word. Translate to \eb / \ef (same bytes as our Alt+Arrow rule)
// so Ctrl+←/→ works for word-nav matching user expectations on those
// platforms without requiring a custom inputrc.
//
// Mac-gated: Ctrl+Arrow on macOS is reserved for Mission Control / Spaces
// navigation at the OS level and should never reach the app.
return { type: 'sendInput', data: event.key === 'ArrowLeft' ? '\x1bb' : '\x1bf' }
}
// Why: with macOptionIsMeta disabled (to let non-US keyboard layouts compose
// characters like @ and €), xterm.js no longer translates Option+letter into
// Esc+letter automatically. We match on event.code (physical key) rather than