# How Meetily Implements System Tray Integration with Close-to-Tray Functionality in Tauri 2

> Discover how Meetily leverages Tauri 2's Rust backend for seamless system tray integration. Learn about close-to-tray functionality and dynamic context menu controls.

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

---

**Meetily uses Tauri 2's Rust backend to create a native system tray icon that intercepts window-close events, hides the main window instead of exiting, and provides dynamic recording controls through a context menu.**

Meetily, an open-source meeting assistant by Zackriya-Solutions, leverages Tauri 2 and Rust to deliver a native desktop experience with robust system tray integration. The implementation allows users to close the main window while keeping the application running in the background, accessing recording controls directly from the tray icon. This article examines the exact implementation details found in the Meetily source code, specifically how the application maintains state synchronization between the tray and the recording engine.

## Tray Architecture and Initialization

The system tray implementation resides in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs), which handles icon creation, menu generation, and event routing. The `create_tray` function (lines 19-30) instantiates the tray using `TrayIconBuilder` and wires it to the application handle.

```rust
pub fn create_tray<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<()> {
    let menu = build_menu(app, RecordingState::Stopped, true)?;
    TrayIconBuilder::with_id("main-tray")
        .menu(&menu)
        .tooltip("Meetily")
        .icon(app.default_window_icon().unwrap().clone())
        .on_menu_event(|app, event| handle_menu_event(app, event.id.as_ref()))
        .build(app)?;
    update_tray_menu(app);
    Ok(())
}

```

The tray is registered during application startup in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) (lines 423-425) via `tray::create_tray(_app.handle())`. The `build_menu` function (lines 16-92) dynamically generates menu items including **Start Recording**, **Pause**, **Resume**, **Stop**, **Open Main Window**, **Settings**, **Check for Updates**, and **Quit**, adapting to whether onboarding is complete (checked via [`frontend/src-tauri/src/onboarding.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/onboarding.rs)) and model availability (verified in [`frontend/src-tauri/src/parakeet_engine/commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/parakeet_engine/commands.rs)).

## Dynamic Menu State Management

To ensure the tray reflects real-time recording status without blocking the UI, Meetily implements a two-tier state synchronization strategy. The `set_tray_state` function (lines 22-35) provides immediate visual feedback for intermediate states like "Starting" or "Pausing," while `update_tray_menu` spawns an async task that calls `update_tray_menu_async` after a 100ms delay (lines 12-20) to synchronize with the ground truth from [`frontend/src-tauri/src/audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/audio/recording_commands.rs).

```rust
pub async fn update_tray_menu_async<R: Runtime>(app: &AppHandle<R>) {
    let recording_state = get_current_recording_state().await;
    let can_record = check_can_record(app).await;
    if let Ok(menu) = build_menu(app, recording_state, can_record) {
        if let Some(tray) = app.tray_by_id("main-tray") {
            tray.set_menu(Some(menu));
        }
    }
}

```

This async approach ensures that the tray menu always displays the correct recording state, whether the user interacts with the main window or the tray icon.

## Implementing Close-to-Tray Behavior

The close-to-tray functionality is implemented in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) (lines 514-525) through the `on_window_event` listener. When a user clicks the window close button, the application intercepts the `CloseRequested` event, prevents the default exit behavior using `api.prevent_close()`, and hides the window instead of terminating the process.

```rust
app.on_window_event(move |event| {
    if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
        api.prevent_close();            // Prevent application exit
        event.window().hide().ok();    // Hide window → stays in tray
        log::info!("Main window hidden to tray on close request");
    }
});

```

This keeps the Rust `AppHandle` alive, allowing the tray icon to remain active and the background recording processes to continue uninterrupted.

## Window Recovery and Command Routing

When the user selects **Open Main Window** from the tray menu, the `handle_menu_event` function (lines 37-53) routes the click to `focus_main_window` (lines 94-100), which restores the hidden window:

- `set_focus()` brings the window to the foreground
- `set_minimized(false)` restores from the taskbar
- `show()` makes the window visible again

For recording controls, the tray handlers invoke the same async APIs used by the frontend UI, ensuring state consistency. The `toggle_recording_handler` (lines 55-73) calls `crate::audio::recording_commands::stop_recording` or triggers recording starts, while the `handle_menu_event` maps menu IDs to specific actions like pausing or resuming. This architecture ensures that whether the user clicks a button in the main window or selects an option from the tray, the same [`audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/recording_commands.rs) functions execute, maintaining a single source of truth for recording state.

## Summary

- **Tray Initialization**: Meetily registers the system tray in [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) using `tray::create_tray()` during application startup (lines 423-425).
- **Dynamic Menus**: The `build_menu()` function in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs) (lines 16-92) adapts tray options based on recording states and onboarding completion status.
- **Close-to-Tray**: The window-close handler in [`lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/lib.rs) (lines 514-525) calls `api.prevent_close()` and `window.hide()` to minimize to tray without exiting the Rust process.
- **State Synchronization**: `update_tray_menu_async` refreshes the tray menu after 100ms to reflect the true recording state from [`audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/recording_commands.rs).
- **Window Recovery**: `focus_main_window()` (lines 94-100) restores the hidden window when users select "Open Main Window" from the tray.
- **Unified Commands**: Recording actions triggered from the tray use the same async functions as the UI, ensuring consistent behavior across interaction methods.

## Frequently Asked Questions

### How does Meetily prevent the application from closing when the user clicks the window close button?

In [`frontend/src-tauri/src/lib.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/lib.rs) (lines 514-525), the application listens for `WindowEvent::CloseRequested` and calls `api.prevent_close()` to intercept the exit process. It then invokes `event.window().hide()` to minimize the window to the system tray while keeping the Rust `AppHandle` and all background processes alive.

### What recording controls are available from the Meetily system tray?

The tray menu dynamically displays **Start Recording**, **Pause**, **Resume**, and **Stop** when appropriate, alongside static options for **Open Main Window**, **Settings**, **Check for Updates**, and **Quit**. The availability of recording controls depends on whether the user has completed onboarding (checked in [`frontend/src-tauri/src/onboarding.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/onboarding.rs)) and whether required AI models are available.

### How does the tray menu stay synchronized with the actual recording state?

Meetily uses `set_tray_state` for immediate UI feedback during state transitions and spawns an asynchronous task via `update_tray_menu` that executes `update_tray_menu_async` after a 100ms delay (lines 12-20). This function queries the current state from [`audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/recording_commands.rs) and rebuilds the menu accordingly, ensuring the tray always reflects the true recording status.

### Can users initiate recording directly from the system tray without opening the main window?

Yes, when the tray menu displays the **Start Recording** option (available after onboarding completion and when no recording is active), clicking it triggers `toggle_recording_handler`, which invokes the same async recording APIs in [`audio/recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/audio/recording_commands.rs) used by the frontend UI, allowing users to start, pause, or stop recordings entirely from the tray.