/** * Tauri adapter for {@link TerminalGateway} (L3). The single place that uses a * {@link Channel} for the high-frequency PTY byte stream and `invoke()` for the * control commands. Components reach it exclusively through the port. * * Flow (ARCHITECTURE ยง2 "Tauri Channels"): * - `openTerminal` creates a `Channel`, passes it to the * `open_terminal` command, and forwards every chunk to `onData` as a * `Uint8Array`. The backend pumps PTY output into that channel via the * `PtyBridge`. * - keystrokes go out through `write_terminal`, resize through * `resize_terminal`, teardown through `close_terminal`. * * Commands and payload keys are camelCase, matching the backend DTO convention. */ import { Channel, invoke } from "@tauri-apps/api/core"; import type { OpenTerminalOptions, ReattachResult, TerminalGateway, TerminalHandle, } from "@/ports"; /** Wire shape returned by the `open_terminal` command. */ interface OpenTerminalResponse { sessionId: string; cwd: string; rows: number; cols: number; } /** Wire shape returned by the `reattach_terminal` command. */ interface ReattachResponse { sessionId: string; scrollback: number[]; } /** * Builds a {@link TerminalHandle} over a session and its local output * {@link Channel}. `detach` stops the channel from delivering further bytes (the * view is gone) without touching the backend PTY; `close` kills the PTY. * * Shared by `openTerminal` and `reattach` so both produce identical handles. */ export function makeTerminalHandle( sessionId: string, channel: Channel, ): TerminalHandle { return { sessionId, async write(data: Uint8Array): Promise { await invoke("write_terminal", { request: { sessionId, data: Array.from(data) }, }); }, async resize(rows: number, cols: number): Promise { await invoke("resize_terminal", { request: { sessionId, rows, cols }, }); }, detach(): void { // Drop the local subscription: the backend PTY keeps running, but this // view stops receiving output. A later `reattach` re-wires a fresh channel. channel.onmessage = () => {}; }, async close(): Promise { await invoke("close_terminal", { sessionId }); }, }; } export class TauriTerminalGateway implements TerminalGateway { async openTerminal( options: OpenTerminalOptions, onData: (bytes: Uint8Array) => void, ): Promise { // Per-session output channel. The backend serialises chunks as byte arrays. const channel = new Channel(); channel.onmessage = (chunk) => onData(Uint8Array.from(chunk)); const res = await invoke("open_terminal", { request: { cwd: options.cwd, rows: options.rows, cols: options.cols }, onOutput: channel, }); return makeTerminalHandle(res.sessionId, channel); } async reattach( sessionId: string, onData: (bytes: Uint8Array) => void, ): Promise { const channel = new Channel(); channel.onmessage = (chunk) => onData(Uint8Array.from(chunk)); const res = await invoke("reattach_terminal", { sessionId, onOutput: channel, }); return { handle: makeTerminalHandle(res.sessionId, channel), scrollback: Uint8Array.from(res.scrollback), }; } }