Spaces:
Running
Running
| 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 { createScopedLogger } from '~/utils/logger'; | |
| import { getTerminalTheme } from './theme'; | |
| const logger = createScopedLogger('Terminal'); | |
| export interface TerminalRef { | |
| reloadStyles: () => void; | |
| } | |
| export interface TerminalProps { | |
| className?: string; | |
| theme: Theme; | |
| readonly?: boolean; | |
| id: string; | |
| onTerminalReady?: (terminal: XTerm) => void; | |
| onTerminalResize?: (cols: number, rows: number) => void; | |
| } | |
| export const Terminal = memo( | |
| forwardRef<TerminalRef, TerminalProps>( | |
| ({ className, theme, readonly, id, onTerminalReady, onTerminalResize }, ref) => { | |
| const terminalElementRef = useRef<HTMLDivElement>(null); | |
| const terminalRef = useRef<XTerm>(); | |
| 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: 12, | |
| 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); | |
| logger.debug(`Attach [${id}]`); | |
| 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 <div className={className} ref={terminalElementRef} />; | |
| }, | |
| ), | |
| ); | |