--- name: App lifecycle type: architecture-map-page slice: 02-tauri-runtime last_verified: 2026/05/09 --- # App lifecycle > **Where you are:** [Architecture map](../README.md) → [Tauri runtime](README.md) → App lifecycle **Plain English summary.** This is the entry point. `main.rs` calls `magnotia_lib::run()` and `lib.rs::run` does everything that has to happen before the user sees a window: sets a Linux Wayland workaround, installs the Rust panic hook, registers Tauri plugins, opens the SQLite database, prunes the error log, builds a JS preferences-injection script, configures the WebKit media-permission grant on Linux, wires close-to-tray, populates `AppState` and the per-domain managed states, emits any runtime warnings, sets up the system tray, and finally registers all 71 Tauri commands. ## At a glance - Path: `src-tauri/src/main.rs`, `src-tauri/src/lib.rs`. - LOC: 5 (main) + 448 (lib). - Tauri commands exposed directly here: `save_preferences` (string preferences -> SQLite settings table). All other commands live under `commands::*` and are registered via `tauri::generate_handler!`. - Events emitted directly here: none (runtime warnings are emitted by `commands::models::emit_runtime_warnings`, called from setup). - Depends on: `tauri`, `sqlx::SqlitePool`, `magnotia_core::types::EngineName`, `magnotia_llm::LlmEngine`, `magnotia_storage::{init, database_path, get_setting, set_setting, prune_error_log}`, `magnotia_transcription::LocalEngine`, plus the `commands::*` and `tray` modules. - Called from frontend at: every `invoke()` site in slice 01 lands in the handler list registered here. ## What's in here ### `main.rs` Single function. Sets `windows_subsystem = "windows"` for release builds (no console window) and calls `magnotia_lib::run()`. (`src-tauri/src/main.rs:1`). ### `lib.rs` Module declarations (`src-tauri/src/lib.rs:1`): - `mod commands;` - `#[cfg(not(target_os = "android"))] mod tray;` — tray uses Tauri's `tray-icon` feature which is desktop-only. Constants: - `ERROR_LOG_RETENTION_DAYS: i64 = 90` (`src-tauri/src/lib.rs:22`). Used by the startup prune. Types managed in Tauri state: - `AppState` (`src-tauri/src/lib.rs:26`). Holds `Arc` for whisper and parakeet, the `SqlitePool`, and an `Arc`. This is the central state that almost every command queries. - `PreferencesScript(pub String)` (`src-tauri/src/lib.rs:34`). Cached preferences-injection JS used when secondary windows are built (so they do not flash unstyled before Svelte mounts). Functions: - `build_preferences_script(prefs_json: Option) -> String` (`src-tauri/src/lib.rs:38`). Builds an IIFE that reads saved preferences (theme, zone, accessibility settings: font family, font size, letter spacing, line height, transcript size, bionic reading, reduce motion) and applies them to `` before the rest of the document loads. Embeds the JSON via `serde_json::to_string` to keep it safe. - `save_preferences(state, preferences) -> Result<(), String>` (`src-tauri/src/lib.rs:73`). The single command in `lib.rs`. Persists the preferences blob to the SQLite settings table under key `magnotia_preferences`. - `ensure_x11_on_wayland()` (`src-tauri/src/lib.rs:99`, Linux only). Sets `WEBKIT_DISABLE_DMABUF_RENDERER=1` unconditionally on Linux (iGPU idle-cost workaround), and additionally sets `GDK_BACKEND=x11` plus `WINIT_UNIX_BACKEND=x11` when `XDG_SESSION_TYPE=wayland`. Idempotent: if a value is already set, the function leaves it alone. Must run before any threads spawn — uses `unsafe { std::env::set_var(...) }`. - `run()` (`src-tauri/src/lib.rs:135`). The Tauri builder pipeline. ### `run()` step-by-step 1. **Linux env-var prelude.** Calls `ensure_x11_on_wayland()` on Linux (`src-tauri/src/lib.rs:137`). 2. **Panic hook.** Calls `commands::diagnostics::install_panic_hook()` to dump panic info to `crashes_dir()` (`src-tauri/src/lib.rs:141`). 3. **Plugin wiring (always-on).** `tauri_plugin_opener`, `tauri_plugin_dialog`, `tauri_plugin_notification` (`src-tauri/src/lib.rs:144`). 4. **Plugin wiring (desktop-only).** `tauri_plugin_global_shortcut`, `tauri_plugin_autostart` (LaunchAgent on macOS), `tauri_plugin_window_state` (`src-tauri/src/lib.rs:158`). 5. **Setup hook.** This is where the bulk of startup work lives: - Initialise SQLite via `magnotia_storage::init(&database_path()).await` using `tauri::async_runtime::block_on` (`src-tauri/src/lib.rs:180`). The `Instant::now()` timing is logged. - Prune `error_log` rows older than 90 days (`src-tauri/src/lib.rs:189`). Best-effort: a failure logs but does not block startup. - Load saved preferences from the settings table; build the JS injection script (`src-tauri/src/lib.rs:204`). - Apply the injection script to the main window via `WebviewWindow.eval()` (`src-tauri/src/lib.rs:215`). - On Linux, configure `webkit2gtk` permission requests: enable `media_stream` and `media_capabilities` settings; auto-grant audio capture but deny everything else (camera, geolocation, pointer lock, etc.) (`src-tauri/src/lib.rs:222`). This is the critical piece that makes `getUserMedia` work on Linux without a permission dialog (because WebKitGTK has no dialog, it just silently denies by default). - Wire close-to-tray on desktop: intercept `WindowEvent::CloseRequested` and call `window.hide()` instead of letting the platform exit (`src-tauri/src/lib.rs:281`). - Stash the `PreferencesScript` and the per-domain managed states (`HotkeyState`, `NativeCaptureState`, `LiveTranscriptionState`, `TtsState`, `MeetingState`) (`src-tauri/src/lib.rs:294`). - Build the `AppState` itself: fresh `LocalEngine`s for whisper and parakeet, the open `SqlitePool`, a fresh `LlmEngine` (`src-tauri/src/lib.rs:302`). - Emit runtime warnings (CPU baseline, Vulkan loader) via `commands::models::emit_runtime_warnings` (`src-tauri/src/lib.rs:312`). - Setup the system tray on desktop (`src-tauri/src/lib.rs:314`). 6. **Command registration.** `tauri::generate_handler![...]` lists 71 commands (`src-tauri/src/lib.rs:321`). The order in the macro is grouped by domain (preferences, models, LLM, transcription, audio, tasks, feedback, TTS, rituals, nudges, intentions, profiles, transcripts, diagnostics, live, windows, clipboard, fs, paste, meeting, hardware, hotkey, updater). 7. **Run.** `.run(tauri::generate_context!())` blocks the main thread until the app exits. Panics are wrapped with `expect("error while running Magnotia")`. ## Data flow - **Frontend bootstrap:** the webview is built per `tauri.conf.json` window config. Tauri's `eval()` hook fires the preferences script before the SvelteKit bundle parses, so the user never sees a flash of unstyled content. - **Database:** opened once, owned by `AppState`, cloned by `Arc` semantics into every `state.db` borrow. - **Engine state:** the two transcription `LocalEngine`s and the `LlmEngine` are shared `Arc`s; commands clone them and run inference inside `tokio::task::spawn_blocking` so the async runtime stays responsive. - **Per-domain state:** `HotkeyState`, `NativeCaptureState`, `LiveTranscriptionState`, `TtsState`, `MeetingState` are all stashed via `app.manage(...)` and retrieved by their command files via `tauri::State<'_, T>`. ## Watch-outs - `tauri::async_runtime::block_on` inside `setup` blocks startup. The DB init and prefs read are explicitly timed and logged so regressions show up. Adding more synchronous async work here directly pushes the time-to-first-paint up. - The Linux media-permission wire-up is non-fatal: if `with_webview` fails the app still boots, but `getUserMedia` will be silently denied or fall back to a prompt the user cannot answer (no UI). The error path logs `[startup] failed to configure webview media permissions: ...` to stderr. - The `unsafe std::env::set_var` calls in `ensure_x11_on_wayland` are sound only because nothing else in the binary has spawned a thread yet at that point. Do not introduce another startup-time env mutation outside this function unless the same invariant is preserved. - Close-to-tray works only on desktop (the `cfg!(not(target_os = "android"))` block). On Android, closing the activity terminates the process, which is the expected platform behaviour. - The `prewarm_default_model` call is *not* wired here. `commands::models::prewarm_default_model` exists, but `setup` does not invoke it. The frontend invokes the matching `prewarm_default_model_cmd` command after the main page mounts. If you ever want to shift pre-warm into setup, watch the spawn_blocking ordering against the engine `Arc` clones. ## See also - [Commands index](commands/README.md) — every command registered by `lib.rs::run`. - [System tray](system-tray.md) — what `tray::setup(app)` builds. - [Tauri config](tauri-config.md) — the window config that drives the `get_webview_window("main")` retrieval. - [Capabilities and ACL](capabilities-and-acl.md) — the permission set that decides which commands each window can call. - [Cargo and features](cargo-and-features.md) — the dependency block that determines which plugins compile in.