feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell)#191
feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell)#191recchia wants to merge 4 commits into
Conversation
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>
Greptile SummaryThis PR adds two Wayland frontmost-window backends (
Confidence Score: 5/5Safe to merge; all changes are Linux-only and the X11 path is untouched, so macOS/Windows and existing X11 users are unaffected. The new backends are well-isolated behind the FrontmostSource trait, each candidate fails gracefully rather than panicking, timeouts are consistently applied, and the wlr reconnect logic handles the common compositor-reload case. The one noted concern — reconnect holding the session mutex for up to 10 s — is bounded and affects only edge-case concurrent callers, not the nominal single-threaded poll loop. The reconnect path in wlr_foreign_toplevel.rs (frontmost_bundle_id lines 420–421) is worth a second look before merge; all other files are straightforward. Important Files Changed
Sequence DiagramsequenceDiagram
participant P as Process (LazyLock init)
participant D as detect_frontmost_source()
participant W as wlr_foreign_toplevel::candidate
participant G as gnome_shell::candidate
participant X as x11_candidate
P->>D: first access to FRONTMOST_SOURCE
D->>D: detect_session_kind()
alt Wayland session
D->>W: candidate()
W->>W: Session::open() [timed_roundtrip x2, up to 10s]
alt wlr protocol advertised
W-->>D: Some(WlrForeignToplevelSource)
D-->>P: wlr backend selected
else not advertised
W-->>D: None
D->>G: candidate()
G->>G: connect() + probe call [5s timeout]
alt extension reachable
G-->>D: Some(GnomeShellSource)
D-->>P: gnome-shell backend selected
else absent
G-->>D: None
D->>X: x11_candidate()
X-->>D: Some/None
D-->>P: x11 or NullSource
end
end
else X11 / Unknown session
D->>X: x11_candidate()
X-->>D: Some/None
D-->>P: x11 or NullSource
end
loop ~1 Hz polling
P->>+D: frontmost_bundle_id()
D->>D: FRONTMOST_SOURCE.frontmost_bundle_id()
note over D: wlr: drain_events (25ms cap), gnome: D-Bus call (5s timeout), x11: X property read
D-->>-P: "Option<String>"
end
Reviews (3): Last reviewed commit: "fix(hook): bound wlr frontmost dispatch ..." | Re-trigger Greptile |
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>
|
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. |
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.