Architecture · 26 Aug 2026
Jarvis Linux hold-to-talk: production pattern
The current approach (uiohook on Wayland, quiet timeouts, forced X11 grabs) is not how production voice apps do push-to-talk. This note compares Discord / Electron / portal reality and the architecture Jarvis should ship.
Verdict
Use org.freedesktop.portal.GlobalShortcuts with Activated + Deactivated. That is the intentional Wayland hold-to-talk API. Discord’s Linux hotkey work, Chromium’s GlobalShortcutsPortal, and Electron 41’s Wayland path all converge here. Evdev/uinput helpers are community workarounds for apps that never implemented the portal.
Portal = prod
Quiet timeout = reject
uiohook on GNOME Wayland = broken
What production apps actually do
| Surface | Hold mechanism | Press/release | Notes |
|---|---|---|---|
| XDG Desktop Portal | GlobalShortcuts D-Bus |
Activated / Deactivated | Designed for PTT. GNOME consent dialog once; bindings persist by app id. |
| Electron 41+ on Wayland | Chromium GlobalShortcutsPortal |
Press only in JS API | globalShortcut.register only implements OnKeyPressed. Portal still emits Deactivated — Electron does not expose it. |
| Discord Linux (2025–26) | Portal / compositor hotkeys | Session-wide | “Year of the Linux Desktop” hotkey work. Older Flatpak/XWayland builds still needed workarounds. |
| Community Discord PTT fixes | evdev read + uinput inject | Real key edges | Requires input group. Explicitly a bypass of Wayland security — not an app-platform pattern. |
| KDE Plasma (upcoming) | Compositor-level mic mute PTT | OS-owned | System mutes mic while held; app-agnostic. Complements portal, does not replace app UX. |
| Windows / macOS prod | OS hooks (RegisterHotKey / CGEvent) | Hold via hooks | uiohook-style listeners are legitimate there. Keep for those platforms. |
What Jarvis is doing today (and why it fails)
| Hack | Intent | Failure mode on this machine |
|---|---|---|
Force --ozone-platform=x11 |
Absolute overlay positioning + GPU workaround | Opts out of Chromium’s portal accelerator path; XWayland grabs are focus/XKB fragile. |
uiohook-napi |
Native press/release | XkbGetKeyboard failed under GNOME Wayland — starts but sees zero keys. |
| 1.2s quiet-timeout “hold” | Fake hold from Electron press-only API | GNOME does not repeat global shortcuts → auto-release at ~1200ms. Matches production traces exactly. |
GNOME GrabAccelerator |
Shell-owned press/release | AccessDenied for normal apps (extension-only). |
| Tap-to-talk fallback | Honest degradation | Correct as fallback; wrong as the primary “hold” story. |
Recommended Jarvis architecture
- Primary (Linux): talk to
org.freedesktop.portal.GlobalShortcutsourselves over the session bus. BindCtrl+Shift+Jwith a human description.Activated→startTalk,Deactivated→releaseTalk. - Why not only Electron
globalShortcut: Electron 41’s JS API still only fires on press even when the portal is used. Hold requires the Deactivated signal — so a thin portal client is the production cut. - Identity: portal binds are keyed by app id. Ship/use
com.abstergo.jarviswith a matching.desktopandapp.setDesktopName(already partially present). - Overlay vs input: absolute overlay positioning can stay on XWayland if needed; the portal is D-Bus and does not require ozone=wayland. Do not couple GPU/compositor flags to the PTT strategy.
- Fallbacks: if portal missing/denied → explicit tap-to-talk (no timers). Windows/macOS → keep native hold hooks.
- Reject permanently: quiet-timeout holds, synthetic key-repeat modeling, and claiming hold while only press is observed.
Implementation slice
DesktopPortalGlobalShortcuts.ts— CreateSession → BindShortcuts → subscribe Activated/Deactivated; typed + unit-tested against a fake bus.- Wire into
DesktopJarvisShellahead of ElectronglobalShortcut/ uiohook on Linux. - First-run UX: GNOME may show a consent dialog; Control Center should surface “Hold shortcut authorized” / “Tap fallback”.
- Keep ozone/GPU decisions independent; only change them if overlay positioning is re-solved for native Wayland later.
- Acceptance: physical hold ≥5s stays capturing; release ends once; no 1200ms auto-release in traces.
Local environment facts
- Session: GNOME Wayland; portal present (
xdg-desktop-portal-gnome50). - Electron: 41.5.0 (portal support landed; JS API still press-only).
- App id already:
com.abstergo.jarvis. - Keyboard
/dev/input/event*not readable withoutinputgroup — another reason not to ship evdev as the primary path.