blob: 4f542f1805e61b0d1d6527dfb7330daa58301cf2 [file] [log] [blame]
gio78a22882025-07-01 18:56:01 +00001import { useEffect, useRef } from "react";
2import { Terminal } from "xterm";
3import { FitAddon } from "xterm-addon-fit";
4import "xterm/css/xterm.css";
5
6interface XTermProps {
7 logs: string;
8}
9
10const scrollbarHack = `
11.xterm-viewport {
12 scrollbar-width: auto;
13 scrollbar-color: #a0a0a0 #f1f5f9;
14}
15.xterm-viewport::-webkit-scrollbar {
16 width: 8px;
17}
18.xterm-viewport::-webkit-scrollbar-track {
19 background: #f1f5f9;
20}
21.xterm-viewport::-webkit-scrollbar-thumb {
22 background-color: #a0a0a0;
23 border-radius: 4px;
24 border: 2px solid #f1f5f9;
25}
26`;
27
28export function XTerm({ logs }: XTermProps) {
29 const termRef = useRef<HTMLDivElement>(null);
30 const termInstance = useRef<Terminal | null>(null);
31 const fitAddon = useRef<FitAddon | null>(null);
32 const prevLogs = useRef<string>("");
33
34 useEffect(() => {
35 if (termRef.current && !termInstance.current) {
36 const term = new Terminal({
37 disableStdin: true,
38 convertEol: true,
39 fontFamily: `'JetBrains Mono', monospace`,
40 fontSize: 12,
41 theme: {
42 background: "#f1f5f9", // bg-muted color
43 foreground: "#020817", // text-foreground color
44 cursor: "#f1f5f9",
45 selectionBackground: "#bfdbfe", // blue-200
46 selectionInactiveBackground: "#e2e8f0", // slate-200
47 },
48 });
49 const addon = new FitAddon();
50 fitAddon.current = addon;
51 term.loadAddon(addon);
52 term.open(termRef.current);
53 addon.fit();
54 termInstance.current = term;
55
56 const resizeObserver = new ResizeObserver(() => {
57 fitAddon.current?.fit();
58 });
59 resizeObserver.observe(termRef.current);
60
61 return () => {
62 resizeObserver.disconnect();
63 term.dispose();
64 };
65 }
66 }, []);
67
68 useEffect(() => {
69 if (termInstance.current) {
70 if (logs === "") {
71 termInstance.current.clear();
72 prevLogs.current = "";
73 return;
74 }
75
76 const buffer = termInstance.current.buffer.active;
77 const wasAtBottom = buffer.viewportY + termInstance.current.rows >= buffer.length;
78
79 const newLogContent = logs.substring(prevLogs.current.length);
80 prevLogs.current = logs;
81
82 if (newLogContent) {
83 termInstance.current.write(newLogContent, () => {
84 if (wasAtBottom) {
85 termInstance.current?.scrollToBottom();
86 }
87 });
88 }
89 }
90 }, [logs]);
91
92 return (
93 <>
94 <style>{scrollbarHack}</style>
95 <div ref={termRef} className="h-full w-full" />
96 </>
97 );
98}