How Meetily Implements System Tray Functionality with Tauri
Meetily implements system tray functionality using Tauri's native tray API, encapsulated in frontend/src-tauri/src/tray.rs with stateless menu generation, async state handling, and cross-platform support.
Meetily is an open-source meeting assistant built on Tauri's Rust backend that provides native desktop integration across macOS, Windows, and Linux. The application's system tray implementation allows users to control recordings and manage the application lifecycle without keeping the main window foregrounded. This architecture separates tray creation, event dispatching, and state management into discrete, testable components.
Core Tray Architecture
Tray Initialization
The tray lifecycle begins in frontend/src-tauri/src/lib.rs, where the application registers the system tray during startup. The create_tray function (lines 19-35 in tray.rs) constructs the initial tray icon and builds the first menu instance, immediately triggering a refresh to reflect the actual recording state.
// In frontend/src-tauri/src/lib.rs – during app setup
if let Err(e) = tray::create_tray(_app.handle()) {
log::error!("Failed to create system tray: {}", e);
}
Event Routing and Action Handlers
User interactions are captured by handle_menu_event (lines 37-52), which dispatches tray menu clicks to specific Rust handlers. Each handler updates the tray's visual state, executes the corresponding audio command, and emits events to the frontend:
toggle_recording_handler(lines 55-71) – Alternates between recording and idle states, injecting JavaScript to trigger auto-start when necessarypause_recording_handler(lines 17-31) – Suspends active recording sessionsresume_recording_handler(lines 34-48) – Resumes from paused statestop_recording_handler(lines 52-64) – Finalizes and saves recordings
Dynamic Menu State Management
Stateless Menu Generation
Meetily employs a stateless menu generation pattern to ensure the tray always reflects current application state. The build_menu function (lines 16-82 in tray.rs) reconstructs the entire menu context based on the current RecordingState and whether recording is permitted. Rather than mutating existing menu items, update_tray_menu spawns an async task that rebuilds the menu from scratch:
// After any recording state change
crate::tray::update_tray_menu(&app);
This approach prevents stale UI states and eliminates race conditions during rapid state transitions.
Async Transition States
To provide responsive feedback during long-running audio operations, set_tray_state (lines 22-34) temporarily replaces the menu with intermediate states like "Starting…" or "Pausing…" while the underlying async command executes. This pattern ensures users receive immediate visual confirmation before the audio backend completes its operation.
Conditional Enablement During Onboarding
The tray respects application lifecycle constraints through check_can_record logic embedded in build_menu. During onboarding, while transcription models download in frontend/src-tauri/src/parakeet_engine/commands.rs, the "Start Recording" menu item remains disabled. This gating prevents premature recording attempts until the parakeet_engine signals readiness.
Cross-Platform Integration
The implementation leverages Tauri's generic Runtime trait, making the tray code fully portable across operating systems without platform-specific branches. The focus_main_window function (lines 94-108) handles window management uniformly, bringing the application to the foreground when users select "Open Main Window" from the tray context menu:
// Inside toggle_recording_handler (tray.rs)
if crate::is_recording().await {
// Stop recording path …
} else {
// Start recording path …
let _ = window.eval("sessionStorage.setItem('autoStartRecording', 'true')");
let _ = window.eval("window.location.assign('/')");
}
Synchronization with Audio Commands
The tray maintains synchronization with the recording backend through explicit update calls. After every recording command in frontend/src-tauri/src/audio/recording_commands.rs—whether start_recording, pause_recording, or stop_recording—the code invokes tray::update_tray_menu to refresh the icon and menu items. Similarly, when transcription models finish downloading in the parakeet engine commands, the tray updates to enable previously disabled controls.
Summary
- Meetily's system tray is implemented in
frontend/src-tauri/src/tray.rsusing Tauri's native API. - Stateless architecture rebuilds the menu from current
RecordingStateon every change, ensuring UI consistency. - Async transition states provide immediate feedback via
set_tray_statewhile background audio operations complete. - Lifecycle integration wires the tray into
lib.rsstartup and syncs it with audio commands throughupdate_tray_menu. - Cross-platform support uses Tauri's
Runtimetrait without platform-specific code branches.
Frequently Asked Questions
What framework does Meetily use to implement system tray functionality?
Meetily uses Tauri's native tray API implemented in Rust. The core logic resides in frontend/src-tauri/src/tray.rs, which handles tray creation, menu building, and event routing across macOS, Windows, and Linux through Tauri's abstraction layer.
How does Meetily keep the system tray menu synchronized with recording states?
The application calls update_tray_menu after every recording operation in frontend/src-tauri/src/audio/recording_commands.rs. This function spawns an async task that reads the current RecordingState and rebuilds the menu via build_menu, ensuring the tray always reflects whether recording is active, paused, or stopped.
Why is the "Start Recording" option sometimes disabled in the system tray?
During onboarding, check_can_record logic in build_menu disables recording controls until the transcription model finishes downloading. This prevents users from attempting to record before the parakeet_engine in frontend/src-tauri/src/parakeet_engine/commands.rs is ready to process audio.
Can users control recordings entirely from the system tray without opening the main window?
Yes. The tray provides full recording lifecycle control through toggle_recording_handler, pause_recording_handler, resume_recording_handler, and stop_recording_handler. Users can start, pause, resume, and stop recordings, plus access settings or bring the main window to the foreground via focus_main_window.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →