added tokio shared states instead of monolithic state
Release / Build and Release (push) Has been cancelled

This commit is contained in:
2026-04-02 18:11:21 +02:00
parent bb3f6e565d
commit bdbd6a8a40
18 changed files with 479 additions and 352 deletions
+17 -18
View File
@@ -1,10 +1,9 @@
use crate::config::Config;
use crate::error::{FluxoError, Result as FluxoResult};
use crate::modules::bt::maestro::{BudsCommand, get_maestro};
use crate::state::SharedState;
use crate::modules::bt::maestro::BudsCommand;
use crate::state::AppReceivers;
use crate::utils::TokenValue;
use futures::future::BoxFuture;
use std::sync::Arc;
pub trait BtPlugin: Send + Sync {
fn name(&self) -> &str;
@@ -12,21 +11,21 @@ pub trait BtPlugin: Send + Sync {
fn get_data(
&self,
config: &Config,
state: &SharedState,
state: &AppReceivers,
mac: &str,
) -> BoxFuture<'static, FluxoResult<Vec<(String, TokenValue)>>>;
fn get_modes(
&self,
mac: &str,
state: &SharedState,
state: &AppReceivers,
) -> BoxFuture<'static, FluxoResult<Vec<String>>>;
fn set_mode(
&self,
mode: &str,
mac: &str,
state: &SharedState,
state: &AppReceivers,
) -> BoxFuture<'static, FluxoResult<()>>;
fn cycle_mode(&self, mac: &str, state: &SharedState) -> BoxFuture<'static, FluxoResult<()>>;
fn cycle_mode(&self, mac: &str, state: &AppReceivers) -> BoxFuture<'static, FluxoResult<()>>;
}
pub struct PixelBudsPlugin;
@@ -43,13 +42,13 @@ impl BtPlugin for PixelBudsPlugin {
fn get_data(
&self,
_config: &Config,
state: &SharedState,
state: &AppReceivers,
mac: &str,
) -> BoxFuture<'static, FluxoResult<Vec<(String, TokenValue)>>> {
let mac = mac.to_string();
let state = Arc::clone(state);
let state = state.clone();
Box::pin(async move {
let maestro = get_maestro(&state);
let maestro = crate::modules::bt::maestro::get_maestro(&state);
maestro.ensure_task(&mac);
let status = maestro.get_status(&mac);
@@ -91,7 +90,7 @@ impl BtPlugin for PixelBudsPlugin {
fn get_modes(
&self,
_mac: &str,
_state: &SharedState,
_state: &AppReceivers,
) -> BoxFuture<'static, FluxoResult<Vec<String>>> {
Box::pin(async move {
Ok(vec![
@@ -106,13 +105,13 @@ impl BtPlugin for PixelBudsPlugin {
&self,
mode: &str,
mac: &str,
state: &SharedState,
state: &AppReceivers,
) -> BoxFuture<'static, FluxoResult<()>> {
let mode = mode.to_string();
let mac = mac.to_string();
let state = Arc::clone(state);
let state = state.clone();
Box::pin(async move {
get_maestro(&state)
crate::modules::bt::maestro::get_maestro(&state)
.send_command(&mac, BudsCommand::SetAnc(mode))
.map_err(|e: anyhow::Error| FluxoError::Module {
module: "bt.buds",
@@ -121,17 +120,17 @@ impl BtPlugin for PixelBudsPlugin {
})
}
fn cycle_mode(&self, mac: &str, state: &SharedState) -> BoxFuture<'static, FluxoResult<()>> {
fn cycle_mode(&self, mac: &str, state: &AppReceivers) -> BoxFuture<'static, FluxoResult<()>> {
let mac = mac.to_string();
let state = Arc::clone(state);
let state = state.clone();
Box::pin(async move {
let status = get_maestro(&state).get_status(&mac);
let status = crate::modules::bt::maestro::get_maestro(&state).get_status(&mac);
let next_mode = match status.anc_state.as_str() {
"active" => "aware",
"aware" => "off",
_ => "active",
};
get_maestro(&state)
crate::modules::bt::maestro::get_maestro(&state)
.send_command(&mac, BudsCommand::SetAnc(next_mode.to_string()))
.map_err(|e: anyhow::Error| FluxoError::Module {
module: "bt.buds",
+9 -9
View File
@@ -1,4 +1,4 @@
use crate::state::SharedState;
use crate::state::AppReceivers;
use anyhow::{Context, Result};
use futures::StreamExt;
use std::collections::HashMap;
@@ -39,11 +39,11 @@ pub struct MaestroManager {
}
impl MaestroManager {
pub fn new(state: SharedState) -> Self {
pub fn new(state: AppReceivers) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<ManagerCommand>();
let statuses = Arc::new(Mutex::new(HashMap::new()));
let statuses_clone = Arc::clone(&statuses);
let state_clone = Arc::clone(&state);
let state_clone = state.clone();
// Start dedicated BT management thread
std::thread::spawn(move || {
@@ -68,7 +68,7 @@ impl MaestroManager {
let mac_clone = mac.clone();
let st_clone = Arc::clone(&statuses_clone);
let state_inner = Arc::clone(&state_clone);
let state_inner = state_clone.clone();
tokio::task::spawn_local(async move {
if let Err(e) = buds_task(&mac_clone, st_clone, buds_rx, state_inner).await {
@@ -122,7 +122,7 @@ async fn buds_task(
mac: &str,
statuses: Arc<Mutex<HashMap<String, BudsStatus>>>,
mut rx: mpsc::Receiver<BudsCommand>,
state: SharedState,
state: AppReceivers,
) -> Result<()> {
info!("Starting native Maestro connection task for {}", mac);
@@ -215,8 +215,8 @@ async fn buds_task(
// Update health
{
let mut lock = state.write().await;
let health = lock.health.entry("bt.buds".to_string()).or_default();
let mut lock = state.health.write().await;
let health = lock.entry("bt.buds".to_string()).or_default();
health.consecutive_failures = 0;
health.backoff_until = None;
}
@@ -357,6 +357,6 @@ pub fn anc_state_to_string(state: &settings::AncState) -> String {
static MAESTRO: OnceLock<MaestroManager> = OnceLock::new();
pub fn get_maestro(state: &SharedState) -> &MaestroManager {
MAESTRO.get_or_init(|| MaestroManager::new(Arc::clone(state)))
pub fn get_maestro(state: &AppReceivers) -> &MaestroManager {
MAESTRO.get_or_init(|| MaestroManager::new(state.clone()))
}
+119 -116
View File
@@ -5,11 +5,11 @@ use crate::config::Config;
use crate::error::Result as FluxoResult;
use crate::modules::WaybarModule;
use crate::output::WaybarOutput;
use crate::state::{BtState, SharedState};
use crate::state::{AppReceivers, BtState};
use crate::utils::{TokenValue, format_template};
use anyhow::Result;
use std::process::Command;
use std::sync::{Arc, LazyLock};
use std::sync::LazyLock;
use tokio::sync::watch;
use tracing::{error, warn};
use self::buds::{BtPlugin, PixelBudsPlugin};
@@ -23,13 +23,23 @@ impl BtDaemon {
Self { session: None }
}
pub async fn poll(&mut self, state: SharedState, config: &Config) {
if let Err(e) = self.poll_async(state, config).await {
pub async fn poll(
&mut self,
tx: &watch::Sender<BtState>,
state: &AppReceivers,
config: &Config,
) {
if let Err(e) = self.poll_async(tx, state, config).await {
error!("BT daemon error: {}", e);
}
}
async fn poll_async(&mut self, state: SharedState, config: &Config) -> Result<()> {
async fn poll_async(
&mut self,
tx: &watch::Sender<BtState>,
state: &AppReceivers,
config: &Config,
) -> Result<()> {
if self.session.is_none() {
self.session = Some(bluer::Session::new().await?);
}
@@ -60,7 +70,7 @@ impl BtDaemon {
for p in PLUGINS.iter() {
if p.can_handle(&bt_state.device_alias, &bt_state.device_address) {
match p.get_data(config, &state, &bt_state.device_address).await {
match p.get_data(config, state, &bt_state.device_address).await {
Ok(data) => {
bt_state.plugin_data = data
.into_iter()
@@ -90,8 +100,7 @@ impl BtDaemon {
}
}
let mut lock = state.write().await;
lock.bluetooth = bt_state;
let _ = tx.send(bt_state);
Ok(())
}
@@ -103,134 +112,128 @@ static PLUGINS: LazyLock<Vec<Box<dyn BtPlugin>>> =
pub struct BtModule;
impl WaybarModule for BtModule {
fn run(
async fn run(
&self,
config: &Config,
state: &SharedState,
state: &AppReceivers,
args: &[&str],
) -> impl std::future::Future<Output = FluxoResult<WaybarOutput>> + Send {
) -> FluxoResult<WaybarOutput> {
let action = args.first().cloned().unwrap_or("show").to_string();
let args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let state = Arc::clone(state);
let config = config.clone();
async move {
let bt_state = {
let lock = state.read().await;
lock.bluetooth.clone()
};
let bt_state = state.bluetooth.borrow().clone();
match action.as_str() {
"disconnect" if bt_state.connected => {
let _ = Command::new("bluetoothctl")
.args(["disconnect", &bt_state.device_address])
.output();
return Ok(WaybarOutput::default());
match action.as_str() {
"disconnect" if bt_state.connected => {
let _ = tokio::process::Command::new("bluetoothctl")
.args(["disconnect", &bt_state.device_address])
.output()
.await;
return Ok(WaybarOutput::default());
}
"cycle_mode" if bt_state.connected => {
let plugin = PLUGINS
.iter()
.find(|p| p.can_handle(&bt_state.device_alias, &bt_state.device_address));
if let Some(p) = plugin {
p.cycle_mode(&bt_state.device_address, state).await?;
}
"cycle_mode" if bt_state.connected => {
return Ok(WaybarOutput::default());
}
"get_modes" if bt_state.connected => {
let plugin = PLUGINS
.iter()
.find(|p| p.can_handle(&bt_state.device_alias, &bt_state.device_address));
let modes = if let Some(p) = plugin {
p.get_modes(&bt_state.device_address, state).await?
} else {
vec![]
};
return Ok(WaybarOutput {
text: modes.join("\n"),
..Default::default()
});
}
"set_mode" if bt_state.connected => {
if let Some(mode) = args.get(1) {
let plugin = PLUGINS
.iter()
.find(|p| p.can_handle(&bt_state.device_alias, &bt_state.device_address));
if let Some(p) = plugin {
p.cycle_mode(&bt_state.device_address, &state).await?;
p.set_mode(mode, &bt_state.device_address, state).await?;
}
return Ok(WaybarOutput::default());
}
"get_modes" if bt_state.connected => {
let plugin = PLUGINS
.iter()
.find(|p| p.can_handle(&bt_state.device_alias, &bt_state.device_address));
let modes = if let Some(p) = plugin {
p.get_modes(&bt_state.device_address, &state).await?
} else {
vec![]
};
return Ok(WaybarOutput {
text: modes.join("\n"),
..Default::default()
});
}
"set_mode" if bt_state.connected => {
if let Some(mode) = args.get(1) {
let plugin = PLUGINS.iter().find(|p| {
p.can_handle(&bt_state.device_alias, &bt_state.device_address)
});
if let Some(p) = plugin {
p.set_mode(mode, &bt_state.device_address, &state).await?;
}
}
return Ok(WaybarOutput::default());
}
"show" => {}
_ => {}
return Ok(WaybarOutput::default());
}
"show" => {}
_ => {}
}
if !bt_state.adapter_powered {
return Ok(WaybarOutput {
text: config.bt.format_disabled.clone(),
tooltip: Some("Bluetooth Disabled".to_string()),
class: Some("disabled".to_string()),
percentage: None,
});
}
if !bt_state.adapter_powered {
return Ok(WaybarOutput {
text: config.bt.format_disabled.clone(),
tooltip: Some("Bluetooth Disabled".to_string()),
class: Some("disabled".to_string()),
percentage: None,
});
}
if bt_state.connected {
let mut tokens: Vec<(String, TokenValue)> = vec![
(
"alias".to_string(),
TokenValue::String(bt_state.device_alias.clone()),
),
(
"mac".to_string(),
TokenValue::String(bt_state.device_address.clone()),
),
];
if bt_state.connected {
let mut tokens: Vec<(String, TokenValue)> = vec![
(
"alias".to_string(),
TokenValue::String(bt_state.device_alias.clone()),
),
(
"mac".to_string(),
TokenValue::String(bt_state.device_address.clone()),
),
];
let mut class = vec!["connected".to_string()];
let mut has_plugin = false;
let mut class = vec!["connected".to_string()];
let mut has_plugin = false;
for (k, v) in &bt_state.plugin_data {
if k == "plugin_class" {
class.push(v.clone());
has_plugin = true;
} else if k == "plugin_error" {
class.push("plugin-error".to_string());
} else {
tokens.push((k.clone(), TokenValue::String(v.clone())));
}
}
let format = if has_plugin {
&config.bt.format_plugin
for (k, v) in &bt_state.plugin_data {
if k == "plugin_class" {
class.push(v.clone());
has_plugin = true;
} else if k == "plugin_error" {
class.push("plugin-error".to_string());
} else {
&config.bt.format_connected
};
let text = format_template(format, &tokens);
let tooltip = format!(
"{} | MAC: {}\nBattery: {}",
bt_state.device_alias,
bt_state.device_address,
bt_state
.battery_percentage
.map(|b| format!("{}%", b))
.unwrap_or_else(|| "N/A".to_string())
);
Ok(WaybarOutput {
text,
tooltip: Some(tooltip),
class: Some(class.join(" ")),
percentage: bt_state.battery_percentage,
})
} else {
Ok(WaybarOutput {
text: config.bt.format_disconnected.clone(),
tooltip: Some("Bluetooth On (Disconnected)".to_string()),
class: Some("disconnected".to_string()),
percentage: None,
})
tokens.push((k.clone(), TokenValue::String(v.clone())));
}
}
let format = if has_plugin {
&config.bt.format_plugin
} else {
&config.bt.format_connected
};
let text = format_template(format, &tokens);
let tooltip = format!(
"{} | MAC: {}\nBattery: {}",
bt_state.device_alias,
bt_state.device_address,
bt_state
.battery_percentage
.map(|b| format!("{}%", b))
.unwrap_or_else(|| "N/A".to_string())
);
Ok(WaybarOutput {
text,
tooltip: Some(tooltip),
class: Some(class.join(" ")),
percentage: bt_state.battery_percentage,
})
} else {
Ok(WaybarOutput {
text: config.bt.format_disconnected.clone(),
tooltip: Some("Bluetooth On (Disconnected)".to_string()),
class: Some("disconnected".to_string()),
percentage: None,
})
}
}
}