//! Mission Control - ADHD-centric personal productivity hub //! //! This crate provides the Rust backend for Mission Control, handling: //! - CLI integration with lore (GitLab data) and br (beads task management) //! - GitLab -> Beads bridge (creating beads from GitLab events) //! - Decision logging and state persistence //! - File watching for automatic sync pub mod app; pub mod commands; pub mod data; pub mod error; pub mod sync; pub mod watcher; use tauri::menu::{MenuBuilder, MenuItemBuilder}; use tauri::tray::TrayIconBuilder; use tauri::{Emitter, Manager}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState}; use tauri_specta::{collect_commands, Builder}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; /// Toggle the main window's visibility. /// /// If the window is visible and focused, hide it. /// If hidden or not focused, show and focus it. fn toggle_window_visibility(app: &tauri::AppHandle) { if let Some(window) = app.get_webview_window("main") { if window.is_visible().unwrap_or(false) && window.is_focused().unwrap_or(false) { if let Err(e) = window.hide() { tracing::warn!("Failed to hide window: {}", e); } } else { if let Err(e) = window.show() { tracing::warn!("Failed to show window: {}", e); } if let Err(e) = window.set_focus() { tracing::warn!("Failed to focus window: {}", e); } } } } /// Set up the system tray icon with a menu. fn setup_tray(app: &tauri::App) -> Result<(), Box> { let show_item = MenuItemBuilder::with_id("show", "Show Mission Control").build(app)?; let quit_item = MenuItemBuilder::with_id("quit", "Quit").build(app)?; let menu = MenuBuilder::new(app) .items(&[&show_item, &quit_item]) .build()?; TrayIconBuilder::new() .icon( app.default_window_icon() .cloned() .expect("default-window-icon must be set in tauri.conf.json"), ) .tooltip("Mission Control") .menu(&menu) .on_menu_event(|app, event| match event.id().as_ref() { "show" => toggle_window_visibility(app), "quit" => { app.exit(0); } _ => {} }) .on_tray_icon_event(|tray, event| { if let tauri::tray::TrayIconEvent::Click { button: tauri::tray::MouseButton::Left, button_state: tauri::tray::MouseButtonState::Up, .. } = event { toggle_window_visibility(tray.app_handle()); } }) .build(app)?; Ok(()) } /// Register global hotkeys: /// - Cmd+Shift+M: toggle window visibility /// - Cmd+Shift+C: quick capture overlay /// /// Must be called AFTER the global-shortcut plugin is initialized. fn setup_global_shortcut(app: &tauri::App) -> Result<(), Box> { let toggle_shortcut = Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyM); let capture_shortcut = Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyC); app.global_shortcut() .register_multiple([toggle_shortcut, capture_shortcut])?; tracing::info!("Registered global shortcuts: Cmd+Shift+M (toggle), Cmd+Shift+C (capture)"); Ok(()) } /// Initialize the Tauri application with all plugins and commands. #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // Initialize tracing tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "mission_control=debug".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); tracing::info!("Starting Mission Control"); // Build tauri-specta builder for type-safe IPC let builder = Builder::::new().commands(collect_commands![ commands::greet, commands::get_lore_status, commands::get_bridge_status, commands::sync_now, commands::reconcile, commands::quick_capture, commands::read_state, commands::write_state, commands::clear_state, commands::get_triage, commands::get_next_pick, commands::close_bead, commands::log_decision, commands::update_item, ]); // Export TypeScript bindings in debug builds #[cfg(debug_assertions)] builder .export( specta_typescript::Typescript::default(), "../src/lib/bindings.ts", ) .expect("Failed to export TypeScript bindings"); tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin( tauri_plugin_global_shortcut::Builder::new() .with_handler(|app, shortcut, event| { if event.state == ShortcutState::Pressed { let capture = Shortcut::new(Some(Modifiers::META | Modifiers::SHIFT), Code::KeyC); if shortcut == &capture { // Show window and signal the frontend to open capture overlay if let Some(window) = app.get_webview_window("main") { if let Err(e) = window.show() { tracing::warn!("Failed to show window for capture: {}", e); } if let Err(e) = window.set_focus() { tracing::warn!("Failed to focus window for capture: {}", e); } } if let Err(e) = app.emit("global-shortcut-triggered", "quick-capture") { tracing::error!("Failed to emit quick-capture event: {}", e); } } else { toggle_window_visibility(app); if let Err(e) = app.emit("global-shortcut-triggered", "toggle-window") { tracing::error!("Failed to emit toggle-window event: {}", e); } } } }) .build(), ) .setup(|app| { use data::beads::RealBeadsCli; use data::bridge::Bridge; use data::lore::RealLoreCli; // 1. Ensure data directories exist let config = app::AppConfig::default(); if let Err(e) = app::ensure_data_dir(&config) { tracing::error!("Failed to create data directory: {}", e); // Continue anyway - commands will fail gracefully } // 2. Clean up orphaned tmp files from previous crashes { let bridge: Bridge = Bridge::new(RealLoreCli, RealBeadsCli); if let Err(e) = bridge.cleanup_tmp_files() { tracing::warn!("Failed to clean up tmp files: {}", e); } } // 3. Verify CLI dependencies (async, spawned to not block startup) let app_handle = app.handle().clone(); tauri::async_runtime::spawn(async move { let (cli_available, warnings) = app::verify_cli_dependencies().await; // Log warnings for warning in &warnings { match warning { app::StartupWarning::LoreMissing => { tracing::warn!("lore CLI not found - GitLab sync will be unavailable"); } app::StartupWarning::BrMissing => { tracing::warn!("br CLI not found - beads integration will be unavailable"); } app::StartupWarning::BvMissing => { tracing::warn!("bv CLI not found - triage features will be unavailable"); } _ => {} } } // Emit startup warnings to frontend if !warnings.is_empty() { if let Err(e) = app_handle.emit("startup-warnings", &warnings) { tracing::error!("Failed to emit startup warnings: {}", e); } } // Emit CLI availability to frontend if let Err(e) = app_handle.emit("cli-availability", &cli_available) { tracing::error!("Failed to emit CLI availability: {}", e); } // 4. Trigger startup reconciliation if CLIs are available if cli_available.lore && cli_available.br { tracing::info!("Triggering startup reconciliation"); // The frontend will call reconcile() command when ready if let Err(e) = app_handle.emit("startup-sync-ready", ()) { tracing::error!("Failed to emit startup-sync-ready: {}", e); } } }); // Set up system tray if let Err(e) = setup_tray(app) { tracing::error!("Failed to setup system tray: {}", e); } // Set up global shortcut if let Err(e) = setup_global_shortcut(app) { tracing::error!("Failed to setup global shortcut: {}", e); } // Start file watcher for lore.db changes if let Some(watcher) = watcher::start_lore_watcher(app.handle().clone()) { // Store the watcher in app state to keep it alive app.manage(std::sync::Mutex::new(Some(watcher))); tracing::info!("Lore file watcher started"); } #[cfg(debug_assertions)] { // Open devtools in debug mode if let Some(window) = app.get_webview_window("main") { window.open_devtools(); } } Ok(()) }) .invoke_handler(builder.invoke_handler()) .run(tauri::generate_context!()) .expect("error while running tauri application"); }