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