summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2024-07-16 00:36:48 -0500
committerToby Vincent <tobyv@tobyvin.dev>2024-07-16 00:36:48 -0500
commit3e7721dfbaeb0b57f48801660a64c13989643198 (patch)
tree06b34c469c6f28fec8530c38fce8c7444cf3ba79
parent712f381b1ba65d40e03e530b871e34a73dbb615d (diff)
wip
-rw-r--r--Cargo.lock31
-rw-r--r--Cargo.toml2
-rw-r--r--src/color.rs4
-rw-r--r--src/dbus/player.rs29
-rw-r--r--src/error.rs9
-rw-r--r--src/i3bar.rs58
-rw-r--r--src/lib.rs3
-rw-r--r--src/main.rs78
-rw-r--r--src/mpris.rs101
-rw-r--r--src/player.rs91
-rw-r--r--src/printer.rs355
11 files changed, 622 insertions, 139 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 85bce40..89bf164 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -457,8 +457,10 @@ dependencies = [
"anyhow",
"futures-util",
"serde",
+ "serde_json",
"thiserror",
"tokio",
+ "unicode-segmentation",
"zbus",
]
@@ -473,6 +475,12 @@ dependencies = [
]
[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -694,6 +702,12 @@ dependencies = [
]
[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
name = "serde"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -714,6 +728,17 @@ dependencies = [
]
[[package]]
+name = "serde_json"
+version = "1.0.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
name = "serde_repr"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -914,6 +939,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
+name = "unicode-segmentation"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
+
+[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index d593174..652ecac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,8 @@ edition = "2021"
anyhow = "1.0.86"
futures-util = "0.3.30"
serde = { version = "1.0.203", features = ["derive"] }
+serde_json = "1.0.120"
thiserror = "1.0.61"
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
+unicode-segmentation = "1.11.0"
zbus = { version = "4.3.0", default-features = false, features = ["tokio"] }
diff --git a/src/color.rs b/src/color.rs
index d8b79e7..076d995 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -23,9 +23,7 @@ impl FromStr for Color {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- let s = s
- .strip_prefix('#')
- .ok_or_else(|| Error::Color(String::from("Missing '#' prefix")))?;
+ let s = s.trim_start_matches('#');
let num = u32::from_str_radix(s, 16)
.map_err(|err| Error::Color(format!("Failed to parse int: {err}")))?;
diff --git a/src/dbus/player.rs b/src/dbus/player.rs
index 9e76e14..63a13ca 100644
--- a/src/dbus/player.rs
+++ b/src/dbus/player.rs
@@ -19,7 +19,32 @@
//!
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
-use zbus::proxy;
+use zbus::{
+ proxy,
+ zvariant::{OwnedValue, Type},
+};
+
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Type)]
+pub enum PlaybackStatus {
+ Playing,
+ Paused,
+ #[default]
+ Stopped,
+}
+
+impl TryFrom<OwnedValue> for PlaybackStatus {
+ type Error = zbus::Error;
+
+ fn try_from(value: OwnedValue) -> Result<Self, Self::Error> {
+ match value.downcast_ref()? {
+ "Playing" => Ok(Self::Playing),
+ "Paused" => Ok(Self::Paused),
+ "Stopped" => Ok(Self::Stopped),
+ _ => Err(zbus::Error::InvalidField),
+ }
+ }
+}
+
#[proxy(
interface = "org.mpris.MediaPlayer2.Player",
default_service = "org.mpris.MediaPlayer2.playerctld",
@@ -107,7 +132,7 @@ trait Player {
/// PlaybackStatus property
#[zbus(property)]
- fn playback_status(&self) -> zbus::Result<String>;
+ fn playback_status(&self) -> zbus::Result<PlaybackStatus>;
/// Position property
#[zbus(property)]
diff --git a/src/error.rs b/src/error.rs
index 26593f2..96d864c 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -8,9 +8,18 @@ pub enum Error {
#[error("Join error: {0}")]
Join(#[from] tokio::task::JoinError),
+ #[error("ZBus error: {0}")]
+ ZBus(#[from] zbus::Error),
+
#[error("Invalid color format: {0}")]
Color(String),
+ #[error("Invalid playback state")]
+ PlaybackState,
+
+ #[error("Invalid volume value")]
+ Volume,
+
#[error("Send error: {0}")]
Send(String),
}
diff --git a/src/i3bar.rs b/src/i3bar.rs
index 8cf5f0c..1d65ada 100644
--- a/src/i3bar.rs
+++ b/src/i3bar.rs
@@ -1,19 +1,17 @@
-use serde::Serialize;
-
-use crate::color::Color;
+use serde::{Deserialize, Serialize};
/// Represent block as described in <https://i3wm.org/docs/i3bar-protocol.html>
-#[derive(Serialize, Debug, Clone)]
+#[derive(Debug, Clone, Default, Serialize)]
pub struct Block {
pub full_text: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub short_text: String,
#[serde(skip_serializing_if = "Option::is_none")]
- pub color: Option<Color>,
+ pub color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub background: Option<Color>,
+ pub background: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
- pub border: Option<Color>,
+ pub border: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub border_top: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -40,30 +38,6 @@ pub struct Block {
pub markup: Option<String>,
}
-impl Default for Block {
- fn default() -> Self {
- Self {
- full_text: String::new(),
- short_text: String::new(),
- color: None,
- background: None,
- border: None,
- border_top: None,
- border_right: None,
- border_bottom: None,
- border_left: None,
- min_width: None,
- align: None,
- name: None,
- instance: String::new(),
- urgent: None,
- separator: Some(false),
- separator_block_width: Some(0),
- markup: Some("pango".to_string()),
- }
- }
-}
-
#[derive(Serialize, Debug, Clone, Copy)]
#[serde(rename_all = "lowercase")]
pub enum Align {
@@ -78,3 +52,25 @@ pub enum MinWidth {
Pixels(usize),
Text(String),
}
+
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+pub struct Click {
+ pub name: Option<String>,
+ pub instance: Option<String>,
+ pub full_text: String,
+ pub short_text: Option<String>,
+ pub color: Option<String>,
+ pub background: Option<String>,
+ pub button: u8,
+ pub event: usize,
+ pub modifiers: Option<Vec<String>>,
+ pub x: usize,
+ pub y: usize,
+ pub relative_x: usize,
+ pub relative_y: usize,
+ pub output_x: Option<usize>,
+ pub output_y: Option<usize>,
+ pub width: usize,
+ pub height: usize,
+ pub scale: usize,
+}
diff --git a/src/lib.rs b/src/lib.rs
index 22661ed..2a9c667 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,4 +4,5 @@ pub mod color;
pub mod dbus;
pub mod error;
pub mod i3bar;
-pub mod mpris;
+pub mod player;
+pub mod printer;
diff --git a/src/main.rs b/src/main.rs
index 263c9d0..276ec05 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,81 @@
+use futures_util::StreamExt;
+use tokio::{
+ sync::mpsc::{self, Sender},
+ task::JoinSet,
+};
+use zbus::fdo::NameOwnerChangedStream;
+use zbus::{
+ fdo::{DBusProxy, NameOwnerChangedArgs},
+ names::OwnedBusName,
+ Connection,
+};
+
+use i3blocks::{i3bar::Click, player::Event, Error};
+
+const IGNORED: [&str; 1] = ["playerctld"];
+
#[tokio::main]
async fn main() -> anyhow::Result<()> {
- println!("Hello, World!");
+ let mut join_set = JoinSet::new();
+ let (tx, rx) = mpsc::channel(128);
+
+ let conn = Connection::session().await?;
+
+ let dbus_proxy = DBusProxy::new(&conn).await?;
+ let names: Vec<OwnedBusName> = dbus_proxy.list_names().await?;
+
+ let players = names.iter().filter(|s| valid_player(s)).collect::<Vec<_>>();
+
+ for player in players {
+ tx.send(Event::Add(player.clone())).await?;
+ }
+
+ let name_owner_changed = dbus_proxy.receive_name_owner_changed().await?;
+
+ join_set.spawn(listen_name_owner_change(tx.clone(), name_owner_changed));
+ join_set.spawn(i3blocks::player::listener(conn.clone(), rx));
+
+ for line in std::io::stdin().lines() {
+ let click: Click = dbg!(serde_json::from_str(&line?)?);
+ let pos =
+ click.full_text.chars().count() as f64 * (click.relative_x as f64 / click.width as f64);
+ tx.send(Event::Click((click.button, pos as usize))).await?;
+ }
+
+ while let Some(res) = join_set.join_next().await {
+ if let Err(err) = res? {
+ eprintln!("{err}")
+ };
+ }
+
+ Ok(())
+}
+
+fn valid_player(name: &str) -> bool {
+ name.strip_prefix("org.mpris.MediaPlayer2.")
+ .and_then(|s| s.split('.').next())
+ .is_some_and(|s| !IGNORED.contains(&s))
+}
+
+async fn listen_name_owner_change(
+ tx: Sender<Event>,
+ mut stream: NameOwnerChangedStream<'static>,
+) -> Result<(), Error> {
+ while let Some(signal) = stream.next().await {
+ match signal.args()? {
+ NameOwnerChangedArgs {
+ name, old_owner, ..
+ } if valid_player(&name) && old_owner.is_some() => {
+ tx.send(Event::Remove(name.into())).await?
+ }
+ NameOwnerChangedArgs {
+ name, new_owner, ..
+ } if valid_player(&name) && new_owner.is_some() => {
+ tx.send(Event::Add(name.into())).await?
+ }
+ _ => {}
+ }
+ }
+
Ok(())
}
diff --git a/src/mpris.rs b/src/mpris.rs
deleted file mode 100644
index 3cb29af..0000000
--- a/src/mpris.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-use futures_util::StreamExt;
-use tokio::{
- sync::mpsc::{self, Sender},
- task::JoinSet,
-};
-use zbus::{
- fdo::{DBusProxy, NameOwnerChangedArgs, NameOwnerChangedStream},
- names::OwnedBusName,
- Connection,
-};
-
-use crate::dbus::player::PlayerProxy;
-
-const IGNORED: [&str; 1] = ["playerctld"];
-const PREFIX: &str = "org.mpris.MediaPlayer2.";
-
-pub async fn block() -> anyhow::Result<()> {
- let mut join_set = JoinSet::new();
- let (tx, mut rx) = mpsc::channel(128);
-
- let connection = Connection::session().await?;
-
- let dbus_proxy = DBusProxy::new(&connection).await?;
- let names: Vec<OwnedBusName> = dbus_proxy.list_names().await?;
-
- let players = names.iter().filter(|s| valid_player(s)).collect::<Vec<_>>();
-
- for player in players {
- tx.send(Message::Add(player.to_string())).await?;
- }
-
- let name_owner_changed = dbus_proxy.receive_name_owner_changed().await?;
-
- join_set.spawn(listen_name_owner_change(tx.clone(), name_owner_changed));
-
- while let Some(msg) = rx.recv().await {
- let res = match msg {
- Message::Add(s) => add_player(&connection, &s).await,
- Message::Remove(s) => remove_player(&s).await,
- };
- }
-
- while let Some(res) = join_set.join_next().await {
- res??;
- }
-
- Ok(())
-}
-
-enum Message {
- Add(String),
- Remove(String),
-}
-
-fn valid_player(name: &str) -> bool {
- name.strip_prefix(PREFIX)
- .and_then(|s| s.split('.').next())
- .is_some_and(|s| !IGNORED.contains(&s))
-}
-
-async fn add_player(connection: &Connection, name: &str) -> Result<(), anyhow::Error> {
- dbg!("Adding: {name}");
- let proxy = PlayerProxy::builder(connection)
- .destination(name)?
- .build()
- .await?;
- Ok(())
-}
-
-async fn remove_player(name: &str) -> Result<(), anyhow::Error> {
- dbg!("Removing: {name}");
- Ok(())
-}
-
-async fn listen_name_owner_change(
- tx: Sender<Message>,
- mut stream: NameOwnerChangedStream<'static>,
-) -> anyhow::Result<()> {
- let tx = tx.clone();
- while let Some(signal) = stream.next().await {
- let Ok(NameOwnerChangedArgs {
- name,
- old_owner,
- new_owner,
- ..
- }) = signal.args()
- else {
- continue;
- };
-
- if !valid_player(&name) {
- continue;
- } else if old_owner.is_some() {
- tx.send(Message::Remove(name.to_string())).await?;
- } else if new_owner.is_some() {
- tx.send(Message::Add(name.to_string())).await?;
- }
- }
-
- Ok(())
-}
diff --git a/src/player.rs b/src/player.rs
new file mode 100644
index 0000000..2f69d20
--- /dev/null
+++ b/src/player.rs
@@ -0,0 +1,91 @@
+use std::collections::VecDeque;
+
+use tokio::{
+ sync::mpsc::{Receiver, Sender},
+ task::JoinHandle,
+};
+use zbus::{names::OwnedBusName, Connection};
+
+use crate::{
+ dbus::player::{PlaybackStatus, PlayerProxy},
+ printer, Result,
+};
+
+pub enum Command {
+ Shift,
+ Raise(String),
+}
+
+#[derive(Debug)]
+pub struct Player {
+ pub name: OwnedBusName,
+ pub handle: JoinHandle<Result<()>>,
+ pub tx: Sender<(u8, usize)>,
+}
+
+#[derive(Debug, Clone)]
+pub enum Event {
+ Add(OwnedBusName),
+ Remove(OwnedBusName),
+ Click((u8, usize)),
+}
+
+pub async fn listener(conn: Connection, mut rx: Receiver<Event>) -> Result<()> {
+ let mut players: VecDeque<OwnedBusName> = VecDeque::new();
+ let mut active: Option<Player> = None;
+
+ while let Some(value) = rx.recv().await {
+ match value {
+ Event::Add(name) => {
+ let player_proxy = PlayerProxy::builder(&conn)
+ .destination(name.clone())?
+ .build()
+ .await?;
+
+ match player_proxy.playback_status().await? {
+ PlaybackStatus::Playing => players.push_front(name),
+ PlaybackStatus::Paused => players.push_back(name),
+ PlaybackStatus::Stopped => players.push_back(name),
+ }
+ }
+ Event::Remove(name) => {
+ if let Some(index) = players.iter().position(|p| *p == name) {
+ players.remove(index);
+ }
+ }
+ Event::Click((3, _)) if players.len() > 1 => players.rotate_left(1),
+ Event::Click(click) => {
+ if let Some(p) = active.as_ref() {
+ p.tx.send(click).await?;
+ }
+ continue;
+ }
+ };
+
+ if let Some(name) = players.front() {
+ if !active.as_ref().is_some_and(|s| &s.name == name) {
+ let (tx, rx) = tokio::sync::mpsc::channel(128);
+
+ if let Some(player) = active.take() {
+ player.handle.abort();
+ }
+
+ let proxy = PlayerProxy::builder(&conn)
+ .destination(name.clone())?
+ .build()
+ .await?;
+
+ let handle = tokio::spawn(printer::printer(proxy, rx));
+ active = Some(Player {
+ name: name.clone(),
+ handle,
+ tx,
+ });
+ }
+ } else if let Some(player) = active.take() {
+ player.handle.abort();
+ }
+ }
+
+ Ok(())
+}
diff --git a/src/printer.rs b/src/printer.rs
new file mode 100644
index 0000000..a609616
--- /dev/null
+++ b/src/printer.rs
@@ -0,0 +1,355 @@
+use std::{collections::HashMap, io::Write, sync::Arc, time::Duration};
+
+use futures_util::stream::StreamExt;
+use tokio::{
+ sync::{mpsc::Receiver, Mutex, Notify},
+ task::{JoinHandle, JoinSet},
+};
+use unicode_segmentation::UnicodeSegmentation;
+use zbus::{proxy::PropertyStream, zvariant::OwnedValue};
+
+use crate::Result;
+use crate::{
+ dbus::{
+ media_player2::MediaPlayer2Proxy,
+ player::{PlaybackStatus, PlayerProxy},
+ },
+ i3bar::Block,
+};
+
+type StatusLock = Arc<Mutex<Status>>;
+
+const TICK_RATE: Duration = Duration::from_millis(500);
+
+const BLACK: &str = "#1d2021";
+const YELLOW: &str = "#fabd2f";
+const CYAN: &str = "#8ec07c";
+
+#[derive(Debug, Clone)]
+pub enum Signal {
+ Print,
+}
+
+pub async fn printer(proxy: PlayerProxy<'static>, rx_click: Receiver<(u8, usize)>) -> Result<()> {
+ let mut join_set: JoinSet<Result<()>> = JoinSet::new();
+ let mut rotator = None;
+
+ let notify = Arc::new(Notify::new());
+
+ let status = Arc::new(Mutex::new(Status {
+ title: None,
+ can_go_previous: proxy.can_go_previous().await.unwrap_or_default(),
+ playback_status: proxy.playback_status().await.unwrap_or_default(),
+ can_go_next: proxy.can_go_next().await.unwrap_or_default(),
+ volume: proxy.volume().await.ok(),
+ indexes: Vec::new(),
+ }));
+
+ process_metadata(
+ notify.clone(),
+ status.clone(),
+ proxy.metadata().await?,
+ &mut rotator,
+ )
+ .await;
+
+ notify.notify_one();
+
+ join_set.spawn(metadata(
+ notify.clone(),
+ proxy.receive_metadata_changed().await,
+ status.clone(),
+ rotator,
+ ));
+
+ join_set.spawn(can_go_previous(
+ notify.clone(),
+ proxy.receive_can_go_previous_changed().await,
+ status.clone(),
+ ));
+
+ join_set.spawn(playback_state(
+ notify.clone(),
+ proxy.receive_playback_status_changed().await,
+ status.clone(),
+ ));
+
+ join_set.spawn(can_go_next(
+ notify.clone(),
+ proxy.receive_can_go_next_changed().await,
+ status.clone(),
+ ));
+
+ join_set.spawn(volume(
+ notify.clone(),
+ proxy.receive_volume_changed().await,
+ status.clone(),
+ ));
+
+ join_set.spawn(click_listener(proxy, rx_click, status.clone()));
+
+ loop {
+ notify.notified().await;
+ let mut status = status.lock().await;
+
+ let mut w = std::io::stdout().lock();
+ let mut v = serde_json::to_vec(&status.build()).unwrap();
+ v.push(b'\n');
+ w.write_all(&v)?;
+ w.flush()?;
+ }
+}
+
+async fn click_listener(
+ player_proxy: PlayerProxy<'static>,
+ mut rx: Receiver<(u8, usize)>,
+ status: StatusLock,
+) -> Result<()> {
+ let mpris_proxy = MediaPlayer2Proxy::builder(player_proxy.inner().connection())
+ .destination(player_proxy.inner().destination())?
+ .build()
+ .await?;
+
+ while let Some((button, index)) = dbg!(rx.recv().await) {
+ let status = status.lock().await;
+ if let Some(c) = dbg!(&status.indexes)
+ .iter()
+ .rev()
+ .find_map(|(i, b)| (*i <= index).then_some(b))
+ {
+ match dbg!((c, button)) {
+ (Component::Icon, 1) if mpris_proxy.can_raise().await? => {
+ mpris_proxy.raise().await?
+ }
+ (Component::Title, _) => {}
+ (Component::Prev, 1) => player_proxy.previous().await?,
+ (Component::Play, 1) | (Component::Pause, 1) => player_proxy.play_pause().await?,
+ (Component::Next, 1) => player_proxy.next().await?,
+ (Component::Volume, 4) => {
+ player_proxy
+ .set_volume(player_proxy.volume().await? - 0.05)
+ .await?
+ }
+ (Component::Volume, 5) => {
+ player_proxy
+ .set_volume(player_proxy.volume().await? + 0.05)
+ .await?
+ }
+ _ => {}
+ }
+ }
+ }
+ Ok(())
+}
+
+async fn process_metadata(
+ notify: Arc<Notify>,
+ status_lock: StatusLock,
+ metadata: HashMap<String, OwnedValue>,
+ rotator: &mut Option<JoinHandle<Result<()>>>,
+) -> Option<()> {
+ let title: String = metadata
+ .get("xesam:title")?
+ .try_to_owned()
+ .ok()?
+ .try_into()
+ .ok()?;
+
+ if (status_lock.lock().await)
+ .title
+ .as_ref()
+ .is_some_and(|s| *s == title)
+ {
+ return None;
+ }
+
+ if let Some(h) = rotator.take() {
+ h.abort()
+ };
+
+ let status = status_lock.clone();
+
+ if title.len() > 10 {
+ *rotator = Some(tokio::spawn(async move {
+ let mut interval = tokio::time::interval(TICK_RATE);
+ let mut chars = title.chars().collect::<Vec<char>>();
+ chars.push(' ');
+ loop {
+ interval.tick().await;
+ let mut status = status.lock().await;
+ status.title = Some(chars[0..10].iter().collect());
+ notify.notify_one();
+ chars.rotate_left(1);
+ }
+ }));
+ } else {
+ let mut status = status.lock().await;
+ status.title = Some(title);
+ notify.notify_one();
+ }
+
+ Some(())
+}
+
+async fn metadata(
+ notify: Arc<Notify>,
+ mut stream: PropertyStream<'_, HashMap<String, OwnedValue>>,
+ status_lock: StatusLock,
+ mut rotator: Option<JoinHandle<Result<()>>>,
+) -> Result<()> {
+ while let Some(signal) = stream.next().await {
+ if let Ok(metadata) = signal.get().await {
+ process_metadata(notify.clone(), status_lock.clone(), metadata, &mut rotator).await;
+ };
+ }
+ Ok(())
+}
+
+async fn can_go_previous(
+ notify: Arc<Notify>,
+ mut stream: PropertyStream<'_, bool>,
+ status_lock: StatusLock,
+) -> Result<()> {
+ while let Some(signal) = stream.next().await {
+ if let Ok(val) = signal.get().await {
+ let mut status = status_lock.lock().await;
+ status.can_go_previous = val;
+ notify.notify_one();
+ };
+ }
+ Ok(())
+}
+
+async fn playback_state(
+ notify: Arc<Notify>,
+ mut stream: PropertyStream<'_, PlaybackStatus>,
+ status_lock: StatusLock,
+) -> Result<()> {
+ while let Some(signal) = stream.next().await {
+ if let Ok(val) = signal.get().await {
+ let mut status = status_lock.lock().await;
+ status.playback_status = val;
+ notify.notify_one();
+ };
+ }
+ Ok(())
+}
+
+async fn can_go_next(
+ notify: Arc<Notify>,
+ mut stream: PropertyStream<'_, bool>,
+ status_lock: StatusLock,
+) -> Result<()> {
+ while let Some(signal) = stream.next().await {
+ if let Ok(val) = signal.get().await {
+ let mut status = status_lock.lock().await;
+ status.can_go_next = val;
+ notify.notify_one();
+ };
+ }
+ Ok(())
+}
+
+async fn volume(
+ notify: Arc<Notify>,
+ mut stream: PropertyStream<'_, f64>,
+ status_lock: StatusLock,
+) -> Result<()> {
+ while let Some(signal) = stream.next().await {
+ if let Ok(val) = signal.get().await {
+ let mut status = status_lock.lock().await;
+ status.volume = Some(val);
+ notify.notify_one();
+ };
+ }
+ Ok(())
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum Component {
+ Icon,
+ Title,
+ Prev,
+ Play,
+ Pause,
+ Next,
+ Volume,
+ Space,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct Status {
+ title: Option<String>,
+ can_go_previous: bool,
+ playback_status: PlaybackStatus,
+ can_go_next: bool,
+ volume: Option<f64>,
+ indexes: Vec<(usize, Component)>,
+}
+
+impl Status {
+ fn build(&mut self) -> Block {
+ let mut full_text = String::new();
+ self.indexes = Vec::new();
+
+ self.indexes
+ .push((full_text.chars().count(), Component::Icon));
+ full_text.push_str(" 󰝚 ");
+
+ if let Some(title) = self.title.as_ref() {
+ self.indexes
+ .push((full_text.chars().count(), Component::Title));
+ full_text.push_str(title);
+ full_text.push(' ');
+ }
+
+ if self.can_go_previous {
+ self.indexes
+ .push((full_text.chars().count(), Component::Prev));
+ full_text.push_str("󰒮 ");
+ }
+
+ if self.playback_status == PlaybackStatus::Playing {
+ self.indexes
+ .push((full_text.chars().count(), Component::Pause));
+ full_text.push_str("󰏤 ");
+ } else {
+ self.indexes
+ .push((full_text.chars().count(), Component::Play));
+ full_text.push_str("󰐊 ");
+ }
+
+ if self.can_go_next {
+ self.indexes
+ .push((full_text.chars().count(), Component::Next));
+ full_text.push_str("󰒭 ");
+ }
+
+ if let Some(volume) = self.volume.map(|v| (v * 100_f64) as u32) {
+ self.indexes
+ .push((full_text.chars().count(), Component::Volume));
+
+ match volume {
+ v @ 66.. => full_text.push_str(&format!("󰕾 {v}% ")),
+ v @ 33.. => full_text.push_str(&format!("󰖀 {v}% ")),
+ v @ 0.. => full_text.push_str(&format!("󰕿 {v}% ")),
+ }
+ }
+
+ self.indexes
+ .push((full_text.chars().count(), Component::Space));
+
+ let (color, background) = match self.playback_status {
+ PlaybackStatus::Playing => (Some(BLACK.into()), Some(CYAN.into())),
+ PlaybackStatus::Paused => (Some(BLACK.into()), Some(YELLOW.into())),
+ PlaybackStatus::Stopped => (None, None),
+ };
+
+ Block {
+ full_text,
+ color,
+ background,
+ ..Default::default()
+ }
+ }
+}