feat(hook): implement frontmost_bundle_id on Linux via X11#122
Conversation
Greptile SummaryImplements
Confidence Score: 5/5Safe to merge; the new X11 path degrades gracefully to None on all failure modes and does not affect non-Linux builds. All error paths (missing DISPLAY, destroyed windows, wrong property type) are handled via ok()? chains that return None, matching the documented contract. The LazyLock-cached connection is used correctly from a single watcher thread and x11rb's RustConnection is Send+Sync. The one notable gap is that a post-init connection drop is silent and permanent, but this is a monitoring convenience feature — returning None is acceptable degradation rather than a correctness failure. crates/openlogi-hook/src/linux.rs — specifically the LazyLock block and its behaviour when the connection breaks after initialisation. Important Files Changed
Sequence DiagramsequenceDiagram
participant AW as app_watcher (1 Hz)
participant LIB as frontmost_bundle_id()
participant LAZY as LazyLock<X11State>
participant X11 as X Server
AW->>LIB: call
LIB->>LAZY: deref (init on first call)
LAZY->>X11: RustConnection::connect()
X11-->>LAZY: connection + screen_num
LAZY->>X11: intern_atom(_NET_ACTIVE_WINDOW)
X11-->>LAZY: atom ID
LAZY-->>LIB: "&X11State (cached for process lifetime)"
LIB->>X11: get_property(root, _NET_ACTIVE_WINDOW, WINDOW)
X11-->>LIB: window XID
LIB->>X11: WmClass::get(window)
X11-->>LIB: WM_CLASS bytes
LIB-->>AW: Some("Code") / None
Reviews (3): Last reviewed commit: "fix(hook): stop wait_readable busy-loopi..." | Re-trigger Greptile |
There was a problem hiding this comment.
Important
The frontmost_bundle_id() implementation is clean and correct. One issue worth addressing before merge: wait_readable can spin on POLLERR/POLLHUP/POLLNVAL from either fd. Also worth noting that the GUI watcher consumer in foreground_app.rs doesn't start on Linux — follow-up needed.
Reviewed changes — implements frontmost_bundle_id() for Linux by querying _NET_ACTIVE_WINDOW via X11 and extracting the WM_CLASS class component. The diff also carries the evdev+uinput hook base from #119.
X11State+LazyLock<Option<X11State>>— one-time X11 connection and atom init;Noneon failure gracefully degrades to no detection_NET_ACTIVE_WINDOWproperty query — standard EWMH path;get_property→value32()→next()?with null-XID guardWmClass::getfor class extraction — targets the stable per-app class identifier (e.g."Code"not"Navigator")- Wire Linux into
src/lib.rs—#[cfg(target_os = "linux")]branches throughoutHookstruct,start/stop/Drop, andfrontmost_bundle_id() - Update
src/tests.rs— Linux error variants inhook_error_display; newlinux_start_does_not_return_unsupportedtest - New
frontmost_appandprint_eventsexamples — Linux-only smoke tests
ℹ️ foreground_app watcher doesn't start on Linux
The frontmost_bundle_id() API is fully functional on Linux, but the consumer that polls it at 1 Hz in the GUI layer never starts. The spawn() function in crates/openlogi-gui/src/watchers/foreground_app.rs:17 returns an empty receiver on non-macOS — if !cfg!(target_os = "macos") { return rx; } skips spawning the watcher thread entirely. This file is outside the openlogi-hook crate and is not in this PR's diff, so it's a follow-up item. Worth either updating the guard in a companion PR or creating a tracking issue so the capability isn't orphaned.
Technical details
# foreground_app watcher doesn't start on Linux
## Affected sites
- `crates/openlogi-gui/src/watchers/foreground_app.rs:17` — `cfg!` guard prevents watcher thread on Linux
## Required outcome
- The watcher should also start on Linux so that `frontmost_bundle_id()` is consumed by the GUI for per-app profile switching.
## Suggested approach
- Change `if !cfg!(target_os = "macos")` to `if !cfg!(any(target_os = "macos", target_os = "linux"))` — or remove the guard entirely if the other platforms gracefully degrade (the function returns `None` on unsupported platforms).Big Pickle (free) (credentials for Anthropic not configured) | 𝕏
- Examples: replace #![cfg(target_os = "linux")] crate-level gate with an unconditional fn main + inner cfg blocks so non-Linux hosts don't hit E0601 when building --all-targets (frontmost_app, print_events) - create_pipe: use pipe2(O_CLOEXEC) instead of pipe(); without it, forked children inherit the write-end and block clean shutdown - foreground_app watcher: extend guard from macOS-only to any(macos, linux) so frontmost_bundle_id() is polled on Linux too Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ Incremental changes are clean — no new issues introduced.
Reviewed changes — wires the foreground_app watcher to start on Linux, switches create_pipe to pipe2(O_CLOEXEC), and restructures both examples for cross-platform compilation. These three changes address the GUI-watcher item from the prior review.
- Wire
foreground_appwatcher for Linux —cfg!guard incrates/openlogi-gui/src/watchers/foreground_app.rschanged from!cfg!(target_os = "macos")to!cfg!(any(target_os = "macos", target_os = "linux")), closing the gap wherefrontmost_bundle_id()was functional at the API level but un-consumed by the GUI - Switch
create_pipetopipe2(O_CLOEXEC)— prevents the shutdown pipe fds from being inherited by forked children, which would delay clean shutdown by keeping the write-end alive pastpoll() - Restructure examples for cross-platform compilation — both
frontmost_app.rsandprint_events.rsnow compile on all platforms via per-function#[cfg]guards instead of crate-level#![cfg]attributes, eliminating the need to feature-gate example targets
All three changes are correct. The pipe2(O_CLOEXEC) swap is the right approach for fd hygiene, and the #[cfg] inlining is idiomatic for examples that should compile unconditionally.
Big Pickle (free) (credentials for Anthropic not configured) | 𝕏
Query _NET_ACTIVE_WINDOW on the root window, then read WM_CLASS from the focused window. Returns the class component (e.g. "Firefox", "Code") — the stable per-app identifier on X11. The X11 connection and _NET_ACTIVE_WINDOW atom are lazily initialised once via LazyLock and reused across the 1 Hz polling calls in app_watcher. Wayland-native windows (not under XWayland) are not visible through this path and return None, as documented.
- Examples: replace #![cfg(target_os = "linux")] crate-level gate with an unconditional fn main + inner cfg blocks so non-Linux hosts don't hit E0601 when building --all-targets (frontmost_app, print_events) - create_pipe: use pipe2(O_CLOEXEC) instead of pipe(); without it, forked children inherit the write-end and block clean shutdown - foreground_app watcher: extend guard from macOS-only to any(macos, linux) so frontmost_bundle_id() is polled on Linux too
- linux.rs: apply rustfmt to the X11 frontmost_bundle_id code (the AprilNEA#122 CI never ran, so the formatting drift wasn't caught). - frontmost_app example: use a cfg'd main + non-linux fallback instead of a single main with a trailing `return`, which trips clippy::needless_return on non-Linux targets under -D warnings. Matches the print_events example.
poll() returns immediately and keeps an fd flagged when it has an error condition, so a grabbed device that errors or hangs up (e.g. is unplugged) left wait_readable spinning at 100% CPU — neither POLLIN branch ever fired. Check POLLERR/POLLHUP/POLLNVAL and return, so the caller exits the device thread and releases the grab. (pullfrog review on AprilNEA#119's hook, carried in.)
3fcbf8b to
d1d6ed9
Compare
- Examples: replace #![cfg(target_os = "linux")] crate-level gate with an unconditional fn main + inner cfg blocks so non-Linux hosts don't hit E0601 when building --all-targets (frontmost_app, print_events) - create_pipe: use pipe2(O_CLOEXEC) instead of pipe(); without it, forked children inherit the write-end and block clean shutdown - foreground_app watcher: extend guard from macOS-only to any(macos, linux) so frontmost_bundle_id() is polled on Linux too
- linux.rs: apply rustfmt to the X11 frontmost_bundle_id code (the #122 CI never ran, so the formatting drift wasn't caught). - frontmost_app example: use a cfg'd main + non-linux fallback instead of a single main with a trailing `return`, which trips clippy::needless_return on non-Linux targets under -D warnings. Matches the print_events example.

Summary
_NET_ACTIVE_WINDOWon the X11 root window to find the focused window XIDWM_CLASSviax11rb::properties::WmClassand returns the class component (e.g."Firefox","Code") — the stable per-app identifier on X11, analogous to a macOS bundle ID for profile-matching purposes_NET_ACTIVE_WINDOWatom once viaLazyLock; subsequent 1 Hz calls inapp_watcherreuse the connection with a single round-tripfrontmost_appexample for manual smoke-testingfrontmost_bundle_id()doc to describe the Linux semantics and the Wayland limitationKnown limitation
Native Wayland windows (not running under XWayland) are not visible through this path and return
None. A compositor-specific D-Bus path (GNOME Shell / KWin) would be needed to cover those; deferred as the vast majority of apps run under XWayland today.Depends on
#119 — this branch is based on it. The diff will include #119's changes until that merges to master; after merge only the
frontmost_bundle_idadditions will remain.Test plan
./target/debug/examples/frontmost_appand switched between GNOME Terminal, Google Chrome, and VS Code — output correctly changed toSome("Gnome-terminal"),Some("Google-chrome"),Some("Code")🤖 Generated with Claude Code