1// Thin React binding over Terminal + TerminalView. Behavior lives in the
2// headless modules; this file only manages lifecycle.
3
4import { useEffect, useRef, type CSSProperties, type HTMLAttributes } from "react";
5
6import { ansiToHtml } from "./ansi.ts";
7import type { GhosttyModule } from "./ghostty/module.ts";
8import { mountTerminal, type MountTerminalOptions, type TerminalHandle } from "./mount.ts";
9
10export interface AnsiTextProps extends Omit<
11 HTMLAttributes<HTMLSpanElement>,
12 "children" | "dangerouslySetInnerHTML"
13> {
14 text: string;
15}
16
17/** Static ANSI text with no terminal viewport, input, or scroll behavior. */
18export function AnsiText({ text, ...props }: AnsiTextProps) {
19 return <span {...props} dangerouslySetInnerHTML={{ __html: ansiToHtml(text) }} />;
20}
21
22export interface TerminalPaneProps extends MountTerminalOptions {
23 module: GhosttyModule;
24 /** @deprecated Use `scrollbar`; retained for existing consumers. */
25 initialScrollbar?: boolean;
26 /** Receives the mounted handle and may return caller cleanup for unmount/remount. */
27 onReady?: (handle: TerminalHandle) => void | (() => void);
28 className?: string;
29 style?: CSSProperties;
30}
31
32export function TerminalPane(props: TerminalPaneProps) {
33 const hostRef = useRef<HTMLDivElement | null>(null);
34 const propsRef = useRef(props);
35 propsRef.current = props;
36
37 useEffect(() => {
38 const host = hostRef.current;
39 if (host === null) return;
40 const p = propsRef.current;
41 const handle = mountTerminal(host, p.module, {
42 ...p,
43 onData: (bytes) => propsRef.current.onData?.(bytes),
44 onBeforeKey: (event) => propsRef.current.onBeforeKey?.(event) ?? true,
45 onResize: (size) => propsRef.current.onResize?.(size),
46 onTitleChanged: (title) => propsRef.current.onTitleChanged?.(title),
47 onPwdChanged: (pwd) => propsRef.current.onPwdChanged?.(pwd),
48 onBell: () => propsRef.current.onBell?.(),
49 onShellMark: (mark) => propsRef.current.onShellMark?.(mark),
50 onCommandStart: (command) => propsRef.current.onCommandStart?.(command),
51 onCommandEnd: (command) => propsRef.current.onCommandEnd?.(command),
52 onClipboardWrite: (write) => propsRef.current.onClipboardWrite?.(write),
53 ...(p.scrollbar !== undefined || p.initialScrollbar !== undefined
54 ? { scrollbar: p.scrollbar ?? p.initialScrollbar }
55 : {}),
56 });
57 let cleanup: void | (() => void);
58 try {
59 cleanup = p.onReady?.(handle);
60 } catch (error) {
61 handle.dispose();
62 throw error;
63 }
64 return () => {
65 try {
66 cleanup?.();
67 } finally {
68 handle.dispose();
69 }
70 };
71 }, [props.module]);
72
73 return (
74 <div
75 className={`clo-term-mount${props.className === undefined ? "" : ` ${props.className}`}`}
76 style={props.style}
77 >
78 <div ref={hostRef} style={{ width: "100%", height: "100%" }} />
79 </div>
80 );
81}