This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "fluxo-rs"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -93,6 +93,10 @@ format_inactive = "<span size='large'></span>"
|
||||
|
||||
[mpris]
|
||||
# enabled = false # set to false to disable this module at runtime
|
||||
# max_length = 30 # truncate text beyond this character length (adds '...')
|
||||
# scroll = true # enable marquee scroll animation (requires max_length)
|
||||
# scroll_speed = 500 # ms between scroll steps (only while playing)
|
||||
# scroll_separator = " /// " # separator shown between loops when scrolling
|
||||
# tokens: {artist}, {title}, {album}, {status_icon}
|
||||
format = "{status_icon} {artist} - {title}"
|
||||
|
||||
|
||||
@@ -290,6 +290,22 @@ pub struct MprisConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub format: String,
|
||||
#[serde(default)]
|
||||
pub max_length: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub scroll: bool,
|
||||
#[serde(default = "default_scroll_speed")]
|
||||
pub scroll_speed: u64,
|
||||
#[serde(default = "default_scroll_separator")]
|
||||
pub scroll_separator: String,
|
||||
}
|
||||
|
||||
fn default_scroll_speed() -> u64 {
|
||||
500
|
||||
}
|
||||
|
||||
fn default_scroll_separator() -> String {
|
||||
" /// ".to_string()
|
||||
}
|
||||
|
||||
impl Default for MprisConfig {
|
||||
@@ -297,6 +313,10 @@ impl Default for MprisConfig {
|
||||
Self {
|
||||
enabled: true,
|
||||
format: "{status_icon} {artist} - {title}".to_string(),
|
||||
max_length: None,
|
||||
scroll: false,
|
||||
scroll_speed: 500,
|
||||
scroll_separator: " /// ".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,10 @@ pub async fn run_daemon(config_path: Option<PathBuf>) -> Result<()> {
|
||||
let (keyboard_tx, keyboard_rx) = watch::channel(Default::default());
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
let (dnd_tx, dnd_rx) = watch::channel(Default::default());
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
let mpris_scroll = Arc::new(RwLock::new(crate::state::MprisScrollState::default()));
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
let (mpris_scroll_tick_tx, mpris_scroll_tick_rx) = watch::channel(0u64);
|
||||
let health = Arc::new(RwLock::new(HashMap::new()));
|
||||
#[cfg(feature = "mod-bt")]
|
||||
let (bt_force_tx, mut bt_force_rx) = mpsc::channel(1);
|
||||
@@ -109,6 +113,10 @@ pub async fn run_daemon(config_path: Option<PathBuf>) -> Result<()> {
|
||||
keyboard: keyboard_rx,
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
dnd: dnd_rx,
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
mpris_scroll: Arc::clone(&mpris_scroll),
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
mpris_scroll_tick: mpris_scroll_tick_rx,
|
||||
health: Arc::clone(&health),
|
||||
#[cfg(feature = "mod-bt")]
|
||||
bt_force_poll: bt_force_tx,
|
||||
@@ -313,6 +321,20 @@ pub async fn run_daemon(config_path: Option<PathBuf>) -> Result<()> {
|
||||
if config.read().await.mpris.enabled {
|
||||
let mpris_daemon = MprisDaemon::new();
|
||||
mpris_daemon.start(mpris_tx);
|
||||
|
||||
// Scroll ticker for MPRIS marquee animation
|
||||
let scroll_config = Arc::clone(&config);
|
||||
let scroll_rx = receivers.mpris.clone();
|
||||
let scroll_state = Arc::clone(&mpris_scroll);
|
||||
tokio::spawn(async move {
|
||||
crate::modules::mpris::mpris_scroll_ticker(
|
||||
scroll_config,
|
||||
scroll_rx,
|
||||
scroll_state,
|
||||
mpris_scroll_tick_tx,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Waybar Signaler Task
|
||||
|
||||
+120
-24
@@ -2,12 +2,66 @@ use crate::config::Config;
|
||||
use crate::error::Result;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::{AppReceivers, MprisState};
|
||||
use crate::state::{AppReceivers, MprisScrollState, MprisState};
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use tokio::sync::watch;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{RwLock, watch};
|
||||
use tokio::time::Duration;
|
||||
use tracing::{debug, info};
|
||||
use zbus::{Connection, proxy};
|
||||
|
||||
fn format_mpris_text(format: &str, mpris: &MprisState) -> (String, &'static str) {
|
||||
let status_icon = if mpris.is_playing {
|
||||
""
|
||||
} else if mpris.is_paused {
|
||||
""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let class = if mpris.is_playing {
|
||||
"playing"
|
||||
} else if mpris.is_paused {
|
||||
"paused"
|
||||
} else {
|
||||
"stopped"
|
||||
};
|
||||
|
||||
let text = format_template(
|
||||
format,
|
||||
&[
|
||||
("artist", TokenValue::String(mpris.artist.clone())),
|
||||
("title", TokenValue::String(mpris.title.clone())),
|
||||
("album", TokenValue::String(mpris.album.clone())),
|
||||
("status_icon", TokenValue::String(status_icon.to_string())),
|
||||
],
|
||||
);
|
||||
|
||||
(text, class)
|
||||
}
|
||||
|
||||
fn apply_scroll_window(full_text: &str, max_len: usize, offset: usize, separator: &str) -> String {
|
||||
let char_count = full_text.chars().count();
|
||||
let total_len = char_count + separator.chars().count();
|
||||
let offset = offset % total_len;
|
||||
full_text
|
||||
.chars()
|
||||
.chain(separator.chars())
|
||||
.cycle()
|
||||
.skip(offset)
|
||||
.take(max_len)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn truncate_with_ellipsis(text: &str, max_len: usize) -> String {
|
||||
let char_count = text.chars().count();
|
||||
if char_count <= max_len {
|
||||
return text.to_string();
|
||||
}
|
||||
let truncated: String = text.chars().take(max_len.saturating_sub(3)).collect();
|
||||
format!("{}...", truncated)
|
||||
}
|
||||
|
||||
pub struct MprisModule;
|
||||
|
||||
impl WaybarModule for MprisModule {
|
||||
@@ -28,32 +82,26 @@ impl WaybarModule for MprisModule {
|
||||
});
|
||||
}
|
||||
|
||||
let status_icon = if mpris.is_playing {
|
||||
""
|
||||
} else if mpris.is_paused {
|
||||
""
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let (full_text, class) = format_mpris_text(&config.mpris.format, &mpris);
|
||||
|
||||
let class = if mpris.is_playing {
|
||||
"playing"
|
||||
} else if mpris.is_paused {
|
||||
"paused"
|
||||
let text = if config.mpris.scroll {
|
||||
if let Some(max_len) = config.mpris.max_length {
|
||||
let scroll = state.mpris_scroll.read().await;
|
||||
apply_scroll_window(
|
||||
&full_text,
|
||||
max_len,
|
||||
scroll.offset,
|
||||
&config.mpris.scroll_separator,
|
||||
)
|
||||
} else {
|
||||
full_text.clone()
|
||||
}
|
||||
} else if let Some(max_len) = config.mpris.max_length {
|
||||
truncate_with_ellipsis(&full_text, max_len)
|
||||
} else {
|
||||
"stopped"
|
||||
full_text.clone()
|
||||
};
|
||||
|
||||
let text = format_template(
|
||||
&config.mpris.format,
|
||||
&[
|
||||
("artist", TokenValue::String(mpris.artist.clone())),
|
||||
("title", TokenValue::String(mpris.title.clone())),
|
||||
("album", TokenValue::String(mpris.album.clone())),
|
||||
("status_icon", TokenValue::String(status_icon.to_string())),
|
||||
],
|
||||
);
|
||||
|
||||
Ok(WaybarOutput {
|
||||
text,
|
||||
tooltip: Some(format!("{} - {}", mpris.artist, mpris.title)),
|
||||
@@ -63,6 +111,54 @@ impl WaybarModule for MprisModule {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mpris_scroll_ticker(
|
||||
config: Arc<RwLock<Config>>,
|
||||
mut mpris_rx: watch::Receiver<MprisState>,
|
||||
scroll_state: Arc<RwLock<MprisScrollState>>,
|
||||
tick_tx: watch::Sender<u64>,
|
||||
) {
|
||||
let mut generation: u64 = 0;
|
||||
let mut last_track_key = String::new();
|
||||
|
||||
loop {
|
||||
let mpris = mpris_rx.borrow_and_update().clone();
|
||||
let cfg = config.read().await;
|
||||
let scroll_enabled = cfg.mpris.scroll;
|
||||
let has_max_length = cfg.mpris.max_length.is_some();
|
||||
let scroll_speed = cfg.mpris.scroll_speed;
|
||||
let format_str = cfg.mpris.format.clone();
|
||||
drop(cfg);
|
||||
|
||||
let (full_text, _) = format_mpris_text(&format_str, &mpris);
|
||||
let track_key = format!("{}|{}|{}", mpris.artist, mpris.title, mpris.album);
|
||||
|
||||
if track_key != last_track_key {
|
||||
let mut state = scroll_state.write().await;
|
||||
state.offset = 0;
|
||||
state.full_text = full_text.clone();
|
||||
last_track_key = track_key;
|
||||
generation += 1;
|
||||
let _ = tick_tx.send(generation);
|
||||
}
|
||||
|
||||
if scroll_enabled && has_max_length && mpris.is_playing {
|
||||
tokio::time::sleep(Duration::from_millis(scroll_speed)).await;
|
||||
let mut state = scroll_state.write().await;
|
||||
state.offset += 1;
|
||||
state.full_text = full_text;
|
||||
drop(state);
|
||||
generation += 1;
|
||||
let _ = tick_tx.send(generation);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not scrolling — wait for next state change
|
||||
if mpris_rx.changed().await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MprisDaemon;
|
||||
|
||||
#[proxy(
|
||||
|
||||
@@ -175,6 +175,13 @@ impl WaybarSignaler {
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
let mpris_changed = receivers.mpris.changed();
|
||||
|
||||
#[cfg(not(feature = "mod-dbus"))]
|
||||
let mpris_scroll_tick_changed = std::future::pending::<
|
||||
std::result::Result<(), tokio::sync::watch::error::RecvError>,
|
||||
>();
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
let mpris_scroll_tick_changed = receivers.mpris_scroll_tick.changed();
|
||||
|
||||
tokio::select! {
|
||||
res = net_changed, if signals.network.is_some() => {
|
||||
if res.is_ok() { check_and_signal!("net", signals.network); }
|
||||
@@ -215,6 +222,10 @@ impl WaybarSignaler {
|
||||
res = mpris_changed, if signals.mpris.is_some() => {
|
||||
if res.is_ok() { check_and_signal!("mpris", signals.mpris); }
|
||||
}
|
||||
res = mpris_scroll_tick_changed, if signals.mpris.is_some() => {
|
||||
if res.is_ok()
|
||||
&& let Some(sig) = signals.mpris { self.send_signal(sig); }
|
||||
}
|
||||
_ = sleep(Duration::from_secs(5)) => {
|
||||
// loop and refresh config
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ pub struct AppReceivers {
|
||||
pub keyboard: watch::Receiver<KeyboardState>,
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
pub dnd: watch::Receiver<DndState>,
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
pub mpris_scroll: Arc<RwLock<MprisScrollState>>,
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
pub mpris_scroll_tick: watch::Receiver<u64>,
|
||||
pub health: Arc<RwLock<HashMap<String, ModuleHealth>>>,
|
||||
#[cfg(feature = "mod-bt")]
|
||||
pub bt_force_poll: mpsc::Sender<()>,
|
||||
@@ -169,6 +173,12 @@ pub struct BacklightState {
|
||||
pub percentage: u8,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MprisScrollState {
|
||||
pub offset: usize,
|
||||
pub full_text: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MprisState {
|
||||
pub is_playing: bool,
|
||||
@@ -295,6 +305,13 @@ pub fn mock_state(state: AppState) -> MockState {
|
||||
keyboard: keyboard_rx,
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
dnd: dnd_rx,
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
mpris_scroll: Arc::new(RwLock::new(MprisScrollState::default())),
|
||||
#[cfg(feature = "mod-dbus")]
|
||||
mpris_scroll_tick: {
|
||||
let (_, rx) = watch::channel(0u64);
|
||||
rx
|
||||
},
|
||||
health: Arc::new(RwLock::new(state.health)),
|
||||
#[cfg(feature = "mod-bt")]
|
||||
bt_force_poll: bt_force_tx,
|
||||
|
||||
Reference in New Issue
Block a user