feat(robin): M1 System Presence — journald/kmsg/inotify watcher, pattern classifier, tray badge, chat panel #2
2 changed files with 70 additions and 23 deletions
|
|
@ -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![
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue