feat: wire up Tauri IPC commands with global shortcuts and tray
Completes the backend command layer, exposing bridge operations to the frontend via Tauri IPC. Also adds system tray support and global hotkeys. New commands: - get_lore_status: Real CLI integration (was stub), returns issue/MR counts - get_bridge_status: Mapping counts, pending items, sync timestamps - sync_now: Trigger incremental sync (since_last_check events) - reconcile: Full reconciliation pass (two-strike orphan detection) - quick_capture: Create a new bead from freeform text All commands use tokio::spawn_blocking for CLI I/O, preventing async executor starvation. Commands accept trait objects for testability. System integration: - Global shortcut: Cmd+Shift+M toggles window visibility - Global shortcut: Cmd+Shift+C opens quick capture overlay - System tray: Left-click toggles window, right-click shows menu - Tray menu: Show Mission Control, Quit Tauri configuration: - Added global-shortcut plugin with permissions - Shell plugin scoped to lore, br, bv commands only - Removed trayIcon config (using TrayIconBuilder instead) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,16 +2,91 @@
|
||||
//!
|
||||
//! 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)
|
||||
//! - GitLab -> Beads bridge (creating beads from GitLab events)
|
||||
//! - Decision logging and state persistence
|
||||
//! - File watching for automatic sync
|
||||
|
||||
pub mod commands;
|
||||
pub mod data;
|
||||
pub mod error;
|
||||
pub mod watcher;
|
||||
|
||||
use tauri::Manager;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder};
|
||||
use tauri::tray::TrayIconBuilder;
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState};
|
||||
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) {
|
||||
let _ = window.hide();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
@@ -28,7 +103,46 @@ pub fn run() {
|
||||
|
||||
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") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
let _ = app.emit("global-shortcut-triggered", "quick-capture");
|
||||
} else {
|
||||
toggle_window_visibility(app);
|
||||
let _ = app.emit("global-shortcut-triggered", "toggle-window");
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.setup(|app| {
|
||||
// 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
|
||||
@@ -41,6 +155,10 @@ pub fn run() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::greet,
|
||||
commands::get_lore_status,
|
||||
commands::get_bridge_status,
|
||||
commands::sync_now,
|
||||
commands::reconcile,
|
||||
commands::quick_capture,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
Reference in New Issue
Block a user