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
4 changed files with 97 additions and 9 deletions
Showing only changes of commit 3b7653d731 - Show all commits

View file

@ -1,6 +1,6 @@
use crate::config::{NotificationLevel, RobinConfig, MigrationConfig, SourceOs};
use tauri::State;
use crate::config::{MigrationConfig, NotificationLevel, RobinConfig, SourceOs};
use std::sync::Mutex;
use tauri::State;
pub struct AppState {
pub config: Mutex<RobinConfig>,
@ -8,14 +8,18 @@ pub struct AppState {
#[tauri::command]
pub fn get_config(state: State<'_, AppState>) -> Result<RobinConfig, String> {
state.config.lock()
state
.config
.lock()
.map(|c| c.clone())
.map_err(|e| e.to_string())
}
#[tauri::command]
pub fn needs_onboarding(state: State<'_, AppState>) -> bool {
state.config.lock()
state
.config
.lock()
.map(|c| c.needs_onboarding())
.unwrap_or(true)
}
@ -49,10 +53,7 @@ pub fn complete_onboarding(
}
#[tauri::command]
pub fn update_notification_level(
level: String,
state: State<'_, AppState>,
) -> Result<(), String> {
pub fn update_notification_level(level: String, state: State<'_, AppState>) -> Result<(), String> {
let parsed = match level.as_str() {
"off" => NotificationLevel::Off,
"badge_only" => NotificationLevel::BadgeOnly,
@ -63,3 +64,8 @@ pub fn update_notification_level(
config.display.notification_level = parsed;
config.save().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_pending_events() -> Vec<crate::patterns::MatchedEvent> {
crate::notify::take_pending()
}

View file

@ -1,6 +1,7 @@
mod commands;
mod config;
mod distro;
mod notify;
mod patterns;
mod tray;
mod watcher;
@ -32,6 +33,7 @@ pub fn run() {
commands::needs_onboarding,
commands::complete_onboarding,
commands::update_notification_level,
commands::get_pending_events,
])
.run(tauri::generate_context!())
.expect("error while running Robin");

50
src-tauri/src/notify.rs Normal file
View file

@ -0,0 +1,50 @@
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Runtime};
use tauri_plugin_notification::NotificationExt;
use crate::commands::AppState;
use crate::config::NotificationLevel;
use crate::patterns::MatchedEvent;
static PENDING: Mutex<Vec<MatchedEvent>> = Mutex::new(vec![]);
pub fn dispatch<R: Runtime>(app: &AppHandle<R>, event: MatchedEvent) {
let level = {
let state = app.state::<AppState>();
let config = state.config.lock().unwrap();
config.display.notification_level.clone()
};
if let Ok(mut pending) = PENDING.lock() {
pending.push(event.clone());
}
match level {
NotificationLevel::Off => {}
NotificationLevel::BadgeOnly => {
crate::tray::badge_on(app);
}
NotificationLevel::BadgeAndToast => {
crate::tray::badge_on(app);
send_toast(app, &event);
}
}
let _ = app.emit("robin:event", &event);
}
fn send_toast<R: Runtime>(app: &AppHandle<R>, event: &MatchedEvent) {
let _ = app
.notification()
.builder()
.title(&event.title)
.body(&event.body)
.show();
}
pub fn take_pending() -> Vec<MatchedEvent> {
PENDING
.lock()
.map(|mut v| std::mem::take(&mut *v))
.unwrap_or_default()
}

View file

@ -1,3 +1,4 @@
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
@ -34,7 +35,7 @@ pub fn build_tray<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<()> {
Ok(())
}
fn toggle_chat_panel<R: Runtime>(app: &AppHandle<R>) {
pub fn toggle_chat_panel<R: Runtime>(app: &AppHandle<R>) {
if let Some(window) = app.get_webview_window("chat") {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
@ -44,3 +45,32 @@ fn toggle_chat_panel<R: Runtime>(app: &AppHandle<R>) {
}
}
}
static BADGE_ACTIVE: AtomicBool = AtomicBool::new(false);
pub fn badge_on<R: Runtime>(app: &AppHandle<R>) {
if BADGE_ACTIVE.swap(true, Ordering::Relaxed) {
return; // already badged
}
if let Some(tray) = app.tray_by_id("robin-tray") {
if let Some(icon) = app.default_window_icon().cloned() {
let _ = tray.set_icon(Some(icon));
}
let _ = tray.set_tooltip(Some("Robin — something to show you"));
}
}
pub fn badge_off<R: Runtime>(app: &AppHandle<R>) {
BADGE_ACTIVE.store(false, Ordering::Relaxed);
if let Some(tray) = app.tray_by_id("robin-tray") {
if let Some(icon) = app.default_window_icon().cloned() {
let _ = tray.set_icon(Some(icon));
}
let _ = tray.set_tooltip(Some("Robin"));
}
}
pub fn clear_badge_and_open<R: Runtime>(app: &AppHandle<R>) {
badge_off(app);
toggle_chat_panel(app);
}