feat(terminal): add word/line cursor shortcuts for shell line-editing

Intercept keyboard events via xterm's custom key handler and emit
readline/zle escape sequences so bindings work regardless of terminfo:

- Alt/Option + Left/Right: word-wise cursor move
- Alt/Option + Backspace: delete previous word
- macOS Cmd + Left/Right: jump to line start/end
- macOS Cmd + Backspace: clear to line start

Uses `e.code` to stay correct on dead-key layouts, skips IME
composition, and excludes AltGr (ctrl+alt) on Windows/Linux.
This commit is contained in:
xintaofei
2026-04-19 09:27:28 +08:00
parent 0fa2a0895f
commit ce289e64c5
2 changed files with 39 additions and 1 deletions

View File

@@ -9,6 +9,7 @@ import {
terminalKill,
} from "@/lib/api"
import { useZoomLevel } from "@/hooks/use-appearance"
import { detectPlatform } from "@/hooks/use-platform"
import type { TerminalEvent } from "@/lib/types"
import type { ITheme, Terminal as XTermTerminal } from "@xterm/xterm"
@@ -162,6 +163,43 @@ export function TerminalView({
fitAddonRef.current = fitAddon
termRef.current = term
// Shell line-editing shortcuts. Sends readline/zle bindings so they
// work regardless of terminfo.
// Alt/Option + ←/→ / Backspace: word-level moves & delete
// macOS Cmd + ←/→ / Backspace : line-level moves & clear
// Uses `e.code` (physical key) to be robust against dead-key layouts on
// macOS where Option can turn some keys into `key: "Dead"`.
// AltGr on Windows/Linux is reported as ctrlKey+altKey and is excluded
// by the `!ctrlKey` guard below.
const isMac = detectPlatform() === "macos"
term.attachCustomKeyEventHandler((e) => {
if (e.type !== "keydown") return true
// Skip during IME composition to avoid corrupting candidate buffer.
if (e.isComposing) return true
const { code, altKey, metaKey, ctrlKey, shiftKey } = e
const writeSeq = (seq: string) => {
terminalWrite(terminalId, seq).catch(() => {})
e.preventDefault()
return false
}
if (altKey && !ctrlKey && !metaKey && !shiftKey) {
if (code === "ArrowLeft") return writeSeq("\x1bb")
if (code === "ArrowRight") return writeSeq("\x1bf")
if (code === "Backspace") return writeSeq("\x1b\x7f")
}
if (isMac && metaKey && !altKey && !ctrlKey && !shiftKey) {
if (code === "ArrowLeft") return writeSeq("\x01")
if (code === "ArrowRight") return writeSeq("\x05")
if (code === "Backspace") return writeSeq("\x15")
}
return true
})
// Watch <html> class changes for theme switching
const themeObserver = new MutationObserver(() => {
term.options.theme = getTerminalTheme(containerRef.current)

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from "react"
export type PlatformType = "macos" | "windows" | "linux" | "unknown"
function detectPlatform(): PlatformType {
export function detectPlatform(): PlatformType {
if (typeof navigator === "undefined") return "unknown"
const platform = navigator.platform.toLowerCase()