f7473797a9
- Terminal-Fenster als eigener Prozess (--terminal <projekt>) mit eigener PTY, startet claude-sync im Projektordner - Projektliste: ein Button pro Zeile — grün Starten (open_terminal) bzw. rot Beenden (stop_project, SIGTERM auf exakte PID / Ghostty-Quit) - is_running erfasst Ghostty oder laufenden Terminal-Prozess - Status-Kreis ohne Prozess unsichtbar
118 lines
3.7 KiB
TypeScript
118 lines
3.7 KiB
TypeScript
import { Terminal } from "@xterm/xterm";
|
|
import { FitAddon } from "@xterm/addon-fit";
|
|
import { WebglAddon } from "@xterm/addon-webgl";
|
|
import { LigaturesAddon } from "@xterm/addon-ligatures";
|
|
import "@xterm/xterm/css/xterm.css";
|
|
import "@fontsource/jetbrains-mono/400.css";
|
|
import "@fontsource/jetbrains-mono/500.css";
|
|
import "@fontsource/jetbrains-mono/700.css";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
|
|
|
// Debug-Instrumentierung: Fehler auf der Seite anzeigen und ins Dev-Log spiegeln.
|
|
function showError(msg: string) {
|
|
const el = document.createElement("pre");
|
|
el.style.cssText =
|
|
"color:#f38ba8;padding:16px;font:12px Menlo,monospace;white-space:pre-wrap";
|
|
el.textContent = msg;
|
|
document.body.appendChild(el);
|
|
invoke("term_log", { msg });
|
|
}
|
|
window.addEventListener("error", (e) =>
|
|
showError(`error: ${e.message}\n${e.error?.stack ?? ""}`),
|
|
);
|
|
window.addEventListener("unhandledrejection", (e) =>
|
|
showError(`rejection: ${e.reason}\n${e.reason?.stack ?? ""}`),
|
|
);
|
|
|
|
const project = new URLSearchParams(location.search).get("project")!;
|
|
const win = getCurrentWebviewWindow();
|
|
|
|
document.getElementById("project-name")!.textContent = project;
|
|
|
|
// Catppuccin Mocha
|
|
const theme = {
|
|
background: "#1e1e2e",
|
|
foreground: "#cdd6f4",
|
|
cursor: "#f5e0dc",
|
|
cursorAccent: "#1e1e2e",
|
|
selectionBackground: "#585b7080",
|
|
black: "#45475a",
|
|
red: "#f38ba8",
|
|
green: "#a6e3a1",
|
|
yellow: "#f9e2af",
|
|
blue: "#89b4fa",
|
|
magenta: "#f5c2e7",
|
|
cyan: "#94e2d5",
|
|
white: "#bac2de",
|
|
brightBlack: "#585b70",
|
|
brightRed: "#f38ba8",
|
|
brightGreen: "#a6e3a1",
|
|
brightYellow: "#f9e2af",
|
|
brightBlue: "#89b4fa",
|
|
brightMagenta: "#f5c2e7",
|
|
brightCyan: "#94e2d5",
|
|
brightWhite: "#a6adc8",
|
|
};
|
|
|
|
const term = new Terminal({
|
|
// Ligatur-Addon nutzt die Character-Joiner-API (Proposed API)
|
|
allowProposedApi: true,
|
|
fontFamily: '"JetBrains Mono", Menlo, monospace',
|
|
fontSize: 13,
|
|
fontWeightBold: "600",
|
|
lineHeight: 1.25,
|
|
letterSpacing: 0,
|
|
cursorBlink: true,
|
|
cursorStyle: "bar",
|
|
scrollback: 10000,
|
|
theme,
|
|
});
|
|
const fit = new FitAddon();
|
|
term.loadAddon(fit);
|
|
|
|
// Shift+Enter als CSI-u-Sequenz senden (Zeilenumbruch in Claude Code,
|
|
// auch bei nicht-leerer Zeile). Neben keydown muss auch keypress unter-
|
|
// drückt werden, sonst sendet xterm.js zusätzlich \r und submittet damit.
|
|
term.attachCustomKeyEventHandler((e) => {
|
|
if (e.key === "Enter" && e.shiftKey) {
|
|
if (e.type === "keydown") invoke("term_write", { data: "\x1b[13;2u" });
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Font muss geladen sein, bevor WebGL den Glyphen-Atlas aufbaut.
|
|
await document.fonts.load('13px "JetBrains Mono"');
|
|
term.open(document.getElementById("term")!);
|
|
// Reihenfolge laut Addon-Doku: Ligaturen vor WebGL aktivieren.
|
|
term.loadAddon(new LigaturesAddon());
|
|
term.loadAddon(new WebglAddon());
|
|
fit.fit();
|
|
|
|
function b64ToBytes(b64: string): Uint8Array {
|
|
const bin = atob(b64);
|
|
const bytes = new Uint8Array(bin.length);
|
|
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
return bytes;
|
|
}
|
|
|
|
await win.listen<string>("pty-output", (e) => term.write(b64ToBytes(e.payload)));
|
|
await win.listen("pty-exit", () => term.write("\r\n\x1b[90m[Prozess beendet]\x1b[0m\r\n"));
|
|
|
|
term.onData((data) => invoke("term_write", { data }));
|
|
term.onResize(({ rows, cols }) => invoke("term_resize", { rows, cols }));
|
|
window.addEventListener("resize", () => fit.fit());
|
|
|
|
await invoke("term_start", { project, rows: term.rows, cols: term.cols });
|
|
term.focus();
|
|
|
|
// Pool-Chip im Titelbalken
|
|
interface Project {
|
|
name: string;
|
|
pool: string | null;
|
|
}
|
|
const projects = await invoke<Project[]>("list_projects");
|
|
const pool = projects.find((p) => p.name === project)?.pool;
|
|
if (pool) document.getElementById("pool")!.textContent = pool;
|