feat: add Tauri event hook and lore.db file watcher

Adds real-time updates when lore syncs new GitLab data by watching
the lore.db file for changes.

React hook (src/hooks/useTauriEvents.ts):
- useTauriEvent(): Subscribe to a single Tauri event with auto-cleanup
- useTauriEvents(): Subscribe to multiple events with a handler map
- Typed payloads for each event type:
  - global-shortcut-triggered: toggle-window | quick-capture
  - lore-data-changed: void (refresh trigger)
  - sync-status: started | completed | failed
  - error-notification: code + message

Rust watcher (src-tauri/src/watcher.rs):
- Watches lore's data directory for lore.db modifications
- Uses notify crate with 2-second poll interval
- Emits "lore-data-changed" event to frontend on file change
- Handles atomic writes by watching parent directory
- Gracefully handles missing lore.db (logs warning, skips watcher)

Test coverage:
- Hook subscription and cleanup behavior
- Focus store test fix: clear localStorage before each test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
teernisse
2026-02-26 09:55:10 -05:00
parent df26eab361
commit 7404acdfb4
4 changed files with 333 additions and 0 deletions

85
src-tauri/src/watcher.rs Normal file
View File

@@ -0,0 +1,85 @@
//! File watcher for lore.db changes
//!
//! Watches the lore database file for modifications and emits events
//! to trigger frontend refresh when lore syncs new data.
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::Duration;
use tauri::{AppHandle, Emitter};
/// Get the path to lore's database file
fn lore_db_path() -> Option<PathBuf> {
dirs::data_local_dir().map(|d| d.join("lore").join("lore.db"))
}
/// Start watching lore.db for changes.
///
/// When the file is modified, emits a "lore-data-changed" event to the frontend.
/// Returns the watcher handle (must be kept alive for watching to continue).
pub fn start_lore_watcher(app: AppHandle) -> Option<RecommendedWatcher> {
let db_path = lore_db_path()?;
// Check if the file exists
if !db_path.exists() {
tracing::warn!("lore.db not found at {:?}, skipping file watcher", db_path);
return None;
}
let parent_dir = db_path.parent()?;
// Create a channel for receiving events
let (tx, rx) = mpsc::channel();
// Create the watcher with a debounce of 1 second
let mut watcher = RecommendedWatcher::new(
move |res: Result<Event, notify::Error>| {
if let Ok(event) = res {
let _ = tx.send(event);
}
},
Config::default().with_poll_interval(Duration::from_secs(2)),
)
.ok()?;
// Watch the lore directory (not just the file, for atomic write detection)
if let Err(e) = watcher.watch(parent_dir, RecursiveMode::NonRecursive) {
tracing::error!("Failed to start lore.db watcher: {}", e);
return None;
}
tracing::info!("Watching lore.db for changes at {:?}", db_path);
// Spawn a thread to handle events
std::thread::spawn(move || {
for event in rx {
// Only react to modify events on the actual db file
if matches!(
event.kind,
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
) {
let affects_db = event.paths.iter().any(|p| p.ends_with("lore.db"));
if affects_db {
tracing::debug!("lore.db changed, emitting refresh event");
let _ = app.emit("lore-data-changed", ());
}
}
}
});
Some(watcher)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lore_db_path_returns_expected_location() {
let path = lore_db_path();
assert!(path.is_some());
let path = path.unwrap();
assert!(path.ends_with("lore/lore.db"));
}
}