| 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!; |
|
|
| |
| 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} />; |
| }, |
| ), |
| ); |
|
|