experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(shortcuts): restore cmd customization + degrade gracefully on Wayland

Cmd shortcut customization went missing when cmd moved into core. This
restores it via a control in the core Keyboard Shortcuts group. Also:
local-fallback shortcuts for peeks/slides, quieted Wayland warnings,
api.platform exposed to renderers, and Linux default switched to
CommandOrControl+Space (Alt+Space conflicts with GNOME).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+283 -14
+8
DEVELOPMENT.md
··· 574 574 - Different security model than traditional browsers 575 575 - Be cautious with cross-origin access and custom APIs 576 576 577 + ## Linux / Wayland 578 + 579 + Global hotkeys (cmd's `Option+Space`, peeks' `Option+0..9`, slides' `Option+arrows`) **don't fire when Peek is in the background on Wayland sessions**. The Wayland security model forbids apps from grabbing keys directly, so Electron's `globalShortcut.register()` returns false silently. Every feature that registers a global shortcut also registers it as a *local* shortcut, so the same accelerators work whenever a Peek window is focused. 580 + 581 + The Settings UI surfaces this in the Keyboard Shortcuts group when running on Wayland. 582 + 583 + For the full history (XWayland attempt, XDG GlobalShortcuts portal research and abandon reasoning, eventual-revisit conditions, and the implementation plan kept on file for that revisit), see [`docs/global-hotkeys-on-wayland.md`](docs/global-hotkeys-on-wayland.md). 584 + 577 585 ## Claude Code Permissions 578 586 579 587 All package.json scripts are pre-approved via wildcard permissions in `.claude/settings.local.json`:
+15 -1
app/cmd/background.js
··· 425 425 }; 426 426 427 427 /** 428 - * Register shortcuts: global (Option+Space) and local (Cmd+K) 428 + * Register shortcuts: configurable global hotkey (default Option+Space on 429 + * macOS/Windows, CommandOrControl+Space on Linux — see app/cmd/config.js) 430 + * plus the always-on local shortcuts Cmd+K and Cmd+L. 429 431 */ 430 432 const LOCAL_SHORTCUT = 'CommandOrControl+K'; 431 433 const URL_MODE_SHORTCUT = 'CommandOrControl+L'; ··· 559 561 560 562 // 2. Load settings from datastore 561 563 currentSettings = await loadSettings(); 564 + 565 + // 2b. One-shot migration: pre-default-change Linux users have 566 + // 'Option+Space' persisted, but Alt+Space is reserved by GNOME's 567 + // window-options menu so the local fallback never fires. Promote to 568 + // the new platform default so it actually works. Idempotent (only 569 + // matches the exact pre-migration value). Remove after a few releases 570 + // once existing installs have rolled forward. 571 + if (api.platform?.os === 'linux' && currentSettings.prefs.shortcutKey === 'Option+Space') { 572 + log('ext:cmd', 'migrating Linux shortcut: Option+Space → CommandOrControl+Space'); 573 + currentSettings.prefs.shortcutKey = 'CommandOrControl+Space'; 574 + await saveSettings(currentSettings); 575 + } 562 576 563 577 // 3. Register the global shortcut 564 578 initShortcut(currentSettings.prefs);
+10 -2
app/cmd/config.js
··· 1 1 const id = 'cmd'; 2 2 3 + // Default global hotkey is Option+Space everywhere except Linux. On GNOME 4 + // (and several other Linux WMs), Alt+Space is reserved for the window 5 + // options menu — the local-fallback registration would conflict and the 6 + // global registration is rejected by the compositor anyway. Use 7 + // CommandOrControl+Space on Linux so the local fallback works cleanly. 8 + const isLinux = typeof window !== 'undefined' && window.app?.platform?.os === 'linux'; 9 + const defaultShortcutKey = isLinux ? 'CommandOrControl+Space' : 'Option+Space'; 10 + 3 11 const labels = { 4 12 name: 'Cmd', 5 13 prefs: { ··· 19 27 "shortcutKey": { 20 28 "description": "Global OS hotkey to open command panel", 21 29 "type": "string", 22 - "default": "Option+Space" 30 + "default": defaultShortcutKey 23 31 }, 24 32 "showUrlsInResults": { 25 33 "description": "Show visited URLs inline in command results", ··· 37 45 38 46 const defaults = { 39 47 prefs: { 40 - shortcutKey: 'Option+Space', 48 + shortcutKey: defaultShortcutKey, 41 49 showUrlsInResults: false 42 50 }, 43 51 };
+1 -1
app/cmd/settings-schema.json
··· 7 7 "type": "object", 8 8 "properties": { 9 9 "shortcutKey": { 10 - "description": "Global OS hotkey to open command panel", 10 + "description": "Global OS hotkey to open command panel (default Option+Space on macOS/Windows, CommandOrControl+Space on Linux — Alt+Space is reserved by GNOME's window options menu)", 11 11 "type": "string", 12 12 "default": "Option+Space" 13 13 },
+40
app/settings/settings.js
··· 1 1 import appConfig from '../config.js'; 2 + import { defaults as cmdConfigDefaults } from '../cmd/config.js'; 2 3 import { createDatastoreStore } from '../utils.js'; 3 4 import api from '../api.js'; 4 5 const DEBUG = api.debug; ··· 421 422 helpText: pProps.quitShortcut.description, 422 423 placeholder: 'e.g. CommandOrControl+Q' 423 424 })); 425 + 426 + // Cmd panel shortcut. Cmd was consolidated into the core background 427 + // renderer but its prefs still persist to feature_settings row 'cmd_prefs' 428 + // (see app/cmd/background.js CMD_PREFS_ROW_ID). Read/write that row 429 + // directly and publish cmd:settings-changed so the cmd module reinits. 430 + const CMD_PREFS_ROW_ID = 'cmd_prefs'; 431 + // Default is platform-aware (CommandOrControl+Space on Linux, Option+Space 432 + // elsewhere) — see app/cmd/config.js for the rationale. 433 + let cmdPrefs = { ...cmdConfigDefaults.prefs }; 434 + try { 435 + const r = await api.datastore.getRow('feature_settings', CMD_PREFS_ROW_ID); 436 + if (r?.success && r.data?.value) { 437 + cmdPrefs = { ...cmdConfigDefaults.prefs, ...JSON.parse(r.data.value) }; 438 + } 439 + } catch { /* fall back to defaults */ } 440 + 441 + shortcutsGroup.appendChild(createInput('Cmd shortcut', cmdPrefs.shortcutKey, async (newVal) => { 442 + cmdPrefs.shortcutKey = newVal; 443 + await api.datastore.setRow('feature_settings', CMD_PREFS_ROW_ID, { 444 + featureId: 'cmd', 445 + key: 'prefs', 446 + value: JSON.stringify(cmdPrefs), 447 + updatedAt: Date.now(), 448 + }); 449 + api.publish('cmd:settings-changed', {}); 450 + }, { 451 + helpText: 'Global OS hotkey to open the command panel', 452 + placeholder: cmdConfigDefaults.prefs.shortcutKey 453 + })); 454 + 455 + // Wayland sessions can't grant global key grabs to apps. Surface this 456 + // so users don't think their shortcuts are misconfigured. 457 + if (api.platform?.isWayland) { 458 + const note = document.createElement('div'); 459 + note.className = 'help-text'; 460 + note.style.cssText = 'margin-top: 8px; padding: 8px 12px; border-left: 3px solid var(--accent, #888); background: var(--bg-tertiary, rgba(255,255,255,0.04));'; 461 + note.textContent = 'Note: this is a Wayland session. The OS does not allow apps to grab global hotkeys, so these shortcuts will only fire when a Peek window is focused.'; 462 + shortcutsGroup.appendChild(note); 463 + } 424 464 425 465 container.appendChild(shortcutsGroup); 426 466
+29 -7
backend/electron/shortcuts.ts
··· 26 26 DEBUG && console.log('[shortcuts] Running without Electron (test mode)'); 27 27 } 28 28 29 + // On Linux/Wayland, Electron's globalShortcut.register cannot grab keys — 30 + // the compositor refuses, and `register` returns false silently. The per-call 31 + // error log noise is misleading because it isn't a bug we can fix in this 32 + // process. Detect once and route those failures through a single startup 33 + // hint instead of per-shortcut errors. 34 + export const isWaylandSession = (): boolean => 35 + process.platform === 'linux' && process.env.XDG_SESSION_TYPE === 'wayland'; 36 + 37 + let waylandHintLogged = false; 38 + const logWaylandHintOnce = () => { 39 + if (waylandHintLogged) return; 40 + waylandHintLogged = true; 41 + console.warn( 42 + '[shortcuts] Wayland session detected — global hotkeys are unavailable. ' + 43 + 'Shortcuts will only fire when a Peek window is focused (local fallback).' 44 + ); 45 + }; 46 + 29 47 // Maps for tracking shortcuts 30 48 // Global shortcuts: shortcut string -> source address 31 49 const globalShortcuts = new Map<string, string>(); ··· 191 209 }); 192 210 193 211 if (ret !== true) { 194 - // Common on Linux Wayland: globalShortcut.register returns false silently 195 - // when the compositor refuses to grab the key. Logging the platform- 196 - // translated accelerator (e.g. "Alt+Space" on Linux for "Option+Space") 197 - // makes it easier to correlate with WM-reserved bindings. 198 - console.error( 199 - `[shortcuts] globalShortcut.register FAILED: requested="${shortcut}" accelerator="${accelerator}" platform=${process.platform}` 200 - ); 212 + // Wayland refuses every grab — log a single startup hint instead of one 213 + // error per shortcut. On other platforms this is a real failure; the 214 + // platform-translated accelerator (e.g. "Alt+Space" for "Option+Space") 215 + // helps correlate with WM-reserved bindings. 216 + if (isWaylandSession()) { 217 + logWaylandHintOnce(); 218 + } else { 219 + console.error( 220 + `[shortcuts] globalShortcut.register FAILED: requested="${shortcut}" accelerator="${accelerator}" platform=${process.platform}` 221 + ); 222 + } 201 223 return new Error(`Failed to register shortcut: ${shortcut} (accelerator: ${accelerator})`); 202 224 } 203 225
+9 -3
backend/electron/tile-ipc.ts
··· 99 99 unregisterGlobalShortcut, 100 100 registerLocalShortcut, 101 101 unregisterLocalShortcut, 102 + isWaylandSession, 102 103 } from './shortcuts.js'; 103 104 import { checkShortcutAllowed } from './tile-shortcuts-enforcement.js'; 104 105 import { checkContextAllowed } from './tile-context-enforcement.js'; ··· 685 686 // from success to the renderer. 686 687 const err = registerGlobalShortcut(args.shortcut, source, callback); 687 688 if (err) { 688 - console.error( 689 - `[tile-ipc] tile:shortcuts:register global failed for tile=${activeGrant.tileId} shortcut="${args.shortcut}": ${err.message}` 690 - ); 689 + // On Wayland, registerGlobalShortcut already logged the one-time 690 + // session hint — don't repeat the per-tile error noise. Still 691 + // reply with the error so the renderer can fall back if it wants. 692 + if (!isWaylandSession()) { 693 + console.error( 694 + `[tile-ipc] tile:shortcuts:register global failed for tile=${activeGrant.tileId} shortcut="${args.shortcut}": ${err.message}` 695 + ); 696 + } 691 697 replyError(err.message); 692 698 return; 693 699 }
+9
backend/electron/tile-preload.cts
··· 93 93 94 94 api.getTileEntry = () => tileEntry; 95 95 96 + // Static platform info for renderer-side feature gating. `process.env` 97 + // is available in the (non-sandboxed) preload context — exposed once at 98 + // build time so settings UIs can detect Wayland (where global hotkeys 99 + // are unavailable) without an IPC round-trip. 100 + api.platform = { 101 + os: process.platform, 102 + isWayland: process.platform === 'linux' && process.env.XDG_SESSION_TYPE === 'wayland', 103 + }; 104 + 96 105 /** 97 106 * Initialize the tile — must be called before using any capability APIs. 98 107 * Validates the capability token with the main process.
+145
docs/global-hotkeys-on-wayland.md
··· 1 + # Global Hotkeys on Wayland 2 + 3 + ## Why this doc exists 4 + 5 + On Linux/Wayland, Electron's `globalShortcut.register()` returns `false` silently for every accelerator we register (cmd's `Option+Space`, peeks' `Option+0..9`, slides' `Option+Up/Down/Left/Right`). The Wayland security model forbids apps from grabbing keys directly. This isn't a bug in our code; it's the platform. 6 + 7 + This doc records what we tried, what we shipped, and what would have to be true for us to revisit a "real fix." 8 + 9 + ## What we ship today 10 + 11 + Two layers in `backend/electron/shortcuts.ts`: 12 + 13 + 1. **Global path** — call Electron's `globalShortcut.register()` anyway. On macOS / Windows / X11-session Linux it works. On Wayland it returns `false`; we surface a single startup hint on the first Wayland failure (not one error per shortcut) and move on. 14 + 2. **Local fallback** — every feature that registers a global shortcut also registers the same accelerator as a *local* shortcut (`api.shortcuts.register(key, cb)` with no `{global:true}`). When a Peek window is focused, the shortcut fires regardless of platform. 15 + 16 + So on Wayland: shortcuts work when any Peek window is focused, and don't fire when Peek is in the background. The Settings UI surfaces this in the Keyboard Shortcuts group when `api.platform.isWayland` is true. 17 + 18 + Implementation files: 19 + - `backend/electron/shortcuts.ts` — `isWaylandSession()`, one-time `logWaylandHintOnce()`, `registerGlobalShortcut()` dispatcher 20 + - `backend/electron/tile-ipc.ts` — IPC entry point; suppresses per-tile error logging on Wayland 21 + - `backend/electron/tile-preload.cts` — `api.platform = { os, isWayland }` exposed to renderers 22 + - `app/cmd/background.js`, `features/peeks/background.js`, `features/slides/background.js` — double-register pattern (global + local fallback) 23 + - `app/settings/settings.js` — Wayland note in core-settings Keyboard Shortcuts group 24 + 25 + ## Things we tried that don't work 26 + 27 + ### Forcing XWayland (`ozone-platform=x11`) 28 + 29 + Tried 2026-05-05. **Doesn't help.** XWayland is not a real X11 server — it's an X11 protocol shim implemented on top of Wayland. XWayland clients still run *inside* the Wayland compositor and inherit its restrictions. `XGrabKey` can grab keys when an XWayland window is focused, but it cannot grab system-wide because there's no X server with global authority — the Wayland compositor still owns input dispatch. Side effects on Asahi: GPU process crashes (exit 139) and GTK assertion failures. 30 + 31 + ### XDG GlobalShortcuts portal (`org.freedesktop.portal.GlobalShortcuts`) 32 + 33 + Researched 2026-05-06. **Right answer in principle, blocked by ecosystem state.** See "Implementation plan" below for the full work breakdown — kept on file for the eventual revisit. Three blockers: 34 + 35 + 1. **GNOME backend not shipping yet on the LTSes that matter.** `xdg-desktop-portal-gnome` only added GlobalShortcuts in v48 (early 2026). Fedora 42 has it; **Ubuntu 24.04 LTS ships v46** — no support. First Ubuntu LTS to ship it will be 26.04. Debian stable is far older. KDE Plasma 6 is fine. wlroots compositors (sway, river) have **no implementation at all** ([open issue since 2023](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)). 36 + 2. **`app_id` is empty for non-Flatpak apps in many launch paths.** The portal keys persistent permission grants on `app_id`. For non-Flatpak Electron, the portal infers app_id from the systemd scope name — which is `app-…` only when launched from GNOME Activities/Files. **Terminal launch, Albert, rofi, AppImage double-click → empty app_id**, which means the consent prompt re-fires every launch. ([GNOME developer post on this exact problem](https://blogs.gnome.org/ignapk/2025/06/04/using-portals-with-unsandboxed-apps/) — 2025-06.) The proposed fix ("host app registry") needs Electron upstream to adopt it; [Electron #38288 is closed as not planned](https://github.com/electron/electron/issues/38288). 37 + 3. **Library side is fine** — `@homebridge/dbus-native` is pure-JS, actively maintained, no native rebuild. Not the bottleneck. 38 + 39 + ## When to revisit 40 + 41 + Two conditions both need to be true: 42 + 43 + - **Ubuntu 26.04 LTS** lands with `xdg-desktop-portal-gnome ≥ 48` as the floor (~April 2026) 44 + - **Electron upstream** adopts the host app registry handshake so app_id is stable regardless of launch method (no current ETA) 45 + 46 + Until then, the local-fallback Peek already has is a better experience than a portal that re-prompts on every launch. 47 + 48 + --- 49 + 50 + ## Implementation plan (for the eventual revisit) 51 + 52 + Keep this section in sync with `backend/electron/shortcuts.ts` if any of the file paths shift. 53 + 54 + ### A. Feasibility verification — what to re-check first 55 + 56 + - Portal version availability on the **then-current** Ubuntu/Fedora/Debian LTS. The doc above is dated 2026-05; numbers shift. `busctl --user introspect org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop` will show whether `org.freedesktop.portal.GlobalShortcuts` is implemented. 57 + - Compositor support: mutter (when paired with portal-gnome 48+) and kwin both genuinely grab keys. Hyprland's portal supports it. Confirm wlroots situation (sway/river) hasn't moved — last checked, no. 58 + - App identification: this is the deal-breaker. The portal infers app_id from the systemd scope name. Test before committing to the work — see step 1 of the implementation order. 59 + 60 + ### B. Library choice 61 + 62 + **`@homebridge/dbus-native`** over `dbus-next`: 63 + - `dbus-next` last released Oct 2021. Stale. 64 + - `@homebridge/dbus-native` actively released, 100% JS, no native build, MIT, supports signal subscription via EventEmitter, ARM-aware, used in Homebridge production. 65 + 66 + ### C. Integration design 67 + 68 + #### Module layout 69 + 70 + New file `backend/electron/portal-shortcuts.ts` exporting: 71 + 72 + ```ts 73 + async function initPortalShortcuts(): Promise<PortalClient | null> 74 + class PortalClient { 75 + async register(shortcut: string, callback: () => void): Promise<Error | undefined> 76 + async unregister(shortcut: string): Promise<void> 77 + } 78 + ``` 79 + 80 + `shortcuts.ts` `registerGlobalShortcut` becomes a dispatcher: on Wayland, prefer the portal client if `initPortalShortcuts()` returned non-null; else fall through to current `globalShortcut.register` (which silently returns false on Wayland — fine, callers already handle this via local fallback). 81 + 82 + #### Activation flow 83 + 84 + Portal `Activated(o session_handle, s shortcut_id, t timestamp, a{sv} options)` signal arrives on the D-Bus connection. Maintain a `Map<shortcut_id, callback>` in the portal client. The callback is the same JS closure that was passed in from the renderer via IPC at `tile-ipc.ts` — already in the main process, so no extra plumbing. 85 + 86 + #### Session lifecycle 87 + 88 + One session for the whole app lifetime. Create lazily on first `register` call so we don't pay D-Bus startup cost or trigger consent for users who never set a global hotkey. Persist `session_handle` in memory only — the portal handles persistence across launches via app_id. 89 + 90 + If the bus connection drops mid-session: queue registrations, attempt reconnect with exponential backoff capped at 30s, log once. Don't crash. (`@homebridge/dbus-native` exposes `connection.on('error')` and `'end'`.) 91 + 92 + #### Shortcut ID mapping 93 + 94 + Portal works with stable string IDs and "preferred trigger" suggestions in `org.freedesktop.GlobalShortcuts.Trigger` form (e.g. `LOGO+space`). Translate Peek's accelerator strings (`Option+Space`, `CommandOrControl+1`) at the boundary. Use the accelerator string itself as the shortcut_id for stability — the user's stored grant survives across launches because the ID is stable. 95 + 96 + #### First-launch consent UX 97 + 98 + Don't trigger eagerly. Trigger on the first user action that requires it (opening the cmd panel via tray click) and surface a one-time toast: "Peek wants to use a global hotkey — your desktop will ask permission once." This avoids a hostile dialog appearing during first launch with no context. 99 + 100 + #### Fallback layering 101 + 102 + Current code in `app/cmd/background.js`, `features/peeks/background.js`, and `features/slides/background.js` already registers the same shortcut as both global and local. **Keep this.** On Wayland-without-portal-backend: global call no-ops, local call works when a Peek window is focused. The new portal path slots in cleanly above this. 103 + 104 + ### D. Risks (sorted by severity) 105 + 106 + 1. **app_id is empty/unstable for terminal-launched and many launcher-launched cases** → consent re-prompt per launch. **Test:** instrument the portal client to log the parsed app_id on every CreateSession call across (a) `.desktop` double-click in GNOME Files, (b) Activities search launch, (c) Albert launch, (d) `electron .` from terminal, (e) AppImage double-click. If any common path yields empty, abandon. 107 + 2. **Older LTSes still in support window** → silent failure on the largest single Linux desktop deployment. **Test:** `busctl --user introspect org.freedesktop.portal.Desktop /org/freedesktop/portal/desktop | grep GlobalShortcuts` on the target distro. If absent, fall back without nagging. 108 + 3. **wlroots compositors (sway/river) may still have no portal at all.** **Test:** same `busctl introspect`. Detect and route to the local-only path. 109 + 4. **`@homebridge/dbus-native` consumed in an Electron main process — D-Bus socket path discovery via `$DBUS_SESSION_BUS_ADDRESS` may differ inside electron-builder AppImage / snap / deb packaging.** **Test:** package each format and verify the bus connects. 110 + 5. **Compositor consent dialog chains for many shortcuts** — peeks and slides each register N shortcuts. **Test:** register 10 shortcuts at once, count user clicks required. 111 + 112 + ### E. Step-by-step implementation order 113 + 114 + Only proceed past step 1 if risks D.1 and D.2 prove benign for the target distro mix. 115 + 116 + 1. **Probe-only spike** (no code changes to Peek). Standalone Node script using `@homebridge/dbus-native` that calls `CreateSession`, registers `Option+Space`, listens for `Activated`. Run on the then-current Fedora / Kubuntu / Ubuntu / Hyprland. Log app_id from each environment via the 5 launch methods listed in risk D.1. **This is a kill-or-proceed gate.** 117 + 2. Add `@homebridge/dbus-native` to `dependencies` in `package.json`. Verify it survives `electron-rebuild` (it shouldn't need rebuilding — pure JS). 118 + 3. New module `backend/electron/portal-shortcuts.ts`. Implement `PortalClient` with just CreateSession + BindShortcuts + Activated subscription. No unregister yet. 119 + 4. Wire into `shortcuts.ts` `registerGlobalShortcut`: when `isWaylandSession()` and `portalClient` is initialized, route to portal; on portal failure, fall through to existing path. **Vertical slice — only `Option+Space` for the cmd panel.** Ship behind an env flag (`PEEK_WAYLAND_PORTAL=1`) for opt-in dogfooding. 120 + 5. Add Activated signal handling and the in-memory `shortcut_id → callback` map. 121 + 6. Implement unregister via the portal's session-close semantics (the portal lacks a per-shortcut unregister; in v2 you re-call BindShortcuts with the new full set). Add a debounced "rebind" pass so churn from peeks/slides reinit doesn't spam the portal. 122 + 7. Reconnect/error handling for D-Bus drops. 123 + 8. Remove the env flag, make portal the default on Wayland when introspection confirms backend availability. 124 + 9. Update `app/cmd/background.js`, `features/peeks/background.js`, `features/slides/background.js` to drop the duplicate local-fallback registration in the portal-available case (still keep it as the no-portal fallback). 125 + 126 + ### F. Bail-out criteria 127 + 128 + Abandon if any of these is true after step 1: 129 + - App_id is empty in any of: GNOME Activities launch, terminal launch, Albert/rofi launch. 130 + - Consent dialog re-fires on second launch from the same launch method. 131 + - KDE and GNOME disagree on shortcut_id syntax such that one accelerator string can't satisfy both. 132 + - `@homebridge/dbus-native` fails to connect inside an electron-builder AppImage. 133 + 134 + Abandon long-term if the host app registry remains the only path and Electron upstream still won't adopt it. 135 + 136 + ## Sources 137 + 138 + - [Global Shortcuts portal docs](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html) 139 + - [xdg-desktop-portal-gnome NEWS](https://github.com/GNOME/xdg-desktop-portal-gnome/blob/main/NEWS) 140 + - [xdg-desktop-portal-kde GitLab](https://invent.kde.org/plasma/xdg-desktop-portal-kde) 141 + - [Using Portals with unsandboxed apps — Ignacy Kuchciński, GNOME blog 2025-06](https://blogs.gnome.org/ignapk/2025/06/04/using-portals-with-unsandboxed-apps/) 142 + - [Electron #38288 — host app registry, closed as not planned](https://github.com/electron/electron/issues/38288) 143 + - [xdg-desktop-portal #579 — empty app_id for host apps](https://github.com/flatpak/xdg-desktop-portal/issues/579) 144 + - [xdg-desktop-portal-wlr #240 — no GlobalShortcuts](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240) 145 + - [@homebridge/dbus-native](https://github.com/homebridge/dbus-native)
+9
features/peeks/background.js
··· 120 120 executeItem(item); 121 121 }, { global: true }); 122 122 123 + // Local fallback so the shortcut still fires when a Peek window is 124 + // focused on platforms (Linux/Wayland) where global registration 125 + // silently fails — the global call returns false but a local handler 126 + // is always available. 127 + api.shortcuts.register(shortcut, () => { 128 + executeItem(item); 129 + }); 130 + 123 131 registeredShortcuts.push(shortcut); 124 132 } 125 133 }); ··· 133 141 134 142 registeredShortcuts.forEach(shortcut => { 135 143 api.shortcuts.unregister(shortcut, { global: true }); 144 + api.shortcuts.unregister(shortcut); 136 145 }); 137 146 138 147 registeredShortcuts = [];
+8
features/slides/background.js
··· 150 150 executeItem(item); 151 151 }, { global: true }); 152 152 153 + // Local fallback so the shortcut still fires when a Peek window is 154 + // focused on platforms (Linux/Wayland) where global registration 155 + // silently fails. 156 + api.shortcuts.register(shortcut, () => { 157 + executeItem(item); 158 + }); 159 + 153 160 registeredShortcuts.push(shortcut); 154 161 } 155 162 }); ··· 163 170 164 171 registeredShortcuts.forEach(shortcut => { 165 172 api.shortcuts.unregister(shortcut, { global: true }); 173 + api.shortcuts.unregister(shortcut); 166 174 }); 167 175 registeredShortcuts = []; 168 176