diff --git a/AppImage/components/terminal-panel.tsx b/AppImage/components/terminal-panel.tsx index 3846c18..8485ece 100644 --- a/AppImage/components/terminal-panel.tsx +++ b/AppImage/components/terminal-panel.tsx @@ -3,16 +3,17 @@ import type React from "react" import { useEffect, useRef, useState, useCallback } from "react" import { API_PORT } from "@/lib/api-config" -import { Activity, Trash2, X, Search, Send, Lightbulb, Terminal, Plus, Split, Grid2X2 } from "lucide-react" +import { Activity, Trash2, X, Search, Send, Lightbulb, Terminal, Plus } from "lucide-react" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import type { CheatSheetResult } from "@/lib/cheat-sheet-result" // Declare CheatSheetResult here type TerminalPanelProps = { - websocketUrl?: string - onClose?: () => void + terminals: TerminalInstance[] + onAddTerminal: (terminal: TerminalInstance) => void + onRemoveTerminal: (id: string) => void + onClearTerminal: (id: string) => void } interface TerminalInstance { @@ -119,10 +120,13 @@ const proxmoxCommands = [ { cmd: "clear", desc: "Clear terminal screen" }, ] -export const TerminalPanel: React.FC = ({ websocketUrl, onClose }) => { - const [terminals, setTerminals] = useState([]) +export const TerminalPanel: React.FC = ({ + terminals, + onAddTerminal, + onRemoveTerminal, + onClearTerminal, +}) => { const [activeTerminalId, setActiveTerminalId] = useState("") - const [layout, setLayout] = useState<"single" | "vertical" | "horizontal" | "grid">("single") const [isMobile, setIsMobile] = useState(false) const [searchModalOpen, setSearchModalOpen] = useState(false) @@ -133,11 +137,11 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl const [searchResults, setSearchResults] = useState([]) const [useOnline, setUseOnline] = useState(true) - const containerRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}) + const terminalRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}) - const setContainerRef = useCallback( + const setTerminalRef = useCallback( (id: string) => (el: HTMLDivElement | null) => { - containerRefs.current[id] = el + terminalRefs.current[id] = el }, [], ) @@ -151,7 +155,13 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl useEffect(() => { if (terminals.length === 0) { - addNewTerminal() + onAddTerminal({ + id: `terminal-${Date.now()}`, + title: `Terminal 1`, + term: null, + ws: null, + isConnected: false, + }) } }, []) @@ -224,187 +234,6 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl return () => clearTimeout(debounce) }, [searchQuery]) - const addNewTerminal = () => { - if (terminals.length >= 4) return - - const newId = `terminal-${Date.now()}` - // containerRefs.current[newId] = useRef(null) // No longer needed - - setTerminals((prev) => [ - ...prev, - { - id: newId, - title: `Terminal ${prev.length + 1}`, - term: null, - ws: null, - isConnected: false, - // containerRef: containerRefs.current[newId], // No longer needed - }, - ]) - setActiveTerminalId(newId) - } - - const closeTerminal = (id: string) => { - const terminal = terminals.find((t) => t.id === id) - if (terminal) { - if (terminal.ws) { - terminal.ws.close() - } - if (terminal.term) { - terminal.term.dispose() - } - } - - setTerminals((prev) => { - const filtered = prev.filter((t) => t.id !== id) - if (filtered.length > 0 && activeTerminalId === id) { - setActiveTerminalId(filtered[0].id) - } - return filtered - }) - - delete containerRefs.current[id] // Clean up the ref - } - - useEffect(() => { - terminals.forEach((terminal) => { - const container = containerRefs.current[terminal.id] - if (!terminal.term && container) { - initializeTerminal(terminal, container) - } - }) - }, [terminals, isMobile]) - - const initializeTerminal = async (terminal: TerminalInstance, container: HTMLDivElement) => { - const [Terminal, FitAddon] = await Promise.all([ - import("xterm").then((mod) => mod.Terminal), - import("xterm-addon-fit").then((mod) => mod.FitAddon), - import("xterm/css/xterm.css"), - ]).then(([Terminal, FitAddon]) => [Terminal, FitAddon]) - - const term = new Terminal({ - fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace", - fontSize: isMobile ? 11 : 13, - cursorBlink: true, - scrollback: 2000, - disableStdin: false, - theme: { - background: "#000000", - foreground: "#ffffff", - cursor: "#ffffff", - cursorAccent: "#000000", - black: "#2e3436", - red: "#cc0000", - green: "#4e9a06", - yellow: "#c4a000", - blue: "#3465a4", - magenta: "#75507b", - cyan: "#06989a", - white: "#d3d7cf", - brightBlack: "#555753", - brightRed: "#ef2929", - brightGreen: "#8ae234", - brightYellow: "#fce94f", - brightBlue: "#729fcf", - brightMagenta: "#ad7fa8", - brightCyan: "#34e2e2", - brightWhite: "#eeeeec", - }, - }) - - const fitAddon = new FitAddon() - term.loadAddon(fitAddon) - - term.open(container) - - const performResize = () => { - const xtermViewport = container.querySelector(".xterm-viewport") as HTMLElement - const xtermScreen = container.querySelector(".xterm-screen") as HTMLElement - if (xtermViewport) { - xtermViewport.style.padding = "0" - xtermViewport.style.width = "100%" - xtermViewport.style.height = "100%" - } - if (xtermScreen) { - xtermScreen.style.padding = "0" - } - - fitAddon.fit() - const cols = term.cols - const rows = term.rows - console.log( - `[v0] Terminal ${terminal.id} resized: ${cols}x${rows} (container: ${container.offsetWidth}x${container.offsetHeight})`, - ) - - // Send resize to backend via HTTP - const apiUrl = getApiUrl() - fetch(`${apiUrl}/api/terminal/${terminal.id}/resize`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ cols, rows }), - }) - .then((res) => res.json()) - .then((data) => { - console.log(`[v0] Backend PTY resized:`, data) - }) - .catch((err) => { - console.error(`[v0] Error resizing backend PTY:`, err) - }) - } - - setTimeout(() => performResize(), 100) - setTimeout(() => performResize(), 300) - setTimeout(() => performResize(), 600) - - const wsUrl = websocketUrl || getWebSocketUrl() - const ws = new WebSocket(wsUrl) - - ws.onopen = () => { - setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: true, term, ws } : t))) - term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m") - - setTimeout(() => performResize(), 200) - setTimeout(() => performResize(), 500) - } - - ws.onmessage = (event) => { - term.write(event.data) - } - - ws.onerror = (error) => { - console.error("[v0] TerminalPanel: WebSocket error:", error) - setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: false } : t))) - term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m") - } - - ws.onclose = () => { - setTerminals((prev) => prev.map((t) => (t.id === terminal.id ? { ...t, isConnected: false } : t))) - term.writeln("\r\n\x1b[33m[INFO] Connection closed\x1b[0m") - } - - term.onData((data) => { - if (ws.readyState === WebSocket.OPEN) { - ws.send(data) - } - }) - - const handleResize = () => { - try { - performResize() - } catch { - // Ignore resize errors - } - } - - window.addEventListener("resize", handleResize) - - return () => { - window.removeEventListener("resize", handleResize) - ws.close() - term.dispose() - } - } - const handleKeyButton = (key: string) => { const activeTerminal = terminals.find((t) => t.id === activeTerminalId) if (!activeTerminal || !activeTerminal.ws || activeTerminal.ws.readyState !== WebSocket.OPEN) return @@ -441,19 +270,12 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl } } - const handleClear = () => { - const activeTerminal = terminals.find((t) => t.id === activeTerminalId) - if (activeTerminal?.term) { - activeTerminal.term.clear() - } - } - const handleClose = () => { terminals.forEach((terminal) => { if (terminal.ws) terminal.ws.close() if (terminal.term) terminal.term.dispose() }) - onClose?.() + onRemoveTerminal(activeTerminalId) } const sendToActiveTerminal = (command: string) => { @@ -485,16 +307,76 @@ export const TerminalPanel: React.FC = ({ websocketUrl, onCl const getLayoutClass = () => { const count = terminals.length if (isMobile || count === 1) return "grid grid-cols-1" - if (layout === "vertical" || count === 2) return "grid grid-cols-2" - if (layout === "horizontal") return "grid grid-rows-2" - if (layout === "grid" || count >= 3) return "grid grid-cols-2 grid-rows-2" + if (count === 2) return "grid grid-cols-2" + if (count >= 3) return "grid grid-cols-2 grid-rows-2" return "grid grid-cols-1" } const activeTerminal = terminals.find((t) => t.id === activeTerminalId) + const renderTerminals = () => { + if (terminals.length === 0) { + return ( +
+

No terminals open. Click "New" to create one.

+
+ ) + } + + if (terminals.length === 1 || isMobile) { + // Single terminal mode - fill entire container + return ( +
+
+ Terminal {terminals[0].id} + +
+
(terminalRefs.current[terminals[0].id] = el)} className="flex-1 w-full bg-black" /> +
+ ) + } + + if (terminals.length === 2) { + // Split view - two terminals side by side + return ( +
+ {terminals.map((terminal) => ( +
+
+ Terminal {terminal.id} + +
+
(terminalRefs.current[terminal.id] = el)} className="flex-1 w-full bg-black" /> +
+ ))} +
+ ) + } + + // Grid view for 3+ terminals + return ( +
+ {terminals.map((terminal) => ( +
+
+ Terminal {terminal.id} + +
+
(terminalRefs.current[terminal.id] = el)} className="flex-1 w-full bg-black" /> +
+ ))} +
+ ) + } + return ( -
+