# Windows Tray Icon vs macOS Menu Bar in OpenLogi: Key Implementation Differences

> Explore the Windows tray icon vs macOS menu bar implementation differences in OpenLogi. Discover distinct approaches using Win32 calls and AppKit APIs.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: internals
- Published: 2026-09-05

---

**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`](https://github.com/AprilNEA/OpenLogi/blob/main/crates/openlogi-agent/src/tray.rs) and the low-level Win32 system calls in [`crates/openlogi-agent/src/tray_windows.rs`](https://github.com/AprilNEA/OpenLogi/blob/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
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:

```rust
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 `TaskbarCreated` messages 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.