diff --git a/src/backend/guacamole/guacamole-server.ts b/src/backend/guacamole/guacamole-server.ts index 1d29c8f5..92100b1d 100644 --- a/src/backend/guacamole/guacamole-server.ts +++ b/src/backend/guacamole/guacamole-server.ts @@ -36,6 +36,13 @@ const clientOptions = { guacLogger.error(args.join(" "), { operation: "guac_error" }); }, }, + // Allow width, height, and dpi to be passed as query parameters + // This allows the client to request the appropriate resolution at connection time + allowedUnencryptedConnectionSettings: { + rdp: ["width", "height", "dpi"], + vnc: ["width", "height", "dpi"], + telnet: ["width", "height"], + }, connectionDefaultSettings: { rdp: { security: "any", @@ -46,10 +53,15 @@ const clientOptions = { "disable-audio": false, "enable-drive": false, "resize-method": "display-update", + width: 1280, + height: 720, + dpi: 96, }, vnc: { "swap-red-blue": false, "cursor": "remote", + width: 1280, + height: 720, }, telnet: { "terminal-type": "xterm-256color", diff --git a/src/types/index.ts b/src/types/index.ts index 83dad26a..3e4695c2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -345,11 +345,26 @@ export interface TabContextTab { | "server" | "admin" | "file_manager" - | "user_profile"; + | "user_profile" + | "rdp" + | "vnc"; title: string; hostConfig?: SSHHost; terminalRef?: any; initialTab?: string; + connectionConfig?: { + type: "rdp" | "vnc" | "telnet"; + hostname: string; + port: number; + username?: string; + password?: string; + domain?: string; + security?: string; + "ignore-cert"?: boolean; + width?: number; + height?: number; + dpi?: number; + }; } export type SplitLayout = "2h" | "2v" | "3l" | "3r" | "3t" | "4grid"; diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx index fb015997..7f47bfff 100644 --- a/src/ui/desktop/DesktopApp.tsx +++ b/src/ui/desktop/DesktopApp.tsx @@ -155,7 +155,9 @@ function AppContent() { const showTerminalView = currentTabData?.type === "terminal" || currentTabData?.type === "server" || - currentTabData?.type === "file_manager"; + currentTabData?.type === "file_manager" || + currentTabData?.type === "rdp" || + currentTabData?.type === "vnc"; const showHome = currentTabData?.type === "home"; const showSshManager = currentTabData?.type === "ssh_manager"; const showAdmin = currentTabData?.type === "admin"; diff --git a/src/ui/desktop/apps/guacamole/GuacamoleDisplay.tsx b/src/ui/desktop/apps/guacamole/GuacamoleDisplay.tsx index cefbf9ac..34b3cc7c 100644 --- a/src/ui/desktop/apps/guacamole/GuacamoleDisplay.tsx +++ b/src/ui/desktop/apps/guacamole/GuacamoleDisplay.tsx @@ -52,7 +52,8 @@ export const GuacamoleDisplay = forwardRef(null); + const containerRef = useRef(null); // Outer container for measuring size + const displayRef = useRef(null); // Inner div for guacamole canvas const clientRef = useRef(null); const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); @@ -86,7 +87,7 @@ export const GuacamoleDisplay = forwardRef => { + const getWebSocketUrl = useCallback(async (containerWidth: number, containerHeight: number): Promise => { const jwtToken = getCookie("jwt"); if (!jwtToken) { setConnectionError("Authentication required"); @@ -118,7 +119,13 @@ export const GuacamoleDisplay = forwardRef { - if (displayRef.current) { - const containerWidth = displayRef.current.clientWidth; - const containerHeight = displayRef.current.clientHeight; - const scale = Math.min(containerWidth / width, containerHeight / height); + // Function to rescale display to fit container + const rescaleDisplay = () => { + if (!containerRef.current) return; + + const cWidth = containerRef.current.clientWidth; + const cHeight = containerRef.current.clientHeight; + const displayWidth = display.getWidth(); + const displayHeight = display.getHeight(); + + if (displayWidth > 0 && displayHeight > 0 && cWidth > 0 && cHeight > 0) { + const scale = Math.min(cWidth / displayWidth, cHeight / displayHeight); display.scale(scale); } }; - // Set up mouse input - const mouse = new Guacamole.Mouse(displayRef.current!); + // Handle display sync (when frames arrive) + display.onresize = () => { + rescaleDisplay(); + }; + + // Set up mouse input on the display element (not the container) + const mouse = new Guacamole.Mouse(displayElement); mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = (state: Guacamole.Mouse.State) => { client.sendMouseState(state); }; @@ -237,12 +267,8 @@ export const GuacamoleDisplay = forwardRef { const handleResize = () => { - if (clientRef.current && displayRef.current) { + if (clientRef.current && containerRef.current) { const display = clientRef.current.getDisplay(); - const width = displayRef.current.clientWidth; - const height = displayRef.current.clientHeight; - display.scale(Math.min(width / display.getWidth(), height / display.getHeight())); + const cWidth = containerRef.current.clientWidth; + const cHeight = containerRef.current.clientHeight; + const displayWidth = display.getWidth(); + const displayHeight = display.getHeight(); + + if (displayWidth > 0 && displayHeight > 0 && cWidth > 0 && cHeight > 0) { + const scale = Math.min(cWidth / displayWidth, cHeight / displayHeight); + display.scale(scale); + } } }; window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); + // Also trigger on initial render after a short delay + const initialTimeout = setTimeout(handleResize, 100); + return () => { + window.removeEventListener("resize", handleResize); + clearTimeout(initialTimeout); + }; }, []); return ( -
+
diff --git a/src/ui/desktop/apps/guacamole/GuacamoleTestDialog.tsx b/src/ui/desktop/apps/guacamole/GuacamoleTestDialog.tsx index 913428f0..75b27373 100644 --- a/src/ui/desktop/apps/guacamole/GuacamoleTestDialog.tsx +++ b/src/ui/desktop/apps/guacamole/GuacamoleTestDialog.tsx @@ -13,7 +13,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { PasswordInput } from "@/components/ui/password-input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Monitor, MonitorPlay, Terminal } from "lucide-react"; -import { GuacamoleDisplay, GuacamoleConnectionConfig } from "./GuacamoleDisplay"; +import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext"; +import type { GuacamoleConnectionConfig } from "./GuacamoleDisplay"; interface GuacamoleTestDialogProps { trigger?: React.ReactNode; @@ -21,8 +22,7 @@ interface GuacamoleTestDialogProps { export function GuacamoleTestDialog({ trigger }: GuacamoleTestDialogProps) { const [isOpen, setIsOpen] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); - const [connectionConfig, setConnectionConfig] = useState(null); + const { addTab } = useTabs(); const [connectionType, setConnectionType] = useState<"rdp" | "vnc" | "telnet">("rdp"); const [hostname, setHostname] = useState(""); @@ -48,22 +48,22 @@ export function GuacamoleTestDialog({ trigger }: GuacamoleTestDialogProps) { "ignore-cert": true, }; - setConnectionConfig(config); - setIsConnecting(true); - }; + // Add a new tab for the remote desktop connection + const tabType = connectionType === "rdp" ? "rdp" : connectionType === "vnc" ? "vnc" : "rdp"; + const title = `${connectionType.toUpperCase()} - ${hostname}`; - const handleDisconnect = () => { - setConnectionConfig(null); - setIsConnecting(false); - }; + addTab({ + type: tabType, + title, + connectionConfig: config, + }); - const handleClose = () => { - handleDisconnect(); + // Close the dialog setIsOpen(false); }; return ( - open ? setIsOpen(true) : handleClose()}> + {trigger || ( )} - + - {isConnecting ? `Connected to ${hostname}` : "Test Remote Connection"} + Remote Connection - {!isConnecting ? ( -
+
{ setConnectionType(v as "rdp" | "vnc" | "telnet"); setPort(""); @@ -177,16 +176,6 @@ export function GuacamoleTestDialog({ trigger }: GuacamoleTestDialogProps) { Connect
- ) : ( -
- console.error("Guacamole error:", err)} - /> -
- )}
); diff --git a/src/ui/desktop/navigation/AppView.tsx b/src/ui/desktop/navigation/AppView.tsx index 51baf6e4..9eeb054b 100644 --- a/src/ui/desktop/navigation/AppView.tsx +++ b/src/ui/desktop/navigation/AppView.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState, useMemo } from "react"; import { Terminal } from "@/ui/desktop/apps/terminal/Terminal.tsx"; import { Server as ServerView } from "@/ui/desktop/apps/server/Server.tsx"; import { FileManager } from "@/ui/desktop/apps/file-manager/FileManager.tsx"; +import { GuacamoleDisplay, type GuacamoleConnectionConfig } from "@/ui/desktop/apps/guacamole/GuacamoleDisplay.tsx"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import { ResizablePanelGroup, @@ -16,7 +17,6 @@ import { TERMINAL_THEMES, DEFAULT_TERMINAL_CONFIG, } from "@/constants/terminal-themes"; -import { SSHAuthDialog } from "@/ui/desktop/navigation/SSHAuthDialog.tsx"; interface TabData { id: number; @@ -30,6 +30,7 @@ interface TabData { }; }; hostConfig?: any; + connectionConfig?: GuacamoleConnectionConfig; [key: string]: unknown; } @@ -58,7 +59,9 @@ export function AppView({ (tab: TabData) => tab.type === "terminal" || tab.type === "server" || - tab.type === "file_manager", + tab.type === "file_manager" || + tab.type === "rdp" || + tab.type === "vnc", ), [tabs], ); @@ -317,6 +320,19 @@ export function AppView({ isTopbarOpen={isTopbarOpen} embedded /> + ) : t.type === "rdp" || t.type === "vnc" ? ( + t.connectionConfig ? ( + removeTab(t.id)} + onError={(err) => console.error("Guacamole error:", err)} + /> + ) : ( +
+ Missing connection configuration +
+ ) ) : ( ) : isUserProfile ? ( + ) : isRemoteDesktop ? ( + ) : ( )}