From 9e7239acfd378dba807493b74b589c016d88a59b Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Thu, 24 Sep 2026 05:07:58 -0500 Subject: [PATCH] fix(desktop): pass wayland ozone on native Wayland sessions Native Linux Wayland stayed on XWayland because the relaunch only ran for WSLg. Append --ozone-platform=wayland when the user did not already choose a platform. An explicit x11 hint and desktop.electron_flags still win. --- apps/desktop/electron/entry.ts | 26 +++++++++- apps/desktop/electron/wslg-launch.test.ts | 42 ++++++++++++++++ apps/desktop/electron/wslg-launch.ts | 58 +++++++++++++++++++++-- website/docs/user-guide/desktop.md | 2 +- 4 files changed, 120 insertions(+), 8 deletions(-) diff --git a/apps/desktop/electron/entry.ts b/apps/desktop/electron/entry.ts index 3830b95a42..da5b31fee9 100644 --- a/apps/desktop/electron/entry.ts +++ b/apps/desktop/electron/entry.ts @@ -1,9 +1,31 @@ +import { readFileSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' + import { app } from 'electron' +import { readDesktopLaunchConfig } from './renderer-heap-flags' import { wslgLaunchArgs } from './wslg-launch' import { spawnWslgLaunch } from './wslg-launch-process' -const args = wslgLaunchArgs(process.argv.slice(1), process.env, process.platform) +function configuredElectronFlags(env: NodeJS.ProcessEnv): string[] { + const raw = env.HERMES_HOME + + const home = raw + ? path.resolve(raw === '~' || raw.startsWith('~/') ? path.join(os.homedir(), raw.slice(1)) : raw) + : env.HERMES_DESKTOP_USER_DATA_DIR + ? path.join(path.resolve(env.HERMES_DESKTOP_USER_DATA_DIR), 'hermes-home') + : path.join(os.homedir(), '.hermes') + + try { + return readDesktopLaunchConfig(readFileSync(path.join(home, 'config.yaml'), 'utf8')).electronFlags + } catch { + return [] + } +} + +const electronFlags = process.platform === 'linux' ? configuredElectronFlags(process.env) : [] +const args = wslgLaunchArgs(process.argv.slice(1), process.env, process.platform, undefined, electronFlags) if (args) { // Keep the launcher alive until the child exits: npm's concurrently must not @@ -13,7 +35,7 @@ if (args) { const child = spawnWslgLaunch(args) child.once('error', error => { - console.error('[hermes] WSLg launch failed:', error) + console.error('[hermes] Wayland ozone launch failed:', error) app.exit(1) }) child.once('exit', code => app.exit(code ?? 1)) diff --git a/apps/desktop/electron/wslg-launch.test.ts b/apps/desktop/electron/wslg-launch.test.ts index 1433f21342..844614fb31 100644 --- a/apps/desktop/electron/wslg-launch.test.ts +++ b/apps/desktop/electron/wslg-launch.test.ts @@ -40,3 +40,45 @@ describe('WSLg launch arguments', () => { expect(wslgLaunchArgs([], { ...env, DISPLAY: 'localhost:10.0' }, 'linux')).toBeNull() }) }) + +const nativeWayland = { XDG_SESSION_TYPE: 'wayland', WAYLAND_DISPLAY: 'wayland-0', DISPLAY: ':0' } + +describe('native Wayland launch arguments', () => { + it('appends wayland on a native Linux Wayland session when the user did not choose a platform', () => { + expect(wslgLaunchArgs(['.'], nativeWayland, 'linux', false)).toEqual(['.', '--ozone-platform=wayland']) + expect(wslgLaunchArgs([], { WAYLAND_DISPLAY: 'wayland-0', DISPLAY: ':0' }, 'linux', false)).toEqual([ + '--ozone-platform=wayland' + ]) + expect(wslgLaunchArgs([], { XDG_SESSION_TYPE: 'wayland', DISPLAY: ':0' }, 'linux', false)).toEqual([ + '--ozone-platform=wayland' + ]) + expect(wslgLaunchArgs(['--ozone-platform=wayland'], nativeWayland, 'linux', false)).toBeNull() + }) + + it('keeps an explicit x11 platform already on the command line', () => { + expect(wslgLaunchArgs(['--ozone-platform=x11'], nativeWayland, 'linux', false)).toBeNull() + expect(wslgLaunchArgs(['--ozone-platform', 'x11'], nativeWayland, 'linux', false)).toBeNull() + }) + + it('lets an x11 hint win over the automatic wayland platform', () => { + expect(wslgLaunchArgs([], { ...nativeWayland, ELECTRON_OZONE_PLATFORM_HINT: 'x11' }, 'linux', false)).toEqual([ + '--ozone-platform=x11' + ]) + expect(wslgLaunchArgs(['--ozone-platform-hint=x11'], nativeWayland, 'linux', false)).toEqual([ + '--ozone-platform-hint=x11', + '--ozone-platform=x11' + ]) + }) + + it('lets desktop.electron_flags choose the ozone platform instead of forcing wayland', () => { + expect(wslgLaunchArgs([], nativeWayland, 'linux', false, ['--ozone-platform=x11'])).toEqual([ + '--ozone-platform=x11' + ]) + expect(wslgLaunchArgs(['.'], nativeWayland, 'linux', false, ['--disable-gpu'])).toEqual([ + '.', + '--ozone-platform=wayland' + ]) + expect(wslgLaunchArgs([], { XDG_SESSION_TYPE: 'x11', DISPLAY: ':0' }, 'linux', false)).toBeNull() + expect(wslgLaunchArgs([], { ...nativeWayland, SSH_CONNECTION: 'remote' }, 'linux', false)).toBeNull() + }) +}) diff --git a/apps/desktop/electron/wslg-launch.ts b/apps/desktop/electron/wslg-launch.ts index 3ec1accbf4..5d47aa2546 100644 --- a/apps/desktop/electron/wslg-launch.ts +++ b/apps/desktop/electron/wslg-launch.ts @@ -6,21 +6,69 @@ export function wslgLaunchArgs( argv: readonly string[], env: NodeJS.ProcessEnv, platform: NodeJS.Platform, - isWsl = isWslEnvironment(env, platform) + isWsl = isWslEnvironment(env, platform), + electronFlags: readonly string[] = [] ): string[] | null { const displayEnv = { ...env, HERMES_DESKTOP_DISABLE_GPU: undefined } - if (platform !== 'linux' || !isWsl || !env.WAYLAND_DISPLAY || detectRemoteDisplay({ env: displayEnv, platform })) { + // Remote/forwarded displays stay on the software-rendering path. A local + // Wayland session — native Linux or WSLg — otherwise defaults to XWayland + // unless the platform is on argv before application JavaScript. + if (platform !== 'linux' || detectRemoteDisplay({ env: displayEnv, platform })) { return null } - if (argv.some(arg => arg === '--ozone-platform' || arg.startsWith('--ozone-platform='))) { + const nativeWayland = env.XDG_SESSION_TYPE === 'wayland' || Boolean(env.WAYLAND_DISPLAY) + const wslgWayland = Boolean(isWsl && env.WAYLAND_DISPLAY) + + if (!nativeWayland && !wslgWayland) { return null } - const hintArg = argv.findLast(arg => arg.startsWith('--ozone-platform-hint=')) - const hint = hintArg?.split('=')[1] ?? env.ELECTRON_OZONE_PLATFORM_HINT + if (hasOzonePlatform(argv)) { + return null + } + + // desktop.electron_flags are not always on argv yet (.desktop entry, source + // launches). An explicit platform there wins over the automatic default, and + // must be on the relaunch command line or appendSwitch applies it too late. + const flagged = explicitOzonePlatform(electronFlags) + + if (flagged !== null) { + return flagged ? [...argv, `--ozone-platform=${flagged}`] : null + } + + const hintArg = findOzoneHint(argv) ?? findOzoneHint(electronFlags) + const hint = hintArg ?? env.ELECTRON_OZONE_PLATFORM_HINT const backend = hint === 'x11' ? 'x11' : 'wayland' return [...argv, `--ozone-platform=${backend}`] } + +function hasOzonePlatform(args: readonly string[]): boolean { + return args.some(arg => arg === '--ozone-platform' || arg.startsWith('--ozone-platform=')) +} + +function explicitOzonePlatform(args: readonly string[]): string | null { + for (let i = 0; i < args.length; i += 1) { + const arg = args[i] ?? '' + + if (arg === '--ozone-platform') { + const next = args[i + 1] + + return next && !next.startsWith('-') ? next : '' + } + + if (arg.startsWith('--ozone-platform=')) { + return arg.slice('--ozone-platform='.length) + } + } + + return null +} + +function findOzoneHint(args: readonly string[]): string | undefined { + const hintArg = args.findLast(arg => arg.startsWith('--ozone-platform-hint=')) + + return hintArg?.slice('--ozone-platform-hint='.length) +} diff --git a/website/docs/user-guide/desktop.md b/website/docs/user-guide/desktop.md index 00caeff2b8..80625c23d1 100644 --- a/website/docs/user-guide/desktop.md +++ b/website/docs/user-guide/desktop.md @@ -195,7 +195,7 @@ Talk to Hermes and hear it back, the same [voice mode](./features/voice-mode.md) #### Linux / Wayland -Electron 20+ already runs as a native Wayland client on a Wayland session. Drag, click-through, and resize work on that path. +On a local Wayland session (`XDG_SESSION_TYPE=wayland`, or `WAYLAND_DISPLAY` set) Hermes launches with `--ozone-platform=wayland` so Electron does not fall back to XWayland. The platform has to be on the process command line before application JavaScript loads. An explicit `--ozone-platform`, `desktop.ozone_platform_hint: x11`, or an ozone platform in `desktop.electron_flags` still wins. Drag, click-through, and resize work on the native Wayland path. On **Hyprland** (including Omarchy) the HUD is floated and pinned through the compositor's IPC after it maps — otherwise Hyprland tiles it like any other window, `always-on-top` is ignored, and compositor drag does nothing. No extra window rule is required.