Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell)#191

Open
recchia wants to merge 8 commits into
AprilNEA:master from
recchia:feat/frontmost-wayland-backends
Open

feat(hook): Wayland frontmost-window backends (wlroots + GNOME Shell) #191
recchia wants to merge 8 commits into
AprilNEA:master from
recchia:feat/frontmost-wayland-backends

Conversation

@recchia

@recchia recchia commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

frontmost_bundle_id() is X11-only today, so on a Wayland session it returns
None for native windows and per-app profiles never fire (XWayland windows
aside). 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
:

  • wlrootszwlr_foreign_toplevel_management_v1 (sway, Hyprland, river, Wayfire)
  • GNOME Shell — a minimal, read-only companion extension that exports the
    focused window's WM_CLASS over 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 to
WAYLAND_DISPLAY/DISPLAY) sets the candidate order; the first that
initializes wins:

  • Wayland → wlroots → GNOME extension → X11/XWayland
  • X11 / unknown → X11

A candidate returns None when it can't initialize (wlr manager absent on
GNOME, 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

Compositor Backend Identifier
wlroots (sway, Hyprland, river, Wayfire) wlr foreign-toplevel xdg app_id (org.mozilla.firefox)
GNOME / Mutter companion extension (D-Bus) WM_CLASS (org.gnome.Nautilus, firefox_firefox)
KDE/KWin, others X11/XWayland fallback X11 WM_CLASS (XWayland windows only)

Files

  • src/linux/wlr_foreign_toplevel.rs — binds the foreign-toplevel manager,
    roundtrips per poll, returns the activated toplevel's app_id.
  • src/linux/gnome_shell.rs — blocking zbus proxy onto org.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. Reads
    only global.display.focus_window.get_wm_class(); no titles, contents, input,
    or UI. Targets GNOME Shell 45–50.
  • src/linux.rs — dispatch refactored to a FrontmostSource trait + ordered
    candidate 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 to
GNOME/Wayland unchanged. wlroots returns the native xdg app_id, a different
namespace
— 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_CLASS guess (stripping reverse-DNS and
re-capitalizing is wrong for many apps); reconciling the namespaces belongs in a
single normalization layer over frontmost_bundle_id(), which I left out to keep
this 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:

  • Extension State: ACTIVE; gdbus call ... GetFocusedWmClass returns and tracks
    the focused window's WM_CLASS.
  • cargo run --example frontmost_app -p openlogi-hook follows focus live across
    native-Wayland apps (Ptyxis → org.gnome.Ptyxis, Nautilus → org.gnome.Nautilus)
    and Firefox (firefox_firefox) — windows the X11 backend reports as None.

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)

UUID=openlogi-frontmost@openlogi.dev
DEST="$HOME/.local/share/gnome-shell/extensions/$UUID"
mkdir -p "$DEST"
cp crates/openlogi-hook/gnome-shell-extension/$UUID/{metadata.json,extension.js} "$DEST"/
# Wayland can't hot-reload the shell — log out/in, then:
gnome-extensions enable "$UUID"

Open questions

  1. Extension UUID / D-Bus name use openlogi.* as placeholders — what namespace
    do you want? (constants mirrored in gnome_shell.rs.)
  2. Extension distribution: bundle-and-document (current), ship to
    extensions.gnome.org, or auto-install from the app?

Checklist

  • Linux-only (cfg(target_os = "linux")); macOS/Windows untouched.
  • Falls through to X11 when no Wayland backend initializes.
  • GNOME backend validated on real hardware (Ubuntu 26.04 / GNOME 50.1 / Wayland).
  • wlroots backend validated on a wlroots compositor (help wanted).

greptile-apps[bot] reacted with thumbs up emoji

greptile-apps Bot commented Jun 9, 2026
edited
Loading

Copy link
Copy Markdown

Greptile Summary

This PR adds two Wayland frontmost-window backends (wlr_foreign_toplevel and gnome_shell) behind a new FrontmostSource trait, fixing per-app profile switching for native Wayland windows on sway/Hyprland and GNOME. The X11 path is kept as an unchanged universal fallback; macOS and Windows are untouched.

Confidence Score: 4/5

Safe 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

Filename Overview
crates/openlogi-hook/src/linux/wlr_foreign_toplevel.rs New wlroots foreign-toplevel backend; protocol dispatch, reconnect on Finished, and timed init are well-structured, but drain_events silently swallows socket errors (previously flagged), leaving the backend in a permanently broken state after a compositor crash without triggering reconnect.
crates/openlogi-hook/src/linux/gnome_shell.rs New GNOME Shell D-Bus backend; method timeout guards the LazyLock initializer, proxy-per-poll is lightweight and correct, and the connection persists across Shell restarts.
crates/openlogi-hook/src/linux.rs Clean refactor from a single X11_STATE LazyLock to a FrontmostSource trait with ordered candidate selection; session detection logic and fallback chain are correct.
crates/openlogi-hook/gnome-shell-extension/openlogi-frontmost@openlogi.dev/extension.js Minimal ESM GNOME Shell extension; correctly exports D-Bus object, handles enable/disable lifecycle, and safely guards the focus_window null case.

Reviews (8): Last reviewed commit: "Merge remote-tracking branch 'origin/mas..." | Re-trigger Greptile

Comment thread crates/openlogi-hook/src/linux/wlr_foreign_toplevel.rs Outdated
Comment thread crates/openlogi-hook/src/linux/wlr_foreign_toplevel.rs
Comment thread crates/openlogi-hook/src/linux.rs Outdated
Comment thread crates/openlogi-hook/src/linux.rs Outdated

recchia commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

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.

recchia and others added 4 commits June 10, 2026 21:31
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>
@recchia recchia force-pushed the feat/frontmost-wayland-backends branch from fd81844 to 4a078ec Compare June 11, 2026 00:31
recchia and others added 2 commits June 10, 2026 23:04
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>
Comment thread crates/openlogi-hook/src/linux/wlr_foreign_toplevel.rs
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>

recchia commented Jun 12, 2026
edited
Loading

Copy link
Copy Markdown
Contributor Author

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 master in 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, ACTIVE after re-login; GetFocusedWmClass answers over D-Bus
  • Device access via the scoped udev rule from Add Linux port support and packaging #233 (uaccess on Logitech event nodes + uinput) — no input group 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's app_id namespace for GNOME apps — the WM_CLASS↔app_id mismatch will mostly bite for non-GNOME/legacy apps

Works as advertised on GNOME Wayland.

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@greptile-apps greptile-apps[bot] greptile-apps[bot] left review comments

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /