summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2024-07-25 21:25:25 -0500
committerToby Vincent <tobyv@tobyvin.dev>2024-07-25 21:27:42 -0500
commit00bb5d778e66c0481dad6f0d57948b71a0652f49 (patch)
tree0f7b715eab0bcf7c8df37a4fe8a50c0def4f0d1e
parent642557c36d5f431e09afd2c3989617af05e6f72b (diff)
feat: refactor into wrapper around i3blocks
-rw-r--r--Cargo.lock136
-rw-r--r--Cargo.toml7
-rw-r--r--contrib/i3blocks.config8
-rw-r--r--contrib/mpris_click.json1
-rw-r--r--src/component.rs232
-rw-r--r--src/component/icon.rs54
-rw-r--r--src/component/next.rs77
-rw-r--r--src/component/play.rs100
-rw-r--r--src/component/prev.rs77
-rw-r--r--src/component/title.rs93
-rw-r--r--src/component/volume.rs82
-rw-r--r--src/dbus/player.rs159
-rw-r--r--src/error.rs18
-rw-r--r--src/i3bar.rs24
-rw-r--r--src/lib.rs24
-rw-r--r--src/listener.rs184
-rw-r--r--src/main.rs82
17 files changed, 591 insertions, 767 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f51a9ae..56647a4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -251,6 +251,16 @@ dependencies = [
]
[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -331,6 +341,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
name = "futures-core"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -453,11 +472,24 @@ dependencies = [
"serde",
"serde_json",
"thiserror",
+ "time",
"tokio",
+ "tokio-stream",
+ "url",
"zbus",
]
[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -540,6 +572,12 @@ dependencies = [
]
[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -581,6 +619,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -619,6 +663,12 @@ dependencies = [
]
[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -837,6 +887,52 @@ dependencies = [
]
[[package]]
+name = "time"
+version = "0.3.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "time-macros"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
name = "tokio"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -867,6 +963,17 @@ dependencies = [
]
[[package]]
+name = "tokio-stream"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
name = "toml_datetime"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -932,12 +1039,39 @@ dependencies = [
]
[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1196,6 +1330,8 @@ dependencies = [
"enumflags2",
"serde",
"static_assertions",
+ "time",
+ "url",
"zvariant_derive",
]
diff --git a/Cargo.toml b/Cargo.toml
index fe34a4b..6c3ebf3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,5 +9,8 @@ main_error = "0.1.2"
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"] }
-zbus = { version = "4.3.0", default-features = false, features = ["tokio"] }
+time = { version = "0.3.36", features = ["serde-human-readable"] }
+tokio = { version = "1.38.0", features = ["io-std", "io-util", "macros", "process", "rt-multi-thread"] }
+tokio-stream = "0.1.15"
+url = "2.5.2"
+zbus = { version = "4.3.0", default-features = false, features = ["time", "tokio", "url"] }
diff --git a/contrib/i3blocks.config b/contrib/i3blocks.config
new file mode 100644
index 0000000..87f7a5e
--- /dev/null
+++ b/contrib/i3blocks.config
@@ -0,0 +1,8 @@
+# vim: set ft=confini:
+
+[mpris]
+full_text=
+
+[time]
+command=date +" %a %m/%d %T "
+interval=1
diff --git a/contrib/mpris_click.json b/contrib/mpris_click.json
new file mode 100644
index 0000000..821f6f2
--- /dev/null
+++ b/contrib/mpris_click.json
@@ -0,0 +1 @@
+{"":"","interval":"persist","name":"clickme","full_text":"Click me!","command":"tee /tmp/mpris_click.json","format":"json","button":1,"event":272,"x":1866,"y":13,"relative_x":50,"relative_y":13,"width":90,"height":25,"scale":1}
diff --git a/src/component.rs b/src/component.rs
deleted file mode 100644
index e06f2cb..0000000
--- a/src/component.rs
+++ /dev/null
@@ -1,232 +0,0 @@
-use std::{
- future::Future,
- io::{BufReader, Read, Write},
- marker::Send,
- sync::Arc,
-};
-
-use tokio::{
- sync::{mpsc::Sender, Mutex},
- task::JoinSet,
-};
-use zbus::Connection;
-
-use crate::{
- dbus::{
- player::{PlaybackStatus, PlayerProxy},
- playerctld::PlayerctldProxy,
- },
- i3bar::{Block, Click},
- Error, IGNORED,
-};
-
-pub use icon::Icon;
-pub use next::Next;
-pub use play::Play;
-pub use prev::Prev;
-pub use title::Title;
-pub use volume::Volume;
-
-mod icon;
-mod next;
-mod play;
-mod prev;
-mod title;
-mod volume;
-
-pub trait Component: Send + 'static {
- type Updater: Update;
- type Colorer: Update;
- type Handler: Button;
-
- fn initialize() -> Block;
-}
-
-pub trait Runner: Component + private::Sealed {
- fn run<R: Read + Send>(reader: R) -> impl Future<Output = Result<(), Error>> + Send {
- async move {
- use std::io::BufRead;
-
- let conn = Connection::session().await?;
-
- let writer = std::io::stdout();
-
- let listeners = tokio::spawn(Self::listeners(conn.clone(), writer));
- let buf_reader = BufReader::new(reader);
-
- for click in buf_reader
- .lines()
- .map_while(Result::ok)
- .flat_map(|s| serde_json::from_str::<Click>(&s))
- {
- let _ = <Self::Handler as Button>::handle(conn.clone(), click).await;
- }
-
- listeners.await?
- }
- }
-
- fn listeners<W: Write + Send>(
- conn: Connection,
- mut writer: W,
- ) -> impl Future<Output = Result<(), Error>> + Send {
- async move {
- let mut join_set = JoinSet::new();
-
- let (tx_player, mut rx_player) = tokio::sync::mpsc::channel(128);
- let (tx_status, mut rx_status) = tokio::sync::mpsc::channel(128);
- let (tx_value, mut rx_value) = tokio::sync::mpsc::channel(128);
-
- let block = Arc::new(Mutex::new(Self::initialize()));
-
- tokio::spawn(Self::active_listener(conn.clone(), tx_player));
-
- loop {
- let output = tokio::select! {
- Some(name) = rx_player.recv() => {
- join_set.shutdown().await;
-
- let mut block = block.lock().await;
- *block = Self::initialize();
- if !name.is_empty() {
- block.instance.clone_from(&Some(name.clone()));
- let proxy = PlayerProxy::builder(&conn)
- .destination(name.clone())?
- .build()
- .await?;
- join_set.spawn(<Self::Colorer as Update>::listen(tx_status.clone(), proxy.clone()));
- join_set.spawn(<Self::Updater as Update>::listen(tx_value.clone(), proxy));
- }
- Output::Clear
- }
- Some(color) = rx_status.recv() => <Self::Colorer as Update>::update(color, block.clone()).await?,
- Some(value) = rx_value.recv() => <Self::Updater as Update>::update(value, block.clone()).await?
- };
-
- match output {
- Output::Print => (block.lock().await).write_to(&mut writer)?,
- Output::Clear => writer.write_all(&[b'\n'])?,
- Output::Skip => continue,
- }
-
- writer.flush()?;
- }
- }
- }
-
- fn active_listener(
- conn: Connection,
- tx: Sender<String>,
- ) -> impl std::future::Future<Output = Result<(), Error>> + Send {
- use futures_util::StreamExt;
-
- async move {
- let proxy = PlayerctldProxy::builder(&conn).build().await?;
- let mut last = String::new();
- let mut stream = proxy.receive_player_names_changed().await;
- while let Some(signal) = stream.next().await {
- let name = signal
- .get()
- .await?
- .into_iter()
- .find(|s| s.split('.').nth(3).is_some_and(|s| !IGNORED.contains(&s)))
- .unwrap_or_default();
- if name != last {
- last.clone_from(&name);
- tx.send(name).await?;
- }
- }
- Ok(())
- }
- }
-}
-
-impl<T: Component> Runner for T {}
-
-mod private {
- pub trait Sealed {}
-
- impl<T: super::Component> Sealed for T {}
-}
-
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
-pub enum Output {
- Print,
- Clear,
- #[default]
- Skip,
-}
-
-pub trait Update: Send + 'static {
- type Value: Send;
-
- fn listen(
- tx: Sender<Self::Value>,
- proxy: PlayerProxy<'_>,
- ) -> impl Future<Output = Result<(), Error>> + Send;
-
- fn update(
- value: Self::Value,
- block: Arc<Mutex<Block>>,
- ) -> impl Future<Output = Result<Output, Error>> + Send;
-}
-
-impl Update for () {
- type Value = ();
-
- async fn listen(_: Sender<Self::Value>, _: PlayerProxy<'_>) -> Result<(), Error> {
- Ok(())
- }
-
- async fn update(_: Self::Value, _: Arc<Mutex<Block>>) -> Result<Output, Error> {
- Ok(Output::Skip)
- }
-}
-
-impl Update for PlaybackStatus {
- type Value = (Option<String>, Option<String>);
-
- async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> {
- use futures_util::StreamExt;
-
- let black = std::env::var("BASE16_COLOR_00_HEX").ok();
- let cyan = std::env::var("BASE16_COLOR_0C_HEX").ok();
- let yellow = std::env::var("BASE16_COLOR_0A_HEX").ok();
-
- let mut stream = proxy.receive_playback_status_changed().await;
- while let Some(signal) = stream.next().await {
- if let Ok(value) = signal.get().await {
- let val = match value {
- PlaybackStatus::Playing => (black.clone(), cyan.clone()),
- PlaybackStatus::Paused => (black.clone(), yellow.clone()),
- PlaybackStatus::Stopped => (None, None),
- };
- tx.send(val).await?;
- }
- }
- Ok(())
- }
-
- async fn update(
- (color, background): Self::Value,
- block: Arc<Mutex<Block>>,
- ) -> Result<Output, Error> {
- let mut block = block.lock().await;
- block.color = color;
- block.background = background;
- Ok(Output::Print)
- }
-}
-
-pub trait Button {
- fn handle(
- conn: zbus::Connection,
- click: crate::i3bar::Click,
- ) -> impl Future<Output = Result<(), Error>> + Send;
-}
-
-impl Button for () {
- async fn handle(_: zbus::Connection, _: crate::i3bar::Click) -> Result<(), Error> {
- Ok(())
- }
-}
diff --git a/src/component/icon.rs b/src/component/icon.rs
deleted file mode 100644
index 287c655..0000000
--- a/src/component/icon.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-use zbus::Connection;
-
-use crate::{
- dbus::{media_player2::MediaPlayer2Proxy, player::PlaybackStatus, playerctld::PlayerctldProxy},
- i3bar::{Block, Click},
- Error,
-};
-
-use super::{Button, Component};
-
-pub struct Icon;
-
-impl Component for Icon {
- type Updater = ();
- type Colorer = PlaybackStatus;
- type Handler = Self;
-
- fn initialize() -> Block {
- Block {
- name: Some("mpris-icon".into()),
- full_text: " 󰝚 ".into(),
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- }
- }
-}
-
-impl Button for Icon {
- async fn handle(conn: Connection, click: Click) -> Result<(), Error> {
- let Some(name) = click.instance else {
- return Ok(());
- };
-
- let proxy = MediaPlayer2Proxy::builder(&conn)
- .destination(name)?
- .build()
- .await?;
-
- match click.button {
- 3 => {
- PlayerctldProxy::builder(&conn)
- .build()
- .await?
- .shift()
- .await?;
- }
- 1 if proxy.can_raise().await? => proxy.raise().await?,
- _ => {}
- }
-
- Ok(())
- }
-}
diff --git a/src/component/next.rs b/src/component/next.rs
deleted file mode 100644
index 52e72ff..0000000
--- a/src/component/next.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-use std::sync::Arc;
-
-use tokio::sync::{mpsc::Sender, Mutex};
-use zbus::Connection;
-
-use crate::{
- dbus::player::{PlaybackStatus, PlayerProxy},
- i3bar::{Align, Block, Click, MinWidth},
- Error,
-};
-
-use super::{Button, Component, Output, Update};
-
-pub struct Next;
-
-impl Component for Next {
- type Updater = Self;
- type Colorer = PlaybackStatus;
- type Handler = Self;
-
- fn initialize() -> Block {
- Block {
- name: Some("mpris-next".into()),
- full_text: '󰒭'.into(),
- min_width: Some(MinWidth::Text("xx".to_string())),
- align: Align::Center,
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- }
- }
-}
-
-impl Update for Next {
- type Value = bool;
-
- async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> {
- use futures_util::StreamExt;
-
- let mut stream = proxy.receive_can_go_next_changed().await;
- while let Some(signal) = stream.next().await {
- if let Ok(value) = signal.get().await {
- tx.send(value).await?;
- }
- }
- Ok(())
- }
-
- async fn update(value: Self::Value, _: Arc<Mutex<Block>>) -> Result<Output, Error> {
- if value {
- Ok(Output::Print)
- } else {
- Ok(Output::Clear)
- }
- }
-}
-
-impl Button for Next {
- async fn handle(conn: Connection, click: Click) -> Result<(), Error> {
- let Some(name) = click.instance else {
- return Ok(());
- };
-
- let proxy = PlayerProxy::builder(&conn)
- .destination(name)?
- .build()
- .await?;
-
- match click.button {
- 1 if proxy.can_go_next().await? => proxy.next().await?,
- 3 if proxy.can_seek().await? => proxy.seek(5000).await?,
- _ => {}
- }
-
- Ok(())
- }
-}
diff --git a/src/component/play.rs b/src/component/play.rs
deleted file mode 100644
index 512bc77..0000000
--- a/src/component/play.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use std::sync::Arc;
-
-use tokio::sync::{mpsc::Sender, Mutex};
-use zbus::Connection;
-
-use crate::{
- dbus::player::{PlaybackStatus, PlayerProxy},
- i3bar::{Align, Block, Click, MinWidth},
- Error,
-};
-
-use super::{Button, Component, Output, Update};
-
-pub struct Play;
-
-impl Component for Play {
- type Updater = Self;
- type Colorer = ();
- type Handler = Self;
-
- fn initialize() -> Block {
- Block {
- name: Some("mpris-play".into()),
- min_width: Some(MinWidth::Text("xx".to_string())),
- align: Align::Center,
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- }
- }
-}
-
-impl Update for Play {
- type Value = (char, Option<String>, Option<String>);
-
- async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> {
- use futures_util::StreamExt;
- let black = std::env::var("BASE16_COLOR_00_HEX").ok();
- let cyan = std::env::var("BASE16_COLOR_0C_HEX").ok();
- let yellow = std::env::var("BASE16_COLOR_0A_HEX").ok();
-
- let mut stream = proxy.receive_playback_status_changed().await;
- while let Some(signal) = stream.next().await {
- if let Ok(value) = signal.get().await {
- tx.send(match value {
- PlaybackStatus::Playing => ('󰏤', black.clone(), cyan.clone()),
- PlaybackStatus::Paused => ('󰐊', black.clone(), yellow.clone()),
- PlaybackStatus::Stopped => ('󰐊', None, None),
- })
- .await?;
- }
- }
- Ok(())
- }
-
- async fn update(value: Self::Value, block: Arc<Mutex<Block>>) -> Result<Output, Error> {
- let (full_text, color, background) = value;
-
- let mut block = block.lock().await;
- block.full_text = full_text.into();
- block.color = color;
- block.background = background;
- Ok(Output::Print)
- }
-}
-
-impl Button for Play {
- async fn handle(conn: Connection, click: Click) -> Result<(), Error> {
- let Some(name) = click.instance else {
- return Ok(());
- };
-
- let proxy = PlayerProxy::builder(&conn)
- .destination(name)?
- .build()
- .await?;
-
- let valid = match proxy.playback_status().await {
- Ok(PlaybackStatus::Playing) => proxy.can_pause().await.unwrap_or_default(),
- Ok(_) => proxy.can_play().await.unwrap_or_default(),
- _ => false,
- };
-
- if !valid {
- return Ok(());
- }
-
- match (click.button, proxy.playback_status().await) {
- (1, Ok(PlaybackStatus::Playing)) if proxy.can_pause().await.unwrap_or_default() => {
- proxy.play_pause().await?
- }
- (1, Ok(PlaybackStatus::Paused)) if proxy.can_play().await.unwrap_or_default() => {
- proxy.play_pause().await?
- }
- _ => (),
- }
-
- Ok(())
- }
-}
diff --git a/src/component/prev.rs b/src/component/prev.rs
deleted file mode 100644
index 73372ab..0000000
--- a/src/component/prev.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-use std::sync::Arc;
-
-use tokio::sync::{mpsc::Sender, Mutex};
-use zbus::Connection;
-
-use crate::{
- dbus::player::{PlaybackStatus, PlayerProxy},
- i3bar::{Align, Block, Click, MinWidth},
- Error,
-};
-
-use super::{Button, Component, Output, Update};
-
-pub struct Prev;
-
-impl Component for Prev {
- type Updater = Self;
- type Colorer = PlaybackStatus;
- type Handler = Self;
-
- fn initialize() -> Block {
- Block {
- name: Some("mpris-prev".into()),
- full_text: '󰒮'.into(),
- min_width: Some(MinWidth::Text("xx".to_string())),
- align: Align::Center,
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- }
- }
-}
-
-impl Update for Prev {
- type Value = bool;
-
- async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> {
- use futures_util::StreamExt;
-
- let mut stream = proxy.receive_can_go_previous_changed().await;
- while let Some(signal) = stream.next().await {
- if let Ok(value) = signal.get().await {
- tx.send(value).await?;
- }
- }
- Ok(())
- }
-
- async fn update(value: Self::Value, _: Arc<Mutex<Block>>) -> Result<Output, Error> {
- if value {
- Ok(Output::Print)
- } else {
- Ok(Output::Clear)
- }
- }
-}
-
-impl Button for Prev {
- async fn handle(conn: Connection, click: Click) -> Result<(), Error> {
- let Some(name) = click.instance else {
- return Ok(());
- };
-
- let proxy = PlayerProxy::builder(&conn)
- .destination(name)?
- .build()
- .await?;
-
- match click.button {
- 1 if proxy.can_go_previous().await? => proxy.previous().await?,
- 3 if proxy.can_seek().await? => proxy.seek(-5000).await?,
- _ => {}
- }
-
- Ok(())
- }
-}
diff --git a/src/component/title.rs b/src/component/title.rs
deleted file mode 100644
index 870fe1e..0000000
--- a/src/component/title.rs
+++ /dev/null
@@ -1,93 +0,0 @@
-use std::{sync::Arc, time::Duration};
-
-use tokio::{
- sync::{mpsc::Sender, Mutex},
- task::{AbortHandle, JoinSet},
-};
-
-use crate::{
- dbus::player::{PlaybackStatus, PlayerProxy},
- i3bar::{Align, Block, MinWidth},
- Error,
-};
-
-use super::{Component, Output, Update};
-
-const TICK_RATE: Duration = Duration::from_millis(500);
-
-pub struct Title;
-
-impl Component for Title {
- type Updater = Self;
- type Colorer = PlaybackStatus;
- type Handler = ();
-
- fn initialize() -> Block {
- Block {
- name: Some("mpris-title".into()),
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- }
- }
-}
-
-impl Update for Title {
- type Value = String;
-
- async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> {
- use futures_util::StreamExt;
-
- let mut join_set = JoinSet::new();
- let mut rotator: Option<AbortHandle> = None;
- let mut old_title = String::new();
- let mut stream = proxy.receive_metadata_changed().await;
- while let Some(signal) = stream.next().await {
- if let Ok(metadata) = signal.get().await {
- let Some(owned_value) = metadata.get("xesam:title") else {
- continue;
- };
-
- let title: String = owned_value.try_to_owned()?.try_into()?;
-
- if old_title == title {
- continue;
- }
-
- if let Some(h) = rotator.take() {
- h.abort();
- };
-
- old_title.clone_from(&title);
-
- if title.len() >= 10 {
- let tx = tx.clone();
- let mut chars = title.chars().collect::<Vec<char>>();
- chars.push(' ');
- rotator = Some(join_set.spawn(async move {
- let mut interval = tokio::time::interval(TICK_RATE);
- loop {
- interval.tick().await;
- tx.send(String::from_iter(chars[0..10].iter()))
- .await
- .unwrap();
- chars.rotate_left(1);
- }
- }));
- } else {
- tx.send(title).await?;
- }
- }
- }
- Ok(())
- }
-
- async fn update(value: Self::Value, block: Arc<Mutex<Block>>) -> Result<Output, Error> {
- let mut block = block.lock().await;
- block.full_text = value;
- block.min_width = Some(MinWidth::Text(format!("x{}", block.full_text)));
- block.align = Align::Center;
-
- Ok(Output::Print)
- }
-}
diff --git a/src/component/volume.rs b/src/component/volume.rs
deleted file mode 100644
index 83341f0..0000000
--- a/src/component/volume.rs
+++ /dev/null
@@ -1,82 +0,0 @@
-use std::sync::Arc;
-
-use tokio::sync::{mpsc::Sender, Mutex};
-use zbus::Connection;
-
-use crate::{
- dbus::player::{PlaybackStatus, PlayerProxy},
- i3bar::{Align, Block, Click, MinWidth},
- Error,
-};
-
-use super::{Button, Component, Output, Update};
-
-pub struct Volume;
-
-impl Component for Volume {
- type Updater = Self;
- type Colorer = PlaybackStatus;
- type Handler = Self;
-
- fn initialize() -> Block {
- Block {
- name: Some("mpris-volume".into()),
- full_text: " ".to_string(),
- align: Align::Center,
- ..Default::default()
- }
- }
-}
-
-impl Update for Volume {
- type Value = Option<f64>;
-
- async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> {
- use futures_util::StreamExt;
- let mut stream = proxy.receive_volume_changed().await;
- while let Some(signal) = stream.next().await {
- if let Ok(value) = signal.get().await {
- tx.send(Some(value)).await?;
- }
- }
- Ok(())
- }
-
- async fn update(value: Self::Value, block: Arc<Mutex<Block>>) -> Result<Output, Error> {
- let volume = match value {
- Some(v) => (v * 100_f64) as u32,
- None => return Ok(Output::Clear),
- };
-
- let mut block = block.lock().await;
- block.full_text = match volume {
- v @ 66.. => format!("󰕾 {v}%"),
- v @ 33.. => format!("󰖀 {v}%"),
- v @ 0.. => format!("󰕿 {v}%"),
- };
- block.min_width = Some(MinWidth::Text(format!("x{}", block.full_text)));
-
- Ok(Output::Print)
- }
-}
-
-impl Button for Volume {
- async fn handle(conn: Connection, click: Click) -> Result<(), Error> {
- let Some(name) = click.instance else {
- return Ok(());
- };
-
- let proxy = PlayerProxy::builder(&conn)
- .destination(name)?
- .build()
- .await?;
-
- match (click.button, proxy.volume().await) {
- (4, Ok(v)) if proxy.can_control().await? => proxy.set_volume(v + 0.05).await?,
- (5, Ok(v)) if proxy.can_control().await? => proxy.set_volume(v - 0.05).await?,
- _ => (),
- }
-
- Ok(())
- }
-}
diff --git a/src/dbus/player.rs b/src/dbus/player.rs
index 6457373..a78d7ed 100644
--- a/src/dbus/player.rs
+++ b/src/dbus/player.rs
@@ -20,6 +20,11 @@
//! [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 std::collections::HashMap;
+
+use serde::{ser::Error, Deserialize, Serialize};
+use time::{format_description::well_known::Iso8601, PrimitiveDateTime};
+use url::Url;
use zbus::{
proxy,
zvariant::{OwnedValue, Type},
@@ -102,9 +107,7 @@ trait Player {
/// Metadata property
#[zbus(property)]
- fn metadata(
- &self,
- ) -> zbus::Result<std::collections::HashMap<String, zbus::zvariant::OwnedValue>>;
+ fn metadata(&self) -> zbus::Result<Metadata>;
/// MinimumRate property
#[zbus(property)]
@@ -137,6 +140,156 @@ trait Player {
fn set_volume(&self, value: f64) -> zbus::Result<()>;
}
+/// MPRIS metadata recomended fields
+///
+/// Found here: [https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata]
+#[derive(Debug, Default, PartialEq, Type, Serialize, Deserialize)]
+#[zvariant(signature = "dict")]
+pub struct Metadata {
+ /// D-Bus path: A unique identity for this track within the context of an MPRIS object (eg: tracklist).
+ #[zvariant(rename = "mpris:trackid")]
+ pub trackid: Option<String>,
+
+ /// 64-bit integer: The duration of the track in microseconds.
+ #[zvariant(rename = "mpris:length")]
+ pub length: Option<u64>,
+
+ /// URI: The location of an image representing the track or album. Clients should not assume this will continue to exist when the media player stops giving out the URL.
+ #[zvariant(rename = "mpris:artUrl")]
+ pub art_url: Option<url::Url>,
+
+ /// String: The album name.
+ #[zvariant(rename = "xesam:album")]
+ pub album: Option<String>,
+
+ /// List of Strings: The album artist(s).
+ #[zvariant(rename = "xesam:albumArtist")]
+ pub album_artist: Option<Vec<String>>,
+
+ /// List of Strings: The track artist(s).
+ #[zvariant(rename = "xesam:artist")]
+ pub artist: Option<Vec<String>>,
+
+ /// String: The track lyrics.
+ #[zvariant(rename = "xesam:asText")]
+ pub as_text: Option<String>,
+
+ /// Integer: The speed of the music, in beats per minute.
+ #[zvariant(rename = "xesam:audioBPM")]
+ pub audio_bpm: Option<u64>,
+
+ /// Float: An automatically-generated rating, based on things such as how often it has been played. This should be in the range 0.0 to 1.0.
+ #[zvariant(rename = "xesam:autoRating")]
+ pub auto_rating: Option<f64>,
+
+ /// List of Strings: A (list of) freeform comment(s).
+ #[zvariant(rename = "xesam:comment")]
+ pub comment: Option<Vec<String>>,
+
+ /// List of Strings: The composer(s) of the track.
+ #[zvariant(rename = "xesam:composer")]
+ pub composer: Option<Vec<String>>,
+
+ /// Date/Time: When the track was created. Usually only the year component will be useful.
+ #[zvariant(rename = "xesam:contentCreated")]
+ pub content_created: Option<PrimitiveDateTime>,
+
+ /// Integer: The disc number on the album that this track is from.
+ #[zvariant(rename = "xesam:discNumber")]
+ pub disc_number: Option<u64>,
+
+ /// Date/Time: When the track was first played.
+ #[zvariant(rename = "xesam:firstUsed")]
+ pub first_used: Option<PrimitiveDateTime>,
+
+ /// List of Strings: The genre(s) of the track.
+ #[zvariant(rename = "xesam:genre")]
+ pub genre: Option<Vec<String>>,
+
+ /// Date/Time: When the track was last played.
+ #[zvariant(rename = "xesam:lastUsed")]
+ pub last_used: Option<PrimitiveDateTime>,
+
+ /// List of Strings: The lyricist(s) of the track.
+ #[zvariant(rename = "xesam:lyricist")]
+ pub lyricist: Option<Vec<String>>,
+
+ /// String: The track title.
+ #[zvariant(rename = "xesam:title")]
+ pub title: Option<String>,
+
+ /// Integer: The track number on the album disc.
+ #[zvariant(rename = "xesam:trackNumber")]
+ pub track_number: Option<u64>,
+
+ /// URI: The location of the media file.
+ #[zvariant(rename = "xesam:url")]
+ pub url: Option<url::Url>,
+
+ /// Integer: The number of times the track has been played.
+ #[zvariant(rename = "xesam:useCount")]
+ pub use_count: Option<u64>,
+
+ /// Float: A user-specified rating. This should be in the range 0.0 to 1.0.
+ #[zvariant(rename = "xesam:userRating")]
+ pub user_rating: Option<f64>,
+}
+
+impl TryFrom<OwnedValue> for Metadata {
+ type Error = zbus::zvariant::Error;
+
+ fn try_from(value: OwnedValue) -> Result<Self, Self::Error> {
+ let map: HashMap<String, OwnedValue> = value.try_into()?;
+
+ macro_rules! get_field {
+ ($n:literal, PrimitiveDateTime) => {
+ get_field!($n)
+ .map(|s: String| PrimitiveDateTime::parse(&s, &Iso8601::DEFAULT))
+ .transpose()
+ .map_err(Self::Error::custom)?
+ };
+ ($n:literal, Url) => {
+ get_field!($n)
+ .map(|s: String| Url::parse(&s))
+ .transpose()
+ .map_err(Self::Error::custom)?
+ };
+ ($n:literal) => {
+ map.get($n)
+ .map(|o| o.try_to_owned())
+ .transpose()?
+ .map(|o| o.try_into())
+ .transpose()?
+ };
+ }
+
+ Ok(Metadata {
+ trackid: get_field!("mpris:trackid"),
+ length: get_field!("mpris:length"),
+ art_url: get_field!("mpris:artUrl", Url),
+ album: get_field!("xesam:album"),
+ album_artist: get_field!("xesam:albumArtist"),
+ artist: get_field!("xesam:artist"),
+ as_text: get_field!("xesam:asText"),
+ audio_bpm: get_field!("xesam:audioBPM"),
+ auto_rating: get_field!("xesam:autoRating"),
+ comment: get_field!("xesam:comment"),
+ composer: get_field!("xesam:composer"),
+ content_created: get_field!("xesam:contentCreated", PrimitiveDateTime),
+ disc_number: get_field!("xesam:discNumber"),
+ first_used: get_field!("xesam:firstUsed", PrimitiveDateTime),
+ genre: get_field!("xesam:genre"),
+ last_used: get_field!("xesam:lastUsed", PrimitiveDateTime),
+ lyricist: get_field!("xesam:lyricist"),
+ title: get_field!("xesam:title"),
+ track_number: get_field!("xesam:trackNumber"),
+ url: get_field!("xesam:url", Url),
+ use_count: get_field!("xesam:useCount"),
+ user_rating: get_field!("xesam:userRating"),
+ })
+ }
+}
+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Type)]
pub enum PlaybackStatus {
Playing,
diff --git a/src/error.rs b/src/error.rs
index 77d832c..ef596d2 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -5,9 +5,6 @@ pub enum Error {
#[error("IO error: {0}")]
IO(#[from] std::io::Error),
- #[error("Poison error: {0}")]
- Poison(String),
-
#[error("Join error: {0}")]
Join(#[from] tokio::task::JoinError),
@@ -22,19 +19,4 @@ pub enum Error {
#[error("Json serde error: {0}")]
Json(#[from] serde_json::Error),
-
- #[error("Send error: {0}")]
- Send(String),
-}
-
-impl<T> From<tokio::sync::mpsc::error::SendError<T>> for Error {
- fn from(err: tokio::sync::mpsc::error::SendError<T>) -> Self {
- Self::Send(err.to_string())
- }
-}
-
-impl<T> From<std::sync::PoisonError<T>> for Error {
- fn from(err: std::sync::PoisonError<T>) -> Self {
- Self::Poison(err.to_string())
- }
}
diff --git a/src/i3bar.rs b/src/i3bar.rs
index bdc92fa..28306e0 100644
--- a/src/i3bar.rs
+++ b/src/i3bar.rs
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::Error;
/// Represent block as described in <https://i3wm.org/docs/i3bar-protocol.html>
-#[derive(Debug, Clone, Default, Serialize)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Block {
pub full_text: String,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -49,7 +49,7 @@ impl Block {
}
}
-#[derive(Debug, Copy, Clone, Default, Serialize)]
+#[derive(Debug, Copy, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Align {
Center,
@@ -64,7 +64,7 @@ impl Align {
}
}
-#[derive(Serialize, Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MinWidth {
Pixels(usize),
@@ -72,24 +72,20 @@ pub enum MinWidth {
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
-#[serde(default)]
pub struct Click {
+ #[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
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 button: u8,
+ pub event: 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,
+ /// The modifiers property is not currently added by swaybar
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub modifiers: Option<Vec<String>>,
}
diff --git a/src/lib.rs b/src/lib.rs
index 01c25aa..6306546 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,8 +1,28 @@
+use std::sync::Arc;
+
+use i3bar::Block;
+use tokio::sync::{Notify, RwLock};
+
pub use crate::error::{Error, Result};
-pub mod component;
pub mod dbus;
pub mod error;
pub mod i3bar;
+pub mod listener;
+
+pub type Blocks = Arc<Watcher<[Option<Block>; 6]>>;
+
+#[derive(Default)]
+pub struct Watcher<T> {
+ pub value: RwLock<T>,
+ pub notify: Notify,
+}
-const IGNORED: [&str; 3] = ["playerctld", "kdeconnect", "spotifyd"];
+impl<T> Watcher<T> {
+ pub fn new(value: T) -> Self {
+ Self {
+ value: RwLock::new(value),
+ notify: Notify::new(),
+ }
+ }
+}
diff --git a/src/listener.rs b/src/listener.rs
new file mode 100644
index 0000000..208c161
--- /dev/null
+++ b/src/listener.rs
@@ -0,0 +1,184 @@
+use std::{ops::ControlFlow, time::Duration};
+
+use tokio::task::JoinHandle;
+use tokio_stream::StreamExt;
+use zbus::{proxy::PropertyChanged, Connection};
+
+use crate::{
+ dbus::player::{Metadata, PlaybackStatus, PlayerProxy},
+ i3bar::{Align, Block, MinWidth},
+ Blocks, Error,
+};
+
+const TICK_RATE: Duration = Duration::from_millis(500);
+
+pub async fn listeners(blocks: Blocks) -> Result<(), Error> {
+ let conn = Connection::session().await?;
+
+ let proxy = PlayerProxy::builder(&conn)
+ .destination("org.mpris.MediaPlayer2.playerctld")?
+ .build()
+ .await?;
+
+ let black = std::env::var("BASE16_COLOR_00_HEX").ok();
+ let cyan = std::env::var("BASE16_COLOR_0C_HEX").ok();
+ let yellow = std::env::var("BASE16_COLOR_0A_HEX").ok();
+
+ let mut rotator: Option<JoinHandle<Result<(), Error>>> = None;
+ let mut old_title = String::new();
+
+ let mut metadata = proxy.receive_metadata_changed().await;
+ let mut prev = proxy.receive_can_go_previous_changed().await;
+ let mut play = proxy.receive_playback_status_changed().await;
+ let mut next = proxy.receive_can_go_next_changed().await;
+ let mut volume = proxy.receive_volume_changed().await;
+
+ loop {
+ tokio::select! {
+ Some(prop) = metadata.next() => handle_metadata(prop, blocks.clone(), &mut old_title, &mut rotator).await,
+ Some(prop) = prev.next() => handle_prev_next(prop, blocks.clone(), '󰒮', "mpris-prev").await,
+ Some(prop) = play.next() => handle_play(prop, blocks.clone(), black.clone(), cyan.clone(), yellow.clone()).await,
+ Some(prop) = next.next() => handle_prev_next(prop, blocks.clone(), '󰒭', "mpris-next").await,
+ Some(prop) = volume.next() => handle_volume(prop, blocks.clone()).await,
+ else => {
+ eprintln!("Failed to get next property");
+ break
+ }
+
+ };
+
+ blocks.notify.notify_one();
+ }
+
+ Ok(())
+}
+
+async fn title_rotator(blocks: Blocks, title: String) -> Result<(), Error> {
+ let mut interval = tokio::time::interval(TICK_RATE);
+ let mut chars = title.chars().collect::<Vec<char>>();
+ chars.push(' ');
+ let full_text = String::from_iter(chars[0..10].iter());
+
+ blocks.value.write().await[1] = Some(Block {
+ name: Some("mpris-title".into()),
+ min_width: Some(MinWidth::Text(format!("x{}", full_text))),
+ full_text,
+ separator: Some(false),
+ separator_block_width: Some(0),
+ ..Default::default()
+ });
+
+ loop {
+ interval.tick().await;
+ let mut value = blocks.value.write().await;
+ if let Some(block) = &mut value[1] {
+ block.full_text = String::from_iter(chars[0..10].iter());
+ };
+ chars.rotate_left(1);
+ }
+}
+
+async fn handle_metadata(
+ prop: PropertyChanged<'_, Metadata>,
+ blocks: Blocks,
+ old_title: &mut String,
+ rotator: &mut Option<JoinHandle<Result<(), Error>>>,
+) -> ControlFlow<Result<(), Error>> {
+ let title = match prop.get().await.map(|m| m.title) {
+ Ok(Some(s)) if *old_title != s => s,
+ _ => return ControlFlow::Continue(()),
+ };
+
+ old_title.clone_from(&title);
+ rotator.take().into_iter().for_each(|h| h.abort());
+
+ if title.len() >= 10 {
+ *rotator = Some(tokio::spawn(title_rotator(blocks.clone(), title)));
+ } else {
+ blocks.value.write().await[1] = Some(Block {
+ name: Some("mpris-title".into()),
+ min_width: Some(MinWidth::Text(format!("x{}", title))),
+ full_text: title,
+ separator: Some(false),
+ separator_block_width: Some(0),
+ ..Default::default()
+ });
+ }
+
+ ControlFlow::Continue(())
+}
+
+async fn handle_prev_next(
+ prop: PropertyChanged<'_, bool>,
+ blocks: Blocks,
+ icon: char,
+ name: &'static str,
+) -> ControlFlow<Result<(), Error>> {
+ if let Ok(value) = prop.get().await {
+ blocks.value.write().await[2] = value.then_some(Block {
+ name: Some(name.into()),
+ full_text: icon.into(),
+ min_width: Some(MinWidth::Text("xx".to_string())),
+ align: Align::Center,
+ separator: Some(false),
+ separator_block_width: Some(0),
+ ..Default::default()
+ });
+ };
+ ControlFlow::Continue(())
+}
+
+async fn handle_play(
+ prop: PropertyChanged<'_, PlaybackStatus>,
+ blocks: Blocks,
+ black: Option<String>,
+ cyan: Option<String>,
+ yellow: Option<String>,
+) -> ControlFlow<Result<(), Error>> {
+ let (icon, color, bg) = match prop.get().await {
+ Ok(PlaybackStatus::Playing) => ('󰏤', black.clone(), cyan.clone()),
+ Ok(PlaybackStatus::Paused) => ('󰐊', black.clone(), yellow.clone()),
+ Ok(PlaybackStatus::Stopped) => ('󰐊', None, None),
+ Err(_) => return ControlFlow::Continue(()),
+ };
+
+ let mut blocks = blocks.value.write().await;
+ blocks[3] = Some(Block {
+ name: Some("mpris-next".into()),
+ full_text: icon.into(),
+ min_width: Some(MinWidth::Text("xx".to_string())),
+ align: Align::Center,
+ separator: Some(false),
+ separator_block_width: Some(0),
+ ..Default::default()
+ });
+
+ for block in blocks.iter_mut().flatten() {
+ block.color.clone_from(&color);
+ block.background.clone_from(&bg);
+ }
+
+ ControlFlow::Continue(())
+}
+
+async fn handle_volume(
+ prop: PropertyChanged<'_, f64>,
+ blocks: Blocks,
+) -> ControlFlow<Result<(), Error>> {
+ let full_text = match prop.get().await.map(|v| (v * 100_f64) as u32) {
+ Ok(v @ 66..) => format!("󰕾 {v}%"),
+ Ok(v @ 33..) => format!("󰖀 {v}%"),
+ Ok(v @ 0..) => format!("󰕿 {v}%"),
+ Err(_) => return ControlFlow::Continue(()),
+ };
+
+ blocks.value.write().await[5] = Some(Block {
+ name: Some("mpris-volume".into()),
+ min_width: Some(MinWidth::Text(format!("x{}", full_text))),
+ full_text,
+ align: Align::Center,
+ ..Default::default()
+ });
+
+ ControlFlow::Continue(())
+}
diff --git a/src/main.rs b/src/main.rs
index e17742e..26b5784 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,19 +1,75 @@
-use i3blocks_mpris::component::{Icon, Next, Play, Prev, Runner, Title, Volume};
+use std::process::Stdio;
+
+use i3blocks_mpris::{
+ i3bar::{Block, Click},
+ listener::listeners,
+ Blocks,
+};
+use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[tokio::main]
async fn main() -> Result<(), main_error::MainError> {
- let stdin = std::io::stdin();
- match std::env::args().nth(1).as_deref().unwrap_or("icon") {
- "icon" => Icon::run(stdin).await,
- "next" => Next::run(stdin).await,
- "play" => Play::run(stdin).await,
- "prev" => Prev::run(stdin).await,
- "title" => Title::run(stdin).await,
- "volume" => Volume::run(stdin).await,
- s => {
- eprintln!("Invalid component name: {s}");
- std::process::exit(1)
+ let args = std::env::args().skip(1).collect::<Vec<_>>();
+ let mut child = tokio::process::Command::new("i3blocks")
+ .args(args)
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .spawn()?;
+
+ let mut stdout = tokio::io::stdout();
+ let mut stdin = BufReader::new(tokio::io::stdin()).lines();
+
+ let mut child_stdin = child.stdin.take().unwrap();
+ let mut child_stdout = BufReader::new(child.stdout.take().unwrap()).lines();
+
+ let blocks: Blocks = Blocks::default();
+
+ let blocks2 = blocks.clone();
+ tokio::spawn(async move {
+ blocks2.value.write().await[0] = Some(Block {
+ name: Some("mpris-icon".into()),
+ full_text: " 󰝚 ".into(),
+ separator: Some(false),
+ separator_block_width: Some(0),
+ ..Default::default()
+ });
+ blocks2.notify.notify_one();
+ });
+
+ tokio::spawn(listeners(blocks.clone()));
+
+ let mut mpris_blocks = String::new();
+ let mut other_blocks = String::new();
+
+ loop {
+ tokio::select! {
+ _ = blocks.notify.notified() => {
+ mpris_blocks = blocks
+ .value
+ .read()
+ .await
+ .iter()
+ .map(serde_json::to_string)
+ .collect::<Result<Vec<_>, _>>()?
+ .join(",");
+ }
+ Ok(Some(line)) = child_stdout.next_line() => other_blocks = line,
+ Ok(Some(line)) = stdin.next_line() => match serde_json::from_str::<Click>(&line) {
+ Err(_) => continue,
+ Ok(Click { name: Some(name), button, .. }) if name.starts_with("mpris-") => println!("{name}, {button}"),
+ Ok(click) => {
+ let mut s = serde_json::to_vec(&click)?;
+ s.push(b'\n');
+ child_stdin.write_all(&s).await?;
+ }
+ },
+ else => break,
}
+
+ let mut output = other_blocks.replace(r#"{"name":"mpris","full_text":""}"#, &mpris_blocks);
+ output.push('\n');
+ stdout.write_all(output.as_bytes()).await?;
}
- .map_err(Into::into)
+
+ Ok(())
}