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
182 lines
No EOL
6.9 KiB
JavaScript
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
|