This commit is contained in:
Breadway 2026-05-11 16:30:05 +08:00
parent 0e3233009b
commit 1a00daf6a8
11 changed files with 1192 additions and 67 deletions

View file

@ -12,8 +12,12 @@ pub struct Config {
#[serde(default)]
pub lua: LuaConfig,
#[serde(default)]
pub modules: ModulesConfig,
#[serde(default)]
pub adapters: AdaptersConfig,
#[serde(default)]
pub notifications: NotificationsConfig,
#[serde(default)]
pub events: EventsConfig,
}
@ -33,6 +37,14 @@ pub struct LuaConfig {
pub module_path: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ModulesConfig {
#[serde(default = "default_true")]
pub builtin: bool,
#[serde(default)]
pub disable: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct AdaptersConfig {
#[serde(default)]
@ -73,12 +85,24 @@ pub struct EventsConfig {
pub dedup_window_ms: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct NotificationsConfig {
#[serde(default = "default_notify_timeout")]
pub default_timeout_ms: i64,
#[serde(default = "default_notify_urgency")]
pub default_urgency: String,
#[serde(default = "default_notify_path")]
pub notify_send_path: String,
}
impl Default for Config {
fn default() -> Self {
Self {
daemon: DaemonConfig::default(),
lua: LuaConfig::default(),
modules: ModulesConfig::default(),
adapters: AdaptersConfig::default(),
notifications: NotificationsConfig::default(),
events: EventsConfig::default(),
}
}
@ -102,6 +126,15 @@ impl Default for LuaConfig {
}
}
impl Default for ModulesConfig {
fn default() -> Self {
Self {
builtin: default_true(),
disable: Vec::new(),
}
}
}
impl Default for AdaptersConfig {
fn default() -> Self {
Self {
@ -147,6 +180,16 @@ impl Default for EventsConfig {
}
}
impl Default for NotificationsConfig {
fn default() -> Self {
Self {
default_timeout_ms: default_notify_timeout(),
default_urgency: default_notify_urgency(),
notify_send_path: default_notify_path(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let path = config_path();
@ -218,6 +261,18 @@ fn default_dedup_window() -> u64 {
100
}
fn default_notify_timeout() -> i64 {
3000
}
fn default_notify_urgency() -> String {
"normal".to_string()
}
fn default_notify_path() -> String {
"notify-send".to_string()
}
fn default_udev_subsystems() -> Vec<String> {
vec![
"usb".to_string(),

View file

@ -80,22 +80,102 @@ impl EventNormalizer {
fn normalize_hyprland(&self, raw: &RawEvent) -> Vec<BreadEvent> {
let kind = raw.payload.get("kind").and_then(Value::as_str).unwrap_or("unknown");
let mapped = match kind {
"workspace" | "workspacev2" => "bread.workspace.changed",
"monitoradded" => "bread.monitor.connected",
"monitorremoved" => "bread.monitor.disconnected",
"activewindow" | "activewindowv2" => "bread.window.focus.changed",
"openwindow" => "bread.window.opened",
"closewindow" => "bread.window.closed",
_ => "bread.hyprland.event",
};
let data = raw
.payload
.get("data")
.and_then(Value::as_str)
.unwrap_or("");
vec![BreadEvent {
event: mapped.to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: raw.payload.clone(),
}]
match kind {
"workspace" | "workspacev2" => vec![BreadEvent {
event: "bread.workspace.changed".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: raw.payload.clone(),
}],
"createworkspace" => vec![BreadEvent {
event: "bread.workspace.created".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: json!({ "workspace": data }),
}],
"destroyworkspace" => vec![BreadEvent {
event: "bread.workspace.destroyed".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: json!({ "workspace": data }),
}],
"monitoradded" => vec![BreadEvent {
event: "bread.monitor.connected".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: raw.payload.clone(),
}],
"monitorremoved" => vec![BreadEvent {
event: "bread.monitor.disconnected".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: raw.payload.clone(),
}],
"activewindow" => vec![BreadEvent {
event: "bread.window.focus.changed".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: raw.payload.clone(),
}],
"activewindowv2" => {
let fields = split_hyprland_fields(data);
vec![BreadEvent {
event: "bread.window.focused".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: json!({
"address": fields.get(0).unwrap_or(&"")
}),
}]
}
"openwindow" => {
let fields = split_hyprland_fields(data);
vec![BreadEvent {
event: "bread.window.opened".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: json!({
"address": fields.get(0).unwrap_or(&""),
"workspace": fields.get(1).unwrap_or(&""),
"class": fields.get(2).unwrap_or(&""),
"title": fields.get(3).unwrap_or(&""),
}),
}]
}
"closewindow" => {
let fields = split_hyprland_fields(data);
vec![BreadEvent {
event: "bread.window.closed".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: json!({ "address": fields.get(0).unwrap_or(&"") }),
}]
}
"movewindow" => {
let fields = split_hyprland_fields(data);
vec![BreadEvent {
event: "bread.window.moved".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: json!({
"address": fields.get(0).unwrap_or(&""),
"workspace": fields.get(1).unwrap_or(&""),
}),
}]
}
_ => vec![BreadEvent {
event: "bread.hyprland.event".to_string(),
timestamp: raw.timestamp,
source: AdapterSource::Hyprland,
data: raw.payload.clone(),
}],
}
}
fn normalize_power(&self, raw: &RawEvent) -> Vec<BreadEvent> {
@ -201,6 +281,13 @@ impl EventNormalizer {
}
}
fn split_hyprland_fields(data: &str) -> Vec<&str> {
if data.is_empty() {
return Vec::new();
}
data.split(">>").collect()
}
fn classify_device(payload: &Value) -> DeviceClass {
let name = payload
.get("name")

View file

@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use anyhow::Result;
use bread_shared::{AdapterSource, BreadEvent};
@ -15,6 +16,7 @@ use crate::lua::LuaMessage;
pub struct StateHandle {
state: Arc<RwLock<RuntimeState>>,
command_tx: mpsc::UnboundedSender<StateCommand>,
subscription_count: Arc<AtomicU64>,
}
pub enum StateCommand {
@ -38,6 +40,7 @@ pub enum StateCommand {
name: String,
status: ModuleLoadState,
last_error: Option<String>,
builtin: bool,
},
SetProfile {
name: String,
@ -45,8 +48,16 @@ pub enum StateCommand {
}
impl StateHandle {
pub fn new(state: Arc<RwLock<RuntimeState>>, command_tx: mpsc::UnboundedSender<StateCommand>) -> Self {
Self { state, command_tx }
pub fn new(
state: Arc<RwLock<RuntimeState>>,
command_tx: mpsc::UnboundedSender<StateCommand>,
subscription_count: Arc<AtomicU64>,
) -> Self {
Self {
state,
command_tx,
subscription_count,
}
}
pub fn state_arc(&self) -> Arc<RwLock<RuntimeState>> {
@ -101,17 +112,28 @@ impl StateHandle {
let _ = self.command_tx.send(StateCommand::ClearSubscriptions);
}
pub fn set_module_status(&self, name: String, status: ModuleLoadState, last_error: Option<String>) {
pub fn set_module_status(
&self,
name: String,
status: ModuleLoadState,
last_error: Option<String>,
builtin: bool,
) {
let _ = self.command_tx.send(StateCommand::SetModuleStatus {
name,
status,
last_error,
builtin,
});
}
pub fn set_profile(&self, name: String) {
let _ = self.command_tx.send(StateCommand::SetProfile { name });
}
pub fn subscription_count(&self) -> Arc<AtomicU64> {
self.subscription_count.clone()
}
}
pub async fn run_state_engine(
@ -120,6 +142,7 @@ pub async fn run_state_engine(
state: Arc<RwLock<RuntimeState>>,
lua_tx: mpsc::UnboundedSender<LuaMessage>,
event_stream_tx: broadcast::Sender<BreadEvent>,
subscription_count: Arc<AtomicU64>,
mut shutdown_rx: watch::Receiver<bool>,
) {
let mut subscriptions = SubscriptionTable::default();
@ -136,7 +159,7 @@ pub async fn run_state_engine(
let Some(cmd) = maybe_cmd else {
break;
};
handle_command(cmd, &state, &mut subscriptions, &mut watches).await;
handle_command(cmd, &state, &mut subscriptions, &mut watches, &subscription_count).await;
}
maybe_event = event_rx.recv() => {
let Some(event) = maybe_event else {
@ -158,7 +181,7 @@ pub async fn run_state_engine(
apply_event_to_state(&mut guard, &event);
}
dispatch_event(&event, &mut subscriptions, &lua_tx, &event_stream_tx);
dispatch_event(&event, &mut subscriptions, &lua_tx, &event_stream_tx, &subscription_count);
if let (Some(before), Some(after)) = (before_snapshot, after_snapshot) {
for (_id, path) in watches.iter() {
@ -174,7 +197,7 @@ pub async fn run_state_engine(
"old": old_val,
}),
);
dispatch_event(&synthetic, &mut subscriptions, &lua_tx, &event_stream_tx);
dispatch_event(&synthetic, &mut subscriptions, &lua_tx, &event_stream_tx, &subscription_count);
}
}
}
@ -190,13 +213,17 @@ async fn handle_command(
state: &Arc<RwLock<RuntimeState>>,
subscriptions: &mut SubscriptionTable,
watches: &mut HashMap<SubscriptionId, String>,
subscription_count: &Arc<AtomicU64>,
) {
match cmd {
StateCommand::RegisterSubscription { id, pattern, once } => {
subscriptions.add_with_id(id, pattern, once);
subscription_count.fetch_add(1, Ordering::Relaxed);
}
StateCommand::RemoveSubscription { id } => {
subscriptions.remove(id);
if subscriptions.remove(id) {
subscription_count.fetch_sub(1, Ordering::Relaxed);
}
}
StateCommand::RegisterWatch { id, path } => {
watches.insert(id, path);
@ -207,21 +234,25 @@ async fn handle_command(
StateCommand::ClearSubscriptions => {
subscriptions.clear();
watches.clear();
subscription_count.store(0, Ordering::Relaxed);
}
StateCommand::SetModuleStatus {
name,
status,
last_error,
builtin,
} => {
let mut guard = state.write().await;
if let Some(existing) = guard.modules.iter_mut().find(|m| m.name == name) {
existing.status = status;
existing.last_error = last_error;
existing.builtin = builtin;
} else {
guard.modules.push(crate::core::types::ModuleStatus {
name,
status,
last_error,
builtin,
store: HashMap::new(),
});
}
@ -242,6 +273,7 @@ fn dispatch_event(
subscriptions: &mut SubscriptionTable,
lua_tx: &mpsc::UnboundedSender<LuaMessage>,
event_stream_tx: &broadcast::Sender<BreadEvent>,
subscription_count: &Arc<AtomicU64>,
) {
let _ = event_stream_tx.send(event.clone());
@ -254,7 +286,9 @@ fn dispatch_event(
}
for sub in matches.into_iter().filter(|s| s.once) {
subscriptions.remove(sub.id);
if subscriptions.remove(sub.id) {
subscription_count.fetch_sub(1, Ordering::Relaxed);
}
let _ = lua_tx.send(LuaMessage::SubscriptionCancelled { id: sub.id });
}
}
@ -302,11 +336,12 @@ fn apply_event_to_state(state: &mut RuntimeState, event: &BreadEvent) {
.map(ToString::to_string);
state.active_workspace = ws;
}
"bread.window.focus.changed" => {
"bread.window.focus.changed" | "bread.window.focused" => {
state.active_window = event
.data
.get("window")
.or_else(|| event.data.get("class"))
.or_else(|| event.data.get("address"))
.and_then(Value::as_str)
.map(ToString::to_string);
}

View file

@ -123,6 +123,8 @@ pub struct ModuleStatus {
pub status: ModuleLoadState,
pub last_error: Option<String>,
#[serde(default)]
pub builtin: bool,
#[serde(default)]
pub store: HashMap<String, Value>,
}