Startup sequence now: 1. Creates data directories (~/.local/share/mc/) 2. Cleans up orphaned tmp files from crashes 3. Verifies CLI dependencies (lore, br, bv) asynchronously 4. Emits startup-warnings event with missing CLI warnings 5. Emits cli-availability event with tool status 6. Emits startup-sync-ready when CLIs available This enables the frontend to: - Display warnings for missing tools - Know which features are available - Trigger reconciliation when ready bd-3jh Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
261 lines
10 KiB
Rust
261 lines
10 KiB
Rust
//! 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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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::<tauri::Wry>::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,
|
|
]);
|
|
|
|
// 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<RealLoreCli, RealBeadsCli> =
|
|
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");
|
|
}
|