Eingebautes Terminal (xterm.js + portable-pty) und Start/Beenden-Button
- 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
This commit is contained in:
@@ -29,9 +29,18 @@ async function refresh() {
|
||||
}
|
||||
}
|
||||
|
||||
async function start(project: Project) {
|
||||
async function stop(project: Project) {
|
||||
try {
|
||||
await invoke("start_project", { project: project.name });
|
||||
await invoke("stop_project", { project: project.name });
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function openTerminal(project: Project) {
|
||||
try {
|
||||
await invoke("open_terminal", { project: project.name });
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
@@ -112,9 +121,8 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button @click="start(p)">
|
||||
{{ p.running ? "Aktivieren" : "Starten" }}
|
||||
</button>
|
||||
<button v-if="p.running" class="stop" @click="stop(p)">Beenden</button>
|
||||
<button v-else class="start" @click="openTerminal(p)">Starten</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
+19
-1
@@ -95,13 +95,31 @@ td small {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #555;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dot.on {
|
||||
background: #3ecf6a;
|
||||
}
|
||||
|
||||
button.start {
|
||||
background: #2f9e4f;
|
||||
border-color: #2f9e4f;
|
||||
}
|
||||
|
||||
button.start:hover {
|
||||
background: #37b85c;
|
||||
}
|
||||
|
||||
button.stop {
|
||||
background: #c0392b;
|
||||
border-color: #c0392b;
|
||||
}
|
||||
|
||||
button.stop:hover {
|
||||
background: #d64434;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff6b6b;
|
||||
white-space: pre-wrap;
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user