wraith/frontend/src/composables/useTerminal.ts
Vantz Stockwell 8362a50460
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 1m6s
fix: SSH terminal [Object Object] — Wails v3 Events.On receives event object, not raw data
Events.On callback gets a CustomEvent with .data property, not the base64
string directly. Added multi-format extraction with debug logging for
unexpected shapes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 11:46:00 -04:00

158 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { onBeforeUnmount } from "vue";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { SearchAddon } from "@xterm/addon-search";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { Call, Events } from "@wailsio/runtime";
import "@xterm/xterm/css/xterm.css";
const SSH = "github.com/vstockwell/wraith/internal/ssh.SSHService";
/** MobaXTerm Classicinspired terminal theme colors. */
const defaultTheme = {
background: "#0d1117",
foreground: "#e0e0e0",
cursor: "#58a6ff",
cursorAccent: "#0d1117",
selectionBackground: "rgba(88, 166, 255, 0.3)",
selectionForeground: "#ffffff",
black: "#0d1117",
red: "#f85149",
green: "#3fb950",
yellow: "#e3b341",
blue: "#58a6ff",
magenta: "#bc8cff",
cyan: "#39c5cf",
white: "#e0e0e0",
brightBlack: "#484f58",
brightRed: "#ff7b72",
brightGreen: "#56d364",
brightYellow: "#e3b341",
brightBlue: "#79c0ff",
brightMagenta: "#d2a8ff",
brightCyan: "#56d4dd",
brightWhite: "#f0f6fc",
};
export interface UseTerminalReturn {
terminal: Terminal;
fitAddon: FitAddon;
mount: (container: HTMLElement) => void;
destroy: () => void;
write: (data: string) => void;
fit: () => void;
}
/**
* Composable that manages an xterm.js Terminal lifecycle.
*
* Wires bidirectional I/O:
* - User keystrokes → SSHService.Write (via Wails Call)
* - SSH stdout → xterm.js (via Wails Events, base64 encoded)
* - Terminal resize → SSHService.Resize (via Wails Call)
*/
export function useTerminal(sessionId: string): UseTerminalReturn {
const fitAddon = new FitAddon();
const searchAddon = new SearchAddon();
const webLinksAddon = new WebLinksAddon();
const terminal = new Terminal({
theme: defaultTheme,
fontFamily: "'JetBrains Mono', 'Cascadia Code', 'Fira Code', Menlo, Monaco, monospace",
fontSize: 14,
lineHeight: 1.2,
cursorBlink: true,
cursorStyle: "block",
scrollback: 10000,
allowProposedApi: true,
convertEol: true,
});
terminal.loadAddon(fitAddon);
terminal.loadAddon(searchAddon);
terminal.loadAddon(webLinksAddon);
// Forward typed data to the SSH backend
terminal.onData((data: string) => {
Call.ByName(`${SSH}.Write`, sessionId, data).catch((err: unknown) => {
console.error("SSH write error:", err);
});
});
// Forward resize events to the SSH backend
terminal.onResize((size: { cols: number; rows: number }) => {
Call.ByName(`${SSH}.Resize`, sessionId, size.cols, size.rows).catch((err: unknown) => {
console.error("SSH resize error:", err);
});
});
// Listen for SSH output events from the Go backend (base64 encoded)
let cleanupEvent: (() => void) | null = null;
let resizeObserver: ResizeObserver | null = null;
function mount(container: HTMLElement): void {
terminal.open(container);
fitAddon.fit();
// Subscribe to SSH output events for this session
// Wails v3 Events.On callback receives a CustomEvent object with .data property
cleanupEvent = Events.On(`ssh:data:${sessionId}`, (event: any) => {
// Extract the base64 string from the event
// event.data is the first argument passed to app.Event.Emit()
let b64data: string;
if (typeof event === "string") {
b64data = event;
} else if (event?.data && typeof event.data === "string") {
b64data = event.data;
} else if (Array.isArray(event?.data)) {
b64data = String(event.data[0] ?? "");
} else {
// Debug: log the actual shape so we can fix it
console.warn("ssh:data unexpected shape:", JSON.stringify(event)?.slice(0, 200));
return;
}
try {
const decoded = atob(b64data);
terminal.write(decoded);
} catch {
// Fallback: write raw if not valid base64
terminal.write(b64data);
}
});
// Auto-fit when the container resizes
resizeObserver = new ResizeObserver(() => {
fitAddon.fit();
});
resizeObserver.observe(container);
}
function destroy(): void {
if (cleanupEvent) {
cleanupEvent();
cleanupEvent = null;
}
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
terminal.dispose();
}
function write(data: string): void {
terminal.write(data);
}
function fit(): void {
fitAddon.fit();
}
onBeforeUnmount(() => {
destroy();
});
return { terminal, fitAddon, mount, destroy, write, fit };
}