diff --git a/web/app/servers/[id]/console/page.tsx b/web/app/servers/[id]/console/page.tsx index bc8d168..05cfa75 100644 --- a/web/app/servers/[id]/console/page.tsx +++ b/web/app/servers/[id]/console/page.tsx @@ -15,7 +15,7 @@ export default function ServerConsolePage() { const serverId = params.id as string; const containerRef = useRef(null); - const connectionRef = useRef<{ disconnect: () => void } | null>(null); + const connectionRef = useRef<{ disconnect: () => void; setScale: (scale: number) => void } | null>(null); const [protocol, setProtocol] = useState(searchParams.get("protocol") || ""); const [keyId, setKeyId] = useState(""); @@ -103,14 +103,20 @@ export default function ServerConsolePage() { const wsProto = location.protocol === "https:" ? "wss" : "ws"; const wsUrl = `${wsProto}://${location.host}${pending.wsPath}`; const rect = containerRef.current.getBoundingClientRect(); - const dpi = Math.round(96 * (window.devicePixelRatio || 1)); + const dpr = window.devicePixelRatio || 1; + // Request the remote at device-pixel resolution with a fixed 96 dpi, then + // scale the display back down by dpr. Folding dpr into `dpi` instead makes + // the remote enlarge everything, which reads as a zoomed-in view. const connectData = `token=${encodeURIComponent(pending.token)}` + - `&width=${Math.floor(rect.width)}` + - `&height=${Math.floor(rect.height)}` + - `&dpi=${dpi}`; + `&width=${Math.floor(rect.width * dpr)}` + + `&height=${Math.floor(rect.height * dpr)}` + + `&dpi=96`; connectionRef.current = openConsole(containerRef.current, wsUrl, connectData); + if (dpr !== 1) { + connectionRef.current.setScale(1 / dpr); + } setPending(null); }, [connected, pending]); diff --git a/web/lib/guacConsole.ts b/web/lib/guacConsole.ts index b01b69d..e5c1104 100644 --- a/web/lib/guacConsole.ts +++ b/web/lib/guacConsole.ts @@ -2,7 +2,11 @@ // The library attaches a global `Guacamole` object when loaded. declare const Guacamole: any; -export function openConsole(container: HTMLElement, wsUrl: string, connectData = ""): { disconnect: () => void } { +export function openConsole( + container: HTMLElement, + wsUrl: string, + connectData = "" +): { disconnect: () => void; setScale: (scale: number) => void } { // Guacamole's WebSocketTunnel builds the socket URL as `wsUrl + "?" + data`, // so wsUrl must NOT already contain a query string — pass params via connectData. const tunnel = new Guacamole.WebSocketTunnel(wsUrl); @@ -25,5 +29,8 @@ export function openConsole(container: HTMLElement, wsUrl: string, connectData = disconnect() { client.disconnect(); }, + setScale(scale: number) { + client.getDisplay().scale(scale); + }, }; }