new additions : What's new

 Rust PTY backend (portable-pty)
     - pty_spawn / pty_write / pty_resize / pty_kill commands
     - Output streamed via pty:data Tauri events (base64-encoded bytes)
     - Spawns /bin/bash (falls back to /bin/bash), starts in /home/code
   xterm.js terminal card (full ANSI + true color)
   Toolbar (top-left) with + Note / + Terminal
   Shared useDragHandle hook (notes + terminals)
   Terminal font-size scales with canvas zoom → crisp at any zoom
   Terminal auto-refits when card resizes or zoom changes
This commit is contained in:
Haapy
2026-05-14 22:22:18 +00:00
parent eaeb4c2d92
commit c3552d08b9
192 changed files with 44595 additions and 132 deletions

View File

@@ -0,0 +1,130 @@
import { useEffect, useRef, useState } from "react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import "@xterm/xterm/css/xterm.css";
import type { TerminalCard } from "../types";
import { useViewport } from "../viewport";
import { useDragHandle } from "../useDragHandle";
import { pty, base64ToBytes, stringToBase64 } from "../../ipc";
const BASE_FONT_SIZE = 13;
interface Props {
card: TerminalCard;
onUpdate: (patch: Partial<TerminalCard>) => void;
}
export function TerminalCardView({ card, onUpdate }: Props) {
const vp = useViewport();
const drag = useDragHandle(card, (p) => onUpdate(p));
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<Terminal | null>(null);
const fitRef = useRef<FitAddon | null>(null);
const ptyIdRef = useRef<string>(card.ptyId);
const [ready, setReady] = useState(false);
useEffect(() => {
let unlistenData: (() => void) | undefined;
let unlistenExit: (() => void) | undefined;
let disposed = false;
const term = new Terminal({
fontSize: BASE_FONT_SIZE * vp.scale,
fontFamily: 'Menlo, "Cascadia Code", Consolas, monospace',
theme: {
background: "#1d1d23",
foreground: "#e8e8ec",
cursor: "#e8e8ec",
},
cursorBlink: true,
allowProposedApi: true,
});
const fit = new FitAddon();
term.loadAddon(fit);
term.open(containerRef.current!);
fit.fit();
termRef.current = term;
fitRef.current = fit;
(async () => {
let id = ptyIdRef.current;
if (!id) {
id = await pty.spawn({ cols: term.cols, rows: term.rows });
if (disposed) {
await pty.kill(id);
return;
}
ptyIdRef.current = id;
onUpdate({ ptyId: id });
}
unlistenData = await pty.onData((e) => {
if (e.id !== id) return;
term.write(base64ToBytes(e.data));
});
unlistenExit = await pty.onExit((e) => {
if (e.id !== id) return;
term.write("\r\n\x1b[2m[process exited]\x1b[0m\r\n");
});
term.onData((data) => {
pty.write(id!, stringToBase64(data)).catch((err) => console.error("pty_write", err));
});
term.onResize(({ cols, rows }) => {
pty.resize(id!, cols, rows).catch(() => {});
});
setReady(true);
})();
return () => {
disposed = true;
unlistenData?.();
unlistenExit?.();
term.dispose();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Update xterm font size when canvas zoom changes so text stays crisp.
useEffect(() => {
if (!ready) return;
const term = termRef.current;
const fit = fitRef.current;
if (!term || !fit) return;
term.options.fontSize = BASE_FONT_SIZE * vp.scale;
fit.fit();
}, [vp.scale, ready]);
// Refit on card resize.
useEffect(() => {
if (!ready) return;
fitRef.current?.fit();
}, [card.width, card.height, ready]);
return (
<div
className="card terminal-card"
style={
{
left: vp.x + card.x * vp.scale,
top: vp.y + card.y * vp.scale,
width: card.width * vp.scale,
height: card.height * vp.scale,
zIndex: card.z,
"--scale": vp.scale,
} as React.CSSProperties
}
>
<div className="card-header" {...drag}>
terminal
</div>
<div
className="card-body terminal-body"
ref={containerRef}
onPointerDown={(e) => e.stopPropagation()}
onWheel={(e) => e.stopPropagation()}
/>
</div>
);
}