// React port of ../../attach.ts's `buildKeyBar` — the Termux-style special-key // bar for the soft keyboard (Esc/Tab/Ctrl/Alt/cursor keys/symbols it hides // behind a page switch). Ctrl/Alt are one-shot sticky modifiers: arming one // folds into the very next key, tapped here or typed on the soft keyboard, // then disarms. import { useCallback, useRef, useState } from "react"; import { applyModsToChar, cursorSeq, tildeSeq } from "../../keys.js"; type Modifier = "ctrl" | "alt"; /** CSI final bytes: arrows A-D, Home `H`, End `F`. */ type CursorFinal = "A" | "B" | "C" | "D" | "H" | "F"; type KeyDef = | { label: string; mod: Modifier } | { label: string; cursor: CursorFinal } | { label: string; tilde: number } | { label: string; char: string } | { label: string; seq: string }; /** * Row-major into an 8-column / 2-row grid (#7): * * Esc Tab Home ↑ End PgUp - / * Ctrl Alt ← ↓ → PgDn | ~ */ const KEY_DEFS: KeyDef[] = [ { label: "Esc", seq: "\x1b" }, { label: "Tab", seq: "\t" }, { label: "Home", cursor: "H" }, { label: "↑", cursor: "A" }, { label: "End", cursor: "F" }, { label: "PgUp", tilde: 5 }, { label: "-", char: "-" }, { label: "/", char: "/" }, { label: "Ctrl", mod: "ctrl" }, { label: "Alt", mod: "alt" }, { label: "←", cursor: "D" }, { label: "↓", cursor: "B" }, { label: "→", cursor: "C" }, { label: "PgDn", tilde: 6 }, { label: "|", char: "|" }, { label: "~", char: "~" }, ]; /** * Pending Ctrl/Alt state plus `consume`, which folds them into a soft-keyboard * character before it reaches the PTY — used by the terminal's own `onData` * handler, not by this component's own button clicks. */ export function useKeyBarModifiers() { const armedRef = useRef<{ ctrl: boolean; alt: boolean }>({ ctrl: false, alt: false }); const [, setTick] = useState(0); const rerender = useCallback(() => setTick((n) => n + 1), []); const setMod = useCallback( (mod: Modifier, on: boolean) => { armedRef.current[mod] = on; rerender(); }, [rerender], ); const clearMods = useCallback(() => { if (!armedRef.current.ctrl && !armedRef.current.alt) return; armedRef.current = { ctrl: false, alt: false }; rerender(); }, [rerender]); const consume = useCallback((data: string): string => { const { ctrl, alt } = armedRef.current; if ((ctrl || alt) && data.length === 1) { const out = applyModsToChar(data, ctrl, alt); armedRef.current = { ctrl: false, alt: false }; return out; } return data; }, []); return { armed: armedRef.current, setMod, clearMods, consume }; } export interface KeyBarProps { armed: { ctrl: boolean; alt: boolean }; onToggleMod(mod: Modifier): void; onClearMods(): void; onSend(data: string): void; } export function KeyBar({ armed, onToggleMod, onClearMods, onSend }: KeyBarProps) { return (
{KEY_DEFS.map((def) => { if ("mod" in def) { return ( ); } const bytes = () => { if ("cursor" in def) return cursorSeq(def.cursor, armed.ctrl, armed.alt); if ("tilde" in def) return tildeSeq(def.tilde, armed.ctrl, armed.alt); if ("char" in def) return applyModsToChar(def.char, armed.ctrl, armed.alt); return def.seq; }; return ( ); })}
); }