fix(desktop): say why read_window_below cannot see the windows
When enumeration was impossible the tool answered "could not determine the window underneath (the desktop app did not answer, or window enumeration is unavailable on this system)" — true, and a dead end. On Linux the two ways it fails have opposite fixes and neither is guessable from that: a Wayland session withholds window identity from applications outright, while an X11 session needs xprop and xwininfo installed, because that is what the enumerator shells out to. Answer with the reason instead of nothing. A session with both WAYLAND_DISPLAY and DISPLAY is XWayland, where xprop can still answer, so it gets the tooling advice rather than being told to change session type.
This commit is contained in:
@@ -9140,6 +9140,7 @@ function startHudCursorFeed(win: BrowserWindow) {
|
||||
win.getBounds(),
|
||||
win.webContents.getZoomFactor()
|
||||
)
|
||||
|
||||
// Off-window is a real answer (it is what hands the mouse back), so it is
|
||||
// sent — once. Only an unchanged answer is dropped, to keep an idle cursor
|
||||
// from waking the renderer 16 times a second.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { type EnumeratedWindow, pickWindowBelow } from './window-below'
|
||||
import { type EnumeratedWindow, enumerationFailureNote, pickWindowBelow } from './window-below'
|
||||
|
||||
const win = (pid: number, x = 0, y = 0, width = 800, height = 600, app = `app-${pid}`): EnumeratedWindow => ({
|
||||
app,
|
||||
@@ -84,3 +84,36 @@ describe('pickWindowBelow', () => {
|
||||
expect(below).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('enumerationFailureNote', () => {
|
||||
it('tells a Wayland user the session is the problem', () => {
|
||||
for (const env of [{ XDG_SESSION_TYPE: 'wayland' }, { WAYLAND_DISPLAY: 'wayland-0' }]) {
|
||||
expect(enumerationFailureNote('linux', env)).toMatch(/Wayland/)
|
||||
}
|
||||
})
|
||||
|
||||
it('tells an X11 user which commands are missing', () => {
|
||||
const note = enumerationFailureNote('linux', { XDG_SESSION_TYPE: 'x11', DISPLAY: ':0' })
|
||||
|
||||
expect(note).toMatch(/xprop/)
|
||||
expect(note).not.toMatch(/Wayland/)
|
||||
})
|
||||
|
||||
// XWayland can still answer through xprop, so the fix is the tooling, not
|
||||
// switching session type.
|
||||
it('treats Wayland with an X display as X11', () => {
|
||||
const note = enumerationFailureNote('linux', { WAYLAND_DISPLAY: 'wayland-0', DISPLAY: ':0' })
|
||||
|
||||
expect(note).toMatch(/xprop/)
|
||||
expect(note).not.toMatch(/Wayland/)
|
||||
})
|
||||
|
||||
it('does not offer Linux advice on other platforms', () => {
|
||||
for (const platform of ['darwin', 'win32']) {
|
||||
const note = enumerationFailureNote(platform, {})
|
||||
|
||||
expect(note).not.toMatch(/xprop|Wayland/)
|
||||
expect(note.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// `window.read.request` from the gateway, asks main over IPC, and answers
|
||||
// with this module's serialized result. Enumeration uses `get-windows`
|
||||
// (front-to-back z-order on macOS/Windows/Linux-X11); the picking logic is a
|
||||
// pure function so the OS-specific part stays a thin provider.
|
||||
// pure function so the OS-specific part stays a thin provider. Where that
|
||||
// provider can't run at all, the answer is why — see `enumerationFailureNote`.
|
||||
//
|
||||
// Privacy contract (matches the tool schema): metadata only — app, title,
|
||||
// bounds. Never pixels. On macOS, window titles require the Screen Recording
|
||||
@@ -31,6 +32,45 @@ export interface WindowBelowResult {
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface WindowBelowUnavailable {
|
||||
error: string
|
||||
platform: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Why enumeration just failed, in terms the user can act on.
|
||||
*
|
||||
* The generic "could not determine the window underneath" this replaces is a
|
||||
* dead end on Linux, where the two ways it fails have opposite fixes and
|
||||
* neither is guessable: a Wayland session withholds window identity from
|
||||
* applications outright, and an X11 session needs `xprop`/`xwininfo` present
|
||||
* because that is what the enumerator shells out to.
|
||||
*
|
||||
* A session with both `WAYLAND_DISPLAY` and `DISPLAY` is Wayland running
|
||||
* XWayland, where `xprop` can still answer — so it is treated as X11 and gets
|
||||
* the tooling advice rather than being told to change session type.
|
||||
*/
|
||||
export function enumerationFailureNote(platform: string, env: NodeJS.ProcessEnv): string {
|
||||
if (platform !== 'linux') {
|
||||
return 'Could not enumerate windows on this system.'
|
||||
}
|
||||
|
||||
const wayland = env.XDG_SESSION_TYPE === 'wayland' || (Boolean(env.WAYLAND_DISPLAY) && !env.DISPLAY)
|
||||
|
||||
if (wayland) {
|
||||
return (
|
||||
'Could not enumerate windows: this is a Wayland session, and Wayland does ' +
|
||||
'not let an application see other applications\u2019 windows. Log in to an ' +
|
||||
'X11/Xorg session, or run Hermes under XWayland with DISPLAY set.'
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
'Could not enumerate windows: this needs the xprop and xwininfo commands ' +
|
||||
'(the x11-utils package on Debian/Ubuntu, xorg-x11-utils on Fedora).'
|
||||
)
|
||||
}
|
||||
|
||||
const overlaps = (a: EnumeratedWindow['bounds'], b: EnumeratedWindow['bounds']): boolean =>
|
||||
a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height
|
||||
|
||||
@@ -81,15 +121,21 @@ const loadGetWindows = (): Promise<GetWindowsModule> => {
|
||||
* Enumerate windows and serialize the one underneath `selfBounds`.
|
||||
*
|
||||
* `titlesAvailable` is the macOS Screen Recording grant (pass true on other
|
||||
* platforms, where titles are free). Returns null only when enumeration
|
||||
* itself is unavailable (Wayland, missing xprop, addon load failure) — the
|
||||
* caller turns that into an empty tool answer.
|
||||
* platforms, where titles are free). When enumeration itself is unavailable
|
||||
* (Wayland, missing xprop, addon load failure) this answers with the reason
|
||||
* rather than nothing, so the agent can tell the user what to fix instead of
|
||||
* reporting a blank failure.
|
||||
*/
|
||||
export async function readWindowBelow(
|
||||
selfPid: number,
|
||||
selfBounds: EnumeratedWindow['bounds'],
|
||||
titlesAvailable: boolean
|
||||
): Promise<WindowBelowResult | null> {
|
||||
): Promise<WindowBelowResult | WindowBelowUnavailable> {
|
||||
const unavailable = (): WindowBelowUnavailable => ({
|
||||
error: enumerationFailureNote(process.platform, process.env),
|
||||
platform: process.platform
|
||||
})
|
||||
|
||||
let raw
|
||||
|
||||
try {
|
||||
@@ -100,11 +146,11 @@ export async function readWindowBelow(
|
||||
: undefined
|
||||
)
|
||||
} catch {
|
||||
return null
|
||||
return unavailable()
|
||||
}
|
||||
|
||||
if (!Array.isArray(raw)) {
|
||||
return null
|
||||
return unavailable()
|
||||
}
|
||||
|
||||
// get-windows documents openWindows() as front-to-back, and macOS/Windows
|
||||
|
||||
@@ -158,9 +158,9 @@ export {
|
||||
bindGeometryPersistence,
|
||||
computeWindowOptions,
|
||||
debounce,
|
||||
GEOMETRY_EVENTS,
|
||||
DEFAULT_HEIGHT,
|
||||
DEFAULT_WIDTH,
|
||||
GEOMETRY_EVENTS,
|
||||
MIN_HEIGHT,
|
||||
MIN_VISIBLE,
|
||||
MIN_WIDTH,
|
||||
|
||||
@@ -50,6 +50,9 @@ READ_WINDOW_BELOW_SCHEMA = {
|
||||
"withholds window titles (e.g. macOS without the Screen Recording "
|
||||
"permission — never prompted for, noted in `note`). Other Hermes "
|
||||
"windows are skipped: the nearest non-Hermes window is reported. "
|
||||
"Returns {error, platform} instead where the OS cannot enumerate "
|
||||
"windows at all (e.g. a Wayland session); `error` says what would fix "
|
||||
"it, so relay it rather than retrying. "
|
||||
"Metadata only; this never captures pixels or content of other windows."
|
||||
),
|
||||
"parameters": {
|
||||
|
||||
Reference in New Issue
Block a user