diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4d3ae38..b26a24f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,11 +8,11 @@ mod watcher; use commands::AppState; use config::RobinConfig; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; +use tauri::Manager; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - // TODO: log a warning when load() fails so users know their config was reset let config = RobinConfig::load().unwrap_or_default(); tauri::Builder::default() @@ -25,7 +25,46 @@ pub fn run() { }) .setup(|app| { tray::build_tray(&app.handle())?; - watcher::spawn(std::collections::HashMap::new()); + + let state = app.state::(); + let cfg = state + .config + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(); + + let pattern_file = if let Some(ref migration) = cfg.migration { + let family = distro::distro_family(&migration.distro); + let source = match migration.source_os { + config::SourceOs::Macos => "macos", + config::SourceOs::Windows => "windows", + config::SourceOs::Linux => "linux", + config::SourceOs::Unknown => "unknown", + }; + patterns::load(source, family).ok() + } else { + None + }; + + let log_paths = pattern_file + .as_ref() + .map(|pf| pf.log_paths.clone()) + .unwrap_or_default(); + + let mut rx = watcher::spawn(log_paths); + let pf = Arc::new(pattern_file); + let app_handle = app.handle().clone(); + + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + if let Some(ref pf) = *pf { + if let Some(matched) = patterns::classify(&event, pf) { + notify::dispatch(&app_handle, matched); + } + } + } + }); + Ok(()) }) .invoke_handler(tauri::generate_handler![ diff --git a/src-tauri/src/patterns.rs b/src-tauri/src/patterns.rs index 13b27c5..7310d89 100644 --- a/src-tauri/src/patterns.rs +++ b/src-tauri/src/patterns.rs @@ -39,28 +39,36 @@ pub struct MatchedEvent { /// Load the pattern file for a source OS and distro family. /// -/// Path is relative to the working directory. In tests this is the crate root -/// (where `patterns/` lives as a sibling of `src/`). At runtime, Task 10 resolves -/// the path via `tauri::path::BaseDirectory::Resource` before calling this function. +/// Tries candidates in order: dev-relative path, src-tauri-relative path, +/// system path. The Tauri resource directory is the authoritative location +/// at runtime; passing a base path is handled in Task 10's caller via candidate list. pub fn load(source_os: &str, distro_family: &str) -> Result { - let resource_path = format!("patterns/{source_os}-to-{distro_family}.toml"); - let content = std::fs::read_to_string(&resource_path) - .with_context(|| format!("pattern file not found: {resource_path}"))?; - let pf: PatternFile = - toml::from_str(&content).with_context(|| format!("failed to parse {resource_path}"))?; - for p in &pf.patterns { - anyhow::ensure!( - !p.match_text.is_empty(), - "pattern '{}' has empty match_text in {resource_path}", - p.id - ); - anyhow::ensure!( - !p.sources.is_empty(), - "pattern '{}' has empty sources list in {resource_path}", - p.id - ); + let filename = format!("{source_os}-to-{distro_family}.toml"); + let candidates = [ + format!("patterns/{filename}"), + format!("src-tauri/patterns/{filename}"), + format!("/usr/share/robin/patterns/{filename}"), + ]; + for path in &candidates { + if let Ok(content) = std::fs::read_to_string(path) { + let pf: PatternFile = + toml::from_str(&content).with_context(|| format!("failed to parse {path}"))?; + for p in &pf.patterns { + anyhow::ensure!( + !p.match_text.is_empty(), + "pattern '{}' has empty match_text in {path}", + p.id + ); + anyhow::ensure!( + !p.sources.is_empty(), + "pattern '{}' has empty sources list in {path}", + p.id + ); + } + return Ok(pf); + } } - Ok(pf) + anyhow::bail!("pattern file not found: {filename}") } #[must_use]