91 lines
2.2 KiB
TypeScript
91 lines
2.2 KiB
TypeScript
import '@xterm/xterm/css/xterm.css';
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
import { api } from '@renderer/api';
|
|
import { FitAddon } from '@xterm/addon-fit';
|
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
|
import { Terminal } from '@xterm/xterm';
|
|
|
|
interface TerminalLogPanelProps {
|
|
/** Raw output chunks (with ANSI codes) to render */
|
|
chunks: string[];
|
|
/** CSS class for container */
|
|
className?: string;
|
|
}
|
|
|
|
export const TerminalLogPanel = ({
|
|
chunks,
|
|
className,
|
|
}: TerminalLogPanelProps): React.JSX.Element => {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const termRef = useRef<Terminal | null>(null);
|
|
const writtenRef = useRef(0);
|
|
|
|
// Create xterm instance once
|
|
useEffect(() => {
|
|
const container = containerRef.current;
|
|
if (!container) return;
|
|
|
|
const term = new Terminal({
|
|
cursorBlink: false,
|
|
disableStdin: true,
|
|
fontSize: 12,
|
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
scrollback: 200,
|
|
theme: {
|
|
background: '#141416',
|
|
foreground: '#fafafa',
|
|
cursor: 'transparent',
|
|
},
|
|
});
|
|
|
|
const fitAddon = new FitAddon();
|
|
term.loadAddon(fitAddon);
|
|
|
|
const webLinksAddon = new WebLinksAddon((_event, uri) => {
|
|
void api.openExternal(uri);
|
|
});
|
|
term.loadAddon(webLinksAddon);
|
|
|
|
term.open(container);
|
|
|
|
const rafId = requestAnimationFrame(() => fitAddon.fit());
|
|
|
|
const observer = new ResizeObserver(() => fitAddon.fit());
|
|
observer.observe(container);
|
|
|
|
termRef.current = term;
|
|
writtenRef.current = 0;
|
|
|
|
return () => {
|
|
cancelAnimationFrame(rafId);
|
|
observer.disconnect();
|
|
term.dispose();
|
|
termRef.current = null;
|
|
writtenRef.current = 0;
|
|
};
|
|
}, []);
|
|
|
|
// Write new chunks incrementally
|
|
useEffect(() => {
|
|
const term = termRef.current;
|
|
if (!term) return;
|
|
|
|
for (let i = writtenRef.current; i < chunks.length; i++) {
|
|
term.write(chunks[i]);
|
|
}
|
|
writtenRef.current = chunks.length;
|
|
}, [chunks]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={`mt-2 overflow-hidden rounded border ${className ?? ''}`}
|
|
style={{
|
|
borderColor: 'var(--color-border)',
|
|
height: '120px',
|
|
}}
|
|
/>
|
|
);
|
|
};
|