react-terminal-ui/dist/Terminal.js
autocommit 1ce3936ff0 chore: initial package split from monorepo
Package: @lilith/react-terminal-ui
Split from: lilith/ui.git or lilith/build.git
Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main
2026-04-20 01:11:11 -07:00

182 lines
No EOL
6.9 KiB
JavaScript

import { jsx as _jsx } from "react/jsx-runtime";
import { useEffect, useRef, useState, useCallback } from 'react';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { CanvasAddon } from 'xterm-addon-canvas';
import { WebglAddon } from 'xterm-addon-webgl';
import styled from 'styled-components';
import { applyThemeToTerminal } from './core/themeAdapter';
import { EffectManager } from './effects/EffectManager';
const TerminalContainer = styled.div `
position: relative;
height: ${props => typeof props.$height === 'number' ? `${props.$height}px` : props.$height || '500px'};
width: ${props => typeof props.$width === 'number' ? `${props.$width}px` : props.$width || '100%'};
background: #000;
overflow: hidden;
.xterm {
height: 100%;
width: 100%;
}
.xterm-viewport {
overflow-y: auto;
}
`;
const EffectLayer = styled.div `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 10;
`;
export const Terminal = ({ theme, renderer = 'canvas', height, width, initialCommand, prompt = '$ ', onReady, onCommand, onData, enableEffects = true, className, style }) => {
const containerRef = useRef(null);
const terminalRef = useRef(null);
const effectManagerRef = useRef(null);
const [isReady, setIsReady] = useState(false);
// Command buffer for line processing
const commandBuffer = useRef('');
const initializeTerminal = useCallback(() => {
if (!containerRef.current || terminalRef.current)
return;
// Create terminal instance
const term = new XTerm({
allowProposedApi: true,
cursorBlink: theme?.cursor?.blink ?? true,
cursorStyle: theme?.cursor?.style ?? 'block',
fontFamily: theme?.font?.family ?? 'monospace',
fontSize: theme?.font?.size ?? 14,
fontWeight: theme?.font?.weight ?? 400,
letterSpacing: theme?.font?.letterSpacing ?? 0,
lineHeight: theme?.font?.lineHeight ?? 1.2,
scrollback: 1000,
theme: theme ? {
background: theme.colors.background,
foreground: theme.colors.foreground,
cursor: theme.colors.cursor,
cursorAccent: theme.colors.cursorAccent,
selectionBackground: theme.colors.selection,
selectionForeground: theme.colors.foreground,
black: theme.colors.black,
red: theme.colors.red,
green: theme.colors.green,
yellow: theme.colors.yellow,
blue: theme.colors.blue,
magenta: theme.colors.magenta,
cyan: theme.colors.cyan,
white: theme.colors.white,
brightBlack: theme.colors.brightBlack,
brightRed: theme.colors.brightRed,
brightGreen: theme.colors.brightGreen,
brightYellow: theme.colors.brightYellow,
brightBlue: theme.colors.brightBlue,
brightMagenta: theme.colors.brightMagenta,
brightCyan: theme.colors.brightCyan,
brightWhite: theme.colors.brightWhite,
} : undefined
});
// Initialize addons
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
const webLinksAddon = new WebLinksAddon();
term.loadAddon(webLinksAddon);
// Load renderer addon based on preference
if (renderer === 'webgl') {
try {
const webglAddon = new WebglAddon();
term.loadAddon(webglAddon);
}
catch (e) {
console.warn('WebGL renderer failed, falling back to canvas', e);
const canvasAddon = new CanvasAddon();
term.loadAddon(canvasAddon);
}
}
else if (renderer === 'canvas') {
const canvasAddon = new CanvasAddon();
term.loadAddon(canvasAddon);
}
// Open terminal in container
term.open(containerRef.current);
// Fit terminal to container
fitAddon.fit();
// Handle resize
const handleResize = () => fitAddon.fit();
window.addEventListener('resize', handleResize);
// Apply theme if provided
if (theme) {
applyThemeToTerminal(term, theme);
}
// Initialize effects if enabled
if (enableEffects && theme?.effects && containerRef.current) {
effectManagerRef.current = new EffectManager({
container: containerRef.current,
theme: theme,
performance: 'medium'
});
}
// Write initial prompt
term.write(prompt);
// Handle input
term.onData((data) => {
if (onData)
onData(data);
// Handle special keys
if (data === '\r') { // Enter
const command = commandBuffer.current;
commandBuffer.current = '';
term.write('\r\n');
if (onCommand && command.trim()) {
onCommand(command);
}
term.write(prompt);
}
else if (data === '\u007F') { // Backspace
if (commandBuffer.current.length > 0) {
commandBuffer.current = commandBuffer.current.slice(0, -1);
term.write('\b \b');
}
}
else if (data === '\u0003') { // Ctrl+C
commandBuffer.current = '';
term.write('^C\r\n' + prompt);
}
else {
// Regular character
commandBuffer.current += data;
term.write(data);
}
});
// Execute initial command if provided
if (initialCommand) {
term.write(initialCommand + '\r\n');
if (onCommand)
onCommand(initialCommand);
}
// Store reference and mark as ready
terminalRef.current = term;
setIsReady(true);
if (onReady) {
onReady(term);
}
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
if (effectManagerRef.current) {
effectManagerRef.current.dispose();
}
term.dispose();
};
}, [theme, renderer, prompt, initialCommand, onReady, onCommand, onData, enableEffects]);
useEffect(() => {
const cleanup = initializeTerminal();
return cleanup;
}, [initializeTerminal]);
return (_jsx(TerminalContainer, { ref: containerRef, "$height": height, "$width": width, className: className, style: style, children: enableEffects && isReady && _jsx(EffectLayer, { id: "terminal-effects" }) }));
};
export default Terminal;
//# sourceMappingURL=Terminal.js.map