How Meetily Manages System Tray and Notifications in Its Tauri App

Meetily handles system tray and notifications through dedicated Rust modules in its Tauri 2.x backend, using TrayIconBuilder for dynamic, state-driven tray menus and a layered notification manager that respects user consent and Do-Not-Disturb preferences.

The Meetily desktop application implements its system tray and notifications through focused Rust modules that keep UI logic out of the frontend bundle. The tray gives users persistent access to recording controls, while the notification stack delivers native OS alerts for transcription completion, meeting reminders, and recording lifecycle events. All logic lives in focused modules under frontend/src-tauri/src/, making the implementation both maintainable and platform-agnostic.

System Tray Implementation in frontend/src-tauri/src/tray.rs

The tray module owns every aspect of the persistent icon, from initial setup to dynamic menu rebuilds. It mirrors the recording lifecycle so users can start, pause, resume, or stop a session without bringing the main window to the foreground.

Bootstrapping the Tray with create_tray()

When the Tauri app launches, create_tray() (lines 19–34) assembles the initial icon and menu. According to the Meetily source code, it starts from a RecordingState::Stopped baseline and assumes recording is permitted (can_record = true) unless onboarding says otherwise. The function chains TrayIconBuilder to bind the tooltip, menu, and a click handler in one builder call.

use tauri::tray::TrayIconBuilder;
use tauri::AppHandle;

pub fn create_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
    let menu = build_menu(app, RecordingState::Stopped, true)?;
    TrayIconBuilder::with_id("main-tray")
        .tooltip("Meetily")
        .menu(&menu)
        .on_menu_event(|app, event| handle_menu_event(app, event))
        .build(app)?;
    Ok(())
}

Dynamic Menu Generation Using build_menu()

Menus are never static. The build_menu() helper (lines 16–92) constructs a fresh MenuBuilder on every state change, enabling or disabling items based on the current RecordingState and the can_record onboarding guard. Intermediate states such as Starting or Pausing are rendered as disabled items so users see immediate visual feedback while async operations run.

  • Stopped — shows Start Recording, Open Main Window, Settings, Check for Updates, and Quit.
  • Recording — swaps in Pause Recording and Stop Recording.
  • Paused — surfaces Resume Recording and Stop Recording.

Async State Updates with update_tray_menu_async()

Recording commands run asynchronously, so the tray must display an intermediate menu while a request is in flight. set_tray_state() (lines 22–34) injects this transitional menu. Once the operation completes, update_tray_menu_async() (lines 94–114) queries get_current_recording_state() and check_can_record(), then rebuilds the final menu through build_menu(). A thin synchronous wrapper, update_tray_menu(), exposes the same refresh logic to callers already on the main thread.

Dispatching User Actions Through handle_menu_event()

All menu clicks flow through handle_menu_event() (lines 37–53), which pattern-matches the item ID and delegates to dedicated handlers:

  • toggle_recording_handler() — starts or stops the audio pipeline via crate::audio::recording_commands.
  • pause_recording_handler() and resume_recording_handler() — transition the recorder between active and paused.
  • stop_recording_handler() — finalizes the session and emits a "recording-stop-complete" event so the frontend can react.
  • focus_main_window() (lines 94–104) — raises the main Tauri window when the user selects Open Main Window.

Desktop Notification Architecture in Meetily

Notifications are handled by a two-tier stack: a high-level manager enforces user preferences, and a low-level handler talks directly to the OS through Tauri’s notification plugin.

Coordinating Alerts with NotificationManager

NotificationManager<R> (lines 12–331 in frontend/src-tauri/src/notifications/manager.rs) is the single entry point for every alert. Its constructor (new()) loads persisted permissions through ConsentManager, initializes a SystemNotificationHandler, and stores both behind Arc wrappers for thread-safe sharing. The initialize() method requests system permission on first launch by calling system_handler.request_permission().

The core method show_notification() (lines 80–98) runs a three-step guard:

  1. Ensure the manager is initialized.
  2. Evaluate should_show_notification() against DND mode, user preferences, and priority.
  3. Forward the Notification object to the system handler if all checks pass.

Typed helpers such as show_recording_started(), show_transcription_complete(), and show_meeting_reminder() (lines 100–144) each inspect a specific preference flag inside settings.notification_preferences before delegating to show_notification(). This design keeps policy out of the UI and guarantees a single place to enforce rules.

Displaying OS Notifications via SystemNotificationHandler

SystemNotificationHandler<R> (lines 8–98 in frontend/src-tauri/src/notifications/system.rs) is a thin, platform-agnostic wrapper around Tauri’s notification API. Its show_notification() method (lines 20–47) builds a native alert through app_handle.notification().builder() and displays it immediately.

use tauri::Manager;

impl<R: tauri::Runtime> SystemNotificationHandler<R> {
    pub fn show_notification(&self, notification: Notification) -> Result<(), String> {
        if self.is_dnd_active() && self.should_respect_dnd(&notification) {
            return Ok(()); // silently drop during DND
        }

        self.app_handle
            .notification()
            .builder()
            .title(&notification.title)
            .body(&notification.body)
            .show()
            .map_err(|e| e.to_string())?;

        Ok(())
    }
}

On most platforms, request_permission() (lines 65–73) is effectively a no-op because Tauri automatically grants notification rights, so initialize() completes without friction.

Consent and preference state are persisted to a local JSON file (notifications.json) managed by ConsentManager. The file tracks:

  • consent_given — whether the user has opted into alerts.
  • system_permission_granted — cached platform permission state.
  • manual_dnd_mode — app-level Do-Not-Disturb toggle.
  • respect_do_not_disturb — whether non-critical alerts should be suppressed.

The manager keeps an in-memory Arc<RwLock<NotificationSettings>> that is read by should_show_notification() and updated whenever the user changes settings in the preferences panel.

How Meetily Coordinates System Tray and Notifications

These two subsystems are decoupled but synchronize through Tauri events and shared Rust commands. When a user clicks Stop Recording in the tray, stop_recording_handler() executes the same command used by the frontend UI and then emits "recording-stop-complete" (lines 94–96 in frontend/src-tauri/src/tray.rs). The frontend listens for this event to refresh the recordings list, and it may also call the notification manager to surface a native Recording Stopped alert.

Conversely, the notification manager can be invoked from any backend thread—such as when a long-running transcription job finishes—without ever touching the tray. This separation keeps the tray focused on quick user actions while the notification stack handles asynchronous, time-sensitive updates.

Front-end code can observe tray-initiated state changes by listening to the standard event bus:

import { listen } from '@tauri-apps/api/event';

listen('recording-stop-complete', (event) => {
  console.log('Tray-triggered stop finished', event.payload);
  // Refresh UI or request a confirmation notification
});

Summary

  • The Meetily system tray and notifications stack lives entirely in the Tauri Rust backend under frontend/src-tauri/src/.
  • frontend/src-tauri/src/tray.rs creates the tray icon with TrayIconBuilder, rebuilds context-aware menus via build_menu(), and routes clicks through handle_menu_event().
  • frontend/src-tauri/src/notifications/manager.rs centralizes policy: consent, DND, and typed helpers like show_recording_started().
  • frontend/src-tauri/src/notifications/system.rs is the low-level Tauri notification plugin wrapper that actually renders OS alerts.
  • Tray handlers emit Tauri events (e.g., "recording-stop-complete") so the frontend stays synchronized without direct coupling to the tray logic.

Frequently Asked Questions

How does Meetily update the tray menu when recording starts or stops?

The tray calls update_tray_menu_async() (lines 94–114), which fetches the current RecordingState via get_current_recording_state() and checks check_can_record(). It then rebuilds the menu through build_menu() so items like Pause Recording or Stop Recording appear or disappear instantly.

Can Meetily show notifications if the user enables Do-Not-Disturb?

It depends on priority. SystemNotificationHandler checks is_dnd_active() and should_respect_dnd() before displaying any alert. Critical notifications can bypass DND, while standard recording or transcription alerts are suppressed until DND is turned off.

Where does Meetily store notification preferences?

Preferences and consent are written to a local notifications.json file by ConsentManager. The NotificationManager mirrors this data in an Arc<RwLock<NotificationSettings>> so decisions happen in memory without blocking on disk I/O.

Does the tray menu work during the onboarding flow?

Yes, but with restricted actions. build_menu() accepts a can_record flag that is set by check_can_record() during onboarding. If the user has not completed setup, recording items are disabled, preventing premature audio capture while still allowing access to Settings and Quit.

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 →