# How Meetily Implements System Tray Integration in Its Tauri Desktop App

> Discover how Meetily achieves seamless system tray integration in its Tauri app. Explore the Rust code for dynamic menus and event handling.

- Repository: [Zackriya Solutions/meetily](https://github.com/Zackriya-Solutions/meetily)
- Tags: how-to-guide
- Published: 2026-07-06

---

**Meetily implements system tray integration using Tauri's native `TrayIconBuilder` API, constructing a dynamic menu in [`tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/tray.rs) that handles recording state changes and dispatches events to both Rust commands and the React frontend.**

Meetily is an open-source AI meeting assistant built with a **Tauri** desktop architecture. The system tray integration provides users with persistent access to recording controls and navigation without keeping the main window focused. This implementation leverages Tauri's cross-platform tray APIs to create a context-aware menu that reflects real-time recording states.

## Tray Creation and Initialization

The system tray is instantiated during the application bootstrap phase in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs). Inside the `.setup()` closure, the application calls `tray::create_tray` with the `AppHandle`:

```rust
// frontend/src-tauri/src/lib.rs
builder.setup(|_app| {
    if let Err(e) = tray::create_tray(_app.handle()) {
        log::error!("Failed to create system tray: {}", e);
    }
    Ok(())
})

```

The `create_tray` function in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs) (lines 19-32) performs four critical operations:

1. **Builds an initial menu** using the default `RecordingState::Stopped` state
2. **Instantiates the tray icon** with the application's default window icon
3. **Registers the menu event callback** (`on_menu_event`) that forwards item IDs to `handle_menu_event`
4. **Triggers an immediate menu refresh** via `update_tray_menu` to ensure the UI reflects the actual recording state after startup

## Dynamic Menu Generation Based on Recording State

The menu construction is handled by `build_menu` in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs) (lines 16-93). This function accepts three parameters:

- `app` – The `AppHandle` for menu construction
- `state` – The current `RecordingState` enum value
- `can_record` – A boolean flag that disables recording controls during onboarding or while downloading transcription models

Based on these inputs, the function constructs a `tauri::menu::Menu` with context-specific items:

- **State-specific controls**: Start, Pause, Resume, Stop, and transitional labels like "…Starting…"
- **Standard navigation**: *Open Main Window*, *Settings*, *Check for Updates*, and *Quit*
- **Disabled placeholders**: A `⏳ Downloading transcription model…` item when `can_record` is `false`

The menu rebuilds dynamically whenever the recording state changes through `update_tray_menu_async`, which queries the current state via `get_current_recording_state` and `check_can_record` before constructing the new menu.

## Event Handling and User Actions

User interactions are processed in `handle_menu_event` (lines 37-53 in [`tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/tray.rs)), which matches the `item_id` against specific handlers:

| Menu ID | Handler Function | Action |
|---------|------------------|--------|
| `toggle_recording` | `toggle_recording_handler` | Starts recording by setting a flag in `sessionStorage` or stops via `audio::recording_commands::stop_recording` |
| `pause_recording` | `pause_recording_handler` | Invokes `audio::recording_commands::pause_recording` |
| `resume_recording` | `resume_recording_handler` | Invokes `audio::recording_commands::resume_recording` |
| `stop_recording` | `stop_recording_handler` | Stops and saves the current recording file |
| `open_window` | `focus_main_window` | Brings the Tauri window to the foreground |
| `settings` | `focus_main_window` + JS eval | Opens the Settings page via `window.location.assign('/settings')` |
| `check_updates` | `check_updates_handler` | Dispatches a custom DOM event `check-updates-from-tray` |
| `quit` | `app.exit(0)` | Terminates the application process |

All handlers run asynchronously on Tauri's `async_runtime` and emit events (such as `recording-stop-complete`) enabling the React frontend to react without polling.

## State Synchronization Mechanisms

The tray menu remains synchronized with the Rust recording subsystem through three key functions:

**`set_tray_state`** – Provides immediate UI feedback for transitional states (e.g., "Starting Recording…"). It rebuilds the menu instantly with the supplied state before the actual recording command completes.

**`update_tray_menu`** – A debounced wrapper that schedules an async task with a 100ms delay before calling `update_tray_menu_async`. This prevents excessive menu rebuilds during rapid state changes.

**`update_tray_menu_async`** – Queries the ground truth of the recording state using `crate::audio::recording_commands::is_recording` and `is_recording_paused`, checks `check_can_record`, and replaces the existing tray menu via `tray.set_menu`.

## Practical Implementation Examples

### Adding a Custom Menu Item

To extend the tray with a new "Show Log" option, modify the builder in [`tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/tray.rs):

```rust
// In frontend/src-tauri/src/tray.rs
builder
    .item(&PredefinedMenuItem::separator(app)?)
    .item(&MenuItemBuilder::with_id("show_log", "Show Log").build(app)?)
    .item(&PredefinedMenuItem::separator(app)?)
    // existing items...
    .build()

```

Then handle the click in `handle_menu_event`:

```rust
match item_id {
    "show_log" => {
        focus_main_window(app);
        if let Some(window) = app.get_webview_window("main") {
            let _ = window.eval("window.location.assign('/log')");
        }
    }
    _ => {}
}

```

### Manually Refreshing the Tray

Force a menu update after custom state mutations:

```rust
use crate::tray;

// After state change
tray::update_tray_menu(&app_handle);

```

### Emitting Frontend Events

Dispatch custom events to the React frontend from tray actions:

```rust
if let Some(window) = app.get_webview_window("main") {
    let _ = window.eval(
        "window.dispatchEvent(new CustomEvent('custom-tray-action'))"
    );
}

```

## Summary

- **Initialization**: The tray is created in [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs) via `tray::create_tray`, which sets up the icon, initial menu, and event handlers.
- **Dynamic Menus**: `build_menu` constructs context-aware menus based on `RecordingState` and `can_record` flags, disabling controls during onboarding.
- **Event Dispatch**: `handle_menu_event` routes clicks to specific handlers that invoke Rust commands in [`audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/recording_commands.rs) or execute JavaScript via `window.eval`.
- **State Sync**: `update_tray_menu_async` and `set_tray_state` ensure the tray reflects the actual recording status without polling.

## Frequently Asked Questions

### How does Meetily handle tray menu updates when recording starts?

Meetily uses `update_tray_menu_async` to rebuild the menu dynamically. When a recording starts, the handler calls this function, which queries `is_recording()` and `check_can_record()` before constructing a new menu with Pause/Stop options instead of Start. A 100ms debounce in `update_tray_menu` prevents excessive rebuilds during rapid state transitions.

### Can the system tray controls work when the main window is closed?

Yes, the tray operates independently of the main window visibility. The handlers use `app.get_webview_window("main")` to lazily access the window only when needed (such as for navigation or focus), while recording commands in [`audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/recording_commands.rs) function regardless of window state.

### Where is the tray icon configured in the Meetily codebase?

The tray icon is set during `create_tray` in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs) using the application's default icon resources. The implementation uses `TrayIconBuilder` with the icon path configured through Tauri's asset protocol, typically referencing the same icon files used for the application window.

### How does the tray communicate with the React frontend?

The tray communicates via JavaScript evaluation using `window.eval()` on the main webview window. For example, the settings handler executes `window.location.assign('/settings')`, while the check updates handler dispatches a `CustomEvent` that React components can listen for using standard DOM event listeners.