Windows Tray Icon vs macOS Menu Bar in OpenLogi: Key Implementation Differences
OpenLogi uses high-level AppKit APIs with main-thread dispatch for macOS menu-bar integration, while the Windows tray icon implementation relies on raw Win32 calls running on a dedicated thread with a hidden message-pump window.
The OpenLogi agent requires a persistent UI entry point across platforms to provide users with immediate access to the main application window. While both implementations serve identical functional purposes—allowing users to show the GUI or quit the agent—the architectural approaches diverge significantly between the Objective-C AppKit bindings in crates/openlogi-agent/src/tray.rs and the low-level Win32 system calls in crates/openlogi-agent/src/tray_windows.rs.
Framework and API Abstraction Levels
The macOS menu bar approach leverages the objc2 crate to bridge Rust with Apple's native AppKit framework. This high-level binding provides direct access to NSStatusItem and NSMenu objects through Objective-C interop, abstracting away the complexity of the underlying Cocoa runtime. The implementation calls methods like status_item.setMenu() and handles template image rendering automatically.
In contrast, the Windows tray icon implementation uses the windows-sys crate to invoke raw C-style Win32 APIs directly. The code manually constructs NOTIFYICONDATAW structures and calls Shell_NotifyIconW to register the notification area icon. Rather than using a framework-provided menu object, it manually builds popup menus with CreatePopupMenu and tracks them via TrackPopupMenu, requiring explicit window procedure handling for user interactions.
Threading Models and Runtime Architecture
Thread safety requirements fundamentally shape each implementation's architecture.
On macOS, all UI work must execute on the main thread due to AppKit's threading constraints. The tray.rs module dispatches operations through DispatchQueue::main() and stores the global TrayState in a thread-local RefCell to ensure single-threaded access. Functions like set_icon() and relocalize() queue closures for main-thread execution using MainThreadMarker validation:
pub fn set_icon(icon: AppIcon) {
DispatchQueue::main().exec_async(move || {
let Some(mtm) = MainThreadMarker::new() else { return };
TRAY.with_borrow(|state| {
if let Some(state) = state.as_ref() {
status_item::set_png_icon(&state.item, mtm, glyph(icon), "OpenLogi");
}
});
});
}
On Windows, the tray runs on its own dedicated thread named "openlogi-tray". The spawn() function creates this thread only when show_in_tray is enabled, leaving the agent invisible otherwise. This thread creates a hidden top-level window via CreateWindowExW, installs the tray icon, and pumps a traditional Windows message loop for the agent's lifetime:
pub fn spawn(show_in_tray: bool) {
if !show_in_tray {
info!("tray icon disabled by preference — agent stays invisible");
return;
}
std::thread::Builder::new()
.name("openlogi-tray".into())
.spawn(run_tray_loop)
.unwrap_or_else(|e| warn!(error = %e, "could not spawn the tray thread"));
}
Menu Content and Interaction Patterns
The macOS menu bar provides a rich contextual menu containing "Show Main Window", "Settings", "About", "Check-for-Updates", and "Quit" options. These items invoke deep-links using the openlogi:// URL scheme, which the GUI receives through Apple Events. This allows the agent to communicate with the main application process using standard macOS inter-process communication mechanisms.
The Windows tray icon offers a simplified menu containing only "Show Main Window" (triggered by left-click) and "Quit OpenLogi". Because OpenLogi does not register a custom URL scheme on Windows, the implementation cannot use deep-links. Instead, menu selections execute direct function calls within the tray thread, with the popup built manually through Win32 menu APIs rather than through a framework abstraction.
Visual Design and System Theming
Icon handling differs significantly between platforms to accommodate native theming conventions.
On macOS, the status item uses a template image loaded from tray-icon@2x.png. AppKit automatically tints this monochrome glyph to match the current menu bar appearance (light or dark mode) without requiring runtime detection logic. The system handles all color inversion and high-DPI scaling internally.
On Windows, the implementation must manually detect the system theme and swap between two pre-rendered PNG assets: a black icon for light taskbars and a white icon for dark taskbars (stored as tray-icon-white@2x.png). The code converts these to HICON handles at runtime and updates the NOTIFYICONDATAW structure when theme changes occur, as Windows lacks automatic template image tinting for notification area icons.
Resilience and Lifecycle Management
The macOS implementation maintains its menu-bar presence continuously while the agent process runs, with no special handling required for system UI restarts. The NSStatusItem persists as long as the owning process maintains its reference.
The Windows implementation must handle Explorer restarts explicitly. When Windows Explorer crashes or restarts, all notification area icons disappear. To maintain visibility, the code registers for the TaskbarCreated broadcast message via RegisterWindowMessageW and re-adds the icon when this message arrives, ensuring the tray icon survives shell restarts without requiring the user to relaunch OpenLogi.
Summary
- Framework Choice: macOS uses high-level AppKit bindings (
objc2) while Windows uses low-level Win32 APIs (windows-sys). - Threading: macOS requires main-thread execution via
DispatchQueue::main(); Windows uses a dedicated"openlogi-tray"thread with a message pump. - Menu Features: macOS supports deep-links and rich menus via Apple Events; Windows uses a simplified manual popup without URL scheme integration.
- Theming: macOS relies on template images for automatic tinting; Windows manually swaps icon assets based on theme detection.
- Resilience: Windows explicitly handles
TaskbarCreatedmessages to survive Explorer restarts; macOS requires no equivalent logic.
Frequently Asked Questions
Why does the Windows implementation use a dedicated thread instead of the main thread?
The Windows tray icon requires a continuous message loop (GetMessageW/DispatchMessageW) to process system notifications like mouse clicks and the TaskbarCreated broadcast. Running this loop on the main thread would block other agent operations, so OpenLogi isolates the Win32 window procedure and message pump on a dedicated thread named "openlogi-tray", allowing the agent core to operate independently.
How does OpenLogi handle icon visibility when users disable the tray icon?
On Windows, if show_in_tray is false, the spawn() function returns immediately without creating the dedicated thread, leaving the agent running without any visible presence. On macOS, the visibility is controlled by a property on the existing NSStatusItem, but the item itself remains allocated; the system simply hides it from the menu bar when disabled.
What happens to the tray icon when Windows Explorer restarts?
The Windows implementation registers for the TaskbarCreated window message using RegisterWindowMessageW. When Explorer restarts and broadcasts this message, the window procedure receives it and re-executes Shell_NotifyIconW to re-add the notification area icon, restoring visibility without user intervention or process restart.
Why does the macOS version support more menu items than the Windows version?
The macOS implementation leverages the openlogi:// URL scheme and Apple Events to communicate with the main GUI process, enabling rich interactions like "Settings" and "Check-for-Updates" that delegate to the main application. The Windows version lacks a registered custom URL scheme, limiting the tray menu to direct local actions ("Show Main Window" and "Quit") that do not require cross-process communication.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →