//! GoingsOn desktop application entry point. // Prevents additional console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use goingson_desktop::backup_scheduler; use goingson_desktop::commands; use goingson_desktop::email_sync_scheduler; use goingson_desktop::sync_scheduler; use goingson_desktop::commands::PluginState; use goingson_desktop::state::AppState; use goingson_plugin_runtime::PluginRegistry; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}; use tauri::{Emitter, Manager, RunEvent}; use tokio_util::sync::CancellationToken; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; // Desktop-only imports #[cfg(not(any(target_os = "ios", target_os = "android")))] use goingson_desktop::db_watcher; #[cfg(not(any(target_os = "ios", target_os = "android")))] use goingson_desktop::notifications; #[cfg(not(any(target_os = "ios", target_os = "android")))] use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; /// Set up the macOS menu bar tray icon showing "GO" in Reglo. #[cfg(not(any(target_os = "ios", target_os = "android")))] fn setup_tray(app: &tauri::App) -> Result<(), Box> { use tauri::menu::{MenuBuilder, MenuItemBuilder}; let show = MenuItemBuilder::with_id("tray_show", "Show GoingsOn").build(app)?; let quit = MenuItemBuilder::with_id("tray_quit", "Quit").build(app)?; let menu = MenuBuilder::new(app).items(&[&show, &quit]).build()?; let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/tray-icon@2x.png"))?; let _tray = TrayIconBuilder::new() .icon(icon) .icon_as_template(true) .menu(&menu) .show_menu_on_left_click(false) .tooltip("GoingsOn") .on_tray_icon_event(|tray, event| { if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { if let Some(window) = tray.app_handle().get_webview_window("main") { let _ = window.show(); let _ = window.unminimize(); let _ = window.set_focus(); } } }) .on_menu_event(|app, event| match event.id().as_ref() { "tray_show" => { if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.unminimize(); let _ = window.set_focus(); } } "tray_quit" => { app.exit(0); } _ => {} }) .build(app)?; Ok(()) } fn main() { // Initialize structured logging with tracing tracing_subscriber::registry() .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| { // Default log level: info for our crates, warn for dependencies "goingson_desktop=info,goingson_core=debug,goingson_db_sqlite=debug,warn".into() })) .with(tracing_subscriber::fmt::layer()) .init(); info!("Starting GoingsOn application"); let mut builder = tauri::Builder::default(); // Desktop-only plugins #[cfg(not(any(target_os = "ios", target_os = "android")))] { builder = builder .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_window_state::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_process::init()); } builder .plugin(tauri_plugin_dialog::init()) .menu(|app| { // App menu (macOS) - contains About, Settings, Quit #[cfg(target_os = "macos")] let app_menu = Submenu::with_items( app, "GoingsOn", true, &[ &MenuItem::with_id(app, "about", "About GoingsOn", true, None::<&str>)?, &PredefinedMenuItem::separator(app)?, &MenuItem::with_id(app, "settings", "Settings...", true, Some("CmdOrCtrl+,"))?, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::hide(app, Some("Hide GoingsOn"))?, &PredefinedMenuItem::hide_others(app, Some("Hide Others"))?, &PredefinedMenuItem::show_all(app, Some("Show All"))?, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::quit(app, Some("Quit GoingsOn"))?, ], )?; // File menu let file_menu = Submenu::with_items( app, "File", true, &[ &MenuItem::with_id(app, "new_task", "New Task", true, Some("CmdOrCtrl+N"))?, &MenuItem::with_id( app, "new_project", "New Project", true, Some("CmdOrCtrl+Shift+N"), )?, &PredefinedMenuItem::separator(app)?, &MenuItem::with_id(app, "import", "Import...", true, Some("CmdOrCtrl+I"))?, &MenuItem::with_id(app, "save_view", "Save View", true, Some("CmdOrCtrl+S"))?, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::close_window(app, Some("Close Window"))?, #[cfg(not(target_os = "macos"))] &PredefinedMenuItem::separator(app)?, #[cfg(not(target_os = "macos"))] &PredefinedMenuItem::quit(app, Some("Exit"))?, ], )?; // Edit menu let edit_menu = Submenu::with_items( app, "Edit", true, &[ &PredefinedMenuItem::undo(app, Some("Undo"))?, &PredefinedMenuItem::redo(app, Some("Redo"))?, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::cut(app, Some("Cut"))?, &PredefinedMenuItem::copy(app, Some("Copy"))?, &PredefinedMenuItem::paste(app, Some("Paste"))?, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::select_all(app, Some("Select All"))?, ], )?; // View menu - grouped by tab let work_submenu = Submenu::with_items( app, "Work", true, &[ &MenuItem::with_id(app, "view_tasks", "Tasks", true, None::<&str>)?, &MenuItem::with_id(app, "view_projects", "Projects", true, None::<&str>)?, ], )?; let time_submenu = Submenu::with_items( app, "Time", true, &[ &MenuItem::with_id(app, "view_day_plan", "Day", true, None::<&str>)?, &MenuItem::with_id( app, "view_weekly_review", "Week", true, None::<&str>, )?, &MenuItem::with_id( app, "view_monthly_review", "Month", true, None::<&str>, )?, &MenuItem::with_id(app, "view_events", "Events", true, None::<&str>)?, ], )?; let messages_submenu = Submenu::with_items( app, "Messages", true, &[ &MenuItem::with_id(app, "view_emails", "Email", true, None::<&str>)?, &MenuItem::with_id(app, "view_contacts", "Contacts", true, None::<&str>)?, ], )?; let view_menu = Submenu::with_items( app, "View", true, &[ &MenuItem::with_id(app, "view_work", "Work", true, Some("CmdOrCtrl+1"))?, &MenuItem::with_id(app, "view_time", "Time", true, Some("CmdOrCtrl+2"))?, &MenuItem::with_id( app, "view_messages", "Messages", true, Some("CmdOrCtrl+3"), )?, &PredefinedMenuItem::separator(app)?, &work_submenu, &time_submenu, &messages_submenu, &PredefinedMenuItem::separator(app)?, &MenuItem::with_id( app, "toggle_sidebar", "Toggle Sidebar", true, Some("CmdOrCtrl+\\"), )?, ], )?; // Tools menu let tools_menu = Submenu::with_items( app, "Tools", true, &[ &MenuItem::with_id( app, "sync_email", "Sync Email", true, Some("CmdOrCtrl+Shift+E"), )?, &PredefinedMenuItem::separator(app)?, &MenuItem::with_id(app, "settings", "Settings", true, Some("CmdOrCtrl+,"))?, ], )?; // Help menu let help_menu = Submenu::with_items( app, "Help", true, &[ &MenuItem::with_id( app, "keyboard_shortcuts", "Keyboard Shortcuts", true, Some("?"), )?, &PredefinedMenuItem::separator(app)?, &MenuItem::with_id(app, "check_updates", "Check for Updates...", true, None::<&str>)?, &PredefinedMenuItem::separator(app)?, &MenuItem::with_id(app, "about", "About GoingsOn", true, None::<&str>)?, ], )?; #[cfg(target_os = "macos")] { Menu::with_items( app, &[&app_menu, &file_menu, &edit_menu, &view_menu, &tools_menu, &help_menu], ) } #[cfg(not(target_os = "macos"))] { Menu::with_items( app, &[&file_menu, &edit_menu, &view_menu, &tools_menu, &help_menu], ) } }) .on_menu_event(|app, event| { let event_id = event.id().as_ref(); if let Some(window) = app.get_webview_window("main") { let _ = window.emit(&format!("menu:{}", event_id), ()); } }) .setup(|app| { // Set up menu bar tray icon (desktop only) #[cfg(not(any(target_os = "ios", target_os = "android")))] { if let Err(e) = setup_tray(app) { tracing::warn!("Failed to set up tray icon: {}", e); } } // Initialize database let app_handle = app.handle().clone(); tauri::async_runtime::block_on(async move { let state = AppState::new(&app_handle).await .expect("Failed to initialize app state"); app_handle.manage(Arc::new(state)); }); // Initialize plugin system let config_dir = app.path().app_config_dir() .expect("Failed to get config dir"); let plugins_dir = config_dir.join("plugins"); let mut plugin_registry = PluginRegistry::new(&plugins_dir) .expect("Failed to initialize plugin registry"); // Load enabled plugins if let Err(e) = plugin_registry.initialize() { tracing::warn!("Failed to load some plugins: {}", e); } app.manage(PluginState::new(plugin_registry)); // Create shutdown coordination handles let cancel_token = CancellationToken::new(); let db_watcher_shutdown = Arc::new(AtomicBool::new(false)); // Store shutdown handles in Tauri managed state for the run event handler app.manage(cancel_token.clone()); app.manage(db_watcher_shutdown.clone()); // Desktop-only background services #[cfg(not(any(target_os = "ios", target_os = "android")))] { // Start background notification checker let handle = app.handle().clone(); let notify_cancel = cancel_token.clone(); tauri::async_runtime::spawn(async move { notifications::start_snooze_watcher(handle, notify_cancel).await; }); // Start database file watcher for external changes let watcher_handle = app.handle().clone(); db_watcher::start_db_watcher(watcher_handle, db_watcher_shutdown); } // Start background backup scheduler (works on all platforms) let backup_handle = app.handle().clone(); let backup_cancel = cancel_token.clone(); tauri::async_runtime::spawn(async move { backup_scheduler::start_backup_scheduler(backup_handle, backup_cancel).await; }); // Start background email sync scheduler (works on all platforms) let sync_handle = app.handle().clone(); let email_cancel = cancel_token.clone(); tauri::async_runtime::spawn(async move { email_sync_scheduler::start_email_sync_scheduler(sync_handle, email_cancel).await; }); // Start background cloud sync scheduler let cloud_sync_handle = app.handle().clone(); let cloud_cancel = cancel_token; tauri::async_runtime::spawn(async move { sync_scheduler::start_sync_scheduler(cloud_sync_handle, cloud_cancel).await; }); // Check for OTA updates after a short delay (desktop only). // Gated on the user's preference (default true). #[cfg(not(any(target_os = "ios", target_os = "android")))] { let update_handle = app.handle().clone(); if commands::load_preferences(&update_handle).update_check_on_launch { tauri::async_runtime::spawn(async move { // Wait before checking to let the app finish starting tokio::time::sleep(std::time::Duration::from_secs(10)).await; check_for_updates(update_handle).await; }); } } Ok(()) }) .invoke_handler(tauri::generate_handler![ // Attachments commands::add_attachment, commands::list_attachments, commands::delete_attachment, commands::open_attachment, commands::save_attachment, commands::convert_email_attachments, commands::open_email_blob, commands::save_email_blob, commands::get_file_size, // Projects commands::list_projects, commands::get_project, commands::create_project, commands::update_project, commands::delete_project, // Tasks commands::list_tasks, commands::list_tasks_filtered, commands::get_task, commands::create_task, commands::quick_add_task, commands::update_task, commands::delete_task, commands::start_task, commands::complete_task, commands::list_snoozed_tasks, commands::snooze_task, commands::unsnooze_task, commands::list_waiting_tasks, commands::mark_task_waiting, commands::clear_task_waiting, // Task Overview commands::get_task_overview, // Annotations commands::list_annotations, commands::add_annotation, commands::delete_annotation, // Subtasks commands::list_subtasks, commands::add_subtask, commands::add_subtask_link, commands::toggle_subtask, commands::update_subtask, commands::delete_subtask, // Milestones commands::list_milestones, commands::create_milestone, commands::update_milestone, commands::delete_milestone, commands::reorder_milestones, // Events commands::list_events, commands::get_event, commands::create_event, commands::update_event, commands::delete_event, commands::bulk_delete_events, commands::list_events_between, commands::list_upcoming_events, commands::get_event_status_indicator, commands::list_snoozed_events, commands::snooze_event, commands::unsnooze_event, // Emails commands::list_emails, commands::list_emails_threaded, commands::get_email, commands::open_email_in_browser, commands::create_email, commands::send_email, commands::save_email_draft, commands::list_email_drafts, commands::send_email_draft, commands::set_email_labels, commands::list_email_folders, commands::list_email_labels, commands::move_email_to_folder, commands::delete_email, commands::mark_email_read, commands::mark_email_unread, commands::archive_email, commands::unarchive_email, commands::mark_all_emails_read, commands::link_email_to_project, commands::get_unread_email_count, commands::list_snoozed_emails, commands::snooze_email, commands::unsnooze_email, commands::list_waiting_emails, commands::mark_email_waiting, commands::clear_email_waiting, // Contacts commands::list_contacts, commands::get_contact, commands::create_contact, commands::update_contact, commands::delete_contact, commands::add_contact_email, commands::remove_contact_email, commands::add_contact_phone, commands::remove_contact_phone, commands::add_contact_social_handle, commands::remove_contact_social_handle, commands::add_contact_custom_field, commands::remove_contact_custom_field, commands::update_contact_email, commands::update_contact_phone, commands::update_contact_social_handle, commands::update_contact_custom_field, commands::find_contact_by_email, commands::validate_email_addresses, commands::promote_contact, commands::list_tasks_for_contact, commands::list_events_for_contact, commands::list_emails_for_contact, commands::list_contacts_filtered, commands::bulk_delete_contacts, commands::bulk_tag_contacts, // Snooze Options commands::get_snooze_options, // Email Accounts commands::list_email_accounts, commands::get_email_account, commands::create_email_account, commands::update_email_account, commands::update_email_sync_interval, commands::update_email_signature, commands::update_email_notify, commands::delete_email_account, commands::test_email_account, commands::sync_email_account, // Project Dashboard commands::list_tasks_for_project, commands::list_events_for_project, commands::list_emails_for_project, commands::list_unlinked_emails, // Email Threading commands::list_emails_by_thread, // Stats commands::get_dashboard_stats, // App info commands::get_changelog, // Window commands::open_compose_window, commands::set_window_title, // Search commands::search, // Daily Notes commands::get_daily_note, commands::upsert_daily_note, // Day Planning commands::get_day_planning, commands::schedule_task, commands::unschedule_task, // Saved Views commands::list_saved_views, commands::list_pinned_views, commands::get_saved_view, commands::create_saved_view, commands::update_saved_view, commands::delete_saved_view, commands::toggle_view_pinned, // OAuth commands::list_oauth_providers, commands::start_oauth, commands::complete_oauth, commands::refresh_oauth_tokens, commands::disconnect_oauth, commands::reconnect_oauth, // Export/Backup commands::get_export_summary, commands::export_json, commands::export_tasks_csv, commands::export_events_ics, commands::create_backup, commands::list_backups, commands::restore_backup, commands::delete_backup, commands::get_backup_settings, commands::save_backup_settings, // Monthly Review commands::get_monthly_review, commands::upsert_monthly_goal, commands::update_monthly_goal_status, commands::delete_monthly_goal, commands::save_monthly_reflection, // Weekly Review commands::get_weekly_review, commands::complete_weekly_review, commands::set_task_focus, commands::clear_all_focus, commands::set_vacation_days, commands::check_weekly_review_nudge, // Sync commands::sync_get_tiers, commands::sync_status, commands::sync_start_auth, commands::sync_complete_auth, commands::sync_disconnect, commands::sync_now, commands::sync_setup_encryption_new, commands::sync_setup_encryption_existing, commands::sync_update_settings, commands::sync_account_info, commands::sync_subscription_status, commands::sync_subscribe, // Themes commands::list_themes, commands::get_theme, commands::get_custom_themes_dir, commands::import_theme, commands::export_theme, // Plugins commands::list_import_plugins, commands::list_enabled_import_plugins, commands::get_plugins_for_extension, commands::preview_import, commands::execute_import, commands::enable_plugin, commands::disable_plugin, commands::reload_plugin, // Import (VCF/ICS) commands::preview_vcf, commands::import_vcf, commands::preview_ics, commands::import_ics, // Time Tracking commands::start_timer, commands::stop_timer, commands::discard_timer, commands::get_active_timer, commands::list_time_sessions, commands::log_manual_time, commands::get_time_summary, // Preferences commands::get_preferences, commands::set_update_check_on_launch, ]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { if let RunEvent::Exit = event { info!("Application exiting, signaling background tasks to stop"); // Cancel all async schedulers if let Some(token) = app_handle.try_state::() { token.cancel(); } // Signal db_watcher threads to stop if let Some(flag) = app_handle.try_state::>() { flag.store(true, Ordering::Relaxed); } } }); } /// Check for OTA updates and emit an event to the frontend if one is available. #[cfg(not(any(target_os = "ios", target_os = "android")))] async fn check_for_updates(app: tauri::AppHandle) { use tauri_plugin_updater::UpdaterExt; let updater = match app.updater() { Ok(u) => u, Err(e) => { tracing::warn!("Failed to initialize updater: {e}"); return; } }; match updater.check().await { Ok(Some(update)) => { info!("Update available: v{}", update.version); let _ = app.emit( "update-available", serde_json::json!({ "version": update.version, "body": update.body.unwrap_or_default(), }), ); } Ok(None) => { info!("App is up to date"); } Err(e) => { tracing::warn!("Update check failed: {e}"); } } }