How Meetily Implements System Tray Icon and Functionality Using Tauri

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 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, 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.

// 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.

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, 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.

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 and 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.

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 configuration must include the tray-icon feature, and 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.
  • 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 and 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 using TrayIconBuilder::with_id("main-tray"), which references the default application icon assets. The feature is enabled in frontend/src-tauri/tauri.conf.json and depends on the tray-icon crate specified in Cargo.toml.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →