feat(robin): M1 System Presence — journald/kmsg/inotify watcher, pattern classifier, tray badge, chat panel #2

Open
pyr0ball wants to merge 21 commits from feat/m1-system-presence 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 commands::AppState;
use config::RobinConfig; use config::RobinConfig;
use std::sync::Mutex; use std::sync::{Arc, Mutex};
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
// TODO: log a warning when load() fails so users know their config was reset
let config = RobinConfig::load().unwrap_or_default(); let config = RobinConfig::load().unwrap_or_default();
tauri::Builder::default() tauri::Builder::default()
@ -25,7 +25,46 @@ pub fn run() {
}) })
.setup(|app| { .setup(|app| {
tray::build_tray(&app.handle())?; 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(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .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. /// 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 /// Tries candidates in order: dev-relative path, src-tauri-relative path,
/// (where `patterns/` lives as a sibling of `src/`). At runtime, Task 10 resolves /// system path. The Tauri resource directory is the authoritative location
/// the path via `tauri::path::BaseDirectory::Resource` before calling this function. /// 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> { pub fn load(source_os: &str, distro_family: &str) -> Result<PatternFile> {
let resource_path = format!("patterns/{source_os}-to-{distro_family}.toml"); let filename = format!("{source_os}-to-{distro_family}.toml");
let content = std::fs::read_to_string(&resource_path) let candidates = [
.with_context(|| format!("pattern file not found: {resource_path}"))?; 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 = 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 { for p in &pf.patterns {
anyhow::ensure!( anyhow::ensure!(
!p.match_text.is_empty(), !p.match_text.is_empty(),
"pattern '{}' has empty match_text in {resource_path}", "pattern '{}' has empty match_text in {path}",
p.id p.id
); );
anyhow::ensure!( anyhow::ensure!(
!p.sources.is_empty(), !p.sources.is_empty(),
"pattern '{}' has empty sources list in {resource_path}", "pattern '{}' has empty sources list in {path}",
p.id p.id
); );
} }
Ok(pf) return Ok(pf);
}
}
anyhow::bail!("pattern file not found: {filename}")
} }
#[must_use] #[must_use]