From 8ccc959dc95ec8ee17a6f36cbb0164a2d08c188b Mon Sep 17 00:00:00 2001 From: Samuel James <143277412+SamuelSJames@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:00:09 -0400 Subject: [PATCH] Wire mouse + keyboard input for RDP sessions (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multi-session RemoteDesktop view rendered the remote display but never captured input — no Guacamole.Mouse / Guacamole.Keyboard were created — so the desktop showed but the mouse and keyboard did nothing. Add per-session input: - Guacamole.Mouse on the display element, forwarding mouse state via client.sendMouseState with coordinates divided by the current display scale so clicks land correctly on the scaled-down canvas. - Guacamole.Keyboard on document, forwarding key events via client.sendKeyEvent, but only while that session is the active/visible tab so a background session can't steal keystrokes. - Detach keyboard handlers on session close and on unmount. Co-authored-by: Samuel James Co-authored-by: Kiro --- src/pages/RemoteDesktop.tsx | 45 +++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/pages/RemoteDesktop.tsx b/src/pages/RemoteDesktop.tsx index 633d46c..797b125 100644 --- a/src/pages/RemoteDesktop.tsx +++ b/src/pages/RemoteDesktop.tsx @@ -18,6 +18,8 @@ interface SessionHandle { client: any container: HTMLDivElement display: any + mouse: any + keyboard: any } // Scale the Guacamole display to fit its visible container. The display canvas @@ -49,7 +51,13 @@ export default function RemoteDesktop() { useEffect(() => { return () => { - handlesRef.current.forEach(({ client }) => client.disconnect()) + handlesRef.current.forEach(({ client, keyboard }) => { + if (keyboard) { + keyboard.onkeydown = null + keyboard.onkeyup = null + } + client.disconnect() + }) } }, []) @@ -79,7 +87,38 @@ export default function RemoteDesktop() { const tunnel = new Guacamole.WebSocketTunnel(`${proto}://${window.location.host}/api/guacamole`) const client = new Guacamole.Client(tunnel) const display = client.getDisplay() - handlesRef.current.set(sessionId, { client, container, display }) + + // --- Input wiring (mouse + keyboard) --- + // Mouse events are captured on the display element. Coordinates must be + // divided by the current display scale so clicks land where the user sees + // them (the canvas is scaled down to fit the panel). + const mouse = new Guacamole.Mouse(display.getElement()) + const sendMouse = (mouseState: any) => { + const scale = display.getScale() || 1 + const scaled = new Guacamole.Mouse.State( + mouseState.x / scale, + mouseState.y / scale, + mouseState.left, + mouseState.middle, + mouseState.right, + mouseState.up, + mouseState.down, + ) + client.sendMouseState(scaled) + } + mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = sendMouse + + // Keyboard is captured on the document but only forwarded while THIS session + // is the active/visible one (otherwise a background session would steal keys). + const keyboard = new Guacamole.Keyboard(document) + keyboard.onkeydown = (keysym: number) => { + if (activeSessionIdRef.current === sessionId) client.sendKeyEvent(1, keysym) + } + keyboard.onkeyup = (keysym: number) => { + if (activeSessionIdRef.current === sessionId) client.sendKeyEvent(0, keysym) + } + + handlesRef.current.set(sessionId, { client, container, display, mouse, keyboard }) client.onerror = (err: { message?: string }) => { patchSession(sessionId, { status: 'error', errorMessage: err?.message ?? 'Connection failed' }) @@ -99,6 +138,8 @@ export default function RemoteDesktop() { function closeSession(sessionId: string) { const handle = handlesRef.current.get(sessionId) + handle?.keyboard?.onkeydown && (handle.keyboard.onkeydown = null) + handle?.keyboard?.onkeyup && (handle.keyboard.onkeyup = null) handle?.client.disconnect() handle?.container.remove() handlesRef.current.delete(sessionId)