feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell)#191
feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell)#191recchia wants to merge 13 commits into
Conversation
Greptile SummaryAdds two Wayland frontmost-window backends —
Confidence Score: 4/5Safe for GNOME and X11 users; the wlr backend has two connection-error paths in In
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[frontmost_bundle_id called] --> B[FRONTMOST_SOURCE LazyLock]
B -->|first call| C[detect_frontmost_source]
C --> D{detect_session_kind}
D -->|Wayland| E[wlr_foreign_toplevel::candidate]
D -->|X11 / Unknown| H[x11_candidate]
E -->|wlr protocol found| I[WlrForeignToplevelSource\ndrain_events / reconnect on Finished]
E -->|protocol absent| F[gnome_shell::candidate]
F -->|extension reachable| J[GnomeShellSource\nD-Bus GetFocusedWmClass]
F -->|extension absent| H
H -->|DISPLAY set| K[X11Source\n_NET_ACTIVE_WINDOW + WM_CLASS]
H -->|no display| L[NullSource → None]
B -->|subsequent calls| M[dispatch to selected backend]
I --> M
J --> M
K --> M
L --> M
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[frontmost_bundle_id called] --> B[FRONTMOST_SOURCE LazyLock]
B -->|first call| C[detect_frontmost_source]
C --> D{detect_session_kind}
D -->|Wayland| E[wlr_foreign_toplevel::candidate]
D -->|X11 / Unknown| H[x11_candidate]
E -->|wlr protocol found| I[WlrForeignToplevelSource\ndrain_events / reconnect on Finished]
E -->|protocol absent| F[gnome_shell::candidate]
F -->|extension reachable| J[GnomeShellSource\nD-Bus GetFocusedWmClass]
F -->|extension absent| H
H -->|DISPLAY set| K[X11Source\n_NET_ACTIVE_WINDOW + WM_CLASS]
H -->|no display| L[NullSource → None]
B -->|subsequent calls| M[dispatch to selected backend]
I --> M
J --> M
K --> M
L --> M
|
|
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 2×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
Live run (debug agent,
Works as advertised on GNOME Wayland. |
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
The GUI shipped no xdg-toplevel app_id (X11 WM_CLASS), so on GNOME Wayland Mutter's get_wm_class() returned empty for our own window — the gnome_shell frontmost backend then reported OpenLogi as None, and the dash couldn't group the window under its launcher icon. Set app_id to the bundle identifier (org.openlogi.openlogi) in main_window_options, and add a matching StartupWMClass to the Linux desktop entry so GNOME ties the running window back to the launcher. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Fixed the Issue: the GUI shipped no Wayland xdg-toplevel Fix:
Verified on GNOME Wayland: querying the extension while OpenLogi is focused now returns OpenLogi's own window is now a stable frontmost key instead of |
The Wayland app_id / X11 WM_CLASS was a bare literal in main_window_options and the .desktop StartupWMClass — two copies that could drift, and a merge hazard against the Linux-port PR which sets its own value. Define it once as openlogi_core::brand::APP_ID and reference it from the GUI; the .desktop file keeps a documented literal copy since it can't import Rust. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.