| 1 | import type { GhosttyModule } from "./ghostty/module.ts"; |
| 2 | import { Terminal, type TerminalEvents, type TerminalOptions } from "./ghostty/terminal.ts"; |
| 3 | import { TerminalView, type TerminalViewOptions } from "./renderer/dom.ts"; |
| 4 | |
| 5 | export interface MountTerminalOptions |
| 6 | extends |
| 7 | Partial<TerminalOptions>, |
| 8 | Omit<TerminalEvents, "onOutput">, |
| 9 | Omit<TerminalViewOptions, "onData"> { |
| 10 | /** Keystrokes, paste, mouse reports, and terminal query responses. Forward to the pty. */ |
| 11 | onData?: (bytes: Uint8Array) => void; |
| 12 | /** Called after a fitted container changes the terminal's cell geometry. */ |
| 13 | onResize?: (size: { cols: number; rows: number }) => void; |
| 14 | } |
| 15 | |
| 16 | export interface TerminalHandle { |
| 17 | readonly terminal: Terminal; |
| 18 | readonly view: TerminalView; |
| 19 | /** Initial font loading, measurement, fit, and resize reporting have settled. */ |
| 20 | readonly ready: Promise<void>; |
| 21 | /** Feed pty output into the emulator and schedule its DOM frame. */ |
| 22 | write(data: string | Uint8Array): void; |
| 23 | /** Release the renderer lease and every native terminal resource. Idempotent. */ |
| 24 | dispose(): void; |
| 25 | } |
| 26 | |
| 27 | /** |
| 28 | * Mount the complete DOM terminal. Transport stays caller-owned: pass pty output |
| 29 | * to `write` and forward `onData` bytes back to the pty. |
| 30 | */ |
| 31 | export function mountTerminal( |
| 32 | container: HTMLElement, |
| 33 | module: GhosttyModule, |
| 34 | options: MountTerminalOptions = {}, |
| 35 | ): TerminalHandle { |
| 36 | const terminal = new Terminal( |
| 37 | module, |
| 38 | { |
| 39 | cols: options.cols ?? 80, |
| 40 | rows: options.rows ?? 24, |
| 41 | ...(options.maxScrollbackBytes !== undefined |
| 42 | ? { maxScrollbackBytes: options.maxScrollbackBytes } |
| 43 | : {}), |
| 44 | ...(options.shellIntegration !== undefined |
| 45 | ? { shellIntegration: options.shellIntegration } |
| 46 | : {}), |
| 47 | ...(options.commandLimit !== undefined ? { commandLimit: options.commandLimit } : {}), |
| 48 | }, |
| 49 | { |
| 50 | onOutput: (bytes) => options.onData?.(bytes), |
| 51 | onBell: () => options.onBell?.(), |
| 52 | onTitleChanged: (title) => options.onTitleChanged?.(title), |
| 53 | onPwdChanged: (pwd) => options.onPwdChanged?.(pwd), |
| 54 | onShellMark: (mark) => options.onShellMark?.(mark), |
| 55 | onCommandStart: (command) => options.onCommandStart?.(command), |
| 56 | onCommandEnd: (command) => options.onCommandEnd?.(command), |
| 57 | onClipboardWrite: (write) => options.onClipboardWrite?.(write), |
| 58 | }, |
| 59 | ); |
| 60 | let view: TerminalView | null = null; |
| 61 | try { |
| 62 | view = new TerminalView(container, terminal, { |
| 63 | onData: (bytes) => options.onData?.(bytes), |
| 64 | ...(options.onBeforeKey ? { onBeforeKey: options.onBeforeKey } : {}), |
| 65 | ...(options.visible !== undefined ? { visible: options.visible } : {}), |
| 66 | ...(options.scrollback !== undefined ? { scrollback: options.scrollback } : {}), |
| 67 | ...(options.scrollbar !== undefined ? { scrollbar: options.scrollbar } : {}), |
| 68 | ...(options.links !== undefined ? { links: options.links } : {}), |
| 69 | ...(options.mouse !== undefined ? { mouse: options.mouse } : {}), |
| 70 | ...(options.stickyScroll !== undefined ? { stickyScroll: options.stickyScroll } : {}), |
| 71 | ...(options.predict !== undefined ? { predict: options.predict } : {}), |
| 72 | }); |
| 73 | view.observeResize((size) => options.onResize?.(size)); |
| 74 | } catch (error) { |
| 75 | view?.dispose(); |
| 76 | terminal.free(); |
| 77 | throw error; |
| 78 | } |
| 79 | |
| 80 | const mountedView = view; |
| 81 | let disposed = false; |
| 82 | return { |
| 83 | terminal, |
| 84 | view: mountedView, |
| 85 | ready: mountedView.ready, |
| 86 | write: (data) => { |
| 87 | let failed = false; |
| 88 | let writeError: unknown; |
| 89 | try { |
| 90 | terminal.write(data); |
| 91 | } catch (error) { |
| 92 | failed = true; |
| 93 | writeError = error; |
| 94 | } |
| 95 | try { |
| 96 | if (!disposed) mountedView.scheduleFrame(); |
| 97 | } catch (error) { |
| 98 | if (!failed) throw error; |
| 99 | } |
| 100 | if (failed) throw writeError; |
| 101 | }, |
| 102 | dispose: () => { |
| 103 | if (disposed) return; |
| 104 | disposed = true; |
| 105 | mountedView.dispose(); |
| 106 | terminal.free(); |
| 107 | }, |
| 108 | }; |
| 109 | } |