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:
@@ -9,6 +9,7 @@ import {
|
|||||||
terminalKill,
|
terminalKill,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
import { useZoomLevel } from "@/hooks/use-appearance"
|
import { useZoomLevel } from "@/hooks/use-appearance"
|
||||||
|
import { detectPlatform } from "@/hooks/use-platform"
|
||||||
import type { TerminalEvent } from "@/lib/types"
|
import type { TerminalEvent } from "@/lib/types"
|
||||||
import type { ITheme, Terminal as XTermTerminal } from "@xterm/xterm"
|
import type { ITheme, Terminal as XTermTerminal } from "@xterm/xterm"
|
||||||
|
|
||||||
@@ -162,6 +163,43 @@ export function TerminalView({
|
|||||||
fitAddonRef.current = fitAddon
|
fitAddonRef.current = fitAddon
|
||||||
termRef.current = term
|
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
|
// Watch <html> class changes for theme switching
|
||||||
const themeObserver = new MutationObserver(() => {
|
const themeObserver = new MutationObserver(() => {
|
||||||
term.options.theme = getTerminalTheme(containerRef.current)
|
term.options.theme = getTerminalTheme(containerRef.current)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react"
|
|||||||
|
|
||||||
export type PlatformType = "macos" | "windows" | "linux" | "unknown"
|
export type PlatformType = "macos" | "windows" | "linux" | "unknown"
|
||||||
|
|
||||||
function detectPlatform(): PlatformType {
|
export function detectPlatform(): PlatformType {
|
||||||
if (typeof navigator === "undefined") return "unknown"
|
if (typeof navigator === "undefined") return "unknown"
|
||||||
|
|
||||||
const platform = navigator.platform.toLowerCase()
|
const platform = navigator.platform.toLowerCase()
|
||||||
|
|||||||
Reference in New Issue
Block a user