Skip to content

feat(hook): implement frontmost_bundle_id on Linux via X11#122

Merged
AprilNEA merged 4 commits into
AprilNEA:masterfrom
cserby:story/linux-frontmost/frontmost-bundle-id-linux
Jun 5, 2026
Merged

feat(hook): implement frontmost_bundle_id on Linux via X11#122
AprilNEA merged 4 commits into
AprilNEA:masterfrom
cserby:story/linux-frontmost/frontmost-bundle-id-linux

Conversation

@cserby

@cserby cserby commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Queries _NET_ACTIVE_WINDOW on the X11 root window to find the focused window XID
  • Reads WM_CLASS via x11rb::properties::WmClass and 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
  • Lazily initialises the X11 connection and _NET_ACTIVE_WINDOW atom once via LazyLock; subsequent 1 Hz calls in app_watcher reuse the connection with a single round-trip
  • Adds frontmost_app example for manual smoke-testing
  • Updates the frontmost_bundle_id() doc to describe the Linux semantics and the Wayland limitation

Known 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_id additions will remain.

Test plan

  • Manual: ran ./target/debug/examples/frontmost_app and switched between GNOME Terminal, Google Chrome, and VS Code — output correctly changed to Some("Gnome-terminal"), Some("Google-chrome"), Some("Code")

🤖 Generated with Claude Code

@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown

Greptile Summary

Implements frontmost_bundle_id() on Linux by querying _NET_ACTIVE_WINDOW on the X11 root window and reading the WM_CLASS class component of the active window, enabling per-app profile switching for X11/XWayland sessions. Pure Wayland windows (not under XWayland) return None as documented.

  • X11State (connection, root, atom) is lazily initialised once via LazyLock; per-call cost is two round-trips (one get_property, one WmClass::get), consistent with the 1 Hz polling cadence.
  • wait_readable gains ERR_FLAGS checks to prevent CPU spinning when a grabbed input device is unplugged (part of the feat(hook): implement Linux evdev+uinput mouse hook #119 base).
  • The example binary uses dual #[cfg]-gated fn main stubs so non-Linux targets compile cleanly.

Confidence Score: 5/5

Safe 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

Filename Overview
crates/openlogi-hook/src/linux.rs Adds frontmost_bundle_id() via X11, LazyLock-initialized connection; wait_readable gains ERR_FLAGS guard for device-unplug spin. Dead connection after X server restart silently yields permanent None with no log.
crates/openlogi-hook/src/lib.rs Adds cfg-gated dispatch to linux::frontmost_bundle_id(); fallback None branch updated to exclude both macOS and Linux. Correct and clean.
crates/openlogi-gui/src/watchers/foreground_app.rs Enables the app-watcher thread on Linux by extending the cfg guard; no logic changes.
crates/openlogi-hook/examples/frontmost_app.rs New smoke-test example; uses dual cfg-gated fn main correctly so non-Linux targets compile cleanly.
crates/openlogi-hook/Cargo.toml Adds x11rb = "0.13" as a Linux-only dependency; correctly scoped under cfg(target_os = "linux").

Sequence Diagram

sequenceDiagram
    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
Loading

Fix All in Codex Fix All in Claude Code

Reviews (3): Last reviewed commit: "fix(hook): stop wait_readable busy-loopi..." | Re-trigger Greptile

Comment thread crates/openlogi-hook/examples/frontmost_app.rs Outdated
Comment thread crates/openlogi-hook/examples/print_events.rs Outdated
Comment thread crates/openlogi-hook/src/linux.rs

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; None on failure gracefully degrades to no detection
  • _NET_ACTIVE_WINDOW property query — standard EWMH path; get_propertyvalue32()next()? with null-XID guard
  • WmClass::get for 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 throughout Hook struct, start/stop/Drop, and frontmost_bundle_id()
  • Update src/tests.rs — Linux error variants in hook_error_display; new linux_start_does_not_return_unsupported test
  • New frontmost_app and print_events examples — 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).

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Big Pickle (free) (credentials for Anthropic not configured) | 𝕏

Comment thread crates/openlogi-hook/src/linux.rs
cserby added a commit to cserby/OpenLogi that referenced this pull request Jun 4, 2026
- 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>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 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_app watcher for Linuxcfg! guard in crates/openlogi-gui/src/watchers/foreground_app.rs changed from !cfg!(target_os = "macos") to !cfg!(any(target_os = "macos", target_os = "linux")), closing the gap where frontmost_bundle_id() was functional at the API level but un-consumed by the GUI
  • Switch create_pipe to pipe2(O_CLOEXEC) — prevents the shutdown pipe fds from being inherited by forked children, which would delay clean shutdown by keeping the write-end alive past poll()
  • Restructure examples for cross-platform compilation — both frontmost_app.rs and print_events.rs now 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.

Pullfrog  | Fix it ➔View workflow run | Using Big Pickle (free) (credentials for Anthropic not configured) | 𝕏

cserby and others added 4 commits June 5, 2026 20:56
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.)
@AprilNEA AprilNEA force-pushed the story/linux-frontmost/frontmost-bundle-id-linux branch from 3fcbf8b to d1d6ed9 Compare June 5, 2026 13:22
@AprilNEA AprilNEA merged commit 384bc58 into AprilNEA:master Jun 5, 2026
8 checks passed
AprilNEA pushed a commit that referenced this pull request Jun 5, 2026
- 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
AprilNEA added a commit that referenced this pull request Jun 5, 2026
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants