diff --git a/bun.lock b/bun.lock index ceb74ff..5af5b0e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,8 @@ "": { "name": "clarc", "dependencies": { + "@xterm/addon-serialize": "^0.14.0", + "@xterm/headless": "^6.0.0", "bun-pty": "^0.4.8", }, "devDependencies": { @@ -39,6 +41,10 @@ "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], + "@xterm/addon-serialize": ["@xterm/addon-serialize@0.14.0", "", {}, "sha512-uteyTU1EkrQa2Ux6P/uFl2fzmXI46jy5uoQMKEOM0fKTyiW7cSn0WrFenHm5vO5uEXX/GpwW/FgILvv3r0WbkA=="], + + "@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="], + "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], diff --git a/package.json b/package.json index e94aaae..dfce470 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "typescript": "^5" }, "dependencies": { + "@xterm/addon-serialize": "^0.14.0", + "@xterm/headless": "^6.0.0", "bun-pty": "^0.4.8" } } diff --git a/src/server.ts b/src/server.ts index be49575..1342e95 100644 --- a/src/server.ts +++ b/src/server.ts @@ -494,7 +494,9 @@ const server = Bun.serve({ // Determine if new tail is an incomplete control sequence and split const [body, carry] = splitAnsiCarryover(combined); if (carry) { - console.debug(`Session ${sessionId}: ANSI carryover detected (${carry.length} bytes)`); + console.debug( + `Session ${sessionId}: ANSI carryover detected (${carry.length} bytes)`, + ); ansiCarryovers.set(sessionId, carry); } else if (prevCarry) { console.debug(`Session ${sessionId}: ANSI carryover resolved`); diff --git a/src/terminal.ts b/src/terminal.ts new file mode 100644 index 0000000..05a5b89 --- /dev/null +++ b/src/terminal.ts @@ -0,0 +1,45 @@ +import { SerializeAddon } from "@xterm/addon-serialize"; +import { Terminal } from "@xterm/headless"; + +export interface TerminalSession { + terminal: Terminal; + serialize: SerializeAddon; +} + +/** + * Create a new headless terminal emulator instance + * @param cols - Terminal width in columns + * @param rows - Terminal height in rows + * @returns Terminal session with emulator and serialization addon + */ +export function createTerminal(cols: number, rows: number): TerminalSession { + const terminal = new Terminal({ + cols, + rows, + scrollback: 1000, // Keep 1000 lines of scrollback + allowProposedApi: true, + }); + + const serialize = new SerializeAddon(); + terminal.loadAddon(serialize); + + return { terminal, serialize }; +} + +/** + * Serialize terminal screen buffer as HTML + */ +export function serializeAsHTML(session: TerminalSession): string { + return session.serialize.serializeAsHTML({ + onlySelection: false, + includeGlobalBackground: true, + }); +} + +/** + * Clean up terminal resources + */ +export function disposeTerminal(session: TerminalSession): void { + session.serialize.dispose(); + session.terminal.dispose(); +}