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.
This commit is contained in:
brooklyn!
2026-09-24 05:07:58 -05:00
parent 41755854ad
commit 9e7239acfd
4 changed files with 120 additions and 8 deletions

View File

@@ -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))

View File

@@ -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()
})
})

View File

@@ -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)
}

View File

@@ -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.