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/600.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(); // Fensterhintergrund beim Öffnen kommt aus terminal.rs (theme_background); // dort nicht gepflegte Themes fallen auf Mocha zurück (nur kurzer Moment // bis die Webview lädt). const THEMES: Record< string, { header: string; border: string; xterm: Record } > = { mocha: { header: "#181825", border: "#313244", xterm: { 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", }, }, dracula: { header: "#21222c", border: "#44475a", xterm: { background: "#282a36", foreground: "#f8f8f2", cursor: "#f8f8f2", cursorAccent: "#282a36", selectionBackground: "#44475a80", black: "#21222c", red: "#ff5555", green: "#50fa7b", yellow: "#f1fa8c", blue: "#bd93f9", magenta: "#ff79c6", cyan: "#8be9fd", white: "#f8f8f2", brightBlack: "#6272a4", brightRed: "#ff6e6e", brightGreen: "#69ff94", brightYellow: "#ffffa5", brightBlue: "#d6acff", brightMagenta: "#ff92df", brightCyan: "#a4ffff", brightWhite: "#ffffff", }, }, "solarized-dark": { header: "#073642", border: "#586e75", xterm: { background: "#002b36", foreground: "#839496", cursor: "#839496", cursorAccent: "#002b36", selectionBackground: "#07364280", black: "#073642", red: "#dc322f", green: "#859900", yellow: "#b58900", blue: "#268bd2", magenta: "#d33682", cyan: "#2aa198", white: "#eee8d5", brightBlack: "#586e75", brightRed: "#cb4b16", brightGreen: "#859900", brightYellow: "#657b83", brightBlue: "#839496", brightMagenta: "#6c71c4", brightCyan: "#93a1a1", brightWhite: "#fdf6e3", }, }, gruvbox: { header: "#1d2021", border: "#3c3836", xterm: { background: "#282828", foreground: "#ebdbb2", cursor: "#ebdbb2", cursorAccent: "#282828", selectionBackground: "#3c383680", black: "#282828", red: "#cc241d", green: "#98971a", yellow: "#d79921", blue: "#458588", magenta: "#b16286", cyan: "#689d6a", white: "#a89984", brightBlack: "#928374", brightRed: "#fb4934", brightGreen: "#b8bb26", brightYellow: "#fabd2f", brightBlue: "#83a598", brightMagenta: "#d3869b", brightCyan: "#8ec07c", brightWhite: "#ebdbb2", }, }, "one-dark": { header: "#21252b", border: "#3e4451", xterm: { background: "#282c34", foreground: "#abb2bf", cursor: "#abb2bf", cursorAccent: "#282c34", selectionBackground: "#3e445180", black: "#282c34", red: "#e06c75", green: "#98c379", yellow: "#e5c07b", blue: "#61afef", magenta: "#c678dd", cyan: "#56b6c2", white: "#abb2bf", brightBlack: "#5c6370", brightRed: "#e06c75", brightGreen: "#98c379", brightYellow: "#e5c07b", brightBlue: "#61afef", brightMagenta: "#c678dd", brightCyan: "#56b6c2", brightWhite: "#ffffff", }, }, "solarized-light": { header: "#eee8d5", border: "#93a1a1", xterm: { background: "#fdf6e3", foreground: "#657b83", cursor: "#657b83", cursorAccent: "#fdf6e3", selectionBackground: "#eee8d5b0", black: "#073642", red: "#dc322f", green: "#859900", yellow: "#b58900", blue: "#268bd2", magenta: "#d33682", cyan: "#2aa198", white: "#eee8d5", brightBlack: "#002b36", brightRed: "#cb4b16", brightGreen: "#586e75", brightYellow: "#657b83", brightBlue: "#839496", brightMagenta: "#6c71c4", brightCyan: "#93a1a1", brightWhite: "#fdf6e3", }, }, "catppuccin-latte": { header: "#e6e9ef", border: "#ccd0da", xterm: { background: "#eff1f5", foreground: "#4c4f69", cursor: "#dc8a78", cursorAccent: "#eff1f5", selectionBackground: "#acb0be80", black: "#5c5f77", red: "#d20f39", green: "#40a02b", yellow: "#df8e1d", blue: "#1e66f5", magenta: "#ea76cb", cyan: "#179299", white: "#acb0be", brightBlack: "#6c6f85", brightRed: "#d20f39", brightGreen: "#40a02b", brightYellow: "#df8e1d", brightBlue: "#1e66f5", brightMagenta: "#ea76cb", brightCyan: "#179299", brightWhite: "#bcc0cc", }, }, "one-light": { header: "#eaeaeb", border: "#d4d4d4", xterm: { background: "#fafafa", foreground: "#383a42", cursor: "#526eff", cursorAccent: "#fafafa", selectionBackground: "#e5e5e6b0", black: "#383a42", red: "#e45649", green: "#50a14f", yellow: "#c18401", blue: "#4078f2", magenta: "#a626a4", cyan: "#0184bc", white: "#a0a1a7", brightBlack: "#696c77", brightRed: "#e45649", brightGreen: "#50a14f", brightYellow: "#c18401", brightBlue: "#4078f2", brightMagenta: "#a626a4", brightCyan: "#0184bc", brightWhite: "#ffffff", }, }, nord: { header: "#3b4252", border: "#4c566a", xterm: { background: "#2e3440", foreground: "#d8dee9", cursor: "#d8dee9", cursorAccent: "#2e3440", selectionBackground: "#434c5e80", black: "#3b4252", red: "#bf616a", green: "#a3be8c", yellow: "#ebcb8b", blue: "#81a1c1", magenta: "#b48ead", cyan: "#88c0d0", white: "#e5e9f0", brightBlack: "#4c566a", brightRed: "#bf616a", brightGreen: "#a3be8c", brightYellow: "#ebcb8b", brightBlue: "#81a1c1", brightMagenta: "#b48ead", brightCyan: "#8fbcbb", brightWhite: "#eceff4", }, }, "tokyo-night": { header: "#16161e", border: "#292e42", xterm: { background: "#1a1b26", foreground: "#c0caf5", cursor: "#c0caf5", cursorAccent: "#1a1b26", selectionBackground: "#28345780", black: "#15161e", red: "#f7768e", green: "#9ece6a", yellow: "#e0af68", blue: "#7aa2f7", magenta: "#bb9af7", cyan: "#7dcfff", white: "#a9b1d6", brightBlack: "#414868", brightRed: "#f7768e", brightGreen: "#9ece6a", brightYellow: "#e0af68", brightBlue: "#7aa2f7", brightMagenta: "#bb9af7", brightCyan: "#7dcfff", brightWhite: "#c0caf5", }, }, monokai: { header: "#1e1f1c", border: "#49483e", xterm: { background: "#272822", foreground: "#f8f8f2", cursor: "#f8f8f2", cursorAccent: "#272822", selectionBackground: "#49483e80", black: "#272822", red: "#f92672", green: "#a6e22e", yellow: "#e6db74", blue: "#66d9ef", magenta: "#ae81ff", cyan: "#a1efe4", white: "#f8f8f2", brightBlack: "#75715e", brightRed: "#f92672", brightGreen: "#a6e22e", brightYellow: "#f4bf75", brightBlue: "#66d9ef", brightMagenta: "#ae81ff", brightCyan: "#a1efe4", brightWhite: "#f9f8f5", }, }, "rose-pine": { header: "#1f1d2e", border: "#26233a", xterm: { background: "#191724", foreground: "#e0def4", cursor: "#e0def4", cursorAccent: "#191724", selectionBackground: "#403d5280", black: "#26233a", red: "#eb6f92", green: "#31748f", yellow: "#f6c177", blue: "#9ccfd8", magenta: "#c4a7e7", cyan: "#ebbcba", white: "#e0def4", brightBlack: "#6e6a86", brightRed: "#eb6f92", brightGreen: "#31748f", brightYellow: "#f6c177", brightBlue: "#9ccfd8", brightMagenta: "#c4a7e7", brightCyan: "#ebbcba", brightWhite: "#e0def4", }, }, everforest: { header: "#232a2e", border: "#475258", xterm: { background: "#2d353b", foreground: "#d3c6aa", cursor: "#d3c6aa", cursorAccent: "#2d353b", selectionBackground: "#47525880", black: "#475258", red: "#e67e80", green: "#a7c080", yellow: "#dbbc7f", blue: "#7fbbb3", magenta: "#d699b6", cyan: "#83c092", white: "#d3c6aa", brightBlack: "#859289", brightRed: "#e67e80", brightGreen: "#a7c080", brightYellow: "#dbbc7f", brightBlue: "#7fbbb3", brightMagenta: "#d699b6", brightCyan: "#83c092", brightWhite: "#d3c6aa", }, }, }; interface Project { name: string; pool: string | null; terminal: { theme: string | null; icon: string | null; title: string | null }; } const projects = await invoke("list_projects"); const cfg = projects.find((p) => p.name === project); if (cfg?.pool) document.getElementById("pool")!.textContent = cfg.pool; document.getElementById("project-name")!.textContent = cfg?.terminal.title ?? project; const picked = THEMES[cfg?.terminal.theme ?? "mocha"]; const theme = picked.xterm; // Seitenfarben ans Theme anpassen (Defaults in terminal.html sind Mocha). document.documentElement.style.background = theme.background; document.body.style.background = theme.background; const header = document.querySelector("header") as HTMLElement; header.style.background = picked.header; header.style.borderBottomColor = picked.border; header.style.color = theme.foreground; 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.0, 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 — // beide verwendeten Gewichte (400 normal, 600 bold). await document.fonts.load('13px "JetBrains Mono"'); await document.fonts.load('600 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("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();