feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell)#191
feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell)#191recchia wants to merge 17 commits into
Conversation
Greptile SummaryThis PR adds two Wayland-native frontmost-window backends — a
Confidence Score: 5/5Safe to merge — all Wayland code is Linux-only, falls through gracefully when backends are unavailable, and the previous round of review findings are all addressed in this revision. The three blocking concerns from the prior review — no timeout on the Wayland poll path, compositor-crash socket errors silently swallowed, and per-call dispatch_pending errors not propagating to state.finished — are all resolved. drain_events now correctly sets state.finished = true on both the early-exit path and the read path. The two timed_roundtrip calls in Session::open share a single deadline, keeping the total init exposure inside INIT_TIMEOUT. No new issues were found. No files require special attention. The core dispatch logic in wlr_foreign_toplevel.rs received the most scrutiny across both review rounds and looks correct. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[frontmost_bundle_id called] --> B{FRONTMOST_SOURCE initialized?}
B -- No --> C[detect_frontmost_source]
B -- Yes --> Z[dispatch to cached backend]
C --> D[detect_session_kind]
D --> E{XDG_SESSION_TYPE?}
E -- wayland --> F[candidates: wlr → gnome-shell → x11]
E -- x11 --> G[candidates: x11]
E -- unset --> H{WAYLAND_DISPLAY set?}
H -- yes --> F
H -- no --> O[try x11_candidate]
G --> O
F --> K[try wlr_foreign_toplevel::candidate]
K -- Some --> L[WlrForeignToplevelSource selected]
K -- None --> M[try gnome_shell::candidate]
M -- Some --> N[GnomeShellSource selected]
M -- None --> O
O -- Some --> P[X11Source selected]
O -- None --> Q[NullSource]
L --> Z
N --> Z
P --> Z
Q --> Z
Z --> R{backend type?}
R -- wlr --> S[drain_events 25ms cap / reconnect on state.finished]
R -- gnome-shell --> T[D-Bus GetFocusedWmClass 5s timeout]
R -- x11 --> U[_NET_ACTIVE_WINDOW + WM_CLASS]
R -- null --> V[return None]
%%{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 initialized?}
B -- No --> C[detect_frontmost_source]
B -- Yes --> Z[dispatch to cached backend]
C --> D[detect_session_kind]
D --> E{XDG_SESSION_TYPE?}
E -- wayland --> F[candidates: wlr → gnome-shell → x11]
E -- x11 --> G[candidates: x11]
E -- unset --> H{WAYLAND_DISPLAY set?}
H -- yes --> F
H -- no --> O[try x11_candidate]
G --> O
F --> K[try wlr_foreign_toplevel::candidate]
K -- Some --> L[WlrForeignToplevelSource selected]
K -- None --> M[try gnome_shell::candidate]
M -- Some --> N[GnomeShellSource selected]
M -- None --> O
O -- Some --> P[X11Source selected]
O -- None --> Q[NullSource]
L --> Z
N --> Z
P --> Z
Q --> Z
Z --> R{backend type?}
R -- wlr --> S[drain_events 25ms cap / reconnect on state.finished]
R -- gnome-shell --> T[D-Bus GetFocusedWmClass 5s timeout]
R -- x11 --> U[_NET_ACTIVE_WINDOW + WM_CLASS]
R -- null --> V[return None]
Reviews (14): Last reviewed commit: "style(hook): apply cargo fmt to drain_ev..." | Re-trigger Greptile |
|
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>
# Conflicts: # crates/openlogi-gui/src/main.rs
drain_events silently ignored errors from flush/dispatch_pending/read, so a compositor crash (abrupt socket close) left the backend returning stale state forever instead of reconnecting, unlike a graceful Finished event. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <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. Alsoreconnects on a genuine connection error during the poll-path drain (socket
closed by a compositor crash), not just on a graceful
Finishedevent —fixed per Greptile's review.
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 — resolved
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.
Decision: keep returning each compositor's native identifier rather than a lossy
WM_CLASSguess (stripping reverse-DNS and re-capitalizing is wrong for manyapps). No normalization layer is added in this PR; the cross-namespace mismatch
is documented as a known, deliberate limitation on the
FrontmostSourcetraitin
linux.rs(a canonical-id scheme or per-profile aliases would be the rightfix, as a separate follow-up if it's ever needed in practice).
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.wlroots backend validated on sway 1.11 (headless):
frontmost_bundle_id()correctly returned
Some("foot")andSome("org.gnome.TextEditor")as focusswitched between two Wayland windows — matching each window's native xdg
app_idexactly as reported by
swaymsg -t get_tree.Install (GNOME)
Open questions
Extension UUID / D-Bus name useResolved: settled onopenlogi.*as placeholders — whatnamespace do you want?
org.openlogi.*throughout — D-Bus name/path (
org.openlogi.Frontmost//org/openlogi/Frontmost), extension UUID(
openlogi-frontmost@openlogi.dev), and the app identifier(
brand::APP_ID = "org.openlogi.openlogi", added later in this PR) allagree. No placeholders remain.
ship to extensions.gnome.org, or auto-install from the app? feat(linux): install artifacts and docs #173/feat(linux): nfpm .deb/.rpm packaging and CI release job #179
(nfpm packaging) have since merged, which was the blocker for the
bundle-and-document follow-up (shipping
openlogi-frontmost@openlogi.dev/as an nfpmcontents:entry under/usr/share/gnome-shell/extensions/<UUID>/), but that follow-up hasn'tbeen done yet —
packaging/linux/nfpm.yamlcurrently has no entry for theextension. Happy to add it here or as a separate follow-up PR once we
agree on distribution.
Checklist
cfg(target_os = "linux")); macOS/Windows untouched.Finished.masteras of 2026-07-03 — clean, no conflicts.