# How Meetily Implements System Tray Icon and Functionality Using Tauri

> Learn how Meetily integrates system tray icons and functionality using Tauri. Explore the Rust code in src-tauri/src/tray.rs for dynamic menu states and frontend communication.

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

---

**Meetily's system tray integration is implemented entirely in Rust using Tauri's `TrayIconBuilder` and `MenuBuilder` APIs, with the core logic centralized in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs) to handle dynamic menu states, recording workflows, and bidirectional communication with the React frontend.**

Meetily is an open-source meeting assistant built by Zackriya-Solutions that leverages Tauri for its cross-platform desktop application. The system tray icon and functionality provides users with instant access to recording controls without requiring the main window to be focused. This implementation relies entirely on Tauri's Rust-side APIs for tray management, menu construction, and event handling, creating a native system integration that stays synchronized with the application's internal recording state.

## Tray Initialization and Icon Setup

The system tray lifecycle begins when the application starts. In [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs), the initialization code calls `tray::create_tray(_app.handle())` to instantiate the tray icon before the main window appears.

The `create_tray` function uses `TrayIconBuilder::with_id("main-tray")` to register the icon with the operating system, pulling the default icon from the main window assets. It establishes the `on_menu_event` callback to route all user interactions through the centralized `handle_menu_event` dispatcher, which matches menu item IDs to their respective handlers.

```rust
// Called from src/lib.rs during app initialization
if let Err(e) = tray::create_tray(_app.handle()) {
    log::error!("Failed to create system tray: {}", e);
}

```

## Dynamic Menu Construction Based on Recording State

Unlike static tray menus, Meetily's menu adapts to the current recording state and application readiness. The `build_menu` function constructs the interface dynamically using Tauri's `MenuBuilder` API, ensuring users only see relevant actions.

### State-Aware Menu Items

The function accepts a `RecordingState` enum (variants: `Stopped`, `Starting`, `Recording`, `Pausing`, `Paused`, `Resuming`, `Stopping`) and a `can_record` boolean flag. When `can_record` is false—indicating the transcription model is still downloading or onboarding is incomplete—the menu displays a disabled item reading "⏳ Downloading transcription model...".

For active recording states, the menu presents contextual controls:
- **Stopped**: Shows "Start Recording"
- **Recording**: Shows "Pause Recording" and "Stop Recording"
- **Paused**: Shows "Resume Recording" and "Stop Recording"

Static items like "Open Main Window", "Settings", "Check for Updates", and "Quit" are always appended with separators.

```rust
fn build_menu<R: Runtime>(
    app: &AppHandle<R>,
    state: RecordingState,
    can_record: bool,
) -> tauri::Result<tauri::menu::Menu<R>> {
    let mut builder = MenuBuilder::new(app);
    if !can_record {
        builder = builder.item(&MenuItemBuilder::new("⏳ Downloading transcription model...")
            .enabled(false).build(app)?);
    } else {
        match state {
            RecordingState::Stopped => {
                builder = builder.item(&MenuItemBuilder::with_id(
                    "toggle_recording", "Start Recording").build(app)?);
            }
            // …other states…
        }
    }
    // Static items
    builder.item(&PredefinedMenuItem::separator(app)?)
        .item(&MenuItemBuilder::with_id("open_window", "Open Main Window").build(app)?)
        .item(&MenuItemBuilder::with_id("settings", "Settings").build(app)?)
        .item(&MenuItemBuilder::with_id("check_updates", "Check for Updates").build(app)?)
        .item(&PredefinedMenuItem::separator(app)?)
        .item(&MenuItemBuilder::with_id("quit", "Quit").build(app)?)
        .build()
}

```

## Event Handling and Recording Workflows

User interactions with the tray menu are processed through the `handle_menu_event` function in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs), which matches menu item IDs including `toggle_recording`, `pause_recording`, `resume_recording`, `stop_recording`, `open_window`, `settings`, `check_updates`, and `quit`.

### Asynchronous State Management

Recording actions follow a strict state-transition pattern. When a user initiates recording, the handler first updates the tray to an intermediate state (e.g., `RecordingState::Starting`) using `set_tray_state`, then spawns an async task that invokes the appropriate recording command from `crate::audio::recording_commands::*`.

After the command completes, `update_tray_menu_async` rebuilds the menu to reflect the new state. This ensures the tray UI remains synchronized with the internal recording manager, even when state changes originate from the frontend React application.

```rust
fn toggle_recording_handler<R: Runtime>(app: &AppHandle<R>) {
    focus_main_window(app);
    let app_clone = app.clone();
    tauri::async_runtime::spawn(async move {
        if crate::is_recording().await {
            // …stop logic…
        } else {
            set_tray_state(&app_clone, RecordingState::Starting);
            // Signal the frontend to auto‑start recording
            if let Some(window) = app_clone.get_webview_window("main") {
                let _ = window.eval("sessionStorage.setItem('autoStartRecording', 'true')");
                let _ = window.eval("window.location.assign('/')");
            }
        }
    });
}

```

### Window Focus Integration

The `focus_main_window` helper brings the main Tauri window to the foreground, un-minimizes it, and executes JavaScript evaluation to trigger `window.focus()`. This ensures that actions like opening settings or starting a recording naturally shift focus to the main application window when appropriate.

## Frontend Communication and Event Emission

The tray implementation communicates with the React frontend through Tauri's event system. The frontend listens for custom events such as `recording-stop-complete` and `check-updates-from-tray` in [`frontend/src/app/layout.tsx`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src/app/layout.tsx) and [`frontend/src/components/UpdateCheckProvider.tsx`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src/components/UpdateCheckProvider.tsx).

This bidirectional flow allows the tray to both drive actions—emitting events that the UI reacts to—and reflect UI state by updating its menu based on frontend status through `update_tray_menu_async`, which queries `get_current_recording_state` and `check_can_record` before rebuilding the interface.

```rust
pub fn update_tray_menu<R: Runtime>(app: &AppHandle<R>) {
    let app_clone = app.clone();
    tauri::async_runtime::spawn(async move {
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
        update_tray_menu_async(&app_clone).await;
    });
}

```

## Configuration Requirements

To enable system tray functionality, the [`frontend/src-tauri/tauri.conf.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/tauri.conf.json) configuration must include the `tray-icon` feature, and [`frontend/src-tauri/Cargo.toml`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/Cargo.toml) must declare the appropriate `tray-icon` dependencies for the Rust build. The tray icon itself is defined in the Tauri configuration and referenced by the `TrayIconBuilder` during initialization.

## Summary

- Meetily implements its system tray entirely in Rust within [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs).
- The `TrayIconBuilder` and `MenuBuilder` APIs construct the icon and dynamic context menus.
- Recording states (`Stopped`, `Recording`, `Paused`, etc.) determine available menu items through the `build_menu` function.
- The `handle_menu_event` dispatcher routes clicks to recording commands or window actions.
- `update_tray_menu_async` ensures the tray stays synchronized with the internal recording state.
- Frontend integration occurs through custom Tauri events emitted to [`layout.tsx`](https://github.com/Zackriya-Solutions/meetily/blob/main/layout.tsx) and [`UpdateCheckProvider.tsx`](https://github.com/Zackriya-Solutions/meetily/blob/main/UpdateCheckProvider.tsx).

## Frequently Asked Questions

### How does Meetily update the tray menu when recording starts?

When recording begins, the tray handler calls `set_tray_state` to set an intermediate state (like `Starting`), then spawns an async task to execute the recording command from `crate::audio::recording_commands::*`. Upon completion, `update_tray_menu_async` queries the current recording state via `get_current_recording_state` and rebuilds the menu with `build_menu` to reflect the new available actions.

### Can users interact with the tray while the transcription model is downloading?

No. The `build_menu` function checks the `can_record` flag, and when false—indicating the model is downloading or onboarding is incomplete—it displays a disabled menu item "⏳ Downloading transcription model..." instead of the recording controls. The menu updates automatically once `check_can_record` returns true.

### What happens when a user clicks "Settings" in the tray menu?

The `handle_menu_event` matches the `settings` ID and invokes the appropriate handler, which calls `focus_main_window` to bring the main Tauri window to the foreground, un-minimize it, and shift focus. The frontend then navigates to the settings view based on the event received.

### Where is the system tray icon configured in the Tauri application?

The icon is configured in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs) using `TrayIconBuilder::with_id("main-tray")`, which references the default application icon assets. The feature is enabled in [`frontend/src-tauri/tauri.conf.json`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/tauri.conf.json) and depends on the `tray-icon` crate specified in [`Cargo.toml`](https://github.com/Zackriya-Solutions/meetily/blob/main/Cargo.toml).