Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions desktop/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,40 @@ function sanitizeAuxiliaryWindowTitle(value, fallback = PRODUCT_NAME) {
return title || fallback;
}

function desktopWindowTitleEndpointIdentity(value) {
let identity = String(value || "").trim().toLowerCase();
if (!identity) return "";
identity = identity.replace(/^\w+:\/\//, "").replace(/\/$/, "");
if (identity.includes("@")) identity = identity.slice(identity.lastIndexOf("@") + 1);
if (/^\[[^\]]+\](?::\d+)?$/.test(identity)) return identity.replace(/^\[|\](?::\d+)?$/g, "");
if ((identity.match(/:/g) || []).length === 1) identity = identity.replace(/:\d+$/, "");
return identity;
}

function normalizeMainWindowTitle(value) {
const sanitized = sanitizeAuxiliaryWindowTitle(value, PRODUCT_NAME);
const parts = sanitized.split(/\s*·\s*/).map(part => part.trim()).filter(Boolean);
if (parts.length < 4 || parts[0].toLowerCase() !== PRODUCT_NAME.toLowerCase()) return sanitized;

const [product, endpoint, originalLabel, ...originalResourceParts] = parts;
const resourceParts = [...originalResourceParts];
let label = originalLabel;
const generatedProtocolTitle = resourceParts.at(-1) || "";
const generatedProtocolMatch = generatedProtocolTitle.match(/^(.*?)(?:\s+#(\d+))?$/);
if (generatedProtocolMatch?.[1]?.trim().toLowerCase() === originalLabel.toLowerCase()) {
resourceParts.pop();
if (generatedProtocolMatch[2]) label = `${originalLabel} #${generatedProtocolMatch[2]}`;
}

const resource = resourceParts.join(" · ").trim();
const uniqueResource = resource
&& desktopWindowTitleEndpointIdentity(resource) !== desktopWindowTitleEndpointIdentity(endpoint)
&& resource.toLowerCase() !== label.toLowerCase()
? resource
: "";
return sanitizeAuxiliaryWindowTitle([product, endpoint, label, uniqueResource].filter(Boolean).join(" · "), PRODUCT_NAME);
}

function configureAuxiliaryDesktopWindow(window) {
if (!window || window.isDestroyed()) return;
window.webContents.setWindowOpenHandler(({ url }) => {
Expand Down Expand Up @@ -2848,6 +2882,12 @@ function writeDesktopClipboardImage(value) {
}

function registerDesktopClipboardHandlers() {
ipcMain.on("terma:set-window-title", (event, value) => {
const window = desktopWindowForSender(event);
if (!window || window !== mainWindow || window.isDestroyed()) return;
const title = normalizeMainWindowTitle(value);
window.setTitle(title);
});
ipcMain.handle("terma:clipboard-read", event => {
assertDesktopClipboardSender(event);
return clipboard.readText();
Expand Down
3 changes: 3 additions & 0 deletions desktop/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ contextBridge.exposeInMainWorld("termaDesktop", {
setInterfaceLanguage(language) {
if (language === "zh-CN" || language === "en-US") ipcRenderer.send("terma:set-interface-language", language);
},
setWindowTitle(title) {
ipcRenderer.send("terma:set-window-title", String(title || "Terma"));
},
readClipboardText() {
return ipcRenderer.invoke("terma:clipboard-read");
},
Expand Down
36 changes: 31 additions & 5 deletions public/app-docking.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,11 +522,11 @@ function workspaceTabHtml(tab, pane) {
return `<button class="tab ${tab.key === pane.activeTabKey ? "active" : ""}${multiSelected ? " multi-selected" : ""}${tab.pinned ? " pinned" : ""}${broadcastSelected ? " broadcast-selected" : ""}${tab.activityState ? ` activity-${escAttr(tab.activityState)}` : ""}" role="tab" aria-selected="${tab.key === pane.activeTabKey}" aria-checked="${multiSelected}" data-tab-key="${escAttr(tab.key)}" data-kind="${escAttr(tab.kind || "")}" title="${esc(fullTitle)}" aria-label="${esc(ariaLabel)}" data-pointerdown-action="workspace-tab-drag-start" data-action="workspace-tab-activate" data-contextmenu-action="workspace-tab-menu" data-dragover-action="workspace-tab-sftp-drag-over" data-dragleave-action="workspace-tab-sftp-drag-leave" data-drop-action="workspace-tab-sftp-drop">${connectionDot}${presentation.icon}${tab.pinned ? `<span class="tab-pin" aria-hidden="true">${icon("pin")}</span>` : ""}<span class="tab-title">${esc(presentation.title)}</span>${tab.closable && !tab.pinned ? `<span class="tab-close" title="${escAttr(closeText)}" aria-label="${escAttr(closeText)}" data-tab-key="${escAttr(tab.key)}" data-pointerdown-action="workspace-event-stop" data-action="workspace-tab-close">x</span>` : ""}</button>`;
}

renderTabs = function() {
renderTabs = function(options={}) {
if (!workspaceDockElement()) return legacyWorkspaceApi.renderTabs();
if (typeof syncSftpTabTitles === "function") syncSftpTabTitles();
reconcileWorkspaceLayoutTabs();
renderWorkspaceLayout();
if (options.rebuildLayout !== false) renderWorkspaceLayout();
for (const pane of workspaceVisiblePanes()) {
const paneElement = workspacePaneElement(pane.id);
if (!paneElement) continue;
Expand All @@ -550,6 +550,26 @@ renderTabs = function() {
if (!window.restoringTabs) saveTabsState();
};

function syncWorkspaceTabActivation(pane, key) {
for (const visiblePane of workspaceVisiblePanes()) {
const paneElement = workspacePaneElement(visiblePane.id);
if (!paneElement) continue;
paneElement.classList.toggle("focused", visiblePane.id === focusedPaneId);
for (const button of paneElement.querySelectorAll(".tabs .tab[data-tab-key]")) {
const active = button.dataset.tabKey === visiblePane.activeTabKey;
button.classList.toggle("active", active);
button.setAttribute("aria-selected", String(active));
if (active) button.classList.remove("activity-info", "activity-success", "activity-error");
}
}
const tab = tabs.find(item => item.key === key);
if (tab) tab.activityState = "";
syncWorkspaceLegacyTabIds();
renderWorkspaceGroupBar();
revealWorkspaceTab(key);
if (!window.restoringTabs) saveTabsState();
}

updateWorkspaceTabScrollControls = function(paneId=currentWorkspacePaneId()) {
const pane = workspacePaneElement(paneId);
const container = pane?.querySelector(".tabs");
Expand Down Expand Up @@ -818,8 +838,7 @@ activateTab = function(key) {
activeTabKey = key;
activeView = tab.viewName || tab.kind || "welcome";
if (typeof restoreSftpRuntimeForTab === "function" && tab.kind === "sftp") restoreSftpRuntimeForTab(tab.key);
renderTabs();
revealWorkspaceTab(key);
syncWorkspaceTabActivation(pane, key);
renderWorkspacePaneContent(pane.id);
syncFocusedWorkspaceClasses();
syncWorkspaceToolbarPlacements();
Expand Down Expand Up @@ -943,6 +962,7 @@ closeTabsByKey = function(keys, anchorKey="") {
}
if (typeof rememberClosedWorkspaceTabs === "function") rememberClosedWorkspaceTabs([...targets]);
const anchorPane = workspaceFindPaneForTab(anchorKey) || workspaceFindPane(focusedPaneId);
const paneActiveKeysBeforeClose = new Map(workspaceLeaves().map(pane => [pane.id, pane.activeTabKey]));
for (const key of targets) {
const tab = tabs.find(item => item.key === key);
closeTerminalSession(key);
Expand All @@ -969,11 +989,17 @@ closeTabsByKey = function(keys, anchorKey="") {
trimWorkspacePaneTabHistory(pane);
if (pane.activeTabKey) rememberWorkspacePaneTab(pane, pane.activeTabKey, pane.activeTabKey);
}
const paneIdsBeforeNormalize = workspaceLeaves().map(pane => pane.id).join("\0");
normalizeWorkspaceLayoutAfterMutation(anchorPane?.id || focusedPaneId);
const paneIdsAfterNormalize = workspaceLeaves().map(pane => pane.id).join("\0");
const focusedPane = workspaceFindPane(focusedPaneId) || workspaceLeaves()[0];
focusedPaneId = focusedPane.id;
activeTabKey = focusedPane.activeTabKey || "";
renderTabs();
renderTabs({rebuildLayout:paneIdsBeforeNormalize !== paneIdsAfterNormalize});
for (const pane of workspaceVisiblePanes()) {
if (pane.id === focusedPane.id || !pane.activeTabKey) continue;
if (paneActiveKeysBeforeClose.get(pane.id) !== pane.activeTabKey) renderWorkspacePaneContent(pane.id);
}
if (activeTabKey) {
const tab = tabs.find(item => item.key === activeTabKey);
activeView = tab?.viewName || tab?.kind || "welcome";
Expand Down
13 changes: 11 additions & 2 deletions public/app-linux-desktop.js
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,12 @@ async function installLinuxDesktop(desktopId, mode="", button=null) {
if (!await confirmModal(message, operationTitle, tr("common:actions.install", {defaultValue:"安装"}), tr("common:actions.cancel", {defaultValue:"取消"}), true)) return null;
let adminAuth = null;
if (!linuxDesktopManagerState.diagnostics?.privileged) {
adminAuth = await requestRemoteAdminAuthorization(id, operationTitle);
const grantScope = normalizedMode === "local-offline"
? "linux-desktop.install-local-offline"
: normalizedMode === "offline"
? "linux-desktop.install-offline"
: "linux-desktop.install";
adminAuth = await requestRemoteAdminAuthorization(id, operationTitle, grantScope);
if (!adminAuth) return null;
}
const task = await api(`/api/connections/${id}/linux-desktop/install`, {method:"POST", body:JSON.stringify({desktop_id:desktopId, mode:normalizedMode, ...(adminAuth ? {admin_auth:adminAuth} : {})})});
Expand Down Expand Up @@ -473,7 +478,11 @@ async function uninstallLinuxDesktop(desktopId, button=null) {
)) return null;
let adminAuth = null;
if (!linuxDesktopManagerState.diagnostics?.privileged) {
adminAuth = await requestRemoteAdminAuthorization(id, tr("remote:linux_desktop.uninstall_title", {defaultValue:"卸载 Linux 桌面"}));
adminAuth = await requestRemoteAdminAuthorization(
id,
tr("remote:linux_desktop.uninstall_title", {defaultValue:"卸载 Linux 桌面"}),
"linux-desktop.uninstall"
);
if (!adminAuth) return null;
}
const task = await api(`/api/connections/${id}/linux-desktop/uninstall`, {method:"POST", body:JSON.stringify({desktop_id:desktopId, ...(adminAuth ? {admin_auth:adminAuth} : {})})});
Expand Down
9 changes: 8 additions & 1 deletion public/app-remote-profiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,10 @@ async function openRemoteDesktop(id, updateTab=true, showManagement=false) {
}
const view = $("view-remote-desktop");
if (!view) return;
const browserReservation = updateTab && !showManagement && remoteDesktopQuickOpen && embeddedVnc && vncQuickOpenUsesNewWindow()
? reserveVncDetachedBrowserWindow(profile.id)
: null;
let browserReservationCommitted = false;
const renderScope = captureRemoteDesktopRenderScope(profile.id, key, view);
const embeddedXdmcp = profile.protocol === "xdmcp";
const managedRdp = profile.protocol === "rdp";
Expand Down Expand Up @@ -652,7 +656,8 @@ async function openRemoteDesktop(id, updateTab=true, showManagement=false) {
if (embeddedVnc && existingVncSession?.presentation === "management") syncEmbeddedVncManagementControls(existingVncSession, activeView);
const xdmcpDirectReady = !embeddedXdmcp || serverState?.ready_for_login || serverState?.management_available === false || Boolean(serverState?.error) || Boolean(serverState?.endpoint_probe?.ok);
if (updateTab && remoteDesktopQuickOpen && clientLaunchable && xdmcpDirectReady && rdpEndpointReady && vncReadyForLaunch) {
if (embeddedVnc) await openEmbeddedVncDesktop(profile.id, key);
if (embeddedVnc && vncQuickOpenUsesNewWindow()) browserReservationCommitted = await openVncInNewWindow(profile.id, key, {closeDetectionTab:true, browserReservation});
else if (embeddedVnc) await openEmbeddedVncDesktop(profile.id, key);
else await launchRemoteDesktop(profile.id, key);
}
});
Expand All @@ -663,5 +668,7 @@ async function openRemoteDesktop(id, updateTab=true, showManagement=false) {
status.className = "connection-test-status error";
status.textContent = error.message;
});
} finally {
if (!browserReservationCommitted) cancelReservedVncDetachedBrowserWindow(browserReservation);
}
}
6 changes: 3 additions & 3 deletions public/app-remote-rdp.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,16 +313,16 @@ async function installRdpServerImpl(profileId, key, button=null, mode="online")
: tr("remote:rdp_status.confirm_online_install", {defaultValue:"将通过远端软件源在线安装 xrdp 和对应的 Xorg 后端。是否继续?"});
const operationTitle = tr("remote:rdp_status.install_action_title", {mode:modeLabel, defaultValue:`${modeLabel}安装 RDP 服务`});
if (!await confirmModal(message, operationTitle, tr("common:actions.install", {defaultValue:"安装"}), tr("common:actions.cancel", {defaultValue:"取消"}), true)) return null;
const action = normalizedMode === "local-offline" ? "install-local-offline" : normalizedMode === "offline" ? "install-offline" : "install";
const sourceId = Number(diagnostics.connection?.id || linuxDesktopManagerConnectionIdForProfile(profile) || 0);
let adminAuth = null;
if (diagnostics.privileged !== true) {
if (!sourceId) return notify(tr("remote:rdp_status.ssh_management_missing", {defaultValue:"该 RDP 连接没有关联的 SSH 管理连接"}), "error");
adminAuth = await requestRemoteAdminAuthorization(sourceId, operationTitle);
adminAuth = await requestRemoteAdminAuthorization(sourceId, operationTitle, `rdp.server.${action}`);
if (!adminAuth) return null;
}
if (button && document.contains(button)) setButtonBusy(button, true, tr("common:auto.installing", {defaultValue:"安装中..."}));
try {
const action = normalizedMode === "local-offline" ? "install-local-offline" : normalizedMode === "offline" ? "install-offline" : "install";
const result = await api(`/api/remote-profiles/${Number(profileId)}/rdp/server`, {method:"POST", body:JSON.stringify({action, ...(adminAuth ? {admin_auth:adminAuth} : {})})});
if (result.task) {
const taskContainer = $("rdpServerState");
Expand Down Expand Up @@ -401,7 +401,7 @@ async function runRdpServerActionImpl(profileId, key, action, button=null) {
let adminAuth = null;
if (diagnostics.privileged !== true) {
if (!sourceId) return notify(tr("remote:rdp_status.ssh_management_missing", {defaultValue:"该 RDP 连接没有关联的 SSH 管理连接"}), "error");
adminAuth = await requestRemoteAdminAuthorization(sourceId, label);
adminAuth = await requestRemoteAdminAuthorization(sourceId, label, `rdp.server.${action}`);
if (!adminAuth) return null;
}
if (button && document.contains(button)) setButtonBusy(button, true, busyRdpServerActionLabel(action));
Expand Down
7 changes: 6 additions & 1 deletion public/app-remote.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const vncSessions = new Map();
let vncFullscreenSessionKey = "";
let remoteDesktopRenderSerial = 0;
const VNC_CLIPBOARD_POLL_INTERVAL_MS = 900;
const VNC_CLIPBOARD_IMAGE_POLL_INTERVAL_MS = 2600;
const VNC_CLIPBOARD_IMAGE_POLL_INTERVAL_MS = 8000;
const VNC_CLIPBOARD_IMAGE_INPUT_IDLE_MS = 1400;
const VNC_CLIPBOARD_ECHO_GUARD_MS = 3000;
const remoteAdminGrantCache = new Map();
let noVncRfbPromise = null;
Expand All @@ -45,6 +46,10 @@ let linuxDesktopTaskLogView = {taskId:"", expanded:false, follow:true, scrollTop
const linuxDesktopTaskMonitors = new Map();
let pendingRemoteGroupSelectValue = "默认分组";

function vncQuickOpenUsesNewWindow() {
return runtimeSettings?.saved?.vnc_quick_open_new_window !== false;
}

function rdpServerActionKey(profileId) {
return `rdp-server:${Number(profileId || 0)}`;
}
Expand Down
18 changes: 18 additions & 0 deletions public/app-settings-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ function normalizeRuntimeSettingsResponse(value={}) {
sftp_download_concurrency: Math.max(1, Math.min(8, Number(savedSource.sftp_download_concurrency) || 3)),
sftp_upload_concurrency: Math.max(1, Math.min(8, Number(savedSource.sftp_upload_concurrency) || 3)),
restore_workspace_tabs: savedSource.restore_workspace_tabs !== false,
remote_desktop_quick_open_enabled: savedSource.remote_desktop_quick_open_enabled === true,
vnc_quick_open_new_window: savedSource.vnc_quick_open_new_window !== false,
workspace_toolbar_placement: normalizeWorkspaceToolbarPlacement(savedSource.workspace_toolbar_placement),
saved: {
...savedSource,
Expand All @@ -130,6 +132,8 @@ function normalizeRuntimeSettingsResponse(value={}) {
sftp_upload_concurrency: Math.max(1, Math.min(8, Number(savedSource.sftp_upload_concurrency) || 3)),
sftp_download_directory: String(savedSource.sftp_download_directory || ""),
restore_workspace_tabs: savedSource.restore_workspace_tabs !== false,
remote_desktop_quick_open_enabled: savedSource.remote_desktop_quick_open_enabled === true,
vnc_quick_open_new_window: savedSource.vnc_quick_open_new_window !== false,
workspace_toolbar_placement: normalizeWorkspaceToolbarPlacement(savedSource.workspace_toolbar_placement)
},
effective: {
Expand All @@ -151,6 +155,10 @@ async function loadRuntimeSettings(refreshUi=false) {
runtimeSettingsCheck = null;
try {
runtimeSettings = normalizeRuntimeSettingsResponse(await api("/api/runtime-settings"));
const legacyQuickOpen = localStorage.getItem("remoteDesktopQuickOpen");
remoteDesktopQuickOpen = legacyQuickOpen === null
? runtimeSettings.saved.remote_desktop_quick_open_enabled === true
: legacyQuickOpen === "1";
await setTermaLanguage(runtimeSettings.saved.language, {render:false, emit:false});
} catch (error) {
runtimeSettings = normalizeRuntimeSettingsResponse({error:error.message || tr("settings:auto.runtime_load_failed", {defaultValue:"监听配置加载失败"})});
Expand Down Expand Up @@ -584,15 +592,21 @@ async function saveWorkspaceSettings() {
const vnc_fullscreen_toolbar = ["always", "never", "edge"].includes($("generalVncFullscreenToolbar")?.value)
? $("generalVncFullscreenToolbar").value
: "always";
const remote_desktop_quick_open_enabled = $("generalRemoteDesktopQuickOpen")?.checked === true;
const vnc_quick_open_new_window = $("generalVncQuickOpenNewWindow")?.checked !== false;
const result = await api("/api/runtime-settings", {
method:"PUT",
body:JSON.stringify({
restore_workspace_tabs:input.checked,
remote_desktop_quick_open_enabled,
vnc_quick_open_new_window,
workspace_toolbar_placement,
vnc_fullscreen_toolbar
})
});
runtimeSettings = normalizeRuntimeSettingsResponse({...runtimeSettings, ...result});
remoteDesktopQuickOpen = runtimeSettings.saved.remote_desktop_quick_open_enabled === true;
localStorage.removeItem("remoteDesktopQuickOpen");
await setTermaLanguage(runtimeSettings.saved.language);
inPane(() => {
renderSettings();
Expand All @@ -605,6 +619,10 @@ async function saveWorkspaceSettings() {
syncWorkspaceToolbarPlacementInputs(runtimeSettings?.saved?.workspace_toolbar_placement);
const vncToolbar = $("generalVncFullscreenToolbar");
if (vncToolbar) vncToolbar.value = runtimeSettings?.saved?.vnc_fullscreen_toolbar || "always";
const quickOpen = $("generalRemoteDesktopQuickOpen");
if (quickOpen) quickOpen.checked = remoteDesktopQuickOpen;
const newWindow = $("generalVncQuickOpenNewWindow");
if (newWindow) newWindow.checked = runtimeSettings?.saved?.vnc_quick_open_new_window !== false;
});
notify(error.message || tr("settings:auto.workspace_save_failed", {defaultValue:"工作区设置保存失败"}), "error");
} finally {
Expand Down
Loading