Skip to content

linux: run as tray icon app with StatusNotifierItem#466

Open
inaiii wants to merge 2 commits into
feschber:mainfrom
inaiii:feature/linux-tray-icon
Open

linux: run as tray icon app with StatusNotifierItem#466
inaiii wants to merge 2 commits into
feschber:mainfrom
inaiii:feature/linux-tray-icon

Conversation

@inaiii

@inaiii inaiii commented Jul 12, 2026

Copy link
Copy Markdown

Summary

Add Linux system-tray support through the StatusNotifierItem D-Bus specification, mirroring the existing macOS menu-bar application behavior:

  • closing the main window hides it while Lan Mouse keeps running;
  • the tray icon can present the window or quit the application;
  • LAN_MOUSE_HIDDEN=1 starts the application quietly in the tray;
  • launching Lan Mouse again presents an existing hidden window;
  • desktops without a StatusNotifierItem host retain the previous close-to-quit behavior; and
  • a GUI connected to an externally managed daemon remains a plain window and does not claim ownership through a tray icon.

Related to #291.

Windows notification-area support is intentionally being submitted separately,so this PR does not close the issue.

Behavior

Startup state Tray behavior Closing the window
GUI owns the service and an SNI host is available Register the tray icon Hide the window and keep running
GUI owns the service but no SNI host is available Log a warning and continue without a tray icon Quit as before
An external daemon already owns the IPC socket Do not create a tray icon Quit only the GUI; leave the daemon running

The tray is owned by the graphical frontend rather than the daemon. A standalone daemon may run without a graphical session or session D-Bus, so making it own desktop UI would couple headless operation to the desktop environment. The frontend therefore creates the tray only when it also owns the service lifecycle.

The socket is probed before spawning the normal service child. The child is still spawned so it can win the bind if the external daemon exits during that startup race.

Implementation

  • Use ksni for StatusNotifierItem registration without introducing a GTK3 tray dependency.
  • Restrict the module, dependency, daemon probe, and tray-specific window behavior to target_os = "linux".
  • Render fallback ARGB icon pixmaps from the existing bundled application icon for environments where the themed icon cannot be resolved.
  • Hold the gio::Application while the window is hidden so the frontend stays alive.
  • Forward tray callbacks from the SNI service thread to the GTK main loop through an async channel.
  • Preserve the existing public lan_mouse_gtk::run(local_commit) API. The main binary uses documented RunOptions only when it needs to describe external service ownership.
  • Document the tray behavior, GNOME limitation, fallback behavior, and LAN_MOUSE_HIDDEN in the README.

Desktop compatibility and fallback

StatusNotifierItem is supported by environments such as KDE Plasma, XFCE, and panels such as waybar. GNOME does not expose an SNI host by default and requires an extension such as AppIndicator and KStatusNotifierItem Support.

Registration failure is intentionally non-fatal. If no host is available, the window is always shown and closing it quits the application, preventing an unreachable hidden process.

The xdg background portal and automatic installation of desktop extensions are outside the scope of this PR.

Relationship to #407

This PR partially overlaps #407 in two limited startup areas:

  • it probes the daemon socket to decide tray ownership semantics; and
  • its GApplication::activate handler presents an existing hidden window instead of constructing another window.

It does not attempt to replace #407's broader daemon-reuse, reconnect, or cross-platform singleton design. If #407 lands first, the overlapping startup code can be reconciled during rebase while keeping the Linux tray behavior unchanged.

Test plan

Automated checks completed locally on Linux:

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-features
  • Fork CI matrix: formatting plus build, check, clippy, and test passed on Ubuntu 24.04, Windows 2022, macOS 15, and macOS 15 Intel. The workflow was dispatched from the fork's main branch, and its source jobs checked out feature/linux-tray-icon at e93f769.

Manual smoke test environment:

  • Distribution: Arch Linux (rolling), kernel 7.1.3-arch1-2
  • Desktop/compositor: niri 26.04 (8ed0da4)
  • Session type: Wayland
  • Tray host/panel: Waybar 0.15.0, with its tray module enabled
  • Display: BOE eDP-1, 3840x2160 physical / 1536x864 logical, scale factor 3
  • Capture/emulation selected during the test: layer-shell / wlroots

Smoke tests completed on this environment:

  • The StatusNotifierWatcher reports a registered Lan Mouse SNI item while Waybar's tray host is active.
  • LAN_MOUSE_HIDDEN=1 lan-mouse starts both the frontend and child daemon without mapping a Lan Mouse window.
  • Activating the SNI item presents the existing Lan Mouse window.
  • Closing the main window removes it from niri's window list while the frontend, child daemon, and SNI registration remain alive.
  • The tray menu exposes Open Lan Mouse and Quit Lan Mouse actions.
  • The tray menu's Open action presents the hidden window.
  • Launching Lan Mouse again exits the secondary invocation and presents the existing hidden window without leaving duplicate processes.
  • The tray menu's Quit action exits the frontend, gracefully terminates its child service, and removes the IPC socket.
  • Input forwarding continues while the main window is hidden.
  • Without an SNI host, the main window remains reachable and closing it quits.
  • With lan-mouse daemon already running, the GUI creates no tray icon and closing it leaves the daemon running.

All manual smoke-test cases above were completed successfully.

No protocol or serialization changes are included, so no protocol version bump is required.

inaiii and others added 2 commits July 11, 2026 23:47
Mirror the macOS menu bar behavior on Linux: closing the window hides
it into the tray, the tray menu re-opens or quits the app, and
LAN_MOUSE_HIDDEN=1 starts quietly into the tray. Implemented with ksni
(StatusNotifierItem over D-Bus, no GTK3 dependency); desktops without
an SNI host (e.g. GNOME without the AppIndicator extension) fall back
to the previous close-means-quit behavior.

The tray follows service ownership: when an external daemon (e.g. the
systemd service) already owns the lan-mouse socket, the GUI runs as a
plain client window without a tray icon, so quitting it does not
suggest stopping the service. The socket is probed before spawning the
service child, which is still spawned either way so it takes over if
the external daemon exits right after.

Launching the binary while an instance is running now re-presents the
existing window instead of building a duplicate UI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keep run(local_commit) as the backward-compatible entry point and expose
service ownership through documented RunOptions. Limit StatusNotifierItem
dependencies and code paths to Linux while retaining the default window
behavior on other platforms.

Co-Authored-By: Codex (GPT-5) <noreply@openai.com>
@inaiii inaiii changed the title linux: run as tray icon app with StatusNotifierItemFeature/linux tray icon linux: run as tray icon app with StatusNotifierItem Jul 12, 2026
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.

1 participant