How Meetily Implements System Tray Integration with Close-to-Tray Functionality in Tauri 2
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, 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.
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 (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) and model availability (verified in 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.
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 (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.
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 foregroundset_minimized(false)restores from the taskbarshow()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 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.rsusingtray::create_tray()during application startup (lines 423-425). - Dynamic Menus: The
build_menu()function infrontend/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(lines 514-525) callsapi.prevent_close()andwindow.hide()to minimize to tray without exiting the Rust process. - State Synchronization:
update_tray_menu_asyncrefreshes the tray menu after 100ms to reflect the true recording state fromaudio/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 (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) 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 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 used by the frontend UI, allowing users to start, pause, or stop recordings entirely from the tray.
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 →