Deletes the vanilla shell and mounts App.tsx on #root. main.ts and the 23 views it drove are gone; App.tsx composes the ported components — sidebar, multi-view, command bar, spawn modal, help and restart overlays, daemon link, drawer — around the catalog and layout stores. The pure parts of four deleted modules moved next to the components that still use them: the spawn-form model and spawnSimilar into components/spawn, the transcript's markdown/format helpers and the diff into components/structured, the help overlay's grouping into components/dialogs. Document-level event names now live in uiEvents.ts. Deduplicated two pairs the migration left side by side: the sidebar uses dialogs/KillDialog and common/ContextMenu. It also listens for the command bar's kill/rename events, which nothing had picked up since catalog.ts went away. styles.css stays as the token and base layer, with `#app` renamed to `#root` and the pane-body rules re-keyed off the classes the React panes actually carry. Closes #253
135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
// 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 (
|
|
<div
|
|
role="toolbar"
|
|
aria-label="Special keys"
|
|
className="keybar-touch flex-wrap gap-1 border-t border-white/10 bg-black/20 p-1"
|
|
>
|
|
{KEY_DEFS.map((def) => {
|
|
if ("mod" in def) {
|
|
return (
|
|
<button
|
|
key={def.label}
|
|
type="button"
|
|
aria-pressed={armed[def.mod]}
|
|
aria-label={`${def.label} modifier`}
|
|
className="min-w-8 rounded px-2 py-1 text-xs aria-pressed:bg-emerald-500/40"
|
|
onClick={() => onToggleMod(def.mod)}
|
|
>
|
|
{def.label}
|
|
</button>
|
|
);
|
|
}
|
|
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 (
|
|
<button
|
|
key={def.label}
|
|
type="button"
|
|
className="min-w-8 rounded px-2 py-1 text-xs"
|
|
onClick={() => {
|
|
onSend(bytes());
|
|
onClearMods();
|
|
}}
|
|
>
|
|
{def.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|