feat: full pattern matrix — M1 complete, M2 LLM chat, 30+ pattern files #10

Open
pyr0ball wants to merge 31 commits from feat/patterns-expansion into main
2 changed files with 70 additions and 23 deletions
Showing only changes of commit 2706b2367d - Show all commits

View file

@ -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::<AppState>();
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![

View file

@ -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<PatternFile> {
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 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 {resource_path}"))?;
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 {resource_path}",
"pattern '{}' has empty match_text in {path}",
p.id
);
anyhow::ensure!(
!p.sources.is_empty(),
"pattern '{}' has empty sources list in {resource_path}",
"pattern '{}' has empty sources list in {path}",
p.id
);
}
Ok(pf)
return Ok(pf);
}
}
anyhow::bail!("pattern file not found: {filename}")
}
#[must_use]