unsure
This commit is contained in:
parent
0e3233009b
commit
1a00daf6a8
11 changed files with 1192 additions and 67 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue