Skip to main content

max / goingson

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