import { useRef, useEffect, useCallback, useImperativeHandle } from 'react'; import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import { debounce, canUseDOM } from '@patternfly/react-core'; export interface XTermProps { /** The number of columns to resize to */ cols?: number; /** The number of rows to resize to */ rows?: number; fontFamily?: string; fontSize?: number; /** Terminal title has been changed. */ onTitleChanged?: (title: string) => void; /** Data to be sent from terminal to backend; (data) => {} */ onData?: (e: string) => void; /** A reference object to attach to the xterm */ innerRef?: React.RefObject; } export const XTerm: React.FunctionComponent = ({ cols = 80, rows = 25, fontFamily, fontSize, onTitleChanged, onData, innerRef }) => { const terminalRef = useRef(null); const ref = useRef(null); useImperativeHandle(innerRef, () => ({ focusTerminal() { if (terminalRef.current) { terminalRef.current.focus(); } }, /** * Backend sent data. * * @param {string} data String content to be writen into the terminal */ onDataReceived: (data: string) => { if (terminalRef.current) { terminalRef.current.write(data); } }, /** * Backend closed connection. * * @param {string} reason String error to be written into the terminal */ onConnectionClosed: (reason: string) => { if (terminalRef.current) { terminalRef.current.write(`\x1b[31m${reason || 'disconnected'}\x1b[m\r\n`); terminalRef.current.refresh(terminalRef.current.rows, terminalRef.current.rows); // start to end row } } })); const onBeforeUnload = useCallback((event: any) => { // Firefox requires this when the page is in an iframe event.preventDefault(); // see "an almost cross-browser solution" at // https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload event.returnValue = ''; return ''; }, []); const onFocusIn = () => { window.addEventListener('beforeunload', onBeforeUnload); }; const onFocusOut = useCallback(() => { window.removeEventListener('beforeunload', onBeforeUnload); }, [onBeforeUnload]); useEffect(() => { const container = ref.current; if (!container) { return; } const fitAddon = new FitAddon(); terminalRef.current = new Terminal({ cols, rows, cursorBlink: true, fontFamily, fontSize, screenReaderMode: true }); const onWindowResize = () => { const geometry = fitAddon.proposeDimensions(); if (geometry && terminalRef.current) { terminalRef.current.resize(geometry.cols, geometry.rows); } }; if (onData) { terminalRef.current.onData(onData); } if (onTitleChanged) { terminalRef.current.onTitleChange(onTitleChanged); } terminalRef.current.loadAddon(fitAddon); let resizeListener: (() => void) | null = null; // Defer open so the container is laid out and xterm's renderer has valid dimensions // before Viewport._innerRefresh runs (avoids "Cannot read properties of undefined (reading 'dimensions')") const rafId = requestAnimationFrame(() => { if (!container.isConnected || !terminalRef.current) { return; } terminalRef.current.open(container); resizeListener = !rows ? debounce(onWindowResize, 100) : null; if (resizeListener && canUseDOM) { window.addEventListener('resize', resizeListener); } if (!rows) { onWindowResize(); } terminalRef.current?.focus(); }); return () => { cancelAnimationFrame(rafId); if (resizeListener && canUseDOM) { window.removeEventListener('resize', resizeListener); } terminalRef.current?.dispose(); terminalRef.current = null; onFocusOut(); }; }, [cols, fontFamily, fontSize, onData, onFocusOut, onTitleChanged, rows]); // ensure react never reuses this div by keying it with the terminal widget // Workaround for xtermjs/xterm.js#3172 return (
); }; XTerm.displayName = 'XTerm';