From 79f135665289f300282874281c48b7583b7b7897 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:31:28 +1000 Subject: [PATCH 1/6] Revert "fix(linux): route X11 capture through real sources" This reverts commit 067070d40515a482fb2594ec834ec4a35a33277c. --- electron/ipc/register/sourceMapping.test.ts | 113 ------------------ electron/ipc/register/sourceMapping.ts | 59 --------- electron/ipc/register/sources.ts | 20 ++-- electron/main.ts | 15 +-- electron/windows.ts | 8 +- .../hooks/useLaunchHudInteractionState.ts | 49 +++----- src/hooks/useScreenRecorder.test.ts | 66 ---------- src/hooks/useScreenRecorder.ts | 39 +----- 8 files changed, 30 insertions(+), 339 deletions(-) delete mode 100644 electron/ipc/register/sourceMapping.test.ts delete mode 100644 electron/ipc/register/sourceMapping.ts diff --git a/electron/ipc/register/sourceMapping.test.ts b/electron/ipc/register/sourceMapping.test.ts deleted file mode 100644 index a4c67c16e..000000000 --- a/electron/ipc/register/sourceMapping.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { - getScreenSourceIdForDisplay, - LINUX_PORTAL_SCREEN_SOURCE_ID, - shouldUseSyntheticLinuxPortalSource, -} from "./sourceMapping"; - -describe("getScreenSourceIdForDisplay", () => { - it("keeps the live Electron screen source when one is available", () => { - expect( - getScreenSourceIdForDisplay({ - displayId: "42", - matchedSourceId: "screen:42:0", - platform: "linux", - }), - ).toBe("screen:42:0"); - }); - - it("routes unmatched Linux Wayland screens through the portal sentinel", () => { - expect( - getScreenSourceIdForDisplay({ - displayId: "42", - env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, - matchedSourceId: null, - platform: "linux", - }), - ).toBe(LINUX_PORTAL_SCREEN_SOURCE_ID); - }); - - it("keeps unmatched Linux X11 screens on the explicit fallback id", () => { - expect( - getScreenSourceIdForDisplay({ - displayId: "42", - env: { XDG_SESSION_TYPE: "x11", DISPLAY: ":0" }, - matchedSourceId: null, - platform: "linux", - }), - ).toBe("screen:fallback:42"); - }); - - it("keeps non-Linux unmatched screens on the explicit fallback id", () => { - expect( - getScreenSourceIdForDisplay({ - displayId: "42", - matchedSourceId: undefined, - platform: "win32", - }), - ).toBe("screen:fallback:42"); - }); -}); - -describe("shouldUseSyntheticLinuxPortalSource", () => { - it("keeps Wayland portal capture on the synthetic source path", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, - platform: "linux", - sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID, - }), - ).toBe(true); - }); - - it("lets X11 use Electron desktopCapturer sources instead of a synthetic id", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { XDG_SESSION_TYPE: "x11", DISPLAY: ":0" }, - platform: "linux", - sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID, - }), - ).toBe(false); - }); - - it("recovers stale fallback ids through the synthetic path on Wayland", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { WAYLAND_DISPLAY: "wayland-0" }, - platform: "linux", - sourceId: "screen:fallback:0", - }), - ).toBe(true); - }); - - it("defaults unknown Linux sessions with WAYLAND_DISPLAY to the synthetic path", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { WAYLAND_DISPLAY: "wayland-0" }, - platform: "linux", - sourceId: null, - }), - ).toBe(true); - }); - - it("does not synthesize for concrete source ids", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, - platform: "linux", - sourceId: "screen:42:0", - }), - ).toBe(false); - }); - - it("does not synthesize outside Linux", () => { - expect( - shouldUseSyntheticLinuxPortalSource({ - env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, - platform: "win32", - sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID, - }), - ).toBe(false); - }); -}); diff --git a/electron/ipc/register/sourceMapping.ts b/electron/ipc/register/sourceMapping.ts deleted file mode 100644 index b6a4366d6..000000000 --- a/electron/ipc/register/sourceMapping.ts +++ /dev/null @@ -1,59 +0,0 @@ -export const LINUX_PORTAL_SCREEN_SOURCE_ID = "screen:linux-portal"; - -export function isLikelyLinuxWaylandSession(env: NodeJS.ProcessEnv) { - const sessionType = env.XDG_SESSION_TYPE?.trim().toLowerCase(); - if (sessionType === "wayland") { - return true; - } - if (sessionType === "x11") { - return false; - } - - return Boolean(env.WAYLAND_DISPLAY); -} - -export function getScreenSourceIdForDisplay({ - displayId, - env = process.env, - matchedSourceId, - platform, -}: { - displayId: string; - env?: NodeJS.ProcessEnv; - matchedSourceId?: string | null; - platform: NodeJS.Platform | string; -}) { - if (matchedSourceId) { - return matchedSourceId; - } - - if (platform === "linux" && isLikelyLinuxWaylandSession(env)) { - return LINUX_PORTAL_SCREEN_SOURCE_ID; - } - - return `screen:fallback:${displayId}`; -} - -export function shouldUseSyntheticLinuxPortalSource({ - env, - platform, - sourceId, -}: { - env: NodeJS.ProcessEnv; - platform: NodeJS.Platform | string; - sourceId?: string | null; -}) { - if (platform !== "linux") { - return false; - } - - if ( - sourceId && - sourceId !== LINUX_PORTAL_SCREEN_SOURCE_ID && - !sourceId.startsWith("screen:fallback:") - ) { - return false; - } - - return isLikelyLinuxWaylandSession(env); -} diff --git a/electron/ipc/register/sources.ts b/electron/ipc/register/sources.ts index b59d9ead6..4ba6b35a6 100644 --- a/electron/ipc/register/sources.ts +++ b/electron/ipc/register/sources.ts @@ -1,20 +1,19 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { app, BrowserWindow, desktopCapturer, ipcMain } from "electron"; -import { reassertHudOverlayMousePassthrough } from "../../windows"; import { ALLOW_RECORDLY_WINDOW_CAPTURE } from "../constants"; +import { selectedSource, setSelectedSource } from "../state"; +import type { SelectedSource } from "../types"; +import { getScreen, parseWindowId } from "../utils"; +import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg"; import { getNativeMacWindowSources, - resolveLinuxWindowBounds, resolveMacWindowBounds, resolveWindowsWindowBounds, + resolveLinuxWindowBounds, stopWindowBoundsCapture, } from "../cursor/bounds"; -import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg"; -import { selectedSource, setSelectedSource } from "../state"; -import type { SelectedSource } from "../types"; -import { getScreen, parseWindowId } from "../utils"; -import { getScreenSourceIdForDisplay } from "./sourceMapping"; +import { reassertHudOverlayMousePassthrough } from "../../windows"; const execFileAsync = promisify(execFile); const SOURCE_LIST_CACHE_TTL_MS = 1200; @@ -126,12 +125,7 @@ export function registerSourceHandlers({ : `Screen ${index + 1}`; return { - id: getScreenSourceIdForDisplay({ - displayId, - env: process.env, - matchedSourceId: matchedSource?.id, - platform: process.platform, - }), + id: matchedSource?.id ?? `screen:fallback:${displayId}`, name: displayName, originalName: matchedSource?.name ?? displayName, display_id: displayId, diff --git a/electron/main.ts b/electron/main.ts index 68829e431..ed05ffeb6 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -25,7 +25,6 @@ import { killWindowsCaptureProcess, registerIpcHandlers, } from "./ipc/handlers"; -import { shouldUseSyntheticLinuxPortalSource } from "./ipc/register/sourceMapping"; import { ensureMediaServer } from "./mediaServer"; import { ensurePackagedRendererServer } from "./rendererServer"; import type { UpdateToastPayload } from "./updater"; @@ -232,7 +231,7 @@ function showHudOverlayFromTray() { if (process.platform === "win32" && isHudOverlayMousePassthroughSupported()) { hud.showInactive(); hud.moveTop(); - reassertHudOverlayMouseState({ interactiveGraceMs: 1200 }); + reassertHudOverlayMouseState(); return true; } @@ -1015,18 +1014,12 @@ app.whenReady().then(async () => { // is set we skip getSources entirely and hand back a synthetic // source id; Chromium then opens the portal once to actually // resolve the capture. - // Default to the sentinel on Linux/Wayland when no source has been + // Default to the sentinel on Linux when no source has been // pre-selected (e.g. fresh session where the renderer skipped the // source picker entirely). This avoids calling getSources() which // would itself trigger an extra portal dialog. - // X11 does not need this synthetic path; use Electron's documented - // desktopCapturer source flow there so getDisplayMedia receives a - // real source id instead of a Wayland-only portal sentinel. - const isLinuxPortalSentinel = shouldUseSyntheticLinuxPortalSource({ - env: process.env, - platform: process.platform, - sourceId, - }); + const isLinuxPortalSentinel = + process.platform === "linux" && (sourceId === "screen:linux-portal" || !sourceId); if (isLinuxPortalSentinel) { callback({ video: { id: "screen:0:0", name: "Entire screen" } }); return; diff --git a/electron/windows.ts b/electron/windows.ts index 6173fc034..811f43c51 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -642,11 +642,7 @@ export function getHudOverlayWindow(): BrowserWindow | null { * hover detection on the HUD is immediately restored without requiring the * user to move their mouse over the bar. */ -export function reassertHudOverlayMousePassthrough({ - interactiveGraceMs = 50, -}: { - interactiveGraceMs?: number; -} = {}): void { +export function reassertHudOverlayMousePassthrough(): void { if (process.platform !== "win32" || !isHudOverlayMousePassthroughSupported()) { return; } @@ -672,7 +668,7 @@ export function reassertHudOverlayMousePassthrough({ if (!hud.isDestroyed()) { setHudOverlayMousePassthrough(hudOverlayIgnoringMouse); } - }, interactiveGraceMs); + }, 50); } export function setHudOverlayRecordingActive(recording: boolean): void { diff --git a/src/components/launch/hooks/useLaunchHudInteractionState.ts b/src/components/launch/hooks/useLaunchHudInteractionState.ts index d7c086b12..9d0160a7b 100644 --- a/src/components/launch/hooks/useLaunchHudInteractionState.ts +++ b/src/components/launch/hooks/useLaunchHudInteractionState.ts @@ -13,23 +13,6 @@ export function useLaunchHudInteractionState({ }) { const isMouseOverHudRef = useRef(false); const timeoutRef = useRef(null); - const lastInteractiveReassertAtRef = useRef(0); - - const setHudMouseInteractive = useCallback((force = false) => { - const now = performance.now(); - if ( - !force && - isMouseOverHudRef.current && - now - lastInteractiveReassertAtRef.current < 250 - ) { - return; - } - - isMouseOverHudRef.current = true; - lastInteractiveReassertAtRef.current = now; - if (timeoutRef.current) clearTimeout(timeoutRef.current); - window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); - }, []); useEffect(() => { if (openId !== null) { @@ -45,7 +28,7 @@ export function useLaunchHudInteractionState({ }, [openId]); useEffect(() => { - const handleMouseTracking = (e: globalThis.MouseEvent) => { + const handleMouseOver = (e: globalThis.MouseEvent) => { const target = e.target as HTMLElement | null; if (!target) return; const isInteractive = !!target.closest( @@ -53,7 +36,9 @@ export function useLaunchHudInteractionState({ ); if (isInteractive) { - setHudMouseInteractive(); + isMouseOverHudRef.current = true; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); } else { isMouseOverHudRef.current = false; if (timeoutRef.current) clearTimeout(timeoutRef.current); @@ -70,26 +55,20 @@ export function useLaunchHudInteractionState({ } }; - window.addEventListener("mouseover", handleMouseTracking); - window.addEventListener("mousemove", handleMouseTracking); - return () => { - window.removeEventListener("mouseover", handleMouseTracking); - window.removeEventListener("mousemove", handleMouseTracking); - }; - }, [ - isHudDraggingRef, - isWebcamPreviewDraggingRef, - setHudMouseInteractive, - webcamPreviewDragStartRef, - ]); + window.addEventListener("mouseover", handleMouseOver); + return () => window.removeEventListener("mouseover", handleMouseOver); + }, [isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef]); const beginInteractiveHudAction = useCallback(() => { - setHudMouseInteractive(true); - }, [setHudMouseInteractive]); + isMouseOverHudRef.current = true; + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); + }, []); const handleHudMouseEnter = useCallback(() => { - setHudMouseInteractive(true); - }, [setHudMouseInteractive]); + isMouseOverHudRef.current = true; + if (timeoutRef.current) clearTimeout(timeoutRef.current); + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); + }, []); const handleHudMouseLeave = useCallback( (event: MouseEvent) => { diff --git a/src/hooks/useScreenRecorder.test.ts b/src/hooks/useScreenRecorder.test.ts index ed16b6a5b..509b049bd 100644 --- a/src/hooks/useScreenRecorder.test.ts +++ b/src/hooks/useScreenRecorder.test.ts @@ -7,8 +7,6 @@ import { normalizeBrowserMicrophoneProfile, resolveBrowserCaptureCursorPolicy, resolveLinuxPortalCursorPresentation, - shouldLockHudDuringDisplaySelection, - shouldUseLinuxPortalCapture, shouldUseNativeWindowsCaptureForSource, } from "./useScreenRecorder"; @@ -209,70 +207,6 @@ describe("resolveLinuxPortalCursorPresentation", () => { }); }); -describe("shouldUseLinuxPortalCapture", () => { - it("uses the portal when the selected source is the Linux sentinel", () => { - expect( - shouldUseLinuxPortalCapture({ - browserCaptureSourceId: "screen:linux-portal", - selectedSourceId: "screen:linux-portal", - }), - ).toBe(true); - }); - - it("uses the portal when a stale screen fallback resolves to the Linux sentinel", () => { - expect( - shouldUseLinuxPortalCapture({ - browserCaptureSourceId: "screen:linux-portal", - selectedSourceId: "screen:fallback:42", - }), - ).toBe(true); - }); - - it("keeps live Electron screen sources on browser getUserMedia", () => { - expect( - shouldUseLinuxPortalCapture({ - browserCaptureSourceId: "screen:42:0", - selectedSourceId: "screen:42:0", - }), - ).toBe(false); - }); - - it("prefers a live Electron source over stale portal selection state", () => { - expect( - shouldUseLinuxPortalCapture({ - browserCaptureSourceId: "screen:42:0", - selectedSourceId: "screen:linux-portal", - }), - ).toBe(false); - }); -}); - -describe("shouldLockHudDuringDisplaySelection", () => { - it("locks HUD fallback resizing while Linux portal selection is active", () => { - expect( - shouldLockHudDuringDisplaySelection({ - platform: "linux", - useLinuxPortal: true, - }), - ).toBe(true); - }); - - it("keeps non-portal capture flows interactive", () => { - expect( - shouldLockHudDuringDisplaySelection({ - platform: "linux", - useLinuxPortal: false, - }), - ).toBe(false); - expect( - shouldLockHudDuringDisplaySelection({ - platform: "win32", - useLinuxPortal: true, - }), - ).toBe(false); - }); -}); - describe("getScreenCaptureCursorSetting", () => { it("normalizes only supported screen-capture cursor settings", () => { expect(getScreenCaptureCursorSetting({ cursor: "motion" } as MediaTrackSettings)).toBe( diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index bb1f4e3da..9be2ff4fc 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -128,33 +128,6 @@ type DesktopCaptureMediaDevices = { getDisplayMedia: (constraints: unknown) => Promise; }; -export function shouldUseLinuxPortalCapture({ - browserCaptureSourceId, - selectedSourceId, -}: { - browserCaptureSourceId?: string; - selectedSourceId?: string; -}) { - if (browserCaptureSourceId && browserCaptureSourceId !== LINUX_PORTAL_SOURCE.id) { - return false; - } - - return ( - selectedSourceId === LINUX_PORTAL_SOURCE.id || - browserCaptureSourceId === LINUX_PORTAL_SOURCE.id - ); -} - -export function shouldLockHudDuringDisplaySelection({ - platform, - useLinuxPortal, -}: { - platform?: string; - useLinuxPortal: boolean; -}) { - return platform === "linux" && useLinuxPortal; -} - type UseScreenRecorderReturn = { recording: boolean; paused: boolean; @@ -711,7 +684,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { // on Wayland that triggers an additional xdg-desktop-portal dialog. // The sentinel is handled later by routing through getDisplayMedia, // which lets the portal pick the source in a single dialog. - if (source.id === LINUX_PORTAL_SOURCE.id) { + if (source.id === "screen:linux-portal") { return source; } @@ -1467,7 +1440,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { // Persist the synthetic Linux portal sentinel to main so that the // setDisplayMediaRequestHandler can short-circuit getSources() and // avoid triggering an extra portal dialog. - if (!existingSource && selectedSource.id === LINUX_PORTAL_SOURCE.id) { + if (!existingSource && selectedSource.id === "screen:linux-portal") { try { await window.electronAPI.selectSource(selectedSource); } catch (err) { @@ -1699,13 +1672,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { let videoTrack: MediaStreamTrack | undefined; let systemAudioIncluded = false; const mediaDevices = navigator.mediaDevices as DesktopCaptureMediaDevices; - const useLinuxPortal = shouldUseLinuxPortalCapture({ - browserCaptureSourceId: browserCaptureSource.id, - selectedSourceId: selectedSource.id, - }); - if (shouldLockHudDuringDisplaySelection({ platform, useLinuxPortal })) { - setHudSourceSelectionActive(true); - } + const useLinuxPortal = selectedSource.id === "screen:linux-portal"; const browserScreenVideoConstraints = { mandatory: { chromeMediaSource: CHROME_MEDIA_SOURCE, From d2796fbf849821b6855470349da8c2c27360e7c8 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:32:11 +1000 Subject: [PATCH 2/6] Revert "fix(linux): expand HUD fallback for menus" This reverts commit e2802bffad587f75049883e6b1565487e072a1e0. --- electron/hudOverlayBounds.test.ts | 52 ------------------------------- electron/hudOverlayBounds.ts | 21 ------------- electron/windows.ts | 31 +++--------------- 3 files changed, 4 insertions(+), 100 deletions(-) diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts index e9daa53ae..dac1058c8 100644 --- a/electron/hudOverlayBounds.test.ts +++ b/electron/hudOverlayBounds.test.ts @@ -3,8 +3,6 @@ import { describe, expect, it } from "vitest"; import { getHudOverlayWindowBounds, resizeHudOverlayFallbackBounds, - shouldExpandHudOverlayFallback, - shouldResizeHudOverlayFallback, } from "./hudOverlayBounds"; describe("getHudOverlayWindowBounds", () => { @@ -145,53 +143,3 @@ describe("resizeHudOverlayFallbackBounds", () => { }); }); }); - -describe("shouldResizeHudOverlayFallback", () => { - it("allows non-passthrough HUD windows to expand for menus when idle", () => { - expect(shouldResizeHudOverlayFallback(false, false)).toBe(true); - }); - - it("does not resize full passthrough HUD windows", () => { - expect(shouldResizeHudOverlayFallback(true, false)).toBe(false); - }); - - it("keeps the recording HUD compact in non-passthrough mode", () => { - expect(shouldResizeHudOverlayFallback(false, true)).toBe(false); - }); - - it("keeps the fallback stable while source selection is active", () => { - expect(shouldResizeHudOverlayFallback(false, false, true)).toBe(false); - }); -}); - -describe("shouldExpandHudOverlayFallback", () => { - it("expands while recording only when the floating webcam preview is visible", () => { - expect( - shouldExpandHudOverlayFallback({ - fallbackExpanded: false, - recordingActive: true, - webcamPreviewVisible: true, - }), - ).toBe(true); - }); - - it("keeps the compact recording fallback when there is no webcam preview", () => { - expect( - shouldExpandHudOverlayFallback({ - fallbackExpanded: false, - recordingActive: true, - webcamPreviewVisible: false, - }), - ).toBe(false); - }); - - it("preserves manual fallback expansion outside recording", () => { - expect( - shouldExpandHudOverlayFallback({ - fallbackExpanded: true, - recordingActive: false, - webcamPreviewVisible: false, - }), - ).toBe(true); - }); -}); diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts index 2a8bff829..22facf93d 100644 --- a/electron/hudOverlayBounds.ts +++ b/electron/hudOverlayBounds.ts @@ -37,27 +37,6 @@ export function getHudOverlayWindowBounds( height, }; } - -export function shouldResizeHudOverlayFallback( - mousePassthroughSupported: boolean, - recordingActive: boolean, - interactionLocked = false, -): boolean { - return !mousePassthroughSupported && !recordingActive && !interactionLocked; -} - -export function shouldExpandHudOverlayFallback({ - fallbackExpanded, - recordingActive, - webcamPreviewVisible, -}: { - fallbackExpanded: boolean; - recordingActive: boolean; - webcamPreviewVisible: boolean; -}): boolean { - return fallbackExpanded || (recordingActive && webcamPreviewVisible); -} - export function resizeHudOverlayFallbackBounds( workArea: HudOverlayWorkArea, currentBounds: HudOverlayWorkArea, diff --git a/electron/windows.ts b/electron/windows.ts index 811f43c51..7da43a64a 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -5,12 +5,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { app, BrowserWindow, ipcMain } from "electron"; import { USER_DATA_PATH } from "./appPaths"; -import { - getHudOverlayWindowBounds, - resizeHudOverlayFallbackBounds, - shouldExpandHudOverlayFallback, - shouldResizeHudOverlayFallback, -} from "./hudOverlayBounds"; +import { getHudOverlayWindowBounds, resizeHudOverlayFallbackBounds } from "./hudOverlayBounds"; import { getPackagedRendererBaseUrl } from "./rendererServer"; const electronWindowsDir = path.dirname(fileURLToPath(import.meta.url)); @@ -196,15 +191,10 @@ function getHudOverlayDisplay() { function getHudOverlayBounds() { const { workArea } = getHudOverlayDisplay(); - const fallbackExpanded = shouldExpandHudOverlayFallback({ - fallbackExpanded: hudOverlayFallbackExpanded, - recordingActive: hudOverlayRecordingActive, - webcamPreviewVisible: hudOverlayWebcamPreviewVisible, - }); return getHudOverlayWindowBounds( workArea, isHudOverlayMousePassthroughSupported() && !hudOverlayRecordingActive, - fallbackExpanded, + hudOverlayFallbackExpanded, ); } @@ -311,21 +301,8 @@ function setHudOverlayMousePassthrough(ignore: boolean) { return; } - if (hudOverlaySourceSelectionActive) { - hudOverlayFallbackExpanded = false; - hudOverlayWindow.setIgnoreMouseEvents(false); - return; - } - - const mousePassthroughSupported = isHudOverlayMousePassthroughSupported(); - if (!mousePassthroughSupported) { - if ( - shouldResizeHudOverlayFallback( - mousePassthroughSupported, - hudOverlayRecordingActive, - hudOverlaySourceSelectionActive, - ) - ) { + if (!isHudOverlayMousePassthroughSupported()) { + if (process.platform !== "linux") { setHudOverlayFallbackExpanded(!ignore); } hudOverlayWindow.setIgnoreMouseEvents(false); From f23a41934f5b3b56bfc5bf3443653bd5dd039bfe Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:32:28 +1000 Subject: [PATCH 3/6] Revert "fix(recording): preserve webcam preview HUD bounds" This reverts commit 46810c5cdbabf28101a650de4e4ce62c5ef6ccea. --- electron/electron-env.d.ts | 1 - electron/preload.ts | 3 --- electron/windows.ts | 14 -------------- src/components/launch/LaunchWindow.tsx | 10 ---------- 4 files changed, 28 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index c0cf0cdb9..c5958754a 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -209,7 +209,6 @@ interface Window { hudOverlayHide: () => void; hudOverlayClose: () => void; hudOverlayRendererReady: () => void; - hudOverlaySetWebcamPreviewVisible: (visible: boolean) => void; getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>; getHudOverlayMousePassthroughSupported: () => Promise<{ success: boolean; diff --git a/electron/preload.ts b/electron/preload.ts index 384682a17..9943095bf 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -182,9 +182,6 @@ contextBridge.exposeInMainWorld("electronAPI", { hudOverlayRendererReady: () => { ipcRenderer.send("hud-overlay-renderer-ready"); }, - hudOverlaySetWebcamPreviewVisible: (visible: boolean) => { - ipcRenderer.send("hud-overlay-set-webcam-preview-visible", visible); - }, getHudOverlayCaptureProtection: () => { return ipcRenderer.invoke("get-hud-overlay-capture-protection"); }, diff --git a/electron/windows.ts b/electron/windows.ts index 7da43a64a..478584b55 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -30,7 +30,6 @@ let hudOverlayIgnoringMouse = true; let hudOverlaySourceSelectionActive = false; let hudOverlayMouseReassertTimer: NodeJS.Timeout | null = null; let hudOverlayRecordingActive = false; -let hudOverlayWebcamPreviewVisible = false; let countdownWindow: BrowserWindow | null = null; let updateToastWindow: BrowserWindow | null = null; @@ -411,18 +410,6 @@ ipcMain.handle("get-hud-overlay-mouse-passthrough-supported", () => { }; }); -ipcMain.on("hud-overlay-set-webcam-preview-visible", (_event, visible: boolean) => { - const nextVisible = Boolean(visible); - if (hudOverlayWebcamPreviewVisible === nextVisible) { - return; - } - - hudOverlayWebcamPreviewVisible = nextVisible; - if (hudOverlayRecordingActive) { - applyHudOverlayBounds(); - } -}); - ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) => { loadHudOverlayCaptureProtectionSetting(); hudOverlayHiddenFromCapture = Boolean(enabled); @@ -445,7 +432,6 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) export function createHudOverlayWindow(): BrowserWindow { loadHudOverlayCaptureProtectionSetting(); hudOverlayFallbackExpanded = false; - hudOverlayWebcamPreviewVisible = false; const initialBounds = getHudOverlayBounds(); let hasShownHudWindow = false; diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 09cca63ef..d7dc51bc4 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -152,16 +152,6 @@ function LaunchWindowContent() { hudOverlayMousePassthroughSupported, }); - useEffect(() => { - window.electronAPI?.hudOverlaySetWebcamPreviewVisible?.(showRecordingWebcamPreview); - }, [showRecordingWebcamPreview]); - - useEffect(() => { - return () => { - window.electronAPI?.hudOverlaySetWebcamPreviewVisible?.(false); - }; - }, []); - const { recordingHudOffset, isHudDragging, From 18041911df192f858b16b4a4b7cf992653dcf101 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:32:51 +1000 Subject: [PATCH 4/6] revert: undo linux cursor-overlay portal guards --- electron/electron-env.d.ts | 9 +- electron/ipc/register/project.ts | 9 +- electron/ipc/register/settings.ts | 2 +- electron/ipc/types.ts | 1 - electron/preload.ts | 2 - src/components/video-editor/SettingsPanel.tsx | 10 --- src/components/video-editor/VideoEditor.tsx | 16 +--- src/hooks/useScreenRecorder.test.ts | 59 ------------- src/hooks/useScreenRecorder.ts | 82 ------------------- 9 files changed, 8 insertions(+), 182 deletions(-) diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index c5958754a..a3f663f21 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -676,7 +676,6 @@ interface Window { options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }, ) => Promise<{ success: boolean; webcamPath: string | null }>; setCurrentRecordingSession: ( @@ -685,7 +684,6 @@ interface Window { webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }, options?: { preserveProjectPath?: boolean }, ) => Promise<{ success: boolean }>; @@ -696,7 +694,6 @@ interface Window { webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }; }>; getCurrentVideoPath: () => Promise<{ success: boolean; path?: string }>; @@ -842,11 +839,7 @@ interface Window { /** Returns the app version from package.json */ getAppVersion: () => Promise; /** Hide the OS cursor before browser capture starts. */ - hideOsCursor: () => Promise<{ - success: boolean; - unsupported?: boolean; - platform?: string; - }>; + hideOsCursor: () => Promise<{ success: boolean }>; /** Recording preferences (mic, system audio) */ getRecordingPreferences: () => Promise<{ success: boolean; diff --git a/electron/ipc/register/project.ts b/electron/ipc/register/project.ts index 7af983b08..a65fd9070 100644 --- a/electron/ipc/register/project.ts +++ b/electron/ipc/register/project.ts @@ -533,7 +533,7 @@ export function registerProjectHandlers() { return { success: false, error: String(error), message: 'Failed to open projects folder.' } } }) - ipcMain.handle('set-current-video-path', async (_, path: string, options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean; nativeCaptureUnavailable?: boolean }) => { + ipcMain.handle('set-current-video-path', async (_, path: string, options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean }) => { setCurrentVideoPath(normalizeVideoSourcePath(path) ?? path) approveUserPath(currentVideoPath) const resolvedSession = await resolveRecordingSession(currentVideoPath) @@ -548,10 +548,6 @@ export function registerProjectHandlers() { hideOverlayCursorByDefault: normalizeBoolean(options?.hideOverlayCursorByDefault) || normalizeBoolean(resolvedSession.hideOverlayCursorByDefault), - nativeCaptureUnavailable: - normalizeBoolean( - options?.nativeCaptureUnavailable ?? resolvedSession.nativeCaptureUnavailable, - ), } setCurrentRecordingSession(nextSession) @@ -577,7 +573,7 @@ export function registerProjectHandlers() { return { success: true, webcamPath: nextSession.webcamPath ?? null } }) - ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; nativeCaptureUnavailable?: boolean }, options?: { preserveProjectPath?: boolean }) => { + ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean }, options?: { preserveProjectPath?: boolean }) => { const normalizedVideoPath = normalizeVideoSourcePath(session.videoPath) ?? session.videoPath setCurrentVideoPath(normalizedVideoPath) setCurrentRecordingSession({ @@ -585,7 +581,6 @@ export function registerProjectHandlers() { webcamPath: normalizeVideoSourcePath(session.webcamPath ?? null), timeOffsetMs: normalizeRecordingTimeOffsetMs(session.timeOffsetMs), hideOverlayCursorByDefault: normalizeBoolean(session.hideOverlayCursorByDefault), - nativeCaptureUnavailable: normalizeBoolean(session.nativeCaptureUnavailable), }); await rememberApprovedLocalReadPath(currentRecordingSession!.videoPath) await rememberApprovedLocalReadPath(currentRecordingSession!.webcamPath) diff --git a/electron/ipc/register/settings.ts b/electron/ipc/register/settings.ts index 5b4498a87..e84f63171 100644 --- a/electron/ipc/register/settings.ts +++ b/electron/ipc/register/settings.ts @@ -114,7 +114,7 @@ export function registerSettingsHandlers() { // --------------------------------------------------------------------------- ipcMain.handle("hide-cursor", () => { if (process.platform !== "win32") { - return { success: false, unsupported: true, platform: process.platform }; + return { success: true }; } return { success: hideCursor() }; diff --git a/electron/ipc/types.ts b/electron/ipc/types.ts index 9680da73b..58f5425bd 100644 --- a/electron/ipc/types.ts +++ b/electron/ipc/types.ts @@ -48,7 +48,6 @@ export type RecordingSessionData = { webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }; export type PauseSegment = { diff --git a/electron/preload.ts b/electron/preload.ts index 9943095bf..04695d6be 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -690,7 +690,6 @@ contextBridge.exposeInMainWorld("electronAPI", { options?: { preserveProjectPath?: boolean; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }, ) => { return ipcRenderer.invoke("set-current-video-path", path, options); @@ -701,7 +700,6 @@ contextBridge.exposeInMainWorld("electronAPI", { webcamPath?: string | null; timeOffsetMs?: number; hideOverlayCursorByDefault?: boolean; - nativeCaptureUnavailable?: boolean; }, options?: { preserveProjectPath?: boolean }, ) => { diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 1a4c19551..1fa526616 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -3587,7 +3587,6 @@ export function SettingsPanel({ @@ -3596,20 +3595,11 @@ export function SettingsPanel({ - {nativeCaptureUnavailableSession ? ( -
- {tSettings( - "effects.cursorOverlayUnavailable", - "Cursor overlay is unavailable for this recording because the captured video already contains the system cursor.", - )} -
- ) : null}
{ - if (nextShowCursor && sessionNativeCaptureUnavailable) { - setNativeCaptureUnavailableModalOpen(true); - return; - } - - setSessionShowCursorOverride(null); - setShowCursor(nextShowCursor); - }, - [sessionNativeCaptureUnavailable], - ); + const handleShowCursorChange = useCallback((nextShowCursor: boolean) => { + setSessionShowCursorOverride(null); + setShowCursor(nextShowCursor); + }, []); const remountPreview = useCallback(() => { setIsPreviewReady(false); diff --git a/src/hooks/useScreenRecorder.test.ts b/src/hooks/useScreenRecorder.test.ts index 509b049bd..85811467e 100644 --- a/src/hooks/useScreenRecorder.test.ts +++ b/src/hooks/useScreenRecorder.test.ts @@ -3,10 +3,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { createBrowserRecordingOptions, createProcessedMicrophoneConstraints, - getScreenCaptureCursorSetting, normalizeBrowserMicrophoneProfile, resolveBrowserCaptureCursorPolicy, - resolveLinuxPortalCursorPresentation, shouldUseNativeWindowsCaptureForSource, } from "./useScreenRecorder"; @@ -147,7 +145,6 @@ describe("resolveBrowserCaptureCursorPolicy", () => { streamCursor: "never", hideOsCursorBeforeRecording: true, hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: false, }); }); @@ -158,64 +155,8 @@ describe("resolveBrowserCaptureCursorPolicy", () => { streamCursor: "always", hideOsCursorBeforeRecording: false, hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, }); }); - - it("does not fake OS cursor hiding on Linux portal capture", () => { - expect(resolveBrowserCaptureCursorPolicy({ platform: "linux" })).toEqual({ - streamCursor: "never", - hideOsCursorBeforeRecording: false, - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, - }); - }); -}); - -describe("resolveLinuxPortalCursorPresentation", () => { - it("enables the Recordly overlay only when the portal confirms cursor-hidden capture", () => { - expect( - resolveLinuxPortalCursorPresentation({ - requestedCursor: "never", - actualCursor: "never", - }), - ).toEqual({ - hideEditorOverlayCursorByDefault: false, - nativeCaptureUnavailable: false, - }); - }); - - it("keeps the overlay disabled when the portal embeds or omits cursor settings", () => { - expect( - resolveLinuxPortalCursorPresentation({ - requestedCursor: "never", - actualCursor: "always", - }), - ).toEqual({ - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, - }); - expect( - resolveLinuxPortalCursorPresentation({ - requestedCursor: "never", - actualCursor: null, - }), - ).toEqual({ - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, - }); - }); -}); - -describe("getScreenCaptureCursorSetting", () => { - it("normalizes only supported screen-capture cursor settings", () => { - expect(getScreenCaptureCursorSetting({ cursor: "motion" } as MediaTrackSettings)).toBe( - "motion", - ); - expect( - getScreenCaptureCursorSetting({ cursor: "hidden" } as MediaTrackSettings), - ).toBeNull(); - }); }); describe("shouldUseNativeWindowsCaptureForSource", () => { diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 9be2ff4fc..a382ea3d5 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -46,12 +46,10 @@ export type BrowserMicrophoneProfile = | "no-noise-suppression" | "raw"; type BrowserCaptureCursorMode = "always" | "never"; -type BrowserCaptureCursorSetting = BrowserCaptureCursorMode | "motion"; export type BrowserCaptureCursorPolicy = { streamCursor: BrowserCaptureCursorMode; hideOsCursorBeforeRecording: boolean; hideEditorOverlayCursorByDefault: boolean; - nativeCaptureUnavailable: boolean; }; const DEFAULT_BROWSER_MICROPHONE_PROFILE: BrowserMicrophoneProfile = "processed"; const BROWSER_MICROPHONE_PROFILES = new Set([ @@ -192,10 +190,8 @@ export function normalizeBrowserMicrophoneProfile(value?: string | null): Browse export function resolveBrowserCaptureCursorPolicy({ nativeWindowsCaptureStartFailed = false, - platform, }: { nativeWindowsCaptureStartFailed?: boolean; - platform?: string; } = {}): BrowserCaptureCursorPolicy { if (nativeWindowsCaptureStartFailed) { // If WGC already failed, avoid the telemetry overlay path that can lag on @@ -204,19 +200,6 @@ export function resolveBrowserCaptureCursorPolicy({ streamCursor: "always", hideOsCursorBeforeRecording: false, hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, - }; - } - - if (platform === "linux") { - // Linux screen capture runs through xdg-desktop-portal/PipeWire. Ask the - // portal to omit the cursor, but do not pretend we can globally hide the - // OS cursor from Electron when the portal/compositor ignores that request. - return { - streamCursor: "never", - hideOsCursorBeforeRecording: false, - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, }; } @@ -224,37 +207,6 @@ export function resolveBrowserCaptureCursorPolicy({ streamCursor: "never", hideOsCursorBeforeRecording: true, hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: false, - }; -} - -export function getScreenCaptureCursorSetting( - settings: MediaTrackSettings | null | undefined, -): BrowserCaptureCursorSetting | null { - const cursor = (settings as { cursor?: unknown } | null | undefined)?.cursor; - return cursor === "always" || cursor === "never" || cursor === "motion" ? cursor : null; -} - -export function resolveLinuxPortalCursorPresentation({ - actualCursor, - requestedCursor, -}: { - actualCursor: BrowserCaptureCursorSetting | null; - requestedCursor: BrowserCaptureCursorMode; -}): Pick< - BrowserCaptureCursorPolicy, - "hideEditorOverlayCursorByDefault" | "nativeCaptureUnavailable" -> { - if (requestedCursor === "never" && actualCursor === "never") { - return { - hideEditorOverlayCursorByDefault: false, - nativeCaptureUnavailable: false, - }; - } - - return { - hideEditorOverlayCursorByDefault: true, - nativeCaptureUnavailable: true, }; } @@ -420,7 +372,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { ); const requestedBrowserMicrophoneProfile = useRef(null); const hideEditorOverlayCursorByDefault = useRef(false); - const nativeCaptureUnavailableForCursorOverlay = useRef(false); const notifyRecordingFinalizationFailure = useCallback(async (message: string) => { setFinalizing(false); @@ -729,7 +680,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const start = performance.now(); console.log("[PERF:RENDERER] Finalize Session & Switch to Editor: STARTED"); const shouldHideOverlayCursor = hideEditorOverlayCursorByDefault.current; - const nativeCaptureUnavailable = nativeCaptureUnavailableForCursorOverlay.current; try { if (webcamPath) { await window.electronAPI.setCurrentRecordingSession({ @@ -737,12 +687,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn { webcamPath, timeOffsetMs: webcamTimeOffsetMs.current, hideOverlayCursorByDefault: shouldHideOverlayCursor, - nativeCaptureUnavailable, }); } else { await window.electronAPI.setCurrentVideoPath(videoPath, { hideOverlayCursorByDefault: shouldHideOverlayCursor, - nativeCaptureUnavailable, }); } } catch (error) { @@ -751,7 +699,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { try { await window.electronAPI.setCurrentVideoPath(videoPath, { hideOverlayCursorByDefault: shouldHideOverlayCursor, - nativeCaptureUnavailable, }); } catch (fallbackError) { console.error("Failed to persist fallback video path:", fallbackError); @@ -1219,8 +1166,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { webcamPath, timeOffsetMs: webcamTimeOffsetMs.current, hideOverlayCursorByDefault: hideEditorOverlayCursorByDefault.current, - nativeCaptureUnavailable: - nativeCaptureUnavailableForCursorOverlay.current, }); console.log( @@ -1429,7 +1374,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { try { const platform = await window.electronAPI.getPlatform(); hideEditorOverlayCursorByDefault.current = false; - nativeCaptureUnavailableForCursorOverlay.current = false; const existingSource = await window.electronAPI.getSelectedSource(); const selectedSource = existingSource ?? (platform === "linux" ? LINUX_PORTAL_SOURCE : null); @@ -1636,12 +1580,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { const browserCursorPolicy = resolveBrowserCaptureCursorPolicy({ nativeWindowsCaptureStartFailed, - platform, }); hideEditorOverlayCursorByDefault.current = browserCursorPolicy.hideEditorOverlayCursorByDefault; - nativeCaptureUnavailableForCursorOverlay.current = - browserCursorPolicy.nativeCaptureUnavailable; const wantsAudioCapture = microphoneEnabled || systemAudioEnabled; const browserCaptureSource = await resolveBrowserCaptureSource(selectedSource); @@ -1825,27 +1766,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { throw new Error("Media stream is not available."); } - if (useLinuxPortal) { - const actualCursor = getScreenCaptureCursorSetting(videoTrack.getSettings()); - const cursorPresentation = resolveLinuxPortalCursorPresentation({ - actualCursor, - requestedCursor: browserCursorPolicy.streamCursor, - }); - hideEditorOverlayCursorByDefault.current = - cursorPresentation.hideEditorOverlayCursorByDefault; - nativeCaptureUnavailableForCursorOverlay.current = - cursorPresentation.nativeCaptureUnavailable; - if (cursorPresentation.nativeCaptureUnavailable) { - console.warn( - "Linux portal did not confirm cursor-hidden capture; disabling Recordly cursor overlay for this recording.", - { - actualCursor, - requestedCursor: browserCursorPolicy.streamCursor, - }, - ); - } - } - try { await videoTrack.applyConstraints({ frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE }, @@ -1951,8 +1871,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn { timeOffsetMs: webcamTimeOffsetMs.current, hideOverlayCursorByDefault: hideEditorOverlayCursorByDefault.current, - nativeCaptureUnavailable: - nativeCaptureUnavailableForCursorOverlay.current, }); } } finally { From 489e3ca2681614e3c6779026e4c2279ef1d7e1f7 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:19:28 +1000 Subject: [PATCH 5/6] fix(hud): keep overlay interactive while menus are open --- .../launch/hooks/useLaunchHudInteractionState.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/launch/hooks/useLaunchHudInteractionState.ts b/src/components/launch/hooks/useLaunchHudInteractionState.ts index 9d0160a7b..42f60b72a 100644 --- a/src/components/launch/hooks/useLaunchHudInteractionState.ts +++ b/src/components/launch/hooks/useLaunchHudInteractionState.ts @@ -16,6 +16,7 @@ export function useLaunchHudInteractionState({ useEffect(() => { if (openId !== null) { + if (timeoutRef.current) clearTimeout(timeoutRef.current); window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); } else { // Proactively check if we should ignore mouse when popover closes @@ -39,11 +40,12 @@ export function useLaunchHudInteractionState({ isMouseOverHudRef.current = true; if (timeoutRef.current) clearTimeout(timeoutRef.current); window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); - } else { + } else if (openId === null) { isMouseOverHudRef.current = false; if (timeoutRef.current) clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => { if ( + openId === null && !isHudDraggingRef.current && !isWebcamPreviewDraggingRef.current && !webcamPreviewDragStartRef.current && @@ -57,7 +59,7 @@ export function useLaunchHudInteractionState({ window.addEventListener("mouseover", handleMouseOver); return () => window.removeEventListener("mouseover", handleMouseOver); - }, [isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef]); + }, [openId, isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef]); const beginInteractiveHudAction = useCallback(() => { isMouseOverHudRef.current = true; @@ -83,6 +85,7 @@ export function useLaunchHudInteractionState({ timeoutRef.current = setTimeout(() => { if ( + openId === null && !isHudDraggingRef.current && !isWebcamPreviewDraggingRef.current && !webcamPreviewDragStartRef.current && @@ -92,7 +95,7 @@ export function useLaunchHudInteractionState({ } }, 300); }, - [isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef], + [openId, isHudDraggingRef, isWebcamPreviewDraggingRef, webcamPreviewDragStartRef], ); return { From e99356a04bd277ee4fb8e85276365fef318ada1a Mon Sep 17 00:00:00 2001 From: webadderall Date: Sat, 6 Jun 2026 20:01:19 +1000 Subject: [PATCH 6/6] fix(linux): preserve portal screen source mapping --- electron/ipc/register/sourceMapping.test.ts | 50 +++++++++++++++++++++ electron/ipc/register/sourceMapping.ts | 35 +++++++++++++++ electron/ipc/register/sources.ts | 8 +++- 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 electron/ipc/register/sourceMapping.test.ts create mode 100644 electron/ipc/register/sourceMapping.ts diff --git a/electron/ipc/register/sourceMapping.test.ts b/electron/ipc/register/sourceMapping.test.ts new file mode 100644 index 000000000..d0b68e750 --- /dev/null +++ b/electron/ipc/register/sourceMapping.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; + +import { + getScreenSourceIdForDisplay, + LINUX_PORTAL_SCREEN_SOURCE_ID, +} from "./sourceMapping"; + +describe("getScreenSourceIdForDisplay", () => { + it("keeps the live Electron screen source when one is available", () => { + expect( + getScreenSourceIdForDisplay({ + displayId: "42", + matchedSourceId: "screen:42:0", + platform: "linux", + }), + ).toBe("screen:42:0"); + }); + + it("routes unmatched Linux Wayland screens through the portal sentinel", () => { + expect( + getScreenSourceIdForDisplay({ + displayId: "42", + env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, + matchedSourceId: null, + platform: "linux", + }), + ).toBe(LINUX_PORTAL_SCREEN_SOURCE_ID); + }); + + it("keeps unmatched Linux X11 screens on the explicit fallback id", () => { + expect( + getScreenSourceIdForDisplay({ + displayId: "42", + env: { XDG_SESSION_TYPE: "x11", DISPLAY: ":0" }, + matchedSourceId: null, + platform: "linux", + }), + ).toBe("screen:fallback:42"); + }); + + it("keeps non-Linux unmatched screens on the explicit fallback id", () => { + expect( + getScreenSourceIdForDisplay({ + displayId: "42", + matchedSourceId: undefined, + platform: "win32", + }), + ).toBe("screen:fallback:42"); + }); +}); \ No newline at end of file diff --git a/electron/ipc/register/sourceMapping.ts b/electron/ipc/register/sourceMapping.ts new file mode 100644 index 000000000..a61b4cf72 --- /dev/null +++ b/electron/ipc/register/sourceMapping.ts @@ -0,0 +1,35 @@ +export const LINUX_PORTAL_SCREEN_SOURCE_ID = "screen:linux-portal"; + +export function isLikelyLinuxWaylandSession(env: NodeJS.ProcessEnv) { + const sessionType = env.XDG_SESSION_TYPE?.trim().toLowerCase(); + if (sessionType === "wayland") { + return true; + } + if (sessionType === "x11") { + return false; + } + + return Boolean(env.WAYLAND_DISPLAY); +} + +export function getScreenSourceIdForDisplay({ + displayId, + env = process.env, + matchedSourceId, + platform, +}: { + displayId: string; + env?: NodeJS.ProcessEnv; + matchedSourceId?: string | null; + platform: NodeJS.Platform | string; +}) { + if (matchedSourceId) { + return matchedSourceId; + } + + if (platform === "linux" && isLikelyLinuxWaylandSession(env)) { + return LINUX_PORTAL_SCREEN_SOURCE_ID; + } + + return `screen:fallback:${displayId}`; +} \ No newline at end of file diff --git a/electron/ipc/register/sources.ts b/electron/ipc/register/sources.ts index 4ba6b35a6..33c9ee74c 100644 --- a/electron/ipc/register/sources.ts +++ b/electron/ipc/register/sources.ts @@ -6,6 +6,7 @@ import { selectedSource, setSelectedSource } from "../state"; import type { SelectedSource } from "../types"; import { getScreen, parseWindowId } from "../utils"; import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg"; +import { getScreenSourceIdForDisplay } from "./sourceMapping"; import { getNativeMacWindowSources, resolveMacWindowBounds, @@ -125,7 +126,12 @@ export function registerSourceHandlers({ : `Screen ${index + 1}`; return { - id: matchedSource?.id ?? `screen:fallback:${displayId}`, + id: getScreenSourceIdForDisplay({ + displayId, + env: process.env, + matchedSourceId: matchedSource?.id, + platform: process.platform, + }), name: displayName, originalName: matchedSource?.name ?? displayName, display_id: displayId,