Skip to content

windows: run as tray icon app in the notification area#467

Open
inaiii wants to merge 1 commit into
feschber:mainfrom
inaiii:feature/windows-tray-icon
Open

windows: run as tray icon app in the notification area#467
inaiii wants to merge 1 commit into
feschber:mainfrom
inaiii:feature/windows-tray-icon

Conversation

@inaiii

@inaiii inaiii commented Jul 12, 2026

Copy link
Copy Markdown

Summary

Add Windows notification-area (system tray) support, mirroring the existing macOS menu-bar application behavior and the Linux StatusNotifierItem support proposed in #466:

  • 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 opens a separate window in the primary instance;
  • if tray registration fails, the previous close-to-quit behavior is retained so a hidden window can never become unreachable.

Related to #291. Together with #466, this covers the platforms requested there.

Behavior

Startup state Tray behavior Closing the window
Tray icon registered successfully Icon in the notification area with a menu Hide the window and keep running
Tray icon could not be registered Log a warning and continue without a tray icon Quit as before

Unlike the Linux PR, there is no external-daemon probe and the tray is set up unconditionally, matching the macOS status item: on Windows the GUI always spawns the service as a child process (a child finding the IPC socket already bound exits gracefully with service already running!). In the uncommon case where the service was started externally (lan-mouse daemon in a terminal), the GUI still creates a tray icon and quitting from it exits only the GUI, leaving the external daemon running.

A single left click on the tray icon presents the window; the menu (Open Lan Mouse, Quit Lan Mouse) stays on right click. The tray-icon crate re-adds the icon after an explorer.exe restart (TaskbarCreated broadcast).

Implementation

  • Use the tray-icon crate (Shell_NotifyIconW under the hood) with default features disabled; the defaults (gtk, libxdo) only apply to its Linux backend, which is unreachable behind the windows-only target dependency.
  • Restrict the module, dependency, and tray-specific window behavior to cfg(windows).
  • Forward tray callbacks from the tray's WndProc to the GTK main loop through an async channel; the handlers are Send + Sync and cannot capture GTK objects.
  • Hold the gio::Application while the window is hidden so the frontend stays alive.
  • Render the tray icon from the bundled application icon: the SVG where the gdk-pixbuf loader is available, with pre-rendered PNG rasters as a fallback since distributed builds may lack the external librsvg loader module. The same rasters let the window icon resolve through IconTheme::add_resource_path on platforms without a system-installed icon theme.
  • Build the binary for the windows subsystem so a GUI launch neither opens a console window nor dies with it (closing an attached console would CTRL_CLOSE_EVENT-terminate the frontend and service child alike, bypassing the tray). Terminal subcommands (daemon, cli, test-*, --help) re-attach to the parent console via AttachConsole(ATTACH_PARENT_PROCESS) whenever command-line arguments are present, so their output keeps working; shell redirection takes precedence over the attached console and keeps working as well.
  • Embed build-aux/windows/lan-mouse.ico into the executable via winresource so Explorer, the Start menu, and pinned taskbar shortcuts display the application icon.
  • Document the tray behavior and LAN_MOUSE_HIDDEN in the README.

Fallback behavior

Registration failure is intentionally non-fatal. If the tray icon cannot be rendered or registered, the window is always shown and closing it quits the application, preventing an unreachable hidden process.

Relationship to #466, #407 and #373

  • linux: run as tray icon app with StatusNotifierItem #466 implements the same feature for Linux and is intentionally submitted separately. The two branches touch overlapping regions of lan-mouse-gtk/src/lib.rs, lan-mouse-gtk/Cargo.toml, src/main.rs, and the README; whichever lands second will be rebased, keeping the per-platform behavior unchanged.
  • feat: single-instance application support #407 also modifies lan-mouse-gtk/src/lib.rs and src/main.rs. This PR does not change activation or instance handling — GApplication already provides single-instance behavior on Windows through its win32 session bus — so the overlap is textual and can be reconciled during rebase.
  • feat: windows service for uac+login support #373 also modifies src/main.rs and Cargo.toml for its Windows service support. The windows subsystem change here affects console behavior of the same binary; both changes are compatible (a service process has no interactive console either way) but will need a textual rebase.

Test plan

Automated checks completed locally on Windows:

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo test --workspace --all-features
  • cargo build / cargo build --release
  • 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/windows-tray-icon at f55e74b.

Manual smoke test environment:

  • OS: Windows 11 Pro for Workstations 25H2 (build 26200.8655)
  • Shell: Windows Explorer (standard taskbar and notification area)
  • Display: single 1920x1080 monitor, 100% scale
  • GTK stack: GTK 4.22.4, libadwaita 1.9.1 (gvsbuild release build)
  • Toolchain: rustc 1.96.0
  • Capture/emulation selected during the test: default Windows backends (low-level hooks capture, SendInput emulation)
  • Binaries under test: cargo build --release of f55e74b; single-instance handoff exercised against an installed build of the same branch running as primary

Smoke tests completed on this environment:

  • Tray icon registration succeeds on startup (windows_status_item registered at debug level; the icon and tooltip render in the notification area).
  • Closing the main window (WM_CLOSE) hides it while the frontend and tray registration remain alive.
  • LAN_MOUSE_HIDDEN=1 starts the frontend without showing a window; the tray icon still registers.
  • The service child spawned by the GUI runs as a separate daemon process; a child that finds the service already running exits gracefully.
  • Launching Lan Mouse again hands off to the primary instance, which presents a window; the second process exits and no duplicate processes remain.
  • The binary is built for the WINDOWS_GUI subsystem (no console window on GUI launch); --help and the daemon subcommand print to redirected output as before.
  • A single left click on the tray icon presents the hidden window.
  • The tray menu exposes Open Lan Mouse and Quit Lan Mouse actions; Open presents the hidden window.
  • The tray menu's Quit action exits the frontend and gracefully terminates the child service.
  • Subcommand output prints to the invoking terminal when run interactively (not just redirected).
  • Restarting explorer.exe re-adds the tray icon.
  • Input forwarding continues while the main window is hidden.
  • An icon group is embedded in the executable (ExtractIconEx reports it; Explorer, Start menu, and pinned shortcuts display it).

All manual smoke-test cases above were completed successfully.

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

Keep Lan Mouse running in the notification area after its window closes,
use the GUI subsystem without breaking CLI output, and bundle reliable
window, tray, and executable icons for distributed builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Codex (GPT-5) <noreply@openai.com>
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