| import { useEffect, useRef } from "react"; |
| import { Terminal } from "xterm"; |
| import { FitAddon } from "xterm-addon-fit"; |
| import "xterm/css/xterm.css"; |
| |
| interface XTermProps { |
| logs: string; |
| } |
| |
| const scrollbarHack = ` |
| .xterm-viewport { |
| scrollbar-width: auto; |
| scrollbar-color: #a0a0a0 #f1f5f9; |
| } |
| .xterm-viewport::-webkit-scrollbar { |
| width: 8px; |
| } |
| .xterm-viewport::-webkit-scrollbar-track { |
| background: #f1f5f9; |
| } |
| .xterm-viewport::-webkit-scrollbar-thumb { |
| background-color: #a0a0a0; |
| border-radius: 4px; |
| border: 2px solid #f1f5f9; |
| } |
| `; |
| |
| export function XTerm({ logs }: XTermProps) { |
| const termRef = useRef<HTMLDivElement>(null); |
| const termInstance = useRef<Terminal | null>(null); |
| const fitAddon = useRef<FitAddon | null>(null); |
| const prevLogs = useRef<string>(""); |
| |
| useEffect(() => { |
| if (termRef.current && !termInstance.current) { |
| const term = new Terminal({ |
| disableStdin: true, |
| convertEol: true, |
| fontFamily: `'JetBrains Mono', monospace`, |
| fontSize: 12, |
| theme: { |
| background: "#f1f5f9", // bg-muted color |
| foreground: "#020817", // text-foreground color |
| cursor: "#f1f5f9", |
| selectionBackground: "#bfdbfe", // blue-200 |
| selectionInactiveBackground: "#e2e8f0", // slate-200 |
| }, |
| }); |
| const addon = new FitAddon(); |
| fitAddon.current = addon; |
| term.loadAddon(addon); |
| term.open(termRef.current); |
| addon.fit(); |
| termInstance.current = term; |
| |
| const resizeObserver = new ResizeObserver(() => { |
| fitAddon.current?.fit(); |
| }); |
| resizeObserver.observe(termRef.current); |
| |
| return () => { |
| resizeObserver.disconnect(); |
| term.dispose(); |
| }; |
| } |
| }, []); |
| |
| useEffect(() => { |
| if (termInstance.current) { |
| if (logs === "") { |
| termInstance.current.clear(); |
| prevLogs.current = ""; |
| return; |
| } |
| |
| const buffer = termInstance.current.buffer.active; |
| const wasAtBottom = buffer.viewportY + termInstance.current.rows >= buffer.length; |
| |
| const newLogContent = logs.substring(prevLogs.current.length); |
| prevLogs.current = logs; |
| |
| if (newLogContent) { |
| termInstance.current.write(newLogContent, () => { |
| if (wasAtBottom) { |
| termInstance.current?.scrollToBottom(); |
| } |
| }); |
| } |
| } |
| }, [logs]); |
| |
| return ( |
| <> |
| <style>{scrollbarHack}</style> |
| <div ref={termRef} className="h-full w-full" /> |
| </> |
| ); |
| } |