"use client" import type React from "react" import { useEffect, useRef, useState } from "react" import { API_PORT } from "@/lib/api-config" import { Trash2, X, Send, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Activity } from "lucide-react" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" type TerminalPanelProps = { websocketUrl?: string onClose?: () => void } function getWebSocketUrl(): string { if (typeof window === "undefined") { return "ws://localhost:8008/ws/terminal" } const { protocol, hostname, port } = window.location const isStandardPort = port === "" || port === "80" || port === "443" const wsProtocol = protocol === "https:" ? "wss:" : "ws:" if (isStandardPort) { return `${wsProtocol}//${hostname}/ws/terminal` } else { return `${wsProtocol}//${hostname}:${API_PORT}/ws/terminal` } } export const TerminalPanel: React.FC = ({ websocketUrl, onClose }) => { const containerRef = useRef(null) const termRef = useRef(null) const fitAddonRef = useRef(null) const wsRef = useRef(null) const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null) const [xtermLoaded, setXtermLoaded] = useState(false) const [isConnected, setIsConnected] = useState(false) const [mobileInput, setMobileInput] = useState("") const [lastKeyPressed, setLastKeyPressed] = useState(null) const [isMobile, setIsMobile] = useState(false) useEffect(() => { setIsMobile(window.innerWidth < 768) const handleResize = () => setIsMobile(window.innerWidth < 768) window.addEventListener("resize", handleResize) return () => window.removeEventListener("resize", handleResize) }, []) useEffect(() => { if (typeof window === "undefined") return Promise.all([ import("xterm").then((mod) => mod.Terminal), import("xterm-addon-fit").then((mod) => mod.FitAddon), import("xterm/css/xterm.css"), ]) .then(([Terminal, FitAddon]) => { if (!containerRef.current) return console.log("[v0] TerminalPanel: Initializing terminal") 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, cols: 150, rows: 30, theme: { background: "#0d1117", foreground: "#e6edf3", cursor: "#58a6ff", cursorAccent: "#0d1117", black: "#484f58", red: "#f85149", green: "#3fb950", yellow: "#d29922", blue: "#58a6ff", magenta: "#bc8cff", cyan: "#39d353", white: "#b1bac4", brightBlack: "#6e7681", brightRed: "#ff7b72", brightGreen: "#56d364", brightYellow: "#e3b341", brightBlue: "#79c0ff", brightMagenta: "#d2a8ff", brightCyan: "#56d364", brightWhite: "#f0f6fc", }, }) const fitAddon = new FitAddon() term.loadAddon(fitAddon) term.open(containerRef.current) fitAddon.fit() termRef.current = term fitAddonRef.current = fitAddon setXtermLoaded(true) const wsUrl = websocketUrl || getWebSocketUrl() console.log("[v0] TerminalPanel: Connecting to WebSocket:", wsUrl) const ws = new WebSocket(wsUrl) wsRef.current = ws ws.onopen = () => { console.log("[v0] TerminalPanel: WebSocket connected") setIsConnected(true) term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m") } ws.onmessage = (event) => { term.write(event.data) } ws.onerror = (error) => { console.error("[v0] TerminalPanel: WebSocket error:", error) setIsConnected(false) term.writeln("\r\n\x1b[31m[ERROR] WebSocket connection error\x1b[0m") } ws.onclose = () => { console.log("[v0] TerminalPanel: WebSocket closed") setIsConnected(false) 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 { fitAddon.fit() } catch { // Ignore resize errors } } window.addEventListener("resize", handleResize) return () => { console.log("[v0] TerminalPanel: Cleaning up") window.removeEventListener("resize", handleResize) ws.close() term.dispose() } }) .catch((error) => { console.error("[v0] TerminalPanel: Failed to load xterm:", error) }) }, [websocketUrl, isMobile]) const sendSequence = (seq: string, keyName?: string) => { const term = termRef.current const ws = wsRef.current if (!term || !ws || ws.readyState !== WebSocket.OPEN) return ws.send(seq) if (keyName) { setLastKeyPressed(keyName) setTimeout(() => setLastKeyPressed(null), 2000) } } const handleKeyButton = (key: string) => { switch (key) { case "UP": sendSequence("\x1b[A", "↑") break case "DOWN": sendSequence("\x1b[B", "↓") break case "RIGHT": sendSequence("\x1b[C", "→") break case "LEFT": sendSequence("\x1b[D", "←") break case "ESC": sendSequence("\x1b", "ESC") break case "TAB": sendSequence("\t", "TAB") break case "CTRL_C": sendSequence("\x03", "CTRL+C") break default: break } } const handleTouchStart = (e: React.TouchEvent) => { const touch = e.touches[0] touchStartRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now(), } } const handleTouchEnd = (e: React.TouchEvent) => { const start = touchStartRef.current if (!start) return const touch = e.changedTouches[0] const dx = touch.clientX - start.x const dy = touch.clientY - start.y const dt = Date.now() - start.time const minDistance = 30 const maxTime = 1000 touchStartRef.current = null if (dt > maxTime) return if (Math.abs(dx) < minDistance && Math.abs(dy) < minDistance) { return } if (Math.abs(dx) > Math.abs(dy)) { if (dx > 0) { handleKeyButton("RIGHT") } else { handleKeyButton("LEFT") } } else { if (dy > 0) { handleKeyButton("DOWN") } else { handleKeyButton("UP") } } } const handleClear = () => { const term = termRef.current if (!term) return term.clear() } const handleClose = () => { const ws = wsRef.current if (ws && ws.readyState === WebSocket.OPEN) { ws.close() } if (onClose) { onClose() } } const handleMobileInputSend = () => { if (!mobileInput.trim()) return const ws = wsRef.current if (ws && ws.readyState === WebSocket.OPEN) { ws.send(mobileInput) setLastKeyPressed(mobileInput) setTimeout(() => setLastKeyPressed(null), 2000) } setMobileInput("") } return (
ProxMenux Terminal
{isConnected ? "Connected" : "Disconnected"}
{!xtermLoaded && (
Initializing terminal...
)}
{isMobile && (
Mobile Input {lastKeyPressed && ( Sent: {lastKeyPressed} )}
setMobileInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleMobileInputSend()} placeholder="Type command..." className="flex-1 px-3 py-2 text-sm border border-zinc-600 rounded-md bg-zinc-800 text-zinc-100 placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-blue-500" disabled={!isConnected} />
)}
) }