diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx index e6f8e50..406f67a 100644 --- a/AppImage/components/proxmox-dashboard.tsx +++ b/AppImage/components/proxmox-dashboard.tsx @@ -632,8 +632,10 @@ export function ProxmoxDashboard() { - - + +
+ +
diff --git a/AppImage/components/terminal-panel.tsx b/AppImage/components/terminal-panel.tsx index f34c65c..9bbe476 100644 --- a/AppImage/components/terminal-panel.tsx +++ b/AppImage/components/terminal-panel.tsx @@ -3,9 +3,13 @@ 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 { @@ -25,7 +29,7 @@ function getWebSocketUrl(): string { } } -export const TerminalPanel: React.FC = ({ websocketUrl }) => { +export const TerminalPanel: React.FC = ({ websocketUrl, onClose }) => { const containerRef = useRef(null) const termRef = useRef(null) const fitAddonRef = useRef(null) @@ -33,6 +37,17 @@ export const TerminalPanel: React.FC = ({ websocketUrl }) => 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 @@ -48,11 +63,35 @@ export const TerminalPanel: React.FC = ({ websocketUrl }) => console.log("[v0] TerminalPanel: Initializing terminal") const term = new Terminal({ - fontFamily: "monospace", - fontSize: 13, + 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() @@ -73,6 +112,7 @@ export const TerminalPanel: React.FC = ({ websocketUrl }) => ws.onopen = () => { console.log("[v0] TerminalPanel: WebSocket connected") + setIsConnected(true) term.writeln("\x1b[32mConnected to ProxMenux terminal.\x1b[0m") } @@ -82,11 +122,13 @@ export const TerminalPanel: React.FC = ({ websocketUrl }) => 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") } @@ -115,40 +157,41 @@ export const TerminalPanel: React.FC = ({ websocketUrl }) => .catch((error) => { console.error("[v0] TerminalPanel: Failed to load xterm:", error) }) - }, [websocketUrl]) + }, [websocketUrl, isMobile]) - const sendSequence = (seq: string) => { + 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") + sendSequence("\x1b[A", "↑") break case "DOWN": - sendSequence("\x1b[B") + sendSequence("\x1b[B", "↓") break case "RIGHT": - sendSequence("\x1b[C") + sendSequence("\x1b[C", "→") break case "LEFT": - sendSequence("\x1b[D") + sendSequence("\x1b[D", "←") break case "ESC": - sendSequence("\x1b") + sendSequence("\x1b", "ESC") break case "TAB": - sendSequence("\t") - break - case "ENTER": - sendSequence("\r") + sendSequence("\t", "TAB") break case "CTRL_C": - sendSequence("\x03") + sendSequence("\x03", "CTRL+C") break default: break @@ -199,44 +242,182 @@ export const TerminalPanel: React.FC = ({ websocketUrl }) => } } + 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...
)}
-
- handleKeyButton("ESC")} /> - handleKeyButton("TAB")} /> - handleKeyButton("UP")} /> - handleKeyButton("DOWN")} /> - handleKeyButton("LEFT")} /> - handleKeyButton("RIGHT")} /> - handleKeyButton("ENTER")} /> - handleKeyButton("CTRL_C")} /> + {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} + /> + +
+
+ )} + +
+ + + + + + +
) } - -type TouchKeyProps = { - label: string - onClick: () => void -} - -const TouchKey: React.FC = ({ label, onClick }) => ( - -) diff --git a/AppImage/scripts/flask_terminal_routes.py b/AppImage/scripts/flask_terminal_routes.py index de0a532..11ea7d7 100644 --- a/AppImage/scripts/flask_terminal_routes.py +++ b/AppImage/scripts/flask_terminal_routes.py @@ -68,6 +68,7 @@ def terminal_websocket(ws): stdout=slave_fd, stderr=slave_fd, preexec_fn=os.setsid, + cwd='/', env=dict(os.environ, TERM='xterm-256color', PS1='\\u@\\h:\\w\\$ ') )