From 2681b5e702c0b0e5980585b4cd56584a569b9d9f Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Wed, 13 May 2026 18:42:22 +0800 Subject: [PATCH 1/3] feat: add mobile remote desktop sessions --- app/contexts/TerminalSessionsContext.tsx | 37 ++- app/main-axios.ts | 24 +- app/tabs/hosts/Hosts.tsx | 2 +- app/tabs/hosts/navigation/Host.tsx | 42 ++- app/tabs/sessions/Sessions.tsx | 39 +-- app/tabs/sessions/navigation/TabBar.tsx | 7 +- .../sessions/remote-desktop/RemoteDesktop.tsx | 295 ++++++++++++++++++ types/index.ts | 2 +- 8 files changed, 398 insertions(+), 50 deletions(-) create mode 100644 app/tabs/sessions/remote-desktop/RemoteDesktop.tsx diff --git a/app/contexts/TerminalSessionsContext.tsx b/app/contexts/TerminalSessionsContext.tsx index 70445c4..c4a01a1 100644 --- a/app/contexts/TerminalSessionsContext.tsx +++ b/app/contexts/TerminalSessionsContext.tsx @@ -11,29 +11,30 @@ import { SSHHost } from "@/types"; import { router } from "expo-router"; import { useAppContext } from "@/app/AppContext"; +export type SessionType = + | "terminal" + | "stats" + | "filemanager" + | "tunnel" + | "remoteDesktop"; + export interface TerminalSession { id: string; host: SSHHost; title: string; isActive: boolean; createdAt: Date; - type: "terminal" | "stats" | "filemanager" | "tunnel"; + type: SessionType; } interface TerminalSessionsContextType { sessions: TerminalSession[]; activeSessionId: string | null; - addSession: ( - host: SSHHost, - type?: "terminal" | "stats" | "filemanager" | "tunnel", - ) => string; + addSession: (host: SSHHost, type?: SessionType) => string; removeSession: (sessionId: string) => void; setActiveSession: (sessionId: string) => void; clearAllSessions: () => void; - navigateToSessions: ( - host?: SSHHost, - type?: "terminal" | "stats" | "filemanager" | "tunnel", - ) => void; + navigateToSessions: (host?: SSHHost, type?: SessionType) => void; isCustomKeyboardVisible: boolean; toggleCustomKeyboard: () => void; lastKeyboardHeight: number; @@ -72,10 +73,7 @@ export const TerminalSessionsProvider: React.FC< const [, forceUpdate] = useState({}); const addSession = useCallback( - ( - host: SSHHost, - type: "terminal" | "stats" | "filemanager" | "tunnel" = "terminal", - ): string => { + (host: SSHHost, type: SessionType = "terminal"): string => { setSessions((prev) => { const existingSessions = prev.filter( (session) => session.host.id === host.id && session.type === type, @@ -88,7 +86,9 @@ export const TerminalSessionsProvider: React.FC< ? "Files" : type === "tunnel" ? "Tunnels" - : ""; + : type === "remoteDesktop" + ? "Remote" + : ""; let title = typeLabel ? `${host.name} - ${typeLabel}` : host.name; if (existingSessions.length > 0) { title = typeLabel @@ -156,7 +156,9 @@ export const TerminalSessionsProvider: React.FC< ? "Files" : session.type === "tunnel" ? "Tunnels" - : ""; + : session.type === "remoteDesktop" + ? "Remote" + : ""; const baseName = typeLabel ? `${session.host.name} - ${typeLabel}` : session.host.name; @@ -208,10 +210,7 @@ export const TerminalSessionsProvider: React.FC< ); const navigateToSessions = useCallback( - ( - host?: SSHHost, - type: "terminal" | "stats" | "filemanager" | "tunnel" = "terminal", - ) => { + (host?: SSHHost, type: SessionType = "terminal") => { if (host) { addSession(host, type); } diff --git a/app/main-axios.ts b/app/main-axios.ts index e86056c..125545f 100644 --- a/app/main-axios.ts +++ b/app/main-axios.ts @@ -4,10 +4,6 @@ import type { SSHHostData, TunnelConfig, TunnelStatus, - Credential, - CredentialData, - HostInfo, - ApiResponse, FileManagerFile, FileManagerShortcut, ServerStatus, @@ -287,6 +283,15 @@ export function getCurrentServerUrl(): string | null { return configuredServerUrl; } +export function getGuacamoleWebSocketUrl(token: string): string { + const base = getRootBase(8081).replace(/\/$/, ""); + const websocketBase = base.replace(/^http/i, (scheme) => + scheme.toLowerCase() === "https" ? "wss" : "ws", + ); + + return `${websocketBase}/guacamole/websocket/?token=${encodeURIComponent(token)}`; +} + export async function isAuthenticated(): Promise { try { const token = await getCookie("jwt"); @@ -822,6 +827,17 @@ export async function exportSSHHostWithCredentials( } } +export async function getGuacamoleTokenFromHost( + hostId: number, +): Promise<{ token: string }> { + try { + const response = await authApi.post(`/guacamole/connect-host/${hostId}`); + return response.data; + } catch (error) { + handleApiError(error, "connect Guacamole host"); + } +} + // ============================================================================ // SSH AUTOSTART MANAGEMENT // ============================================================================ diff --git a/app/tabs/hosts/Hosts.tsx b/app/tabs/hosts/Hosts.tsx index c5ea5d4..432eb48 100644 --- a/app/tabs/hosts/Hosts.tsx +++ b/app/tabs/hosts/Hosts.tsx @@ -110,7 +110,7 @@ export default function Hosts() { }); } - hosts.filter((host: SSHHost) => !host.connectionType || host.connectionType === "ssh").forEach((host: SSHHost) => { + hosts.forEach((host: SSHHost) => { const folderName = host.folder || "No Folder"; if (!folderMap.has(folderName)) { folderMap.set(folderName, { diff --git a/app/tabs/hosts/navigation/Host.tsx b/app/tabs/hosts/navigation/Host.tsx index 8960c49..94d868c 100644 --- a/app/tabs/hosts/navigation/Host.tsx +++ b/app/tabs/hosts/navigation/Host.tsx @@ -10,13 +10,11 @@ import { } from "react-native"; import { Terminal, - Server, FolderOpen, - Key, - Lock, MoreVertical, X, Activity, + Monitor, } from "lucide-react-native"; import { SSHHost } from "@/types"; import { useTerminalSessions } from "@/app/contexts/TerminalSessionsContext"; @@ -37,6 +35,14 @@ function Host({ host, status, isLast = false }: HostProps) { const [tagsContainerWidth, setTagsContainerWidth] = useState(0); const statusLabel = status === "online" ? "UP" : status === "offline" ? "DOWN" : "UNK"; + const connectionType = (host.connectionType || "ssh").toLowerCase(); + const isRemoteDesktopHost = ["rdp", "vnc", "telnet"].includes(connectionType); + const remoteDesktopLabel = + connectionType === "vnc" + ? "Open VNC Session" + : connectionType === "telnet" + ? "Open Telnet Session" + : "Open RDP Session"; const parsedStatsConfig: StatsConfig = (() => { try { @@ -114,6 +120,11 @@ function Host({ host, status, isLast = false }: HostProps) { setShowContextMenu(false); }; + const handleRemoteDesktopPress = () => { + navigateToSessions(host, "remoteDesktop"); + setShowContextMenu(false); + }; + const handleStatsPress = () => { navigateToSessions(host, "stats"); setShowContextMenu(false); @@ -136,7 +147,7 @@ function Host({ host, status, isLast = false }: HostProps) { <> @@ -361,7 +372,28 @@ function Host({ host, status, isLast = false }: HostProps) { - {host.enableTerminal && ( + {isRemoteDesktopHost && ( + + + + + {remoteDesktopLabel} + + + {host.ip}:{host.port} + + + + )} + + {host.enableTerminal && !isRemoteDesktopHost && ( session.id === activeSessionId, + ); + const getTabBarBottomPosition = () => { if (activeSession?.type !== "terminal") { return insets.bottom; @@ -146,7 +143,7 @@ export default function Sessions() { }; const getBottomMargin = ( - sessionType: "terminal" | "stats" | "filemanager" = "terminal", + sessionType: SessionType = "terminal", ) => { if (sessionType !== "terminal") { return SESSION_TAB_BAR_HEIGHT + insets.bottom; @@ -523,10 +520,6 @@ export default function Sessions() { [], ); - const activeSession = sessions.find( - (session) => session.id === activeSessionId, - ); - const activeTerminalBgColor = activeSession?.type === "terminal" && activeSessionId ? terminalBackgroundColors[activeSessionId] || BACKGROUNDS.DARKEST @@ -625,6 +618,16 @@ export default function Sessions() { onClose={() => handleTabClose(session.id)} /> ); + } else if (session.type === "remoteDesktop") { + return ( + handleTabClose(session.id)} + /> + ); } return null; })} diff --git a/app/tabs/sessions/navigation/TabBar.tsx b/app/tabs/sessions/navigation/TabBar.tsx index 3157c14..074eada 100644 --- a/app/tabs/sessions/navigation/TabBar.tsx +++ b/app/tabs/sessions/navigation/TabBar.tsx @@ -16,7 +16,10 @@ import { ChevronDown, ChevronUp, } from "lucide-react-native"; -import { TerminalSession } from "@/app/contexts/TerminalSessionsContext"; +import { + SessionType, + TerminalSession, +} from "@/app/contexts/TerminalSessionsContext"; import { useRouter } from "expo-router"; import { useKeyboard } from "@/app/contexts/KeyboardContext"; import { useOrientation } from "@/app/utils/orientation"; @@ -40,7 +43,7 @@ interface TabBarProps { onHideKeyboard?: () => void; onShowKeyboard?: () => void; keyboardIntentionallyHiddenRef: React.MutableRefObject; - activeSessionType?: "terminal" | "stats" | "filemanager"; + activeSessionType?: SessionType; } export default function TabBar({ diff --git a/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx b/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx new file mode 100644 index 0000000..6935602 --- /dev/null +++ b/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx @@ -0,0 +1,295 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + ActivityIndicator, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { WebView } from "react-native-webview"; +import { RotateCcw } from "lucide-react-native"; +import type { SSHHost } from "@/types"; +import { + getGuacamoleTokenFromHost, + getGuacamoleWebSocketUrl, +} from "@/app/main-axios"; + +type ConnectionState = "idle" | "connecting" | "connected" | "disconnected" | "failed"; + +interface RemoteDesktopProps { + host: SSHHost; + isVisible: boolean; + title: string; + onClose?: () => void; +} + +export function RemoteDesktop({ + host, + isVisible, + title, +}: RemoteDesktopProps) { + const webViewRef = useRef(null); + const [connectionState, setConnectionState] = + useState("idle"); + const [errorMessage, setErrorMessage] = useState(null); + const [webSocketUrl, setWebSocketUrl] = useState(null); + const [webViewKey, setWebViewKey] = useState(0); + + const connect = useCallback(async () => { + try { + setConnectionState("connecting"); + setErrorMessage(null); + + const { token } = await getGuacamoleTokenFromHost(Number(host.id)); + setWebSocketUrl(getGuacamoleWebSocketUrl(token)); + } catch (error) { + setConnectionState("failed"); + setErrorMessage( + error instanceof Error ? error.message : "Failed to start remote session", + ); + } + }, [host.id]); + + useEffect(() => { + connect(); + }, [connect, webViewKey]); + + const handleMessage = useCallback((event: any) => { + try { + const payload = JSON.parse(event.nativeEvent.data); + + if (payload.type === "state") { + if (payload.state === "connected") { + setConnectionState("connected"); + setErrorMessage(null); + } else if (payload.state === "disconnected") { + setConnectionState("disconnected"); + } + } else if (payload.type === "error") { + setConnectionState("failed"); + setErrorMessage(payload.message || "Remote session failed"); + } + } catch { + setConnectionState("failed"); + setErrorMessage("Remote session returned an invalid message"); + } + }, []); + + const htmlContent = useMemo(() => { + if (!webSocketUrl) return ""; + + return ` + + + + + + + +
+ + +`; + }, [webSocketUrl]); + + const reconnect = useCallback(() => { + setWebSocketUrl(null); + setWebViewKey((current) => current + 1); + }, []); + + const protocol = (host.connectionType || "rdp").toUpperCase(); + + return ( + + {htmlContent ? ( + { + setConnectionState("failed"); + setErrorMessage(event.nativeEvent.description); + }} + onHttpError={(event) => { + setConnectionState("failed"); + setErrorMessage(`WebView HTTP error: ${event.nativeEvent.statusCode}`); + }} + scrollEnabled={false} + overScrollMode="never" + bounces={false} + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + setSupportMultipleWindows={false} + /> + ) : null} + + {(connectionState === "connecting" || connectionState === "idle") && ( + + + Connecting {protocol} + {title} + + )} + + {(connectionState === "failed" || connectionState === "disconnected") && ( + + + {connectionState === "failed" ? "Connection Failed" : "Disconnected"} + + {errorMessage ? {errorMessage} : null} + + + Reconnect + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + ...StyleSheet.absoluteFillObject, + backgroundColor: "#050608", + }, + webView: { + flex: 1, + width: "100%", + height: "100%", + backgroundColor: "#050608", + }, + overlay: { + ...StyleSheet.absoluteFillObject, + alignItems: "center", + justifyContent: "center", + paddingHorizontal: 24, + backgroundColor: "#050608", + }, + overlayTitle: { + marginTop: 12, + color: "#ffffff", + fontSize: 16, + fontWeight: "600", + textAlign: "center", + }, + overlayText: { + marginTop: 8, + color: "#9CA3AF", + fontSize: 13, + lineHeight: 18, + textAlign: "center", + }, + retryButton: { + marginTop: 18, + minHeight: 40, + paddingHorizontal: 16, + borderRadius: 8, + borderWidth: 1, + borderColor: "#16A34A", + backgroundColor: "#22C55E", + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + retryText: { + color: "#ffffff", + fontSize: 14, + fontWeight: "600", + }, +}); diff --git a/types/index.ts b/types/index.ts index 97c094e..c2b312e 100644 --- a/types/index.ts +++ b/types/index.ts @@ -13,7 +13,7 @@ export interface QuickAction { export interface SSHHost { id: number; - connectionType?: string; + connectionType?: "ssh" | "rdp" | "vnc" | "telnet" | string; name: string; ip: string; port: number; From 74881378cdab65053ce6d25c6e77263f04a991ac Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Wed, 13 May 2026 18:44:58 +0800 Subject: [PATCH 2/3] feat: add remote desktop input controls --- app/main-axios.ts | 12 +- .../sessions/remote-desktop/RemoteDesktop.tsx | 182 +++++++++++++++++- 2 files changed, 187 insertions(+), 7 deletions(-) diff --git a/app/main-axios.ts b/app/main-axios.ts index 125545f..b568352 100644 --- a/app/main-axios.ts +++ b/app/main-axios.ts @@ -283,13 +283,21 @@ export function getCurrentServerUrl(): string | null { return configuredServerUrl; } -export function getGuacamoleWebSocketUrl(token: string): string { +export function getGuacamoleWebSocketUrl( + token: string, + width?: number, + height?: number, +): string { const base = getRootBase(8081).replace(/\/$/, ""); const websocketBase = base.replace(/^http/i, (scheme) => scheme.toLowerCase() === "https" ? "wss" : "ws", ); + const params = new URLSearchParams({ token }); - return `${websocketBase}/guacamole/websocket/?token=${encodeURIComponent(token)}`; + if (width) params.set("width", String(width)); + if (height) params.set("height", String(height)); + + return `${websocketBase}/guacamole/websocket/?${params.toString()}`; } export async function isAuthenticated(): Promise { diff --git a/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx b/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx index 6935602..4fccf87 100644 --- a/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx +++ b/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx @@ -3,11 +3,13 @@ import { ActivityIndicator, StyleSheet, Text, + TextInput, TouchableOpacity, + useWindowDimensions, View, } from "react-native"; import { WebView } from "react-native-webview"; -import { RotateCcw } from "lucide-react-native"; +import { Keyboard, RotateCcw } from "lucide-react-native"; import type { SSHHost } from "@/types"; import { getGuacamoleTokenFromHost, @@ -28,12 +30,19 @@ export function RemoteDesktop({ isVisible, title, }: RemoteDesktopProps) { + const { width, height } = useWindowDimensions(); + const initialSizeRef = useRef({ + width: Math.round(width), + height: Math.round(height), + }); const webViewRef = useRef(null); + const inputRef = useRef(null); const [connectionState, setConnectionState] = useState("idle"); const [errorMessage, setErrorMessage] = useState(null); const [webSocketUrl, setWebSocketUrl] = useState(null); const [webViewKey, setWebViewKey] = useState(0); + const [inputValue, setInputValue] = useState(""); const connect = useCallback(async () => { try { @@ -41,7 +50,10 @@ export function RemoteDesktop({ setErrorMessage(null); const { token } = await getGuacamoleTokenFromHost(Number(host.id)); - setWebSocketUrl(getGuacamoleWebSocketUrl(token)); + const initialSize = initialSizeRef.current; + setWebSocketUrl( + getGuacamoleWebSocketUrl(token, initialSize.width, initialSize.height), + ); } catch (error) { setConnectionState("failed"); setErrorMessage( @@ -123,16 +135,17 @@ export function RemoteDesktop({ displayContainer.appendChild(displayElement); + let currentScale = 1; const resizeDisplay = () => { const width = display.getWidth(); const height = display.getHeight(); if (!width || !height) return; - const scale = Math.min( + currentScale = Math.min( displayContainer.clientWidth / width, displayContainer.clientHeight / height ); - display.scale(Math.max(scale, 0.1)); + display.scale(Math.max(currentScale, 0.1)); }; display.onresize = resizeDisplay; @@ -141,10 +154,36 @@ export function RemoteDesktop({ const mouse = Guacamole.Mouse.Touchscreen ? new Guacamole.Mouse.Touchscreen(displayElement) : new Guacamole.Mouse(displayElement); + const sendMouseState = (state) => { + const scale = Math.max(currentScale, 0.1); + state.x = Math.round(state.x / scale); + state.y = Math.round(state.y / scale); + client.sendMouseState(state); + }; mouse.onmousedown = mouse.onmouseup = mouse.onmousemove = - (state) => client.sendMouseState(state); + sendMouseState; + + window.termixRemote = { + sendKeysym: (keysym) => { + client.sendKeyEvent(1, keysym); + client.sendKeyEvent(0, keysym); + }, + sendKeysyms: (keysyms) => { + keysyms.forEach((keysym) => client.sendKeyEvent(1, keysym)); + keysyms.slice().reverse().forEach((keysym) => client.sendKeyEvent(0, keysym)); + }, + sendText: (text) => { + Array.from(text).forEach((char) => { + const codepoint = char.codePointAt(0); + if (!codepoint) return; + const keysym = codepoint <= 0xff ? codepoint : (0x01000000 | codepoint); + client.sendKeyEvent(1, keysym); + client.sendKeyEvent(0, keysym); + }); + }, + }; client.onstatechange = (state) => { if (state === Guacamole.Client.State.CONNECTED) { @@ -175,7 +214,49 @@ export function RemoteDesktop({ setWebViewKey((current) => current + 1); }, []); + const injectRemoteCommand = useCallback((script: string) => { + webViewRef.current?.injectJavaScript(`${script}; true;`); + }, []); + + const sendKeysym = useCallback( + (keysym: number) => { + injectRemoteCommand( + `window.termixRemote && window.termixRemote.sendKeysym(${keysym})`, + ); + }, + [injectRemoteCommand], + ); + + const sendKeysyms = useCallback( + (keysyms: number[]) => { + injectRemoteCommand( + `window.termixRemote && window.termixRemote.sendKeysyms(${JSON.stringify(keysyms)})`, + ); + }, + [injectRemoteCommand], + ); + + const sendText = useCallback( + (text: string) => { + injectRemoteCommand( + `window.termixRemote && window.termixRemote.sendText(${JSON.stringify(text)})`, + ); + }, + [injectRemoteCommand], + ); + + const handleInputChange = useCallback( + (text: string) => { + if (text) { + sendText(text); + } + setInputValue(""); + }, + [sendText], + ); + const protocol = (host.connectionType || "rdp").toUpperCase(); + const canSendInput = connectionState === "connected"; return ( )} + + {canSendInput && ( + + { + if (nativeEvent.key === "Backspace") { + sendKeysym(0xff08); + } else if (nativeEvent.key === "Enter") { + sendKeysym(0xff0d); + } + }} + autoCapitalize="none" + autoCorrect={false} + spellCheck={false} + style={styles.hiddenInput} + /> + } + onPress={() => inputRef.current?.focus()} + /> + sendKeysym(0xff1b)} /> + sendKeysym(0xff09)} /> + sendKeysym(0xff0d)} /> + sendKeysym(0xff08)} /> + sendKeysyms([0xffe3, 0xffe9, 0xffff])} + /> + + )}
); } +function RemoteKeyButton({ + label, + icon, + onPress, +}: { + label: string; + icon?: React.ReactNode; + onPress: () => void; +}) { + return ( + + {icon} + {label} + + ); +} + const styles = StyleSheet.create({ container: { ...StyleSheet.absoluteFillObject, @@ -292,4 +424,44 @@ const styles = StyleSheet.create({ fontSize: 14, fontWeight: "600", }, + toolbar: { + position: "absolute", + left: 0, + right: 0, + bottom: 0, + minHeight: 48, + paddingHorizontal: 8, + paddingVertical: 6, + backgroundColor: "#111827", + borderTopWidth: 1, + borderTopColor: "#303032", + flexDirection: "row", + alignItems: "center", + gap: 6, + }, + hiddenInput: { + position: "absolute", + width: 1, + height: 1, + opacity: 0, + color: "transparent", + }, + keyButton: { + minWidth: 44, + height: 36, + paddingHorizontal: 8, + borderRadius: 8, + borderWidth: 1, + borderColor: "#303032", + backgroundColor: "#1F2937", + alignItems: "center", + justifyContent: "center", + flexDirection: "row", + gap: 4, + }, + keyButtonText: { + color: "#ffffff", + fontSize: 12, + fontWeight: "600", + }, }); From 74ecbaf98cb13a3b151101d877840d26f2b17617 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Wed, 13 May 2026 18:52:38 +0800 Subject: [PATCH 3/3] feat: polish remote desktop controls --- .../sessions/remote-desktop/RemoteDesktop.tsx | 89 ++++++++++++++----- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx b/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx index 4fccf87..768a3f7 100644 --- a/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx +++ b/app/tabs/sessions/remote-desktop/RemoteDesktop.tsx @@ -1,6 +1,13 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { ActivityIndicator, + ScrollView, StyleSheet, Text, TextInput, @@ -16,7 +23,12 @@ import { getGuacamoleWebSocketUrl, } from "@/app/main-axios"; -type ConnectionState = "idle" | "connecting" | "connected" | "disconnected" | "failed"; +type ConnectionState = + | "idle" + | "connecting" + | "connected" + | "disconnected" + | "failed"; interface RemoteDesktopProps { host: SSHHost; @@ -25,11 +37,7 @@ interface RemoteDesktopProps { onClose?: () => void; } -export function RemoteDesktop({ - host, - isVisible, - title, -}: RemoteDesktopProps) { +export function RemoteDesktop({ host, isVisible, title }: RemoteDesktopProps) { const { width, height } = useWindowDimensions(); const initialSizeRef = useRef({ width: Math.round(width), @@ -57,7 +65,9 @@ export function RemoteDesktop({ } catch (error) { setConnectionState("failed"); setErrorMessage( - error instanceof Error ? error.message : "Failed to start remote session", + error instanceof Error + ? error.message + : "Failed to start remote session", ); } }, [host.id]); @@ -174,6 +184,10 @@ export function RemoteDesktop({ keysyms.forEach((keysym) => client.sendKeyEvent(1, keysym)); keysyms.slice().reverse().forEach((keysym) => client.sendKeyEvent(0, keysym)); }, + resize: (width, height) => { + client.sendSize(Math.max(1, Math.round(width)), Math.max(1, Math.round(height))); + window.requestAnimationFrame(resizeDisplay); + }, sendText: (text) => { Array.from(text).forEach((char) => { const codepoint = char.codePointAt(0); @@ -258,6 +272,14 @@ export function RemoteDesktop({ const protocol = (host.connectionType || "rdp").toUpperCase(); const canSendInput = connectionState === "connected"; + useEffect(() => { + if (!canSendInput) return; + + injectRemoteCommand( + `window.termixRemote && window.termixRemote.resize(${Math.round(width)}, ${Math.round(height)})`, + ); + }, [canSendInput, height, injectRemoteCommand, width]); + return ( { setConnectionState("failed"); - setErrorMessage(`WebView HTTP error: ${event.nativeEvent.statusCode}`); + setErrorMessage( + `WebView HTTP error: ${event.nativeEvent.statusCode}`, + ); }} scrollEnabled={false} overScrollMode="never" @@ -311,9 +335,13 @@ export function RemoteDesktop({ {(connectionState === "failed" || connectionState === "disconnected") && ( - {connectionState === "failed" ? "Connection Failed" : "Disconnected"} + {connectionState === "failed" + ? "Connection Failed" + : "Disconnected"} - {errorMessage ? {errorMessage} : null} + {errorMessage ? ( + {errorMessage} + ) : null} Reconnect @@ -339,19 +367,30 @@ export function RemoteDesktop({ spellCheck={false} style={styles.hiddenInput} /> - } - onPress={() => inputRef.current?.focus()} - /> - sendKeysym(0xff1b)} /> - sendKeysym(0xff09)} /> - sendKeysym(0xff0d)} /> - sendKeysym(0xff08)} /> - sendKeysyms([0xffe3, 0xffe9, 0xffff])} - /> + + } + onPress={() => inputRef.current?.focus()} + /> + sendKeysym(0xff1b)} /> + sendKeysym(0xff09)} /> + sendKeysym(0xff0d)} /> + sendKeysym(0xff08)} /> + sendKeysym(0xff51)} /> + sendKeysym(0xff52)} /> + sendKeysym(0xff54)} /> + sendKeysym(0xff53)} /> + sendKeysyms([0xffe3, 0xffe9, 0xffff])} + /> + )} @@ -435,6 +474,8 @@ const styles = StyleSheet.create({ backgroundColor: "#111827", borderTopWidth: 1, borderTopColor: "#303032", + }, + toolbarContent: { flexDirection: "row", alignItems: "center", gap: 6,