feat(robin): M1 System Presence — journald/kmsg/inotify watcher, pattern classifier, tray badge, chat panel #2
4 changed files with 97 additions and 9 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
50
src-tauri/src/notify.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue