From a3aed9b8b680f4125d5324cfc166599912bd1f3a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 28 Mar 2026 11:14:43 +0700 Subject: [PATCH] Fix desktop terminal link and context menu behavior --- .../app/src/components/terminal-emulator.tsx | 16 ++++++++ packages/app/src/desktop/host.ts | 8 ++++ .../runtime/terminal-emulator-runtime.ts | 8 +++- packages/desktop/src/features/menu.ts | 39 ++++++++++++++++++- packages/desktop/src/preload.ts | 4 ++ 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/terminal-emulator.tsx b/packages/app/src/components/terminal-emulator.tsx index 867522f22..2416241ad 100644 --- a/packages/app/src/components/terminal-emulator.tsx +++ b/packages/app/src/components/terminal-emulator.tsx @@ -565,6 +565,18 @@ export default function TerminalEmulator({ const handleInsetTop = Math.max(0, (thumbRegionHeight - scrollbarGeometry.handleSize) / 2); const handleTravelDurationMs = isDraggingScrollbar || isScrollActive ? 0 : SCROLLBAR_HANDLE_TRAVEL_DURATION_MS; + const handleContextMenu = () => { + const showContextMenu = window.paseoDesktop?.menu?.showContextMenu; + if (typeof showContextMenu !== "function") { + return; + } + + const hasSelection = Boolean(window.getSelection()?.toString()); + void showContextMenu({ + kind: "terminal", + hasSelection, + }); + }; return (
{ runtimeRef.current?.focus(); }} + onContextMenu={(event) => { + event.preventDefault(); + handleContextMenu(); + }} >
Promise; } +export interface DesktopMenuBridge { + showContextMenu?: (input?: { + kind?: "terminal"; + hasSelection?: boolean; + }) => Promise; +} + export interface DesktopWindowBridge { label?: string; startMove?: (screenX: number, screenY: number) => void; @@ -73,6 +80,7 @@ export interface DesktopHostBridge { dialog?: DesktopDialogBridge; notification?: DesktopNotificationBridge; opener?: DesktopOpenerBridge; + menu?: DesktopMenuBridge; } declare global { diff --git a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts index bf766c052..e2a4b06f0 100644 --- a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts +++ b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts @@ -8,6 +8,7 @@ import { WebLinksAddon } from "@xterm/addon-web-links"; import { WebglAddon } from "@xterm/addon-webgl"; import { Terminal, type ITheme } from "@xterm/xterm"; import type { TerminalState } from "@server/shared/messages"; +import { openExternalUrl } from "@/utils/open-external-url"; import { type PendingTerminalModifiers, isTerminalModifierDomKey, @@ -168,7 +169,12 @@ export class TerminalEmulatorRuntime { let webglAddon: WebglAddon | null = null; terminal.loadAddon(fitAddon); terminal.loadAddon(unicode11Addon); - terminal.loadAddon(new WebLinksAddon()); + terminal.loadAddon( + new WebLinksAddon((event, uri) => { + event.preventDefault(); + void openExternalUrl(uri); + }), + ); terminal.loadAddon(new SearchAddon({ highlightLimit: 20_000 })); terminal.loadAddon(new ClipboardAddon()); try { diff --git a/packages/desktop/src/features/menu.ts b/packages/desktop/src/features/menu.ts index 6bb67006d..0ccb119a9 100644 --- a/packages/desktop/src/features/menu.ts +++ b/packages/desktop/src/features/menu.ts @@ -1,4 +1,9 @@ -import { app, Menu, BrowserWindow } from "electron"; +import { app, Menu, BrowserWindow, ipcMain } from "electron"; + +type ShowContextMenuInput = { + kind?: "terminal"; + hasSelection?: boolean; +}; function withBrowserWindow( callback: (win: BrowserWindow) => void, @@ -89,4 +94,36 @@ export function setupApplicationMenu(): void { const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); + + ipcMain.handle("paseo:menu:showContextMenu", (event, input?: ShowContextMenuInput) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (!win) { + return; + } + + if (input?.kind !== "terminal") { + return; + } + + const menu = Menu.buildFromTemplate([ + { + label: "Copy", + role: "copy", + enabled: input.hasSelection === true, + }, + { + label: "Paste", + role: "paste", + }, + { + type: "separator", + }, + { + label: "Select All", + role: "selectAll", + }, + ]); + + menu.popup({ window: win }); + }); } diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts index e79096815..54b429362 100644 --- a/packages/desktop/src/preload.ts +++ b/packages/desktop/src/preload.ts @@ -51,4 +51,8 @@ contextBridge.exposeInMainWorld("paseoDesktop", { opener: { openUrl: (url: string) => ipcRenderer.invoke("paseo:opener:openUrl", url), }, + menu: { + showContextMenu: (input?: Record) => + ipcRenderer.invoke("paseo:menu:showContextMenu", input), + }, });