import { FitAddon } from '@xterm/addon-fit'; import { WebLinksAddon } from '@xterm/addon-web-links'; import { Terminal as XTerm } from '@xterm/xterm'; import { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react'; import type { Theme } from '~/lib/stores/theme'; import { getTerminalTheme } from './theme'; import '@xterm/xterm/css/xterm.css'; export interface TerminalRef { reloadStyles: () => void; } export interface TerminalProps { className?: string; theme: Theme; readonly?: boolean; onTerminalReady?: (terminal: XTerm) => void; onTerminalResize?: (cols: number, rows: number) => void; } export const Terminal = memo( forwardRef(({ className, theme, readonly, onTerminalReady, onTerminalResize }, ref) => { const terminalElementRef = useRef(null); const terminalRef = useRef(); useEffect(() => { const element = terminalElementRef.current!; const fitAddon = new FitAddon(); const webLinksAddon = new WebLinksAddon(); const terminal = new XTerm({ cursorBlink: true, convertEol: true, disableStdin: readonly, theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}), fontSize: 13, fontFamily: 'Menlo, courier-new, courier, monospace', }); terminalRef.current = terminal; terminal.loadAddon(fitAddon); terminal.loadAddon(webLinksAddon); terminal.open(element); const resizeObserver = new ResizeObserver(() => { fitAddon.fit(); onTerminalResize?.(terminal.cols, terminal.rows); }); resizeObserver.observe(element); onTerminalReady?.(terminal); return () => { resizeObserver.disconnect(); terminal.dispose(); }; }, []); useEffect(() => { const terminal = terminalRef.current!; // we render a transparent cursor in case the terminal is readonly terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {}); terminal.options.disableStdin = readonly; }, [theme, readonly]); useImperativeHandle(ref, () => { return { reloadStyles: () => { const terminal = terminalRef.current!; terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {}); }, }; }, []); return
; }), );