Files
spaceshell/app/src/TerminalView.tsx
T

58 lines
1.8 KiB
TypeScript

import { useEffect, useRef } from "react";
import { Terminal } from "@xterm/xterm";
import { WebglAddon } from "@xterm/addon-webgl";
import { SearchAddon } from "@xterm/addon-search";
import { attachSurface, detachSurface, sendInput, resizeSurface } from "./socketBridge";
import { registerSearch, unregisterSearch } from "./searchRegistry";
const decoder = new TextDecoder();
const encoder = new TextEncoder();
export function TerminalView({ surfaceId }: { surfaceId: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
const term = new Terminal({ fontFamily: "'JetBrains Mono Variable', 'JetBrains Mono', monospace", fontSize: 13, convertEol: false, scrollback: 10000 });
try {
term.loadAddon(new WebglAddon());
} catch {
// webgl unavailable → fall back to canvas/dom renderer silently
}
term.open(ref.current);
const search = new SearchAddon();
term.loadAddon(search);
registerSearch(surfaceId, search);
// Input → daemon.
const inputDisposable = term.onData((data) => {
void sendInput(surfaceId, encoder.encode(data));
});
let disposed = false;
// Attach: fresh xterm instance, write snapshot, then stream live output.
void attachSurface(surfaceId, (bytes) => {
if (!disposed) term.write(decoder.decode(bytes));
}).then((res) => {
if (disposed) return;
if (res.snapshot) term.write(res.snapshot);
if (res.cols && res.rows) {
term.resize(res.cols, res.rows);
void resizeSurface(surfaceId, res.cols, res.rows);
}
});
return () => {
disposed = true;
inputDisposable.dispose();
void detachSurface(surfaceId);
unregisterSearch(surfaceId);
term.dispose();
};
}, [surfaceId]);
return <div ref={ref} style={{ width: "100%", height: "100%" }} />;
}