-
-
Notifications
You must be signed in to change notification settings - Fork 91
feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell)#191
feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell) #191recchia wants to merge 8 commits into
Conversation
Greptile SummaryThis PR adds two Wayland frontmost-window backends ( Confidence Score: 4/5Safe to merge for GNOME users; the wlr backend has an unresolved issue where a hard compositor crash silently disables profile switching without triggering the existing reconnect path. The wlr poll path (drain_events) discards errors from guard.read() without setting state.finished, so a compositor crash leaves needs_reconnect false on all subsequent polls. This was surfaced in a prior review round and the fix was not yet applied. All other previously-raised concerns appear resolved. crates/openlogi-hook/src/linux/wlr_foreign_toplevel.rs — specifically the drain_events function and its handling of guard.read() failures. Important Files Changed
Reviews (8): Last reviewed commit: "Merge remote-tracking branch 'origin/mas..." | Re-trigger Greptile |
recchia
commented
Jun 10, 2026
Cross-referencing #173/#179: once that packaging lands, open question 2 here (extension distribution) has a natural answer — ship openlogi-frontmost@openlogi.dev/ as an nfpm contents: entry (system-wide path /usr/share/gnome-shell/extensions//) plus an install.sh step. Users would still need gnome-extensions enable + a session restart, but it removes the manual copy. Happy to add that as a follow-up once both PRs are in, whichever merges first.
Introduces a FrontmostSource trait so display-server backends can be selected at startup without touching callers, then ships two backends: - wlr_foreign_toplevel: uses zwlr_foreign_toplevel_management_v1 for wlroots compositors (sway, Hyprland, river). Drains the event queue each poll (~1 Hz) and tracks per-toplevel app_id / activated state. Emits warn! on compositor Finished (e.g. sway config reload). - gnome_shell: talks to a companion GNOME Shell extension over D-Bus (session bus, blocking proxy). Returns WM_CLASS to keep profile keys consistent with the X11 backend. Backend selection order on Wayland: wlr → gnome-shell → X11/XWayland → NullSource. X11 sessions and unknown sessions skip straight to X11. Also adds gnome-shell-extension/ with the extension source (ESM, targets GNOME Shell 45+) and Cargo deps wayland-client, wayland-protocols-wlr, zbus. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the compositor sends `Finished` (e.g. on swaymsg reload), the wlr backend now tries to reopen the session on the next poll instead of permanently disabling per-app profiles. The session (conn + queue + state) is grouped behind a single mutex so the whole thing can be rebuilt atomically; a failed reconnect retries at the next 1 Hz tick. Also update two stale doc comments in linux.rs that still described the pre-PR state (X11-only / "None until a Wayland backend is added"). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The three unbounded `EventQueue::roundtrip` calls are replaced by two deadline-aware primitives: - `timed_roundtrip` (init path): sends `wl_display.sync`, then loops `flush → poll(2) → read → dispatch_pending` until the `WlCallback::Done` fires or `INIT_TIMEOUT = 5 s` is reached. Symmetric to `gnome_shell::METHOD_TIMEOUT`; both guard the `FRONTMOST_SOURCE` `LazyLock` initializer so a stalled compositor socket makes the candidate fall through instead of blocking every thread that touches frontmost. - `drain_events` (poll path): the protocol is event-driven so no sync barrier is needed. Flushes outgoing writes, then does a non-blocking `prepare_read → poll(2, 25 ms cap) → read → dispatch_pending`. If nothing arrives within the cap the last known state is returned — millisecond-stale frontmost data is acceptable by design. Both paths use `poll(2)` via the existing `libc` dependency with `Instant`-based remaining-time accounting per iteration and `EINTR` retry. A read error marks the session finished, consistent with the existing reconnect behavior. A small `millis_until` helper converts an `Instant` deadline to a `poll(2)` timeout; two unit tests cover the boundary cases. Compositor death and reconnect behavior are unchanged from the prior commit. Runtime validation on a wlroots compositor is still pending (this machine runs GNOME/Mutter, which doesn't advertise the protocol). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fd81844 to
4a078ec
Compare
cargo generate-lockfile during the rebase upgraded gpui to cafbf4b5 (HEAD of zed), which broke gpui-component. Restore master's lockfile (gpui at eb2223c0) — all new deps from the branch are already present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously each timed_roundtrip call created its own Instant::now() + INIT_TIMEOUT, allowing Session::open() to block for up to ×ばつINIT_TIMEOUT (10 s) — double the stated guard. A single shared deadline keeps the total wall-clock exposure within INIT_TIMEOUT regardless of how many round-trips are needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
End-to-end verification of the GNOME Wayland path on Ubuntu 26.04 (GNOME Shell 50.1, native Wayland session):
Setup
- This branch merged onto latest
masterin a scratch worktree: clean merge,cargo check/clippy clean, full workspace test suite green (225 passed / 0 failed) - GNOME extension from this PR installed per its README,
ACTIVEafter re-login;GetFocusedWmClassanswers over D-Bus - Device access via the scoped udev rule from Add Linux port support and packaging #233 (
uaccesson Logitech event nodes + uinput) — noinputgroup needed
Live run (debug agent, OPENLOGI_LOG=...openlogi_hook=debug):
frontmost: session kind = Wayland
hook started on /dev/input/event13 # MX Master 3S (Bolt)
hook started on /dev/input/event15 # MX Keys (keyboard) pointer subdevice
frontmost: using 'gnome-shell' backend
frontmost app changed current=Some("org.gnome.Ptyxis") last=None
frontmost app changed current=Some("org.gnome.Nautilus") last=Some("org.gnome.Ptyxis")
frontmost app changed current=Some("org.gnome.Ptyxis") last=Some("org.gnome.Nautilus")
- Backend candidate order behaves as designed: wlr-foreign-toplevel correctly falls through on Mutter, gnome-shell wins, X11 fallback untouched
- Focus changes (terminal → Nautilus → terminal) propagate through the foreground watcher within its 1s poll
- Mouse stayed fully usable while grabbed — uinput pass-through working; clean grab release on SIGTERM
- One observation, matching the namespace caveat documented in
linux.rs: on GNOME Wayland, Mutter reports WM_CLASS in reverse-DNS app-id form (org.gnome.Nautilus), so it coincides with the wlr backend'sapp_idnamespace for GNOME apps — the WM_CLASS↔app_id mismatch will mostly bite for non-GNOME/legacy apps
Works as advertised on GNOME Wayland.
Want your agent to iterate on Greptile's feedback? Try greploops.
Summary
frontmost_bundle_id()is X11-only today, so on a Wayland session it returnsNonefor native windows and per-app profiles never fire (XWayland windowsaside). This adds two Wayland backends behind the existing selection, keeping
the X11 path as the universal fallback — no behavior change off Wayland, and
macOS/Windows untouched:
zwlr_foreign_toplevel_management_v1(sway, Hyprland, river, Wayfire)focused window's
WM_CLASSover D-Bus (Mutter offers no protocol/portal for this)Implements the Wayland half of #95; complements the X11 backend from #122.
Backend selection
detect_session_kind()(XDG_SESSION_TYPE, falling back toWAYLAND_DISPLAY/DISPLAY) sets the candidate order; the first thatinitializes wins:
A candidate returns
Nonewhen it can't initialize (wlr manager absent onGNOME, extension not installed, ...), so unsupported compositors fall through to
X11 exactly as today. Selection is once-per-process; landing on X11 while on
Wayland logs a hint to install the extension.
Compositor coverage
app_id(org.mozilla.firefox)WM_CLASS(org.gnome.Nautilus,firefox_firefox)WM_CLASS(XWayland windows only)Files
src/linux/wlr_foreign_toplevel.rs— binds the foreign-toplevel manager,roundtrips per poll, returns the
activatedtoplevel'sapp_id.src/linux/gnome_shell.rs— blockingzbusproxy ontoorg.openlogi.Frontmost,with a per-call timeout so a stalled Shell can't wedge the poll thread.
gnome-shell-extension/openlogi-frontmost@openlogi.dev/— the extension. Readsonly
global.display.focus_window.get_wm_class(); no titles, contents, input,or UI. Targets GNOME Shell 45–50.
src/linux.rs— dispatch refactored to aFrontmostSourcetrait + orderedcandidate list. New deps (
wayland-client,wayland-protocols-wlr,zbus)under the existing
cfg(target_os = "linux")target.Identifier semantics — a design call I'd like your read on
GNOME and X11 both return
WM_CLASS, so a profile created on X11 carries over toGNOME/Wayland unchanged. wlroots returns the native xdg
app_id, a differentnamespace — and since profile lookup is an exact match, a profile created under
wlroots won't match one created under GNOME/X11. I return each compositor's
native identifier rather than a lossy
WM_CLASSguess (stripping reverse-DNS andre-capitalizing is wrong for many apps); reconciling the namespaces belongs in a
single normalization layer over
frontmost_bundle_id(), which I left out to keepthis PR focused. Happy to add that pass, or to standardize on one identifier
across all Linux backends — which do you prefer?
Testing
Validated end-to-end on Ubuntu 26.04, GNOME Shell 50.1, Wayland, rustc 1.96:
State: ACTIVE;gdbus call ... GetFocusedWmClassreturns and tracksthe focused window's
WM_CLASS.cargo run --example frontmost_app -p openlogi-hookfollows focus live acrossnative-Wayland apps (Ptyxis →
org.gnome.Ptyxis, Nautilus →org.gnome.Nautilus)and Firefox (
firefox_firefox) — windows the X11 backend reports asNone.Not yet hardware-tested: the wlroots backend. It compiles and follows the
protocol spec, but I don't run a wlroots compositor — a sanity check from a
sway/Hyprland user would be welcome, or I can spin one up before merge.
Install (GNOME)
Open questions
openlogi.*as placeholders — what namespacedo you want? (constants mirrored in
gnome_shell.rs.)extensions.gnome.org, or auto-install from the app?
Checklist
cfg(target_os = "linux")); macOS/Windows untouched.