Merge pull request #847 from victorwon2001/fix/issue-815

feat: add Windows Remote Desktop client option
This commit is contained in:
Kang authored and GitHub committed 2026-10-07 11:13:51 +08:00
commit 39cc98b843
19 files changed
+431 -3

No files matched your search

+81 -1
View File
@@ -1,9 +1,63 @@
use crate::config::ConnectionType;
use crate::core::rdp::{self, RdpInputEvent, RdpSessionManager};
use crate::error::AppResult;
use crate::error::{AppError, AppResult};
#[cfg(windows)]
use std::process::Command;
use std::sync::Arc;
use tauri::Emitter;
use tauri::ipc::{Channel, InvokeResponseBody};
const DEFAULT_RDP_PORT: u16 = 3389;
fn mstsc_target(host: &str, port: u16) -> String {
let host = host.trim();
let server = if host.contains(':') && !(host.starts_with('[') && host.ends_with(']')) {
format!("[{host}]")
} else {
host.to_string()
};
if port == DEFAULT_RDP_PORT {
server
} else {
format!("{server}:{port}")
}
}
#[cfg(windows)]
fn spawn_windows_rdp(target: &str) -> AppResult<()> {
Command::new("mstsc.exe")
.arg(format!("/v:{target}"))
.spawn()?;
Ok(())
}
#[cfg(not(windows))]
fn spawn_windows_rdp(_target: &str) -> AppResult<()> {
Err(AppError::Unsupported(
"Windows Remote Desktop is only available on Windows".to_string(),
))
}
#[tauri::command]
pub fn launch_windows_rdp(app: tauri::AppHandle, connection_id: String) -> AppResult<()> {
let connection = crate::storage::get_connection_without_secret(&connection_id)?
.ok_or_else(|| AppError::Config("RDP connection not found".to_string()))?;
let ConnectionType::Rdp { host, port, .. } = connection.config else {
return Err(AppError::Config(
"Connection is not an RDP connection".to_string(),
));
};
let target = mstsc_target(&host, port);
spawn_windows_rdp(&target)?;
if let Err(error) = crate::storage::mark_connection_used(&connection_id) {
tracing::warn!(connection_id, %error, "Failed to mark RDP connection as recently used");
} else {
let _ = app.emit("connections-changed", ());
}
Ok(())
}
#[tauri::command]
pub async fn create_rdp_session(
app: tauri::AppHandle,
@@ -99,3 +153,29 @@ pub async fn respond_rdp_certificate(
.respond_certificate(&request_id, accepted, remember)
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mstsc_target_formats_hosts_and_non_default_ports() {
assert_eq!(
mstsc_target("server.example.com", 3389),
"server.example.com"
);
assert_eq!(mstsc_target("192.0.2.10", 3390), "192.0.2.10:3390");
assert_eq!(mstsc_target("2001:db8::1", 3389), "[2001:db8::1]");
assert_eq!(mstsc_target("2001:db8::1", 3390), "[2001:db8::1]:3390");
assert_eq!(mstsc_target("[2001:db8::1]", 3390), "[2001:db8::1]:3390");
}
#[cfg(not(windows))]
#[test]
fn system_rdp_launch_is_explicitly_unsupported_off_windows() {
assert!(matches!(
spawn_windows_rdp("server.example.com"),
Err(AppError::Unsupported(_))
));
}
}
+26
View File
@@ -1,6 +1,10 @@
use super::super::default_true;
use serde::{Deserialize, Serialize};
fn default_rdp_client_mode() -> String {
"builtin".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralSettings {
#[serde(default = "default_true")]
@@ -13,6 +17,8 @@ pub struct GeneralSettings {
pub boss_key: Option<String>,
#[serde(default = "default_true")]
pub confirm_on_close: bool,
#[serde(default = "default_rdp_client_mode")]
pub rdp_client_mode: String,
}
impl Default for GeneralSettings {
@@ -23,6 +29,26 @@ impl Default for GeneralSettings {
minimize_to_tray: false,
boss_key: None,
confirm_on_close: true,
rdp_client_mode: default_rdp_client_mode(),
}
}
}
#[cfg(test)]
mod tests {
use super::GeneralSettings;
#[test]
fn missing_rdp_client_mode_defaults_to_builtin() {
let settings: GeneralSettings = serde_json::from_value(serde_json::json!({
"startup_restore": true,
"startup_restore_window_layout": true,
"minimize_to_tray": false,
"boss_key": null,
"confirm_on_close": true
}))
.expect("general settings");
assert_eq!(settings.rdp_client_mode, "builtin");
}
}
+1
View File
@@ -261,6 +261,7 @@ pub fn run() {
cmd::session::create_telnet_session,
cmd::session::create_serial_session,
cmd::rdp::create_rdp_session,
cmd::rdp::launch_windows_rdp,
cmd::rdp::rdp_attach_frame_channel,
cmd::rdp::rdp_input_batch,
cmd::rdp::rdp_set_keyboard_capture,
+6
View File
@@ -152,6 +152,12 @@ pub(crate) fn get_connection(
storage()?.get_connection_with_secret(connection_id)
}
pub(crate) fn get_connection_without_secret(
connection_id: &str,
) -> AppResult<Option<crate::config::SavedConnection>> {
storage()?.get_connection(connection_id)
}
pub(crate) fn mark_connection_used(connection_id: &str) -> AppResult<()> {
storage()?.mark_connection_used(connection_id)
}
+56
View File
@@ -55,6 +55,7 @@ import {
getRemoteDesktopPaneDisplay,
getTemporaryLinkSessionType,
isSessionCreationCancelled,
launchSavedRdpWithSystemClient,
type StartupCommandRequest,
sendStartupCommandToSession,
} from "./lib/appSessionFactory";
@@ -480,6 +481,7 @@ function App() {
setActiveTabId,
updatePaneSession,
updateTabSession,
rdpClientMode: appSettings.general.rdp_client_mode,
setTerminalWindows,
updateAutoIconForSessionStart,
});
@@ -626,6 +628,33 @@ function App() {
onSuccess?: (sessionId: string) => void;
},
) => {
try {
if (
await launchSavedRdpWithSystemClient(
connection,
appSettings.general.rdp_client_mode,
)
) {
recordRecentConnection(connection.id);
return;
}
} catch (error) {
const errorMessage = getErrorMessage(error);
logger.error({
domain: "session.lifecycle",
event: "connection.external_rdp_open_failed",
message:
options?.failureContext ?? "Windows Remote Desktop launch failed",
ids: { connection_id: connection.id },
error,
});
toast.error(
t("savedConnections.connectionFailed", { error: errorMessage }),
);
if (options?.propagateError) throw error;
return;
}
const pending = addPendingTab(
connection.name,
getConnectionSessionType(connection),
@@ -680,6 +709,7 @@ function App() {
},
[
addPendingTab,
appSettings.general.rdp_client_mode,
hasTab,
markTabConnectionFailed,
maybePromptConnectionEdit,
@@ -1051,6 +1081,31 @@ function App() {
const anchorTabId =
targetLeaf?.activeTabId ?? targetLeaf?.tabIds[targetLeaf.tabIds.length - 1] ?? null;
try {
if (
await launchSavedRdpWithSystemClient(
connection,
appSettings.general.rdp_client_mode,
)
) {
recordRecentConnection(connection.id);
return;
}
} catch (error) {
const errorMessage = getErrorMessage(error);
logger.error({
domain: "session.lifecycle",
event: "connection.external_rdp_open_failed",
message: "Windows Remote Desktop launch failed from tab menu",
ids: { connection_id: connection.id },
error,
});
toast.error(
t("savedConnections.connectionFailed", { error: errorMessage }),
);
return;
}
if (targetLeaf?.activeTabId) {
handleSelectLeafTab(leafId, targetLeaf.activeTabId);
}
@@ -1112,6 +1167,7 @@ function App() {
},
[
addPendingTab,
appSettings.general.rdp_client_mode,
hasTab,
handleSelectLeafTab,
setTerminalWindows,
@@ -0,0 +1,90 @@
import { fireEvent, render, screen, within } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { GeneralTab } from "./GeneralTab";
const state = vi.hoisted(() => ({
isWindows: true,
updateAppSettings: vi.fn(),
updateUi: vi.fn(),
}));
vi.mock("@/lib/platform", () => ({
get isWindows() {
return state.isWindows;
},
}));
vi.mock("@/context/AppContext", () => ({
useApp: () => ({
appSettings: {
general: {
startup_restore: true,
startup_restore_window_layout: true,
minimize_to_tray: false,
boss_key: null,
confirm_on_close: true,
rdp_client_mode: "builtin",
},
ui: {
language: "en",
header_status_visible: true,
header_status_mode: "session",
},
diagnostics: { level: "info", retention_days: 7 },
},
updateAppSettings: state.updateAppSettings,
updateUi: state.updateUi,
}),
}));
vi.mock("@/hooks/useConfigTransfer", () => ({
useConfigTransfer: () => ({
handleExportDiagnostics: vi.fn(),
handleOpenLogs: vi.fn(),
}),
}));
vi.mock("react-i18next", async (importOriginal) => ({
...(await importOriginal<typeof import("react-i18next")>()),
useTranslation: () => ({
i18n: { changeLanguage: vi.fn() },
t: (key: string) => key,
}),
}));
describe("GeneralTab RDP client setting", () => {
beforeEach(() => {
vi.clearAllMocks();
state.isWindows = true;
});
it("shows the RDP client selector on Windows and saves Windows Remote Desktop", () => {
render(<GeneralTab />);
const field =
screen.getByText("settings.rdpClient").parentElement?.parentElement;
expect(field).not.toBeNull();
const combo = within(field as HTMLElement).getByRole("combobox");
fireEvent.keyDown(combo, { key: "ArrowDown" });
fireEvent.click(
screen.getByRole("option", { name: "settings.rdpClientWindows" }),
);
expect(state.updateAppSettings).toHaveBeenCalledWith({
general: {
startup_restore: true,
startup_restore_window_layout: true,
minimize_to_tray: false,
boss_key: null,
confirm_on_close: true,
rdp_client_mode: "windows",
},
});
});
it("does not show the Windows-only RDP client selector off Windows", () => {
state.isWindows = false;
render(<GeneralTab />);
expect(screen.queryByText("settings.rdpClient")).toBeNull();
});
});
+24
View File
@@ -5,6 +5,7 @@ import { useApp } from "@/context/AppContext";
import { useConfigTransfer } from "@/hooks/useConfigTransfer";
import { AVAILABLE_LANGUAGES } from "@/i18n";
import { HEADER_STATUS_MODES, normalizeHeaderStatusMode } from "@/lib/headerStatus";
import { isWindows } from "@/lib/platform";
import {
SettingFieldGrid,
SettingRow,
@@ -69,6 +70,29 @@ export function GeneralTab() {
</SettingSection>
<SettingSection contentClassName="space-y-4">
{isWindows && (
<SettingSelect
label={t("settings.rdpClient")}
desc={t("settings.rdpClientDesc")}
value={appSettings.general.rdp_client_mode ?? "builtin"}
onValueChange={(value) =>
updateAppSettings({
general: {
...appSettings.general,
rdp_client_mode: value === "windows" ? "windows" : "builtin",
},
})
}
>
<SelectItem value="builtin">
{t("settings.rdpClientBuiltin")}
</SelectItem>
<SelectItem value="windows">
{t("settings.rdpClientWindows")}
</SelectItem>
</SettingSelect>
)}
<SettingRow label={t("settings.startupRestore")} desc={t("settings.startupRestoreDesc")}>
<SettingSwitch
checked={appSettings.general.startup_restore}
+1
View File
@@ -77,6 +77,7 @@ const DEFAULT_APP_SETTINGS: AppSettings = {
minimize_to_tray: false,
boss_key: null,
confirm_on_close: true,
rdp_client_mode: "builtin",
},
appearance: {
theme: "github-dark",
+1
View File
@@ -46,6 +46,7 @@ const DEFAULT_APP_SETTINGS: AppSettings = {
minimize_to_tray: false,
boss_key: null,
confirm_on_close: true,
rdp_client_mode: "builtin",
},
appearance: {
theme: "github-dark",
+29
View File
@@ -9,6 +9,7 @@ const mocks = vi.hoisted(() => ({
invoke: vi.fn(),
closeStaleCreatedSession: vi.fn(),
focusTerminalSession: vi.fn(),
launchSavedRdpWithSystemClient: vi.fn(),
openNewSession: vi.fn(),
}));
vi.mock("@/lib/invoke", () => ({ invoke: mocks.invoke }));
@@ -17,6 +18,7 @@ vi.mock("@/lib/appSessionFactory", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/appSessionFactory")>()),
closeStaleCreatedSession: mocks.closeStaleCreatedSession,
focusTerminalSession: mocks.focusTerminalSession,
launchSavedRdpWithSystemClient: mocks.launchSavedRdpWithSystemClient,
}));
function deferred<T>() {
@@ -45,6 +47,7 @@ function renderConnect(overrides: Partial<Parameters<typeof useConnectAfterEdit>
setActiveTabId: vi.fn(),
updatePaneSession: vi.fn(),
updateTabSession: vi.fn(),
rdpClientMode: "builtin",
setTerminalWindows: vi.fn(),
updateAutoIconForSessionStart: vi.fn(),
...overrides,
@@ -62,6 +65,7 @@ beforeEach(() => {
command === "get_saved_connections" ? [connection] : "new-session",
);
mocks.closeStaleCreatedSession.mockResolvedValue(undefined);
mocks.launchSavedRdpWithSystemClient.mockResolvedValue(false);
});
describe("useConnectAfterEdit", () => {
@@ -183,6 +187,31 @@ describe("useConnectAfterEdit", () => {
});
});
it("launches a system RDP connection before creating a pending pane", async () => {
const rdpConnection = { ...connection, type: "rdp" as const };
mocks.invoke.mockResolvedValueOnce([rdpConnection]);
mocks.launchSavedRdpWithSystemClient.mockResolvedValueOnce(true);
const { result, options } = renderConnect({ rdpClientMode: "windows" });
await act(() => result.current({ connectionId: "conn-1" }));
expect(mocks.launchSavedRdpWithSystemClient).toHaveBeenCalledWith(
rdpConnection,
"windows",
);
expect(options.addPendingTab).not.toHaveBeenCalled();
expect(options.markPaneConnecting).not.toHaveBeenCalled();
expect(options.updatePaneSession).not.toHaveBeenCalled();
expect(options.updateTabSession).not.toHaveBeenCalled();
expect(options.updateAutoIconForSessionStart).not.toHaveBeenCalled();
expect(options.recordRecentConnection).toHaveBeenCalledWith("conn-1");
expect(mocks.invoke).toHaveBeenCalledTimes(1);
expect(mocks.invoke).not.toHaveBeenCalledWith(
"create_rdp_session",
expect.anything(),
);
});
it("preserves the ID fallback when the saved connection is missing", async () => {
mocks.invoke.mockResolvedValueOnce([]);
const { result, options } = renderConnect();
+23 -1
View File
@@ -1,4 +1,6 @@
import { type Dispatch, type SetStateAction, useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import type { AppContextType } from "@/context/AppContext";
import {
closeStaleCreatedSession,
@@ -6,13 +8,14 @@ import {
getConnectionSessionType,
getRemoteDesktopPaneDisplay,
isSessionCreationCancelled,
launchSavedRdpWithSystemClient,
} from "@/lib/appSessionFactory";
import { getErrorMessage, shouldPromptConnectionEditOnFailure } from "@/lib/errors";
import { invoke } from "@/lib/invoke";
import { insertTabIntoLeaf, type TerminalWindowNode } from "@/lib/tabWindows";
import { openNewSession } from "@/lib/windowManager";
import { findSessionPaneById, getActivePane } from "@/lib/workspaceTabs";
import type { SavedConnection } from "@/types/global";
import type { GeneralSettings, SavedConnection } from "@/types/global";
export interface SessionConnectAfterEditPayload {
connectionId: string;
@@ -41,6 +44,7 @@ interface ConnectAfterEditOptions
> {
setTerminalWindows: Dispatch<SetStateAction<TerminalWindowNode | null>>;
updateAutoIconForSessionStart: (connectionId: string, sessionId: string) => void;
rdpClientMode: GeneralSettings["rdp_client_mode"];
}
export function useConnectAfterEdit({
@@ -58,7 +62,9 @@ export function useConnectAfterEdit({
updateTabSession,
setTerminalWindows,
updateAutoIconForSessionStart,
rdpClientMode,
}: ConnectAfterEditOptions) {
const { t } = useTranslation();
const tabsRef = useRef(tabs);
useEffect(() => {
tabsRef.current = tabs;
@@ -71,6 +77,20 @@ export function useConnectAfterEdit({
const conns = await invoke<SavedConnection[]>("get_saved_connections");
const conn = conns.find((c) => c.id === connectionId);
const connName = conn?.name ?? connectionId;
if (conn) {
try {
if (await launchSavedRdpWithSystemClient(conn, rdpClientMode)) {
recordRecentConnection(connectionId);
return;
}
} catch (error) {
const errorMessage = getErrorMessage(error);
toast.error(
t("savedConnections.connectionFailed", { error: errorMessage }),
);
return;
}
}
const sessionType = getConnectionSessionType(conn);
const sourceTab = sourceTabId
? (tabsRef.current.find((item) => item.id === sourceTabId) ?? null)
@@ -200,12 +220,14 @@ export function useConnectAfterEdit({
markPaneConnectionFailed,
markTabConnectionFailed,
recordRecentConnection,
rdpClientMode,
setActivePane,
setActiveTabId,
updatePaneSession,
updateTabSession,
setTerminalWindows,
updateAutoIconForSessionStart,
t,
],
);
}
@@ -22,6 +22,7 @@ function renderLayout(tabs: Tab[], savedLayout: RestorableTerminalWindowNode | n
minimize_to_tray: false,
boss_key: null,
confirm_on_close: true,
rdp_client_mode: "builtin",
},
savedLayout,
};
+4
View File
@@ -2500,6 +2500,10 @@
"rdpCertificateVerifyNew": "New RDP certificate. Verify the fingerprint below.",
"rdpCertificateVerifyTitle": "RDP Certificate Verification",
"rdpCertificateVerifyWarning": "WARNING: The RDP certificate changed. Accept only if you trust this server.",
"rdpClient": "RDP client",
"rdpClientBuiltin": "NyaTerm built-in RDP",
"rdpClientDesc": "Choose how saved RDP connections open on Windows.",
"rdpClientWindows": "Windows Remote Desktop",
"reconnectRestoreCwd": "Restore last working directory on reconnect",
"reconnectRestoreCwdDesc": "When enabled, an SSH session reconnects to the last known working directory. Requires shell integration (bash/zsh/fish); falls back to the default behavior when no directory was recorded.",
"recordingAutoStart": "Auto-record new sessions",
+4
View File
@@ -2498,6 +2498,10 @@
"rdpCertificateVerifyNew": "새 RDP 인증서입니다. 아래 지문을 확인하세요.",
"rdpCertificateVerifyTitle": "RDP 인증서 검증",
"rdpCertificateVerifyWarning": "경고: RDP 인증서가 변경되었습니다. 이 서버를 신뢰할 때만 수락하세요.",
"rdpClient": "RDP 클라이언트",
"rdpClientBuiltin": "NyaTerm 내장 RDP",
"rdpClientDesc": "Windows에서 저장된 RDP 연결을 여는 방식을 선택합니다.",
"rdpClientWindows": "Windows 원격 데스크톱",
"reconnectRestoreCwd": "재연결 시 마지막 작업 디렉터리 복원",
"reconnectRestoreCwdDesc": "활성화하면 SSH 세션 재연결 시 연결이 끊어지기 전 작업 디렉터리로 cd 합니다. 셸 통합(bash/zsh/fish)이 필요하며 기록이 없으면 기존 동작을 유지합니다.",
"recordingAutoStart": "새 세션 자동 녹화",
+4
View File
@@ -2498,6 +2498,10 @@
"rdpCertificateVerifyNew": "新的 RDP 证书,请验证以下指纹。",
"rdpCertificateVerifyTitle": "RDP 证书验证",
"rdpCertificateVerifyWarning": "警告:RDP 证书已更改。请仅在你信任此服务器时接受。",
"rdpClient": "RDP 客户端",
"rdpClientBuiltin": "NyaTerm 内置 RDP",
"rdpClientDesc": "选择在 Windows 上打开已保存 RDP 连接的方式。",
"rdpClientWindows": "Windows 远程桌面",
"reconnectRestoreCwd": "重连后恢复上次工作目录",
"reconnectRestoreCwdDesc": "启用后,SSH 会话重连时自动 cd 回断开前的工作目录。需要 shell 集成(bash/zsh/fish)支持,无记录时保持原有行为。",
"recordingAutoStart": "自动录制新会话",
+4
View File
@@ -2493,6 +2493,10 @@
"rdpCertificateVerifyNew": "新的 RDP 憑證,請驗證以下指紋。",
"rdpCertificateVerifyTitle": "RDP 憑證驗證",
"rdpCertificateVerifyWarning": "警告:RDP 憑證已更改。請僅在你信任此伺服器時接受。",
"rdpClient": "RDP 用戶端",
"rdpClientBuiltin": "NyaTerm 內建 RDP",
"rdpClientDesc": "選擇在 Windows 上開啟已儲存 RDP 連線的方式。",
"rdpClientWindows": "Windows 遠端桌面",
"reconnectRestoreCwd": "重連後恢復上次工作目錄",
"reconnectRestoreCwdDesc": "啟用後,SSH 工作階段重連時自動 cd 回中斷前的工作目錄。需要 shell 整合(bash/zsh/fish)支援,無記錄時保持原有行為。",
"recordingAutoStart": "自動錄製新工作階段",
+53 -1
View File
@@ -1,7 +1,12 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { setOwnerMainWindowLabel } from "./windowManager";
import type { SavedConnection, TerminalSessionPane } from "@/types/global";
import { createSessionForConnection, createSessionForPane } from "./appSessionFactory";
import {
createSessionForConnection,
createSessionForPane,
launchSavedRdpWithSystemClient,
shouldLaunchSavedRdpWithSystemClient,
} from "./appSessionFactory";
const { invokeMock } = vi.hoisted(() => ({ invokeMock: vi.fn() }));
@@ -94,3 +99,50 @@ describe("VNC owner window routing", () => {
},
);
});
describe("Windows system RDP routing", () => {
beforeEach(() => {
invokeMock.mockReset().mockResolvedValue(undefined);
});
it("launches a saved RDP connection externally only on Windows system mode", async () => {
const connection = { id: "rdp-1", type: "rdp" } as SavedConnection;
expect(
shouldLaunchSavedRdpWithSystemClient(connection, "windows", true),
).toBe(true);
expect(
await launchSavedRdpWithSystemClient(connection, "windows", true),
).toBe(true);
expect(invokeMock).toHaveBeenCalledExactlyOnceWith("launch_windows_rdp", {
connectionId: "rdp-1",
});
});
it("keeps built-in and non-Windows RDP on the existing session path", async () => {
const connection = { id: "rdp-1", type: "rdp" } as SavedConnection;
expect(
shouldLaunchSavedRdpWithSystemClient(connection, "builtin", true),
).toBe(false);
expect(
shouldLaunchSavedRdpWithSystemClient(connection, "windows", false),
).toBe(false);
expect(
await launchSavedRdpWithSystemClient(connection, "builtin", true),
).toBe(false);
expect(
await launchSavedRdpWithSystemClient(connection, "windows", false),
).toBe(false);
expect(invokeMock).not.toHaveBeenCalled();
});
it("does not route non-RDP saved connections to Windows Remote Desktop", async () => {
const connection = { id: "ssh-1", type: "ssh" } as SavedConnection;
expect(
await launchSavedRdpWithSystemClient(connection, "windows", true),
).toBe(false);
expect(invokeMock).not.toHaveBeenCalled();
});
});
+22
View File
@@ -5,6 +5,7 @@ import { getErrorMessage } from "@/lib/errors";
import { invoke } from "@/lib/invoke";
import { getOwnerMainWindowLabel } from "@/lib/windowManager";
import { logger } from "@/lib/logger";
import { isWindows } from "@/lib/platform";
import {
buildTerminalCommandInput,
clearSessionCommandHistory,
@@ -13,6 +14,7 @@ import {
import type { TemporaryLinkConfig } from "@/lib/temporaryLink";
import { captureTerminalReconnectContent } from "@/lib/terminalReconnectHistory";
import type {
GeneralSettings,
SavedConnection,
SessionPane,
SessionType,
@@ -63,6 +65,26 @@ export function getRemoteDesktopPaneDisplay(
return undefined;
}
export function shouldLaunchSavedRdpWithSystemClient(
connection: Pick<SavedConnection, "type"> | null | undefined,
mode: GeneralSettings["rdp_client_mode"] | undefined,
windows = isWindows,
) {
return windows && mode === "windows" && connection?.type === "rdp";
}
export async function launchSavedRdpWithSystemClient(
connection: Pick<SavedConnection, "id" | "type">,
mode: GeneralSettings["rdp_client_mode"] | undefined,
windows = isWindows,
) {
if (!shouldLaunchSavedRdpWithSystemClient(connection, mode, windows)) {
return false;
}
await invoke("launch_windows_rdp", { connectionId: connection.id });
return true;
}
export function isSessionCreationCancelled(error: unknown) {
return getErrorMessage(error)
.toLowerCase()
+1
View File
@@ -1197,6 +1197,7 @@ export interface GeneralSettings {
minimize_to_tray: boolean;
boss_key: string | null;
confirm_on_close: boolean;
rdp_client_mode: "builtin" | "windows";
}
export type BackgroundImageFit = "cover" | "contain" | "stretch" | "tile";