# How Meetily's System Tray Integration Manages Recording State and Quick Actions

> Explore how Meetily's system tray integration uses Tauri for seamless recording control. Start, pause, resume, and stop meetings directly from your desktop with real-time state sync.

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

---

**Meetily implements a native system tray using Tauri that enables users to control meeting recordings—start, pause, resume, and stop—directly from the desktop without opening the main window, while maintaining strict state synchronization between the tray menu, backend recording commands, and the React frontend.**

Meetily, an open-source meeting transcription tool built by Zackriya-Solutions, leverages Tauri's system tray capabilities to provide instant recording controls. The implementation in [`frontend/src-tauri/src/tray.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs) establishes a state-driven menu system that reflects real-time recording status, from idle through active transcription to paused states. This integration ensures users can trigger quick actions while the backend handles complex audio processing asynchronously.

## Tray Initialization and State Model

When the application launches, `create_tray` initializes the system tray icon with a default **Stopped** state and a placeholder allowing recording (`can_record = true`). This immediate initialization ensures the tray is visible before the full application state is determined.

```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);   // refresh with the real state later
    Ok(())
}

```

*Source:* [`tray.rs:L19-L34`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L19-L34)

The `RecordingState` enum defines every UI-visible phase to prevent invalid transitions:

```rust
pub enum RecordingState {
    Stopped,
    Starting,
    Recording,
    Pausing,
    Paused,
    Resuming,
    Stopping,
}

```

*Source:* [`tray.rs:L8-L17`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L8-L17)

## Routing Menu Events to Action Handlers

The tray registers a single callback `handle_menu_event` that matches the clicked menu-item ID and forwards requests to specific handlers. This centralization ensures consistent state validation before executing recording commands.

```rust
fn handle_menu_event<R: Runtime>(app: &AppHandle<R>, item_id: &str) {
    match item_id {
        "toggle_recording" => toggle_recording_handler(app),
        "pause_recording" => pause_recording_handler(app),
        "resume_recording" => resume_recording_handler(app),
        "stop_recording" => stop_recording_handler(app),
        "open_window" => focus_main_window(app),
        "settings" => { … },
        "check_updates" => check_updates_handler(app),
        "quit" => app.exit(0),
        _ => {}
    }
}

```

*Source:* [`tray.rs:L37-L53`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L37-L53)

The mapping covers core recording actions (**toggle**, **pause**, **resume**, **stop**) alongside utility functions (**open_window**, **settings**, **check_updates**, **quit**).

## Async Recording Handlers and State Transitions

Each quick-action handler follows a three-step pattern: update the tray UI immediately with an intermediate state, invoke the appropriate Tauri command asynchronously, and handle completion or errors.

### Toggle Recording Handler

The `toggle_recording_handler` checks the current recording status via `crate::is_recording().await` and branches accordingly. When starting, it sets the intermediate `Starting` state and focuses the main window; when stopping, it transitions to `Stopping` and initiates the save process.

```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 case
            set_tray_state(&app_clone, RecordingState::Stopping);
            // … generate save path, call stop_recording (see recording_commands.rs) …
        } else {
            // Start case
            set_tray_state(&app_clone, RecordingState::Starting);
            // Let the UI navigate to the recording page; the actual start happens in the
            // front‑end which calls `start_recording` via Tauri.
        }
    });
}

```

*Source:* [`tray.rs:L55-L73`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L55-L73)

### Pause and Stop Handlers

The `pause_recording_handler` immediately sets the `Pausing` state before spawning an async task to call `pause_recording` from [`recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_commands.rs). If the command fails, it logs the error and triggers a menu refresh.

```rust
fn pause_recording_handler<R: Runtime>(app: &AppHandle<R>) {
    set_tray_state(app, RecordingState::Pausing);
    let app_clone = app.clone();
    tauri::async_runtime::spawn(async move {
        if let Err(e) = crate::audio::recording_commands::pause_recording(app_clone.clone()).await {
            log::error!("Failed to pause recording from tray: {}", e);
            update_tray_menu_async(&app_clone).await;
        }
    });
}

```

*Source:* [`tray.rs:L17-L31`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L17-L31)

Similarly, `stop_recording_handler` transitions to `Stopping`, focuses the window, and calls `stop_recording` with constructed `RecordingArgs`, then emits `recording-stop-complete` upon success.

*Source:* [`tray.rs:L52-L73`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L52-L73)

The heavy lifting occurs in [`recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_commands.rs), where `stop_recording` (lines 842-885) performs graceful shutdown, forces final audio flush, waits for transcription workers, unloads models, and emits `recording-stopped` for frontend persistence.

## Dynamic Menu Reconstruction

After state changes, the tray must reflect new available actions. Meetily uses two functions for this:

- **`set_tray_state`**: Immediately updates the menu to show intermediate states (e.g., "🔄 Starting Recording…") with disabled items to prevent double-clicks.
- **`update_tray_menu_async`**: Queries the actual recording status (`is_recording`, `is_recording_paused`) and rebuilds the menu with correct enabled/disabled states.

```rust
pub fn set_tray_state<R: Runtime>(app: &AppHandle<R>, state: RecordingState) {
    if let Ok(menu) = build_menu(app, state, true) {
        if let Some(tray) = app.tray_by_id("main-tray") {
            tray.set_menu(Some(menu));
        }
    }
}

```

*Source:* [`tray.rs:L22-L35`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L22-L35)

```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));
        }
    }
}

```

*Source:* [`tray.rs:L93-L114`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L93-L114)

## State-Specific Menu Layout

The `build_menu` function constructs different menu items based on the current `RecordingState`:

| State | Available Actions |
|-------|-------------------|
| **Stopped** | Start Recording |
| **Starting** | 🔄 Starting Recording… (disabled) |
| **Recording** | ⏸ Pause Recording, ⏹ Stop Recording |
| **Pausing** | ⏸ Pausing… (disabled), ⏹ Stop Recording |
| **Paused** | ▶ Resume Recording, ⏹ Stop Recording |
| **Resuming** | ▶ Resuming… (disabled), ⏹ Stop Recording |
| **Stopping** | ⏹ Stopping… (disabled) |

Below the state-specific group, static items always appear: **Open Main Window**, **Settings**, **Check for Updates**, and **Quit**.

*Source:* [`tray.rs:L31-L90`](https://github.com/Zackriya-Solutions/meetily/blob/main/frontend/src-tauri/src/tray.rs#L31-L90)

## Summary

Meetily's system tray integration provides a responsive, state-aware control layer that operates independently of the main application window:

- **State Machine Enforcement**: The `RecordingState` enum prevents invalid transitions by disabling menu items during intermediate states (Starting, Pausing, etc.).
- **Async Command Routing**: Tray handlers use `tauri::async_runtime::spawn` to invoke recording commands without blocking the UI thread.
- **Bidirectional Synchronization**: `set_tray_state` provides immediate visual feedback, while `update_tray_menu_async` ensures the menu eventually reflects ground truth from [`recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_commands.rs).
- **Direct Recording Control**: The tray invokes `start_recording`, `pause_recording`, `resume_recording`, and `stop_recording` 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), which manage the actual audio pipeline and model lifecycle.

## Frequently Asked Questions

### How does Meetily's system tray integration handle state transitions during recording operations?

The tray uses intermediate states like `Starting` and `Pausing` to immediately update the menu via `set_tray_state`, disabling user interaction during async operations. Once the underlying Tauri command in [`recording_commands.rs`](https://github.com/Zackriya-Solutions/meetily/blob/main/recording_commands.rs) completes, `update_tray_menu_async` rebuilds the menu to reflect the final state (e.g., `Recording` or `Paused`), ensuring the UI never shows conflicting options.

### What is the difference between `set_tray_state` and `update_tray_menu_async` in Meetily's tray implementation?

`set_tray_state` applies a temporary menu for immediate feedback (e.g., showing "🔄 Starting…" with disabled items), while `update_tray_menu_async` performs an asynchronous check against the actual recording globals (`IS_RECORDING`, `RECORDING_MANAGER`) and rebuilds the menu based on the real backend state. The former handles optimistic UI updates; the latter ensures eventual consistency.

### How does the tray menu prevent user actions during intermediate states like Starting or Stopping?

The `build_menu` function constructs menu items with disabled states for intermediate phases (`Starting`, `Pausing`, `Resuming`, `Stopping`). When `set_tray_state` sets these phases, the resulting menu contains non-clickable items, preventing duplicate commands while the backend processes the initial request. Users can only trigger new actions once the state machine reaches a stable phase (Stopped, Recording, or Paused).

### Which Tauri commands does the system tray invoke to control the actual recording process?

The tray handlers invoke four main commands 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): `start_recording` (via the toggle handler when idle), `pause_recording`, `resume_recording`, and `stop_recording`. These commands manage the audio capture pipeline, VAD processing, and transcription model loading/unloading, emitting events like `recording-started` and `recording-stopped` that synchronize the React frontend.