mod commands; mod tray; use std::sync::Arc; use std::time::Instant; use sqlx::SqlitePool; use tauri::Manager; use kon_core::types::EngineName; use kon_llm::LlmEngine; use kon_storage::{database_path, get_setting, init as init_db, set_setting}; use kon_transcription::LocalEngine; /// Shared app state holding the transcription engines and database pool. pub struct AppState { pub whisper_engine: Arc, pub parakeet_engine: Arc, pub db: SqlitePool, pub llm_engine: Arc, } /// Holds the preferences init script for injection into secondary windows. pub struct PreferencesScript(pub String); /// Build a JavaScript snippet that applies saved preferences to the DOM /// before Svelte mounts, preventing any flash of unstyled content. fn build_preferences_script(prefs_json: Option) -> String { let json = prefs_json.unwrap_or_default(); if json.is_empty() { return String::new(); } // Serialise the JSON string as a JS string literal for safe embedding let js_str = serde_json::to_string(&json).unwrap_or_else(|_| "\"\"".to_string()); format!( r#"(function() {{ try {{ var p = JSON.parse({js_str}); var el = document.documentElement; if (p.theme === 'system') {{ el.dataset.theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; }} else if (p.theme) {{ el.dataset.theme = p.theme; }} if (p.zone && p.zone !== 'default') el.dataset.zone = p.zone; if (p.accessibility) {{ var a = p.accessibility; var fonts = {{ lexend: "'Lexend', system-ui, sans-serif", atkinson: "'Atkinson Hyperlegible Next', system-ui, sans-serif", opendyslexic: "'OpenDyslexic', system-ui, sans-serif" }}; if (a.fontFamily) {{ el.style.setProperty('--font-family-body', fonts[a.fontFamily] || fonts.lexend); el.dataset.fontFamily = a.fontFamily; }} if (a.fontSize) el.style.setProperty('--font-size-body', a.fontSize + 'px'); if (a.letterSpacing != null) el.style.setProperty('--letter-spacing-body', a.letterSpacing + 'em'); if (a.lineHeight) el.style.setProperty('--line-height-body', String(a.lineHeight)); if (a.transcriptSize) el.style.setProperty('--text-transcript', a.transcriptSize + 'px'); if (a.bionicReading) el.dataset.bionicReading = 'true'; if (a.reduceMotion === 'on' || (a.reduceMotion === 'system' && window.matchMedia('(prefers-reduced-motion: reduce)').matches)) el.dataset.reduceMotion = 'true'; }} }} catch (e) {{ console.warn('Preferences injection failed:', e); }} }})();"# ) } /// Save preferences JSON to the SQLite settings table. #[tauri::command] async fn save_preferences( state: tauri::State<'_, AppState>, preferences: String, ) -> Result<(), String> { set_setting(&state.db, "kon_preferences", &preferences) .await .map_err(|e| e.to_string()) } /// On Linux Wayland sessions (KDE, GNOME) WebKitGTK + DMABUF rendering hits /// known crashes that the HANDOVER documents working around with a manual /// env-var prefix: /// /// env GDK_BACKEND=x11 WINIT_UNIX_BACKEND=x11 \ /// WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri dev /// /// Detect the Wayland session at startup and apply the env vars before /// anything else loads, so users do not need to remember the prefix and /// the dev launcher / packaged app both work out of the box. /// /// Inspired by Open-Whispr's similar Chromium self-relaunch (with /// --ozone-platform=x11). Day 6 of the upgrade plan. #[cfg(target_os = "linux")] fn ensure_x11_on_wayland() { // If the user explicitly opted in to Wayland (XDG_SESSION_TYPE set by // their session, KDE / GNOME compositor flags), force WebKitGTK + GDK // onto X11 via XWayland. Idempotent: if a value is already set, keep // it (the user knows best). let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_default(); let is_wayland = session_type.eq_ignore_ascii_case("wayland"); if !is_wayland { return; } let set_if_unset = |key: &str, value: &str| { if std::env::var_os(key).is_none() { // SAFETY: setting env vars before any threads spawn (we are // pre-Tauri-Builder here). This block is the only place these // are written. unsafe { std::env::set_var(key, value); } eprintln!("[startup] Wayland workaround: {key}={value}"); } }; set_if_unset("GDK_BACKEND", "x11"); set_if_unset("WINIT_UNIX_BACKEND", "x11"); set_if_unset("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); // PipeWire screen-capture portal still works fine through XWayland; // we only redirect the GUI rendering path. } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { #[cfg(target_os = "linux")] ensure_x11_on_wayland(); // Capture Rust panics to disk so the diagnostic-report bundler in // Settings → About can attach them. Local only; nothing transmitted. commands::diagnostics::install_panic_hook(); tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build()) .setup(|app| { // Initialise database (blocking in setup — runs once at startup) let db_path = database_path(); let t0 = Instant::now(); let db = tauri::async_runtime::block_on(async { init_db(&db_path).await }) .map_err(|e| Box::new(e) as Box)?; eprintln!("[startup] DB init: {:?}", t0.elapsed()); // Load saved preferences for webview injection let t1 = Instant::now(); let prefs_json = tauri::async_runtime::block_on(async { get_setting(&db, "kon_preferences").await.unwrap_or(None) }); eprintln!("[startup] Preferences load: {:?}", t1.elapsed()); let init_script = build_preferences_script(prefs_json); // Apply preferences to the main window (defined in tauri.conf.json) if let Some(main_window) = app.get_webview_window("main") { if !init_script.is_empty() { // Tauri's WebviewWindow.eval() is the standard API for // running JS in a webview — not the JS eval() function let _ = main_window.eval(&init_script); } // Auto-grant microphone permission on Linux (WebKitGTK). // WebKitGTK doesn't show a permission dialog — it just denies // getUserMedia by default. We connect to the permission-request // signal and grant audio capture requests automatically. #[cfg(target_os = "linux")] { main_window.with_webview(|webview| { use webkit2gtk::{ PermissionRequest, PermissionRequestExt, SettingsExt, WebViewExt, }; let wv: webkit2gtk::WebView = webview.inner().clone(); // Enable media stream in WebKit settings if let Some(settings) = WebViewExt::settings(&wv) { settings.set_enable_media_stream(true); settings.set_enable_media_capabilities(true); } // Auto-grant all permission requests (audio/video capture) WebViewExt::connect_permission_request(&wv, |_wv, request: &PermissionRequest| { request.allow(); true }); }) .unwrap_or_else(|e| { // Non-fatal: WebKitGTK may already have media // capture wired by some compositors, or the // signal binding may fail on unusual builds. // Falling back means getUserMedia() prompts (or // silently denies) instead of auto-granting, // which is degraded but recoverable. eprintln!( "[startup] failed to configure webview media permissions: {e}", ); }); } // Close-to-tray: hide window instead of exiting let win = main_window.clone(); main_window.on_window_event(move |event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); let _ = win.hide(); } }); } // Store init script for secondary windows (float, viewer) app.manage(PreferencesScript(init_script)); app.manage(commands::hotkey::HotkeyState::new()); app.manage(commands::audio::NativeCaptureState::new()); app.manage(commands::live::LiveTranscriptionState::default()); app.manage(AppState { whisper_engine: Arc::new(LocalEngine::new( EngineName::new("whisper"), )), parakeet_engine: Arc::new(LocalEngine::new( EngineName::new("parakeet"), )), db, llm_engine: Arc::new(LlmEngine::new()), }); { let whisper = app.state::().whisper_engine.clone(); crate::commands::models::prewarm_default_model(whisper); } if let Err(e) = tray::setup(app) { eprintln!("Failed to setup tray: {e}"); } Ok(()) }) .invoke_handler(tauri::generate_handler![ // Preferences save_preferences, // Whisper model management commands::models::download_model, commands::models::check_model, commands::models::list_models, commands::models::load_model, commands::models::check_engine, commands::models::get_runtime_capabilities, // Parakeet model management commands::models::download_parakeet_model, commands::models::check_parakeet_model, commands::models::list_parakeet_models, commands::models::load_parakeet_model, commands::models::check_parakeet_engine, // Transcription commands::transcription::transcribe_pcm, commands::transcription::transcribe_file, commands::transcription::transcribe_pcm_parakeet, // Audio commands::audio::save_audio, commands::audio::start_native_capture, commands::audio::stop_native_capture, commands::audio::list_audio_devices, // Tasks (canonical SQLite-backed task CRUD) commands::tasks::create_task_cmd, commands::tasks::list_tasks_cmd, commands::tasks::update_task_cmd, commands::tasks::complete_task_cmd, commands::tasks::delete_task_cmd, commands::tasks::uncomplete_task_cmd, commands::tasks::decompose_and_store, commands::tasks::list_subtasks_cmd, commands::tasks::complete_subtask_cmd, // Transcripts (canonical SQLite-backed history) — Day 4 commands::transcripts::add_transcript, commands::transcripts::list_transcripts, commands::transcripts::count_transcripts_command, commands::transcripts::get_transcript, commands::transcripts::update_transcript, commands::transcripts::update_transcript_meta_cmd, commands::transcripts::delete_transcript, commands::transcripts::search_transcripts, commands::transcripts::list_dictionary_command, commands::transcripts::add_dictionary_entry_command, commands::transcripts::delete_dictionary_entry_command, // Diagnostics (panic + error capture, manual report bundle) commands::diagnostics::log_frontend_error, commands::diagnostics::list_recent_errors_command, commands::diagnostics::list_crash_files, commands::diagnostics::generate_diagnostic_report, commands::diagnostics::save_diagnostic_report, commands::diagnostics::get_os_info, commands::live::start_live_transcription_session, commands::live::stop_live_transcription_session, // Windows commands::windows::open_task_window, commands::windows::open_viewer_window, // Clipboard commands::clipboard::copy_to_clipboard, // Hardware commands::hardware::probe_system, commands::hardware::rank_models, // Hotkey (evdev — Wayland-compatible) commands::hotkey::is_wayland_session, commands::hotkey::check_hotkey_access, commands::hotkey::start_evdev_hotkey, commands::hotkey::update_evdev_hotkey, commands::hotkey::stop_evdev_hotkey, // Updater commands::update::check_for_update, commands::update::install_update, ]) .run(tauri::generate_context!()) .expect("error while running Kon"); }