Skip to main content

max / goingson

25.9 KB · 679 lines History Blame Raw
1 //! GoingsOn desktop application entry point.
2
3 // Prevents additional console window on Windows in release
4 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
5
6 use goingson_desktop::backup_scheduler;
7 use goingson_desktop::commands;
8 use goingson_desktop::email_sync_scheduler;
9 use goingson_desktop::sync_scheduler;
10 use goingson_desktop::commands::PluginState;
11 use goingson_desktop::state::AppState;
12 use goingson_plugin_runtime::PluginRegistry;
13 use std::sync::Arc;
14 use std::sync::atomic::{AtomicBool, Ordering};
15 use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu};
16 use tauri::{Emitter, Manager, RunEvent};
17 use tokio_util::sync::CancellationToken;
18 use tracing::info;
19 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
20
21 // Desktop-only imports
22 #[cfg(not(any(target_os = "ios", target_os = "android")))]
23 use goingson_desktop::db_watcher;
24 #[cfg(not(any(target_os = "ios", target_os = "android")))]
25 use goingson_desktop::notifications;
26 #[cfg(not(any(target_os = "ios", target_os = "android")))]
27 use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
28
29 /// Set up the macOS menu bar tray icon showing "GO" in Reglo.
30 #[cfg(not(any(target_os = "ios", target_os = "android")))]
31 fn setup_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
32 use tauri::menu::{MenuBuilder, MenuItemBuilder};
33
34 let show = MenuItemBuilder::with_id("tray_show", "Show GoingsOn").build(app)?;
35 let quit = MenuItemBuilder::with_id("tray_quit", "Quit").build(app)?;
36 let menu = MenuBuilder::new(app).items(&[&show, &quit]).build()?;
37
38 let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/tray-icon@2x.png"))?;
39
40 let _tray = TrayIconBuilder::new()
41 .icon(icon)
42 .icon_as_template(true)
43 .menu(&menu)
44 .show_menu_on_left_click(false)
45 .tooltip("GoingsOn")
46 .on_tray_icon_event(|tray, event| {
47 if let TrayIconEvent::Click {
48 button: MouseButton::Left,
49 button_state: MouseButtonState::Up,
50 ..
51 } = event
52 {
53 if let Some(window) = tray.app_handle().get_webview_window("main") {
54 let _ = window.show();
55 let _ = window.unminimize();
56 let _ = window.set_focus();
57 }
58 }
59 })
60 .on_menu_event(|app, event| match event.id().as_ref() {
61 "tray_show" => {
62 if let Some(window) = app.get_webview_window("main") {
63 let _ = window.show();
64 let _ = window.unminimize();
65 let _ = window.set_focus();
66 }
67 }
68 "tray_quit" => {
69 app.exit(0);
70 }
71 _ => {}
72 })
73 .build(app)?;
74
75 Ok(())
76 }
77
78 fn main() {
79 // Initialize structured logging with tracing
80 tracing_subscriber::registry()
81 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| {
82 // Default log level: info for our crates, warn for dependencies
83 "goingson_desktop=info,goingson_core=debug,goingson_db_sqlite=debug,warn".into()
84 }))
85 .with(tracing_subscriber::fmt::layer())
86 .init();
87
88 info!("Starting GoingsOn application");
89
90 let mut builder = tauri::Builder::default();
91
92 // Desktop-only plugins
93 #[cfg(not(any(target_os = "ios", target_os = "android")))]
94 {
95 builder = builder
96 .plugin(tauri_plugin_shell::init())
97 .plugin(tauri_plugin_notification::init())
98 .plugin(tauri_plugin_window_state::Builder::new().build())
99 .plugin(tauri_plugin_updater::Builder::new().build())
100 .plugin(tauri_plugin_process::init());
101 }
102
103 builder
104 .plugin(tauri_plugin_dialog::init())
105 .menu(|app| {
106 // App menu (macOS) - contains About, Settings, Quit
107 #[cfg(target_os = "macos")]
108 let app_menu = Submenu::with_items(
109 app,
110 "GoingsOn",
111 true,
112 &[
113 &MenuItem::with_id(app, "about", "About GoingsOn", true, None::<&str>)?,
114 &PredefinedMenuItem::separator(app)?,
115 &MenuItem::with_id(app, "settings", "Settings...", true, Some("CmdOrCtrl+,"))?,
116 &PredefinedMenuItem::separator(app)?,
117 &PredefinedMenuItem::hide(app, Some("Hide GoingsOn"))?,
118 &PredefinedMenuItem::hide_others(app, Some("Hide Others"))?,
119 &PredefinedMenuItem::show_all(app, Some("Show All"))?,
120 &PredefinedMenuItem::separator(app)?,
121 &PredefinedMenuItem::quit(app, Some("Quit GoingsOn"))?,
122 ],
123 )?;
124
125 // File menu
126 let file_menu = Submenu::with_items(
127 app,
128 "File",
129 true,
130 &[
131 &MenuItem::with_id(app, "new_task", "New Task", true, Some("CmdOrCtrl+N"))?,
132 &MenuItem::with_id(
133 app,
134 "new_project",
135 "New Project",
136 true,
137 Some("CmdOrCtrl+Shift+N"),
138 )?,
139 &PredefinedMenuItem::separator(app)?,
140 &MenuItem::with_id(app, "import", "Import...", true, Some("CmdOrCtrl+I"))?,
141 &MenuItem::with_id(app, "save_view", "Save View", true, Some("CmdOrCtrl+S"))?,
142 &PredefinedMenuItem::separator(app)?,
143 &PredefinedMenuItem::close_window(app, Some("Close Window"))?,
144 #[cfg(not(target_os = "macos"))]
145 &PredefinedMenuItem::separator(app)?,
146 #[cfg(not(target_os = "macos"))]
147 &PredefinedMenuItem::quit(app, Some("Exit"))?,
148 ],
149 )?;
150
151 // Edit menu
152 let edit_menu = Submenu::with_items(
153 app,
154 "Edit",
155 true,
156 &[
157 &PredefinedMenuItem::undo(app, Some("Undo"))?,
158 &PredefinedMenuItem::redo(app, Some("Redo"))?,
159 &PredefinedMenuItem::separator(app)?,
160 &PredefinedMenuItem::cut(app, Some("Cut"))?,
161 &PredefinedMenuItem::copy(app, Some("Copy"))?,
162 &PredefinedMenuItem::paste(app, Some("Paste"))?,
163 &PredefinedMenuItem::separator(app)?,
164 &PredefinedMenuItem::select_all(app, Some("Select All"))?,
165 ],
166 )?;
167
168 // View menu - grouped by tab
169 let work_submenu = Submenu::with_items(
170 app,
171 "Work",
172 true,
173 &[
174 &MenuItem::with_id(app, "view_tasks", "Tasks", true, None::<&str>)?,
175 &MenuItem::with_id(app, "view_projects", "Projects", true, None::<&str>)?,
176 ],
177 )?;
178 let time_submenu = Submenu::with_items(
179 app,
180 "Time",
181 true,
182 &[
183 &MenuItem::with_id(app, "view_day_plan", "Day", true, None::<&str>)?,
184 &MenuItem::with_id(
185 app,
186 "view_weekly_review",
187 "Week",
188 true,
189 None::<&str>,
190 )?,
191 &MenuItem::with_id(
192 app,
193 "view_monthly_review",
194 "Month",
195 true,
196 None::<&str>,
197 )?,
198 &MenuItem::with_id(app, "view_events", "Events", true, None::<&str>)?,
199 ],
200 )?;
201 let messages_submenu = Submenu::with_items(
202 app,
203 "Messages",
204 true,
205 &[
206 &MenuItem::with_id(app, "view_emails", "Email", true, None::<&str>)?,
207 &MenuItem::with_id(app, "view_contacts", "Contacts", true, None::<&str>)?,
208 ],
209 )?;
210 let view_menu = Submenu::with_items(
211 app,
212 "View",
213 true,
214 &[
215 &MenuItem::with_id(app, "view_work", "Work", true, Some("CmdOrCtrl+1"))?,
216 &MenuItem::with_id(app, "view_time", "Time", true, Some("CmdOrCtrl+2"))?,
217 &MenuItem::with_id(
218 app,
219 "view_messages",
220 "Messages",
221 true,
222 Some("CmdOrCtrl+3"),
223 )?,
224 &PredefinedMenuItem::separator(app)?,
225 &work_submenu,
226 &time_submenu,
227 &messages_submenu,
228 &PredefinedMenuItem::separator(app)?,
229 &MenuItem::with_id(
230 app,
231 "toggle_sidebar",
232 "Toggle Sidebar",
233 true,
234 Some("CmdOrCtrl+\\"),
235 )?,
236 ],
237 )?;
238
239 // Tools menu
240 let tools_menu = Submenu::with_items(
241 app,
242 "Tools",
243 true,
244 &[
245 &MenuItem::with_id(
246 app,
247 "sync_email",
248 "Sync Email",
249 true,
250 Some("CmdOrCtrl+Shift+E"),
251 )?,
252 &PredefinedMenuItem::separator(app)?,
253 &MenuItem::with_id(app, "settings", "Settings", true, Some("CmdOrCtrl+,"))?,
254 ],
255 )?;
256
257 // Help menu
258 let help_menu = Submenu::with_items(
259 app,
260 "Help",
261 true,
262 &[
263 &MenuItem::with_id(
264 app,
265 "keyboard_shortcuts",
266 "Keyboard Shortcuts",
267 true,
268 Some("?"),
269 )?,
270 &PredefinedMenuItem::separator(app)?,
271 &MenuItem::with_id(app, "check_updates", "Check for Updates...", true, None::<&str>)?,
272 &PredefinedMenuItem::separator(app)?,
273 &MenuItem::with_id(app, "about", "About GoingsOn", true, None::<&str>)?,
274 ],
275 )?;
276
277 #[cfg(target_os = "macos")]
278 {
279 Menu::with_items(
280 app,
281 &[&app_menu, &file_menu, &edit_menu, &view_menu, &tools_menu, &help_menu],
282 )
283 }
284 #[cfg(not(target_os = "macos"))]
285 {
286 Menu::with_items(
287 app,
288 &[&file_menu, &edit_menu, &view_menu, &tools_menu, &help_menu],
289 )
290 }
291 })
292 .on_menu_event(|app, event| {
293 let event_id = event.id().as_ref();
294 if let Some(window) = app.get_webview_window("main") {
295 let _ = window.emit(&format!("menu:{}", event_id), ());
296 }
297 })
298 .setup(|app| {
299 // Set up menu bar tray icon (desktop only)
300 #[cfg(not(any(target_os = "ios", target_os = "android")))]
301 {
302 if let Err(e) = setup_tray(app) {
303 tracing::warn!("Failed to set up tray icon: {}", e);
304 }
305 }
306
307 // Initialize database
308 let app_handle = app.handle().clone();
309 tauri::async_runtime::block_on(async move {
310 let state = AppState::new(&app_handle).await
311 .expect("Failed to initialize app state");
312 app_handle.manage(Arc::new(state));
313 });
314
315 // Initialize plugin system
316 let config_dir = app.path().app_config_dir()
317 .expect("Failed to get config dir");
318 let plugins_dir = config_dir.join("plugins");
319 let mut plugin_registry = PluginRegistry::new(&plugins_dir)
320 .expect("Failed to initialize plugin registry");
321
322 // Load enabled plugins
323 if let Err(e) = plugin_registry.initialize() {
324 tracing::warn!("Failed to load some plugins: {}", e);
325 }
326
327 app.manage(PluginState::new(plugin_registry));
328
329 // Create shutdown coordination handles
330 let cancel_token = CancellationToken::new();
331 let db_watcher_shutdown = Arc::new(AtomicBool::new(false));
332
333 // Store shutdown handles in Tauri managed state for the run event handler
334 app.manage(cancel_token.clone());
335 app.manage(db_watcher_shutdown.clone());
336
337 // Desktop-only background services
338 #[cfg(not(any(target_os = "ios", target_os = "android")))]
339 {
340 // Start background notification checker
341 let handle = app.handle().clone();
342 let notify_cancel = cancel_token.clone();
343 tauri::async_runtime::spawn(async move {
344 notifications::start_snooze_watcher(handle, notify_cancel).await;
345 });
346
347 // Start database file watcher for external changes
348 let watcher_handle = app.handle().clone();
349 db_watcher::start_db_watcher(watcher_handle, db_watcher_shutdown);
350 }
351
352 // Start background backup scheduler (works on all platforms)
353 let backup_handle = app.handle().clone();
354 let backup_cancel = cancel_token.clone();
355 tauri::async_runtime::spawn(async move {
356 backup_scheduler::start_backup_scheduler(backup_handle, backup_cancel).await;
357 });
358
359 // Start background email sync scheduler (works on all platforms)
360 let sync_handle = app.handle().clone();
361 let email_cancel = cancel_token.clone();
362 tauri::async_runtime::spawn(async move {
363 email_sync_scheduler::start_email_sync_scheduler(sync_handle, email_cancel).await;
364 });
365
366 // Start background cloud sync scheduler
367 let cloud_sync_handle = app.handle().clone();
368 let cloud_cancel = cancel_token;
369 tauri::async_runtime::spawn(async move {
370 sync_scheduler::start_sync_scheduler(cloud_sync_handle, cloud_cancel).await;
371 });
372
373 // Check for OTA updates after a short delay (desktop only).
374 // Gated on the user's preference (default true).
375 #[cfg(not(any(target_os = "ios", target_os = "android")))]
376 {
377 let update_handle = app.handle().clone();
378 if commands::load_preferences(&update_handle).update_check_on_launch {
379 tauri::async_runtime::spawn(async move {
380 // Wait before checking to let the app finish starting
381 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
382 check_for_updates(update_handle).await;
383 });
384 }
385 }
386
387 Ok(())
388 })
389 .invoke_handler(tauri::generate_handler![
390 // Attachments
391 commands::add_attachment,
392 commands::list_attachments,
393 commands::delete_attachment,
394 commands::open_attachment,
395 commands::save_attachment,
396 commands::convert_email_attachments,
397 commands::open_email_blob,
398 commands::save_email_blob,
399 commands::get_file_size,
400 // Projects
401 commands::list_projects,
402 commands::get_project,
403 commands::create_project,
404 commands::update_project,
405 commands::delete_project,
406 // Tasks
407 commands::list_tasks,
408 commands::list_tasks_filtered,
409 commands::get_task,
410 commands::create_task,
411 commands::quick_add_task,
412 commands::update_task,
413 commands::delete_task,
414 commands::start_task,
415 commands::complete_task,
416 commands::list_snoozed_tasks,
417 commands::snooze_task,
418 commands::unsnooze_task,
419 commands::list_waiting_tasks,
420 commands::mark_task_waiting,
421 commands::clear_task_waiting,
422 // Task Overview
423 commands::get_task_overview,
424 // Annotations
425 commands::list_annotations,
426 commands::add_annotation,
427 commands::delete_annotation,
428 // Subtasks
429 commands::list_subtasks,
430 commands::add_subtask,
431 commands::add_subtask_link,
432 commands::toggle_subtask,
433 commands::update_subtask,
434 commands::delete_subtask,
435 // Milestones
436 commands::list_milestones,
437 commands::create_milestone,
438 commands::update_milestone,
439 commands::delete_milestone,
440 commands::reorder_milestones,
441 // Events
442 commands::list_events,
443 commands::get_event,
444 commands::create_event,
445 commands::update_event,
446 commands::delete_event,
447 commands::bulk_delete_events,
448 commands::list_events_between,
449 commands::list_upcoming_events,
450 commands::get_event_status_indicator,
451 commands::list_snoozed_events,
452 commands::snooze_event,
453 commands::unsnooze_event,
454 // Emails
455 commands::list_emails,
456 commands::list_emails_threaded,
457 commands::get_email,
458 commands::open_email_in_browser,
459 commands::create_email,
460 commands::send_email,
461 commands::save_email_draft,
462 commands::list_email_drafts,
463 commands::send_email_draft,
464 commands::set_email_labels,
465 commands::list_email_folders,
466 commands::list_email_labels,
467 commands::move_email_to_folder,
468 commands::delete_email,
469 commands::mark_email_read,
470 commands::mark_email_unread,
471 commands::archive_email,
472 commands::unarchive_email,
473 commands::mark_all_emails_read,
474 commands::link_email_to_project,
475 commands::get_unread_email_count,
476 commands::list_snoozed_emails,
477 commands::snooze_email,
478 commands::unsnooze_email,
479 commands::list_waiting_emails,
480 commands::mark_email_waiting,
481 commands::clear_email_waiting,
482 // Contacts
483 commands::list_contacts,
484 commands::get_contact,
485 commands::create_contact,
486 commands::update_contact,
487 commands::delete_contact,
488 commands::add_contact_email,
489 commands::remove_contact_email,
490 commands::add_contact_phone,
491 commands::remove_contact_phone,
492 commands::add_contact_social_handle,
493 commands::remove_contact_social_handle,
494 commands::add_contact_custom_field,
495 commands::remove_contact_custom_field,
496 commands::update_contact_email,
497 commands::update_contact_phone,
498 commands::update_contact_social_handle,
499 commands::update_contact_custom_field,
500 commands::find_contact_by_email,
501 commands::validate_email_addresses,
502 commands::promote_contact,
503 commands::list_tasks_for_contact,
504 commands::list_events_for_contact,
505 commands::list_emails_for_contact,
506 commands::list_contacts_filtered,
507 commands::bulk_delete_contacts,
508 commands::bulk_tag_contacts,
509 // Snooze Options
510 commands::get_snooze_options,
511 // Email Accounts
512 commands::list_email_accounts,
513 commands::get_email_account,
514 commands::create_email_account,
515 commands::update_email_account,
516 commands::update_email_sync_interval,
517 commands::update_email_signature,
518 commands::update_email_notify,
519 commands::delete_email_account,
520 commands::test_email_account,
521 commands::sync_email_account,
522 // Project Dashboard
523 commands::list_tasks_for_project,
524 commands::list_events_for_project,
525 commands::list_emails_for_project,
526 commands::list_unlinked_emails,
527 // Email Threading
528 commands::list_emails_by_thread,
529 // Stats
530 commands::get_dashboard_stats,
531 // App info
532 commands::get_changelog,
533 // Window
534 commands::open_compose_window,
535 commands::set_window_title,
536 // Search
537 commands::search,
538 // Daily Notes
539 commands::get_daily_note,
540 commands::upsert_daily_note,
541 // Day Planning
542 commands::get_day_planning,
543 commands::schedule_task,
544 commands::unschedule_task,
545 // Saved Views
546 commands::list_saved_views,
547 commands::list_pinned_views,
548 commands::get_saved_view,
549 commands::create_saved_view,
550 commands::update_saved_view,
551 commands::delete_saved_view,
552 commands::toggle_view_pinned,
553 // OAuth
554 commands::list_oauth_providers,
555 commands::start_oauth,
556 commands::complete_oauth,
557 commands::refresh_oauth_tokens,
558 commands::disconnect_oauth,
559 commands::reconnect_oauth,
560 // Export/Backup
561 commands::get_export_summary,
562 commands::export_json,
563 commands::export_tasks_csv,
564 commands::export_events_ics,
565 commands::create_backup,
566 commands::list_backups,
567 commands::restore_backup,
568 commands::delete_backup,
569 commands::get_backup_settings,
570 commands::save_backup_settings,
571 // Monthly Review
572 commands::get_monthly_review,
573 commands::upsert_monthly_goal,
574 commands::update_monthly_goal_status,
575 commands::delete_monthly_goal,
576 commands::save_monthly_reflection,
577 // Weekly Review
578 commands::get_weekly_review,
579 commands::complete_weekly_review,
580 commands::set_task_focus,
581 commands::clear_all_focus,
582 commands::set_vacation_days,
583 commands::check_weekly_review_nudge,
584 // Sync
585 commands::sync_get_tiers,
586 commands::sync_status,
587 commands::sync_start_auth,
588 commands::sync_complete_auth,
589 commands::sync_disconnect,
590 commands::sync_now,
591 commands::sync_setup_encryption_new,
592 commands::sync_setup_encryption_existing,
593 commands::sync_update_settings,
594 commands::sync_account_info,
595 commands::sync_subscription_status,
596 commands::sync_subscribe,
597 // Themes
598 commands::list_themes,
599 commands::get_theme,
600 commands::get_custom_themes_dir,
601 commands::import_theme,
602 commands::export_theme,
603 // Plugins
604 commands::list_import_plugins,
605 commands::list_enabled_import_plugins,
606 commands::get_plugins_for_extension,
607 commands::preview_import,
608 commands::execute_import,
609 commands::enable_plugin,
610 commands::disable_plugin,
611 commands::reload_plugin,
612 // Import (VCF/ICS)
613 commands::preview_vcf,
614 commands::import_vcf,
615 commands::preview_ics,
616 commands::import_ics,
617 // Time Tracking
618 commands::start_timer,
619 commands::stop_timer,
620 commands::discard_timer,
621 commands::get_active_timer,
622 commands::list_time_sessions,
623 commands::log_manual_time,
624 commands::get_time_summary,
625 // Preferences
626 commands::get_preferences,
627 commands::set_update_check_on_launch,
628 ])
629 .build(tauri::generate_context!())
630 .expect("error while building tauri application")
631 .run(|app_handle, event| {
632 if let RunEvent::Exit = event {
633 info!("Application exiting, signaling background tasks to stop");
634
635 // Cancel all async schedulers
636 if let Some(token) = app_handle.try_state::<CancellationToken>() {
637 token.cancel();
638 }
639
640 // Signal db_watcher threads to stop
641 if let Some(flag) = app_handle.try_state::<Arc<AtomicBool>>() {
642 flag.store(true, Ordering::Relaxed);
643 }
644 }
645 });
646 }
647
648 /// Check for OTA updates and emit an event to the frontend if one is available.
649 #[cfg(not(any(target_os = "ios", target_os = "android")))]
650 async fn check_for_updates(app: tauri::AppHandle) {
651 use tauri_plugin_updater::UpdaterExt;
652
653 let updater = match app.updater() {
654 Ok(u) => u,
655 Err(e) => {
656 tracing::warn!("Failed to initialize updater: {e}");
657 return;
658 }
659 };
660 match updater.check().await {
661 Ok(Some(update)) => {
662 info!("Update available: v{}", update.version);
663 let _ = app.emit(
664 "update-available",
665 serde_json::json!({
666 "version": update.version,
667 "body": update.body.unwrap_or_default(),
668 }),
669 );
670 }
671 Ok(None) => {
672 info!("App is up to date");
673 }
674 Err(e) => {
675 tracing::warn!("Update check failed: {e}");
676 }
677 }
678 }
679