diff options
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/bin/icon.rs | 78 | ||||
-rw-r--r-- | src/bin/next.rs | 97 | ||||
-rw-r--r-- | src/bin/play.rs | 129 | ||||
-rw-r--r-- | src/bin/prev.rs | 97 | ||||
-rw-r--r-- | src/bin/title.rs | 183 | ||||
-rw-r--r-- | src/bin/volume.rs | 109 | ||||
-rw-r--r-- | src/client.rs | 31 | ||||
-rw-r--r-- | src/color.rs | 116 | ||||
-rw-r--r-- | src/component.rs | 90 | ||||
-rw-r--r-- | src/components.rs | 234 | ||||
-rw-r--r-- | src/dbus_server.rs | 190 | ||||
-rw-r--r-- | src/i3bar.rs | 9 | ||||
-rw-r--r-- | src/lib.rs | 139 | ||||
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/mpris.rs | 129 | ||||
-rw-r--r-- | src/player.rs | 97 | ||||
-rw-r--r-- | src/printer.rs | 255 | ||||
-rw-r--r-- | src/server.rs | 173 |
19 files changed, 524 insertions, 1641 deletions
@@ -11,6 +11,6 @@ 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"] } +tokio = { version = "1.38.0", features = ["io-std", "macros", "rt-multi-thread"] } unicode-segmentation = "1.11.0" zbus = { version = "4.3.0", default-features = false, features = ["tokio"] } diff --git a/src/bin/icon.rs b/src/bin/icon.rs index 6f2487f..4ffaaae 100644 --- a/src/bin/icon.rs +++ b/src/bin/icon.rs @@ -1,54 +1,52 @@ -use std::sync::Arc; - use i3blocks::{ - color_listener, - dbus::{player::PlayerProxy, playerctld::PlayerctldProxy}, - i3bar::Block, - player_listener, + dbus::{media_player2::MediaPlayer2Proxy, player::PlaybackStatus, playerctld::PlayerctldProxy}, + i3bar::{Block, Click}, + Button, Component, Error, }; -use tokio::{sync::Mutex, task::JoinSet}; use zbus::Connection; #[tokio::main] async fn main() -> Result<(), main_error::MainError> { - 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 conn = Connection::session().await?; - let proxy = PlayerctldProxy::builder(&conn).build().await?; - - tokio::spawn(player_listener(tx_player, proxy)); - - let status: Arc<Mutex<Block>> = Arc::new(Mutex::new(Block { + i3blocks::run::<Icon>(Block { full_text: " ".into(), ..Default::default() - })); + }) + .await + .map_err(Into::into) +} - loop { - tokio::select! { - Some(name) = rx_player.recv() => { - join_set.shutdown().await; - if name.is_empty() { - let mut status = status.lock().await; - status.full_text = Default::default(); - } else { - let proxy = PlayerProxy::builder(&conn) - .destination(name)? - .build() - .await?; - join_set.spawn(color_listener(tx_status.clone(), proxy.clone())); - } - } - Some((color, background)) = rx_status.recv() => { - let mut status = status.lock().await; - status.color = color; - status.background = background; +pub struct Icon; + +impl Component for Icon { + const NAME: &'static str = "icon"; + type Updater = (); + type Colorer = PlaybackStatus; + type Handler = Self; +} + +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?, + _ => {} } - let s = status.lock().await; - s.write_stdout()?; + Ok(()) } } diff --git a/src/bin/next.rs b/src/bin/next.rs index 108432c..2e3fd95 100644 --- a/src/bin/next.rs +++ b/src/bin/next.rs @@ -1,72 +1,69 @@ use std::sync::Arc; -use futures_util::StreamExt; use i3blocks::{ - color_listener, - dbus::{player::PlayerProxy, playerctld::PlayerctldProxy}, - i3bar::Block, - player_listener, Error, -}; -use tokio::{ - sync::{mpsc::Sender, Mutex}, - task::JoinSet, + dbus::player::{PlaybackStatus, PlayerProxy}, + i3bar::{Block, Click}, + Button, Component, Error, Update, }; +use tokio::sync::{mpsc::Sender, Mutex}; use zbus::Connection; #[tokio::main] async fn main() -> Result<(), main_error::MainError> { - let mut join_set = JoinSet::new(); + i3blocks::run::<Next>(Default::default()) + .await + .map_err(Into::into) +} - 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); +pub struct Next; - let conn = Connection::session().await?; - let proxy = PlayerctldProxy::builder(&conn).build().await?; +impl Component for Next { + const NAME: &'static str = "next"; + type Updater = Self; + type Colorer = PlaybackStatus; + type Handler = Self; +} - tokio::spawn(player_listener(tx_player, proxy)); +impl Update for Next { + type Value = bool; - let status: Arc<Mutex<Block>> = Default::default(); + async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> { + use futures_util::StreamExt; - loop { - tokio::select! { - Some(name) = rx_player.recv() => { - join_set.shutdown().await; - if name.is_empty() { - let mut status = status.lock().await; - status.full_text = Default::default(); - } else { - let proxy = PlayerProxy::builder(&conn) - .destination(name)? - .build() - .await?; - join_set.spawn(color_listener(tx_status.clone(), proxy.clone())); - join_set.spawn(value_listener(tx_value.clone(), proxy)); - } - } - Some((color, background)) = rx_status.recv() => { - let mut status = status.lock().await; - status.color = color; - status.background = background; - } - Some(value) = rx_value.recv() => { - let mut status = status.lock().await; - status.full_text = value.then_some( " ".into()).unwrap_or_default() + tx.send(proxy.can_go_previous().await?).await?; + 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(()) + } - let s = status.lock().await; - s.write_stdout()?; + async fn update(value: Self::Value, block: Arc<Mutex<Block>>) -> Result<bool, Error> { + let mut block = block.lock().await; + block.full_text = value.then_some(" ".into()).unwrap_or_default(); + Ok(true) } } -pub async fn value_listener(tx: Sender<bool>, proxy: PlayerProxy<'_>) -> Result<(), Error> { - tx.send(proxy.can_go_next().await?).await?; - 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?; +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(()) } - Ok(()) } diff --git a/src/bin/play.rs b/src/bin/play.rs index 0863b7a..bf4f489 100644 --- a/src/bin/play.rs +++ b/src/bin/play.rs @@ -1,96 +1,79 @@ use std::sync::Arc; -use futures_util::StreamExt; use i3blocks::{ - dbus::{ - player::{PlaybackStatus, PlayerProxy}, - playerctld::PlayerctldProxy, - }, - i3bar::Block, - Error, -}; -use tokio::{ - sync::{mpsc::Sender, Mutex}, - task::JoinSet, + dbus::player::{PlaybackStatus, PlayerProxy}, + i3bar::{Block, Click}, + Button, Component, Error, Update, }; +use tokio::sync::{mpsc::Sender, Mutex}; use zbus::Connection; #[tokio::main] async fn main() -> Result<(), main_error::MainError> { - let mut join_set = JoinSet::new(); + i3blocks::run::<Play>(Default::default()) + .await + .map_err(Into::into) +} - let (tx_player, mut rx_player) = tokio::sync::mpsc::channel(128); - let (tx_value, mut rx_value) = tokio::sync::mpsc::channel(128); +pub struct Play; - let conn = Connection::session().await?; - let proxy = PlayerctldProxy::builder(&conn).build().await?; +impl Component for Play { + const NAME: &'static str = "play"; + type Updater = Self; + type Colorer = (); + type Handler = Self; +} - tokio::spawn(player_listener(tx_player, proxy)); +impl Update for Play { + type Value = PlaybackStatus; - let status: Arc<Mutex<Block>> = Default::default(); + async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> { + use futures_util::StreamExt; - loop { - tokio::select! { - Some(name) = rx_player.recv() => { - println!("name: {name:?}"); - join_set.shutdown().await; - if name.is_empty() { - let mut status = status.lock().await; - status.full_text = Default::default(); - } else { - let proxy = PlayerProxy::builder(&conn) - .destination(name)? - .build() - .await?; - join_set.spawn(value_listener(tx_value.clone(), proxy)); - } - } - Some(value) = rx_value.recv() => { - println!("value: {value:?}"); - let mut status = status.lock().await; - let (color, background) = value.into(); - status.color = color; - status.background = background; - status.full_text = match value { - PlaybackStatus::Playing => " ", - _ => " ", - }.into(); + tx.send(proxy.playback_status().await?).await?; + 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(value).await?; } } - - let s = status.lock().await; - s.write_stdout()?; + Ok(()) } -} -pub async fn value_listener( - tx: Sender<PlaybackStatus>, - proxy: PlayerProxy<'_>, -) -> Result<(), Error> { - tx.send(proxy.playback_status().await?).await?; - 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(value).await?; - } + async fn update(value: Self::Value, block: Arc<Mutex<Block>>) -> Result<bool, Error> { + 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 (full_text, color, background) = match value { + PlaybackStatus::Playing => (" ", black.clone(), cyan.clone()), + PlaybackStatus::Paused => (" ", black.clone(), yellow.clone()), + PlaybackStatus::Stopped => (" ", None, None), + }; + + let mut block = block.lock().await; + block.full_text = full_text.into(); + block.color = color; + block.background = background; + Ok(true) } - Ok(()) } -async fn player_listener(tx: Sender<String>, proxy: PlayerctldProxy<'_>) -> Result<(), Error> { - tx.send( - proxy - .player_names() - .await? - .into_iter() - .next() - .unwrap_or_default(), - ) - .await?; +impl Button for Play { + async fn handle(conn: Connection, click: Click) -> Result<(), Error> { + let Some(name) = click.instance else { + return Ok(()); + }; + + if click.button == 1 { + PlayerProxy::builder(&conn) + .destination(name)? + .build() + .await? + .play_pause() + .await? + } - let mut stream = proxy.receive_active_player_change_begin().await?; - while let Some(signal) = stream.next().await { - tx.send(signal.args()?.name.to_owned()).await?; + Ok(()) } - Ok(()) } diff --git a/src/bin/prev.rs b/src/bin/prev.rs index f7f8dd9..7eec6c3 100644 --- a/src/bin/prev.rs +++ b/src/bin/prev.rs @@ -1,72 +1,69 @@ use std::sync::Arc; -use futures_util::StreamExt; use i3blocks::{ - color_listener, - dbus::{player::PlayerProxy, playerctld::PlayerctldProxy}, - i3bar::Block, - player_listener, Error, -}; -use tokio::{ - sync::{mpsc::Sender, Mutex}, - task::JoinSet, + dbus::player::{PlaybackStatus, PlayerProxy}, + i3bar::{Block, Click}, + Button, Component, Error, Update, }; +use tokio::sync::{mpsc::Sender, Mutex}; use zbus::Connection; #[tokio::main] async fn main() -> Result<(), main_error::MainError> { - let mut join_set = JoinSet::new(); + i3blocks::run::<Previous>(Default::default()) + .await + .map_err(Into::into) +} - 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); +pub struct Previous; - let conn = Connection::session().await?; - let proxy = PlayerctldProxy::builder(&conn).build().await?; +impl Component for Previous { + const NAME: &'static str = "previous"; + type Updater = Self; + type Colorer = PlaybackStatus; + type Handler = Self; +} - tokio::spawn(player_listener(tx_player, proxy)); +impl Update for Previous { + type Value = bool; - let status: Arc<Mutex<Block>> = Default::default(); + async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> { + use futures_util::StreamExt; - loop { - tokio::select! { - Some(name) = rx_player.recv() => { - join_set.shutdown().await; - if name.is_empty() { - let mut status = status.lock().await; - status.full_text = Default::default(); - } else { - let proxy = PlayerProxy::builder(&conn) - .destination(name)? - .build() - .await?; - join_set.spawn(color_listener(tx_status.clone(), proxy.clone())); - join_set.spawn(value_listener(tx_value.clone(), proxy)); - } - } - Some((color, background)) = rx_status.recv() => { - let mut status = status.lock().await; - status.color = color; - status.background = background; - } - Some(value) = rx_value.recv() => { - let mut status = status.lock().await; - status.full_text = value.then_some( " ".into()).unwrap_or_default() + tx.send(proxy.can_go_previous().await?).await?; + 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(()) + } - let s = status.lock().await; - s.write_stdout()?; + async fn update(value: Self::Value, block: Arc<Mutex<Block>>) -> Result<bool, Error> { + let mut block = block.lock().await; + block.full_text = value.then_some(" ".into()).unwrap_or_default(); + Ok(true) } } -pub async fn value_listener(tx: Sender<bool>, proxy: PlayerProxy<'_>) -> Result<(), Error> { - tx.send(proxy.can_go_previous().await?).await?; - 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?; +impl Button for Previous { + 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(()) } - Ok(()) } diff --git a/src/bin/title.rs b/src/bin/title.rs index 165e715..3daee26 100644 --- a/src/bin/title.rs +++ b/src/bin/title.rs @@ -1,113 +1,120 @@ -use std::{sync::Arc, time::Duration}; +use std::{collections::HashMap, sync::Arc, time::Duration}; -use futures_util::StreamExt; use i3blocks::{ - color_listener, - dbus::{player::PlayerProxy, playerctld::PlayerctldProxy}, + dbus::player::{PlaybackStatus, PlayerProxy}, i3bar::Block, - player_listener, Error, + Component, Error, Update, }; use tokio::{ sync::{mpsc::Sender, Mutex}, task::{AbortHandle, JoinSet}, }; -use zbus::Connection; +use zbus::zvariant::OwnedValue; const TICK_RATE: Duration = Duration::from_millis(500); #[tokio::main] async fn main() -> Result<(), main_error::MainError> { - 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 conn = Connection::session().await?; - let proxy = PlayerctldProxy::builder(&conn).build().await?; - - tokio::spawn(player_listener(tx_player, proxy)); - - let status: Arc<Mutex<Block>> = Default::default(); - let mut rotator: Option<AbortHandle> = None; - - let mut interval = tokio::time::interval(TICK_RATE); - let mut chars = Vec::new(); - - loop { - tokio::select! { - Some(name) = rx_player.recv() => { - join_set.shutdown().await; - if name.is_empty() { - let mut status = status.lock().await; - status.full_text = Default::default(); - } else { - let proxy = PlayerProxy::builder(&conn) - .destination(name)? - .build() - .await?; - join_set.spawn(color_listener(tx_status.clone(), proxy.clone())); - join_set.spawn(value_listener(tx_value.clone(), proxy)); - } - } - Some((color, background)) = rx_status.recv() => { - let mut status = status.lock().await; - status.color = color; - status.background = background; - } - Some(mut value) = rx_value.recv() => { - if let Some(h) = rotator.take() { - h.abort(); - } + i3blocks::run::<Title>(Default::default()) + .await + .map_err(Into::into) +} - value.push(' '); - if value.len() >= 10 { - chars = value.chars().collect::<Vec<char>>(); - let mut status = status.lock().await; - status.full_text = format!("{} ", String::from_iter(chars[0..10].iter())); - } else { - chars = Default::default(); - let mut status = status.lock().await; - status.full_text = value; - } - } - _ = interval.tick(), if !chars.is_empty() => { - let mut status = status.lock().await; - status.full_text = format!("{} ", String::from_iter(chars[0..10].iter())); - chars.rotate_left(1); +pub struct Title; + +impl Component for Title { + const NAME: &'static str = "title"; + type Updater = Self; + type Colorer = PlaybackStatus; + type Handler = (); +} + +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 = None; + let mut old_title = None; + + Self::handle_metadata( + tx.clone(), + proxy.metadata().await?, + &mut old_title, + &mut rotator, + &mut join_set, + ) + .await?; + + let mut stream = proxy.receive_metadata_changed().await; + while let Some(signal) = stream.next().await { + if let Ok(metadata) = signal.get().await { + Self::handle_metadata( + tx.clone(), + metadata, + &mut old_title, + &mut rotator, + &mut join_set, + ) + .await?; } } + Ok(()) + } + + async fn update(mut value: Self::Value, block: Arc<Mutex<Block>>) -> Result<bool, Error> { + value.push(' '); + let mut block = block.lock().await; + block.full_text = value; - let s = status.lock().await; - s.write_stdout()?; + Ok(true) } } -pub async fn value_listener(tx: Sender<String>, proxy: PlayerProxy<'_>) -> Result<(), Error> { - let mut old_title = if let Some(value) = proxy.metadata().await?.get("xesam:title") { - let title: String = value.try_to_owned()?.try_into()?; - tx.send(title.clone()).await?; - Some(title) - } else { - None - }; - - let mut stream = proxy.receive_metadata_changed().await; - while let Some(signal) = stream.next().await { - if let Ok(value) = signal.get().await { - let Some(owned_value) = value.get("xesam:title") else { - continue; - }; - - let title: String = owned_value.try_to_owned()?.try_into()?; - - if old_title.as_ref().is_some_and(|s| *s == title) { - continue; - } +impl Title { + async fn handle_metadata( + tx: Sender<<<Self as Component>::Updater as Update>::Value>, + metadata: HashMap<String, OwnedValue>, + old_title: &mut Option<String>, + rotator: &mut Option<AbortHandle>, + join_set: &mut JoinSet<Result<(), Error>>, + ) -> Result<(), Error> { + let Some(owned_value) = metadata.get("xesam:title") else { + return Ok(()); + }; + + let title: String = owned_value.try_to_owned()?.try_into()?; + + if old_title.as_ref().is_some_and(|s| *s == title) { + return Ok(()); + } + + if let Some(h) = rotator.take() { + h.abort(); + }; - old_title = Some(title.clone()); + *old_title = Some(title.clone()); + + if title.len() >= 10 { + let mut chars = title.clone().chars().collect::<Vec<char>>(); + let tx = tx.clone(); + + *rotator = Some(join_set.spawn(async move { + let mut interval = tokio::time::interval(TICK_RATE); + loop { + interval.tick().await; + tx.send(format!("{} ", String::from_iter(chars[0..10].iter()))) + .await + .unwrap(); + chars.rotate_left(1); + } + })); + } else { tx.send(title).await?; } + + Ok(()) } - Ok(()) } diff --git a/src/bin/volume.rs b/src/bin/volume.rs index 5e9d1d1..b5182b3 100644 --- a/src/bin/volume.rs +++ b/src/bin/volume.rs @@ -1,76 +1,77 @@ use std::sync::Arc; -use futures_util::StreamExt; use i3blocks::{ - color_listener, - dbus::{player::PlayerProxy, playerctld::PlayerctldProxy}, - i3bar::Block, - player_listener, Error, -}; -use tokio::{ - sync::{mpsc::Sender, Mutex}, - task::JoinSet, + dbus::player::{PlaybackStatus, PlayerProxy}, + i3bar::{Block, Click}, + Button, Component, Error, Update, }; +use tokio::sync::{mpsc::Sender, Mutex}; use zbus::Connection; #[tokio::main] async fn main() -> Result<(), main_error::MainError> { - let mut join_set = JoinSet::new(); + i3blocks::run::<Volume>(Default::default()) + .await + .map_err(Into::into) +} - 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); +pub struct Volume; - let conn = Connection::session().await?; - let proxy = PlayerctldProxy::builder(&conn).build().await?; +impl Component for Volume { + const NAME: &'static str = "volume"; + type Updater = Self; + type Colorer = PlaybackStatus; + type Handler = Self; +} - tokio::spawn(player_listener(tx_player, proxy)); +impl Update for Volume { + type Value = f64; - let status: Arc<Mutex<Block>> = Default::default(); + async fn listen(tx: Sender<Self::Value>, proxy: PlayerProxy<'_>) -> Result<(), Error> { + use futures_util::StreamExt; - loop { - tokio::select! { - Some(name) = rx_player.recv() => { - join_set.shutdown().await; - if name.is_empty() { - let mut status = status.lock().await; - status.full_text = Default::default(); - } else { - let proxy = PlayerProxy::builder(&conn) - .destination(name)? - .build() - .await?; - join_set.spawn(color_listener(tx_status.clone(), proxy.clone())); - join_set.spawn(value_listener(tx_value.clone(), proxy)); - } - } - Some((color, background)) = rx_status.recv() => { - let mut status = status.lock().await; - status.color = color; - status.background = background; - } - Some(value) = rx_value.recv() => { - let mut status = status.lock().await; - status.full_text = match (value * 100_f64) as u32 { - v @ 66.. => format!(" {v}% "), - v @ 33.. => format!(" {v}% "), - v @ 0.. => format!(" {v}% "), - }; + tx.send(proxy.volume().await?).await?; + let mut stream = proxy.receive_volume_changed().await; + while let Some(signal) = stream.next().await { + if let Ok(value) = signal.get().await { + tx.send(value).await?; } } + Ok(()) + } - let s = status.lock().await; - s.write_stdout()?; + async fn update(value: Self::Value, block: Arc<Mutex<Block>>) -> Result<bool, Error> { + let mut block = block.lock().await; + block.full_text = match (value * 100_f64) as u32 { + v @ 66.. => format!(" {v}% "), + v @ 33.. => format!(" {v}% "), + v @ 0.. => format!(" {v}% "), + }; + Ok(true) } } -pub async fn value_listener(tx: Sender<f64>, proxy: PlayerProxy<'_>) -> Result<(), Error> { - tx.send(proxy.volume().await?).await?; - let mut stream = proxy.receive_volume_changed().await; - while let Some(signal) = stream.next().await { - if let Ok(value) = signal.get().await { - tx.send(value).await?; +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 { + 4 if proxy.can_control().await? => { + proxy.set_volume(proxy.volume().await? - 0.05).await? + } + 5 if proxy.can_control().await? => { + proxy.set_volume(proxy.volume().await? + 0.05).await? + } + _ => {} } + + Ok(()) } - Ok(()) } diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 1e38cce..0000000 --- a/src/client.rs +++ /dev/null @@ -1,31 +0,0 @@ -use serde::{Deserialize, Serialize}; -use tokio::{io::AsyncWriteExt, net::UnixStream}; - -use crate::Error; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub enum ClientKind { - #[serde(rename = "mpris-icon")] - Icon, - #[serde(rename = "mpris-title")] - Title, - #[serde(rename = "mpris-prev")] - Prev, - #[serde(rename = "mpris-playPause")] - PlayPause, - #[serde(rename = "mpris-next")] - Next, - #[serde(rename = "mpris-volume")] - Volume, -} - -async fn client(kind: ClientKind) -> Result<(), Error> { - let stream = UnixStream::connect("/tmp/i3blocks-mpris.sock").await?; - let (mut reader, mut writer) = stream.into_split(); - - let buf = bincode::serialize(&kind)?; - writer.write_all(&buf).await?; - - todo!() -} - diff --git a/src/color.rs b/src/color.rs deleted file mode 100644 index 076d995..0000000 --- a/src/color.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::{fmt::Display, str::FromStr}; - -use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; - -use crate::Error; - -/// An RGBA color. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] -pub struct Color { - pub r: u8, - pub g: u8, - pub b: u8, - pub a: u8, -} - -impl Color { - pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Color { - Color { r, g, b, a } - } -} - -impl FromStr for Color { - type Err = Error; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - 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}")))?; - let mut bytes = num.to_be_bytes(); - - if s.len() == 6 { - bytes[0] = 0xff; - bytes.rotate_left(1); - } else if s.len() != 9 { - return Err(Error::Color(format!("Invalid color format: {s}"))); - } - - let [r, g, b, a] = bytes; - - Ok(Self { r, g, b, a }) - } -} - -impl Display for Color { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "#{:02X}{:02X}{:02X}{:02X}", - self.r, self.g, self.b, self.a - ) - } -} - -impl Serialize for Color { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for Color { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: Deserializer<'de>, - { - struct ColorVisitor; - - impl<'de> Visitor<'de> for ColorVisitor { - type Value = Color; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("color") - } - - fn visit_str<E>(self, s: &str) -> Result<Color, E> - where - E: serde::de::Error, - { - s.parse().map_err(serde::de::Error::custom) - } - } - - deserializer.deserialize_any(ColorVisitor) - } -} - -pub struct Scheme { - pub black: Color, - pub red: Color, - pub green: Color, - pub yellow: Color, - pub blue: Color, - pub magenta: Color, - pub cyan: Color, - pub white: Color, - pub gray: Color, -} - -impl Default for Scheme { - fn default() -> Self { - Self { - black: Color::new(0, 0, 0, 255), - red: Color::new(255, 0, 0, 255), - green: Color::new(0, 255, 0, 255), - yellow: Color::new(255, 255, 0, 255), - blue: Color::new(0, 0, 255, 255), - magenta: Color::new(255, 0, 255, 255), - cyan: Color::new(0, 255, 255, 255), - white: Color::new(255, 255, 255, 255), - gray: Color::new(128, 128, 128, 255), - } - } -} diff --git a/src/component.rs b/src/component.rs new file mode 100644 index 0000000..b1d6b0a --- /dev/null +++ b/src/component.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use tokio::sync::{mpsc::Sender, Mutex}; + +use crate::{ + dbus::player::{PlaybackStatus, PlayerProxy}, + i3bar::Block, + Error, +}; + +pub trait Update: Send + 'static { + type Value: Send; + + fn listen( + tx: Sender<Self::Value>, + proxy: PlayerProxy<'_>, + ) -> impl std::future::Future<Output = Result<(), Error>> + Send; + + fn update( + value: Self::Value, + block: Arc<Mutex<Block>>, + ) -> impl std::future::Future<Output = Result<bool, Error>> + Send; +} + +pub trait Button { + fn handle( + conn: zbus::Connection, + click: crate::i3bar::Click, + ) -> impl std::future::Future<Output = Result<(), Error>> + Send; +} + +pub trait Component: Send + 'static { + const NAME: &'static str; + type Updater: Update; + type Colorer: Update; + type Handler: Button; +} + +impl Update for () { + type Value = (); + + async fn listen(_: Sender<Self::Value>, _: PlayerProxy<'_>) -> Result<(), Error> { + Ok(()) + } + + async fn update(_: Self::Value, _: Arc<Mutex<Block>>) -> Result<bool, Error> { + Ok(false) + } +} + +impl Button for () { + async fn handle(_: zbus::Connection, _: crate::i3bar::Click) -> Result<(), Error> { + Ok(()) + } +} + +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<bool, Error> { + let mut block = block.lock().await; + block.color = color; + block.background = background; + Ok(true) + } +} diff --git a/src/components.rs b/src/components.rs deleted file mode 100644 index 1548365..0000000 --- a/src/components.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::{collections::HashMap, option::Option, time::Duration}; - -use futures_util::StreamExt; -use tokio::{sync::mpsc::Sender, task::JoinHandle}; -use zbus::zvariant::OwnedValue; - -use crate::{ - dbus::player::{PlaybackStatus, PlayerProxy}, - error::Result, - i3bar::Block, -}; - -const TICK_RATE: Duration = Duration::from_millis(500); - -pub trait Component: std::marker::Sized { - fn listen( - &self, - proxy: PlayerProxy<'static>, - tx: Sender<Block>, - ) -> impl std::future::Future<Output = Result<()>> + Send; -} - -#[derive(Debug, Clone, Default)] -pub struct Icon; - -impl Component for Icon { - async fn listen(&self, _: PlayerProxy<'static>, tx: Sender<Block>) -> Result<()> { - tx.send(Block { - full_text: " ".into(), - instance: "mpris-icon".into(), - ..Default::default() - }) - .await - .map_err(Into::into) - } -} - -#[derive(Debug, Clone, Default)] -pub struct Title; - -impl Title { - async fn process_metadata( - tx: Sender<Block>, - metadata: HashMap<String, OwnedValue>, - rotator: &mut Option<JoinHandle<Result<()>>>, - last: &mut Option<String>, - ) -> Result<()> { - let Some(val) = metadata.get("xesam:title") else { - return Ok(()); - }; - - let title: String = val.try_to_owned()?.try_into()?; - - if last.as_ref().is_some_and(|s| *s == title) { - return Ok(()); - } - - let mut block = Block { - instance: "mpris-title".into(), - ..Default::default() - }; - - if let Some(h) = rotator.take() { - h.abort() - }; - - if title.len() > 10 { - let mut chars = title.chars().collect::<Vec<char>>(); - *last = Some(title); - *rotator = Some(tokio::spawn(async move { - let mut interval = tokio::time::interval(TICK_RATE); - chars.push(' '); - - loop { - interval.tick().await; - block.full_text = chars[0..10].iter().collect(); - tx.send(block.clone()).await?; - chars.rotate_left(1); - } - })); - } else { - *last = Some(title.clone()); - block.full_text = title; - tx.send(block).await?; - } - - Ok(()) - } -} - -impl Component for Title { - async fn listen(&self, proxy: PlayerProxy<'static>, tx: Sender<Block>) -> Result<()> { - let mut stream = proxy.receive_metadata_changed().await; - let mut rotator = None; - let mut last = None; - - Self::process_metadata(tx.clone(), proxy.metadata().await?, &mut rotator, &mut last) - .await?; - - while let Some(signal) = stream.next().await { - if let Ok(metadata) = signal.get().await { - Self::process_metadata(tx.clone(), metadata, &mut rotator, &mut last).await?; - }; - } - Ok(()) - } -} - -#[derive(Debug, Clone, Default)] -pub struct Prev; - -impl Component for Prev { - async fn listen(&self, proxy: PlayerProxy<'static>, tx: Sender<Block>) -> Result<()> { - let mut block = Block { - full_text: proxy - .can_go_previous() - .await - .unwrap_or_default() - .then_some(" ".into()) - .unwrap_or_default(), - instance: "mpris-prev".into(), - ..Default::default() - }; - - tx.send(block.clone()).await?; - - let mut stream = proxy.receive_can_go_previous_changed().await; - while let Some(signal) = stream.next().await { - if let Ok(val) = signal.get().await { - block.full_text = val.then_some(" ".into()).unwrap_or_default(); - - tx.send(block.clone()).await?; - }; - } - Ok(()) - } -} - -#[derive(Debug, Clone, Default)] -pub struct PlayPause; - -impl Component for PlayPause { - async fn listen(&self, proxy: PlayerProxy<'static>, tx: Sender<Block>) -> Result<()> { - let mut block = Block { - instance: "mpris-playpause".into(), - ..Default::default() - }; - - block.full_text = match proxy.playback_status().await? { - PlaybackStatus::Playing => " ", - PlaybackStatus::Paused | PlaybackStatus::Stopped => " ", - } - .to_string(); - - tx.send(block.clone()).await?; - - let mut stream = proxy.receive_playback_status_changed().await; - while let Some(signal) = stream.next().await { - if let Ok(val) = signal.get().await { - block.full_text = match val { - PlaybackStatus::Playing => " ", - PlaybackStatus::Paused | PlaybackStatus::Stopped => " ", - } - .to_string(); - - tx.send(block.clone()).await?; - }; - } - Ok(()) - } -} - -#[derive(Debug, Clone, Default)] -pub struct Next; - -impl Component for Next { - async fn listen(&self, proxy: PlayerProxy<'static>, tx: Sender<Block>) -> Result<()> { - let mut block = Block { - full_text: proxy - .can_go_next() - .await - .unwrap_or_default() - .then_some(" ".into()) - .unwrap_or_default(), - instance: "mpris-next".into(), - ..Default::default() - }; - - tx.send(block.clone()).await?; - - let mut stream = proxy.receive_can_go_next_changed().await; - while let Some(signal) = stream.next().await { - if let Ok(val) = signal.get().await { - block.full_text = val.then_some(" ".into()).unwrap_or_default(); - tx.send(block.clone()).await?; - }; - } - Ok(()) - } -} - -#[derive(Debug, Clone, Default)] -pub struct Volume; - -impl Component for Volume { - async fn listen(&self, proxy: PlayerProxy<'static>, tx: Sender<Block>) -> Result<()> { - let mut block = Block { - instance: "mpris-volume".into(), - ..Default::default() - }; - - block.full_text = match (proxy.volume().await? * 100_f64) as u32 { - v @ 66.. => format!(" {v}% "), - v @ 33.. => format!(" {v}% "), - v @ 0.. => format!(" {v}% "), - }; - - tx.send(block.clone()).await?; - - let mut stream = proxy.receive_volume_changed().await; - while let Some(signal) = stream.next().await { - if let Ok(val) = signal.get().await { - block.full_text = match (val * 100_f64) as u32 { - v @ 66.. => format!(" {v}% "), - v @ 33.. => format!(" {v}% "), - v @ 0.. => format!(" {v}% "), - }; - - tx.send(block.clone()).await?; - }; - } - Ok(()) - } -} diff --git a/src/dbus_server.rs b/src/dbus_server.rs deleted file mode 100644 index 6f7fee4..0000000 --- a/src/dbus_server.rs +++ /dev/null @@ -1,190 +0,0 @@ -use futures_util::StreamExt; -use std::sync::Arc; - -use tokio::{ - io::{AsyncReadExt, BufReader}, - net::{UnixListener, UnixStream}, - sync::{mpsc::Sender, Mutex, Notify}, - task::JoinSet, -}; -use zbus::{ - fdo::{DBusProxy, NameOwnerChangedArgs}, - interface, - names::OwnedBusName, - zvariant::Optional, - Connection, -}; - -use crate::{ - client::ClientKind, - dbus::{ - media_player2::MediaPlayer2Proxy, - player::{PlaybackStatus, PlayerProxy}, - }, - i3bar::Click, - player::Event, - printer::Status, - Error, -}; - -pub struct MprisStatus { - player: Option<String>, - title: String, - can_go_previous: bool, - playback_status: PlaybackStatus, - can_go_next: bool, - volume: Option<String> - color: Option<String>, - background: Option<String>, -} - -#[interface( - name = "org.tobyvin.i3blocks.Mpris", - proxy( - default_path = "/org/i3blocks/Mpris", - default_service = "org.i3blocks.Mpris", - ) -)] -impl MprisStatus { - #[zbus(property)] - async fn title(&self) -> Optional<String> { - if self.player.is_none() { - return None.into(); - } - - Some(self.title.clone()).into() - } - - #[zbus(property)] - async fn can_go_previous(&self) -> Optional<String> { - if self.player.is_none() { - return None.into(); - } - self.can_go_previous.then_some(" ".into()).into() - } - - #[zbus(property)] - async fn playback_status(&self) -> Optional<String> { - if self.player.is_none() { - return None.into(); - } - - match self.playback_status { - PlaybackStatus::Playing => Some(" ".into()), - _ => Some(" ".into()), - } - .into() - } - - #[zbus(property)] - async fn can_go_next(&self) -> Optional<String> { - if self.player.is_none() { - return None.into(); - } - - self.can_go_next.then_some(" ".into()).into() - } -} - -impl MprisStatus { - async fn set_title(conn: Connection, value: String) -> Result<(), Error> { - let iface_ref = conn - .object_server() - .interface::<_, Self>("/org/i3blocks/Mpris") - .await?; - let mut iface = iface_ref.get_mut().await; - iface.title = value; - iface - .title_changed(iface_ref.signal_context()) - .await - .map_err(Into::into) - } - - async fn set_can_go_previous(conn: Connection, value: bool) -> Result<(), Error> { - let iface_ref = conn - .object_server() - .interface::<_, Self>("/org/i3blocks/Mpris") - .await?; - let mut iface = iface_ref.get_mut().await; - iface.can_go_previous = value; - iface - .can_go_previous_changed(iface_ref.signal_context()) - .await - .map_err(Into::into) - } - - async fn set_playback_status(conn: Connection, value: PlaybackStatus) -> Result<(), Error> { - let iface_ref = conn - .object_server() - .interface::<_, Self>("/org/i3blocks/Mpris") - .await?; - let mut iface = iface_ref.get_mut().await; - iface.playback_status = value; - iface - .playback_status_changed(iface_ref.signal_context()) - .await - .map_err(Into::into) - } - - async fn set_can_go_next(conn: Connection, value: bool) -> Result<(), Error> { - let iface_ref = conn - .object_server() - .interface::<_, Self>("/org/i3blocks/Mpris") - .await?; - let mut iface = iface_ref.get_mut().await; - iface.can_go_next = value; - iface - .can_go_next_changed(iface_ref.signal_context()) - .await - .map_err(Into::into) - } - - async fn set_volume(conn: Connection, value: f64) -> Result<(), Error> { - let iface_ref = conn - .object_server() - .interface::<_, Self>("/org/i3blocks/Mpris") - .await?; - let mut iface = iface_ref.get_mut().await; - - iface.volume = match (value * 100_f64) as u32 { - v @ 66.. => format!(" {v}% "), - v @ 33.. => format!(" {v}% "), - v @ 0.. => format!(" {v}% "), - }; - - iface - .volume_changed(iface_ref.signal_context()) - .await - .map_err(Into::into) - } -} - -//full_text.push_str(" "); -// -//if let Some(title) = self.title.as_ref() { -// full_text.push_str(title); -// full_text.push(' '); -//} -// -//if self.can_go_previous { -// full_text.push_str(" "); -//} -// -//if self.playback_status == PlaybackStatus::Playing { -// full_text.push_str(" "); -//} else { -// full_text.push_str(" "); -//} -// -//if self.can_go_next { -// full_text.push_str(" "); -//} -// -//if let Some(volume) = self.volume.map(|v| (v * 100_f64) as u32) { -// 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}% ")), -// } -//} - diff --git a/src/i3bar.rs b/src/i3bar.rs index e9c5959..94cd036 100644 --- a/src/i3bar.rs +++ b/src/i3bar.rs @@ -6,8 +6,8 @@ use crate::Error; #[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 short_text: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub color: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] @@ -28,8 +28,8 @@ pub struct Block { pub align: Option<Align>, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option<String>, - #[serde(skip_serializing_if = "String::is_empty")] - pub instance: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub instance: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub urgent: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")] @@ -69,6 +69,7 @@ pub enum MinWidth { } #[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(default)] pub struct Click { pub name: Option<String>, pub instance: Option<String>, @@ -1,68 +1,109 @@ -use dbus::{ - player::{PlaybackStatus, PlayerProxy}, - playerctld::PlayerctldProxy, -}; -pub use error::{Error, Result}; +use std::sync::Arc; use futures_util::StreamExt; -use tokio::sync::mpsc::Sender; +use tokio::{sync::Mutex, task::JoinSet}; +use zbus::Connection; -pub mod color; +pub use crate::{ + component::{Button, Component, Update}, + error::{Error, Result}, +}; +use crate::{ + dbus::{player::PlayerProxy, playerctld::PlayerctldProxy}, + i3bar::{Block, Click}, +}; + +pub mod component; pub mod dbus; pub mod error; pub mod i3bar; -const BLACK: &str = "#1d2021"; -const YELLOW: &str = "#fabd2f"; -const CYAN: &str = "#8ec07c"; - const IGNORED: [&str; 2] = ["playerctld", "kdeconnect"]; -impl From<PlaybackStatus> for (Option<String>, Option<String>) { - fn from(value: PlaybackStatus) -> Self { - match value { - PlaybackStatus::Playing => (Some(BLACK.into()), Some(CYAN.into())), - PlaybackStatus::Paused => (Some(BLACK.into()), Some(YELLOW.into())), - PlaybackStatus::Stopped => (None, None), - } - } -} +pub async fn run<C>(mut block: Block) -> Result<(), Error> +where + C: Component, +{ + let conn = Connection::session().await?; -pub async fn color_listener( - tx: Sender<(Option<String>, Option<String>)>, - proxy: PlayerProxy<'_>, -) -> Result<()> { - 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(value.into()).await?; - } + block.name = Some(format!("mpris-{}", C::NAME)); + tokio::spawn(listeners::<C>(conn.clone(), block)); + + for click in std::io::stdin() + .lines() + .map_while(Result::ok) + .flat_map(|s| serde_json::from_str::<Click>(&s)) + { + <<C as Component>::Handler as Button>::handle(conn.clone(), click).await?; } + Ok(()) } -pub async fn player_listener(tx: Sender<String>, proxy: PlayerctldProxy<'_>) -> Result<(), Error> { - let mut last = proxy - .player_names() - .await? - .into_iter() - .next() - .unwrap_or_default(); +async fn listeners<C>(conn: Connection, block: Block) -> Result<(), Error> +where + C: Component, +{ + 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 proxy = PlayerctldProxy::builder(&conn).build().await?; - tx.send(last.clone()).await?; + tokio::spawn(async move { + let mut last = proxy + .player_names() + .await? + .into_iter() + .next() + .unwrap_or_default(); + tx_player.send(last.clone()).await?; + let mut stream = proxy.receive_active_player_change_end().await?; + while let Some(signal) = stream.next().await { + let name = signal.args()?.name.to_owned(); + if name != last + && name + .split('.') + .nth(3) + .is_some_and(|s| !IGNORED.contains(&s)) + { + last.clone_from(&name); + tx_player.send(name).await?; + } + } + Result::<_, Error>::Ok(()) + }); + + let block = Arc::new(Mutex::new(block)); - let mut stream = proxy.receive_active_player_change_end().await?; - while let Some(signal) = stream.next().await { - let name = signal.args()?.name.to_owned(); - if name != last - && name - .split('.') - .nth(3) - .is_some_and(|s| !IGNORED.contains(&s)) - { - last.clone_from(&name); - tx.send(name).await?; + loop { + let updated = tokio::select! { + Some(name) = rx_player.recv() => { + join_set.shutdown().await; + if name.is_empty() { + let mut block = block.lock().await; + block.full_text = String::new(); + block.instance = None; + true + } else { + let proxy = PlayerProxy::builder(&conn) + .destination(name)? + .build() + .await?; + join_set.spawn(<<C as Component>::Colorer as Update>::listen(tx_status.clone(), proxy.clone())); + join_set.spawn(<<C as Component>::Updater as Update>::listen(tx_value.clone(), proxy)); + false + } + } + Some(color) = rx_status.recv() => <<C as Component>::Colorer as Update>::update(color, block.clone()).await?, + Some(value) = rx_value.recv() => <<C as Component>::Updater as Update>::update(value, block.clone()).await? + }; + + if updated { + let s = block.lock().await; + s.write_stdout()?; } } - Ok(()) } diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 84e5e51..0000000 --- a/src/main.rs +++ /dev/null @@ -1,7 +0,0 @@ -use main_error::MainError; - -#[tokio::main] -async fn main() -> Result<(), MainError> { - todo!() - //i3blocks::mpris::mpris().await.map_err(Into::into) -} diff --git a/src/mpris.rs b/src/mpris.rs deleted file mode 100644 index 09aab74..0000000 --- a/src/mpris.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::marker::Sync; - -use futures_util::StreamExt; -use tokio::{sync::mpsc::Sender, task::JoinSet}; -use zbus::{ - fdo::{DBusProxy, NameOwnerChangedArgs, NameOwnerChangedStream}, - names::OwnedBusName, - Connection, -}; - -use crate::{ - components::Component, - dbus::{media_player2::MediaPlayer2Proxy, player::PlayerProxy}, - i3bar::Click, - player::{self, Event}, - Result, -}; - -const IGNORED: [&str; 1] = ["playerctld"]; - -#[derive(Debug, Clone)] -pub enum Instance { - Icon, - Title, - Prev, - PlayPause, - Next, - Volume, -} - -pub async fn mpris<T: Component + Sync>(component: &'static T) -> Result<()> { - let mut join_set = JoinSet::new(); - let (tx, rx) = tokio::sync::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(player::listener(conn.clone(), rx, component)); - - for line in std::io::stdin().lines() { - let click: Click = dbg!(serde_json::from_str(&line?)?); - - let Some(name) = click.name else { - continue; - }; - - let Some(instance) = click.instance else { - continue; - }; - - let player_proxy = PlayerProxy::builder(&conn) - .destination(name)? - .build() - .await?; - - let mpris_proxy = MediaPlayer2Proxy::builder(player_proxy.inner().connection()) - .destination(player_proxy.inner().destination())? - .build() - .await?; - - match (instance.as_ref(), click.button) { - ("mpris-icon", 1) if mpris_proxy.can_raise().await? => mpris_proxy.raise().await?, - ("mpris-title", _) => {} - ("mpris-prev", 1) => player_proxy.previous().await?, - ("mpris-playPause", 1) => player_proxy.play_pause().await?, - ("mpris-next", 1) => player_proxy.next().await?, - ("mpris-volume", 4) => { - player_proxy - .set_volume(player_proxy.volume().await? - 0.05) - .await? - } - ("mpris-volume", 5) => { - player_proxy - .set_volume(player_proxy.volume().await? + 0.05) - .await? - } - (_, 3) => tx.send(Event::Shift).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<()> { - 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/player.rs b/src/player.rs deleted file mode 100644 index 8dd3a51..0000000 --- a/src/player.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::collections::VecDeque; - -use tokio::{ - sync::{mpsc::Receiver, watch::Sender}, - task::{AbortHandle, JoinHandle, JoinSet}, -}; -use zbus::{names::OwnedBusName, Connection}; - -use crate::{ - components::Component, - dbus::player::{PlaybackStatus, PlayerProxy}, - printer::Status, - Result, -}; - -const BLACK: &str = "#1d2021"; -const YELLOW: &str = "#fabd2f"; -const CYAN: &str = "#8ec07c"; - -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), - Shift, -} - -pub async fn listener( - conn: Connection, - mut rx: Receiver<Event>, - wtx: Sender<Status>, -) -> Result<()> { - let mut players: VecDeque<OwnedBusName> = VecDeque::new(); - let mut active: Option<OwnedBusName> = Default::default(); - let mut listener: Option<AbortHandle> = None; - let mut printer: Option<AbortHandle> = None; - let mut join_set = JoinSet::new(); - - 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::Shift if !players.is_empty() => players.rotate_left(1), - _ => {} - }; - - if players.front() != active.as_ref() { - if active.is_some() { - if let Some(h) = listener.take() { - h.abort(); - } - - if let Some(h) = printer.take() { - h.abort(); - } - active = None; - } - - if let Some(name) = players.front().cloned() { - let proxy = PlayerProxy::builder(&conn) - .destination(name.clone())? - .build() - .await?; - - listener = Some(join_set.spawn(crate::printer::printer(proxy, wtx.clone()))); - } - } - } - - Ok(()) -} diff --git a/src/printer.rs b/src/printer.rs deleted file mode 100644 index 5a6b0ae..0000000 --- a/src/printer.rs +++ /dev/null @@ -1,255 +0,0 @@ -use std::{collections::HashMap, sync::Arc, time::Duration}; - -use futures_util::stream::StreamExt; -use serde::{Deserialize, Serialize}; -use tokio::{ - sync::{watch::Sender, Mutex}, - task::{JoinHandle, JoinSet}, -}; -use zbus::{proxy::PropertyStream, zvariant::OwnedValue}; - -use crate::{ - dbus::player::{PlaybackStatus, PlayerProxy}, - i3bar::Block, - Result, -}; - -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"; - -pub async fn printer( - proxy: PlayerProxy<'static>, - wtx: tokio::sync::watch::Sender<Status>, -) -> Result<()> { - let mut join_set: JoinSet<Result<()>> = JoinSet::new(); - let mut rotator = None; - - let status = Status { - 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(), - ..Default::default() - }; - - wtx.send_modify(|s| *s = status); - - process_metadata( - wtx.clone(), - proxy.metadata().await?, - &mut rotator, - &mut None, - ) - .await; - - join_set.spawn(metadata( - wtx.clone(), - proxy.receive_metadata_changed().await, - rotator, - )); - - join_set.spawn(can_go_previous( - wtx.clone(), - proxy.receive_can_go_previous_changed().await, - )); - - join_set.spawn(playback_state( - wtx.clone(), - proxy.receive_playback_status_changed().await, - )); - - join_set.spawn(can_go_next( - wtx.clone(), - proxy.receive_can_go_next_changed().await, - )); - - join_set.spawn(volume(wtx.clone(), proxy.receive_volume_changed().await)); - - while let Some(res) = join_set.join_next().await { - res??; - } - - Ok(()) -} - -async fn process_metadata( - wtx: Sender<Status>, - metadata: HashMap<String, OwnedValue>, - rotator: &mut Option<JoinHandle<Result<()>>>, - old_title: &mut Option<String>, -) { - let title: Option<String> = metadata - .get("xesam:title") - .and_then(|o| o.try_to_owned().ok()) - .and_then(|o| o.try_into().ok()); - - if *old_title == title { - return; - } - - *old_title = title.clone(); - let Some(title) = title else { - wtx.send_modify(|s| { - s.title = None; - }); - return; - }; - - if title.len() > 10 { - if let Some(h) = rotator.take() { - h.abort() - }; - - let wtx = wtx.clone(); - *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; - wtx.send_modify(|s| s.title = Some(chars[0..10].iter().collect())); - chars.rotate_left(1); - } - })); - } else { - wtx.send_modify(|s| s.title = Some(title)); - } -} - -async fn metadata( - wtx: Sender<Status>, - mut stream: PropertyStream<'_, HashMap<String, OwnedValue>>, - mut rotator: Option<JoinHandle<Result<()>>>, -) -> Result<()> { - let mut old_title = None; - while let Some(signal) = stream.next().await { - if let Ok(metadata) = signal.get().await { - process_metadata(wtx.clone(), metadata, &mut rotator, &mut old_title).await - }; - } - Ok(()) -} - -async fn can_go_previous(wtx: Sender<Status>, mut stream: PropertyStream<'_, bool>) -> Result<()> { - while let Some(signal) = stream.next().await { - if let Ok(val) = signal.get().await { - wtx.send_modify(|status| status.can_go_previous = val); - }; - } - Ok(()) -} - -async fn playback_state( - wtx: Sender<Status>, - mut stream: PropertyStream<'_, PlaybackStatus>, -) -> Result<()> { - while let Some(signal) = stream.next().await { - if let Ok(val) = signal.get().await { - let (color, background) = match val { - PlaybackStatus::Playing => (Some(BLACK.into()), Some(CYAN.into())), - PlaybackStatus::Paused => (Some(BLACK.into()), Some(YELLOW.into())), - PlaybackStatus::Stopped => (None, None), - }; - - wtx.send_modify(|status| { - status.playback_status = val; - status.color = color; - status.background = background; - }); - } - } - Ok(()) -} - -async fn can_go_next(wtx: Sender<Status>, mut stream: PropertyStream<'_, bool>) -> Result<()> { - while let Some(signal) = stream.next().await { - if let Ok(val) = signal.get().await { - wtx.send_modify(|status| status.can_go_next = val); - }; - } - Ok(()) -} - -async fn volume(wtx: Sender<Status>, mut stream: PropertyStream<'_, f64>) -> Result<()> { - while let Some(signal) = stream.next().await { - if let Ok(val) = signal.get().await { - wtx.send_modify(|status| status.volume = Some(val)); - }; - } - Ok(()) -} - -#[derive(Debug, Clone, Copy)] -pub enum Component { - Icon, - Title, - Prev, - Play, - Pause, - Next, - Volume, - Space, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct Status { - title: Option<String>, - can_go_previous: bool, - playback_status: PlaybackStatus, - can_go_next: bool, - volume: Option<f64>, - color: Option<String>, - background: Option<String>, -} - -impl Status { - fn build(&mut self) -> Block { - let mut full_text = String::new(); - full_text.push_str(" "); - - if let Some(title) = self.title.as_ref() { - full_text.push_str(title); - full_text.push(' '); - } - - if self.can_go_previous { - full_text.push_str(" "); - } - - if self.playback_status == PlaybackStatus::Playing { - full_text.push_str(" "); - } else { - full_text.push_str(" "); - } - - if self.can_go_next { - full_text.push_str(" "); - } - - if let Some(volume) = self.volume.map(|v| (v * 100_f64) as u32) { - 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}% ")), - } - } - - 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() - } - } -} diff --git a/src/server.rs b/src/server.rs deleted file mode 100644 index 4346923..0000000 --- a/src/server.rs +++ /dev/null @@ -1,173 +0,0 @@ -use futures_util::StreamExt; - -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt, BufReader}, - net::{UnixListener, UnixStream}, - sync::mpsc::Sender, - task::JoinSet, -}; -use zbus::{ - fdo::{DBusProxy, NameOwnerChangedArgs}, - names::OwnedBusName, - Connection, -}; - -use crate::{ - dbus::{media_player2::MediaPlayer2Proxy, player::PlayerProxy}, - i3bar::Click, - player::Event, - printer::Status, - Error, -}; - -pub async fn server() -> Result<(), Error> { - let mut join_set = JoinSet::new(); - let conn = Connection::session().await?; - - let status = Status::default(); - - let (tx, rx) = tokio::sync::mpsc::channel(128); - let (wtx, wrx) = tokio::sync::watch::channel(status); - - join_set.spawn(handle_name_owner_change(tx.clone(), conn.clone())); - join_set.spawn(crate::player::listener(conn.clone(), rx, wtx)); - - let listener = UnixListener::bind("/tmp/i3blocks-mpris.sock").unwrap(); - - loop { - match listener.accept().await { - Ok((stream, _addr)) => { - tokio::spawn(handle_client(stream, conn.clone(), wrx.clone(), tx.clone())); - } - Err(err) => { - println!("Error: {}", err); - break; - } - } - } - - while let Some(res) = join_set.join_next().await { - if let Err(err) = res? { - eprintln!("{err}") - }; - } - - Ok(()) -} - -async fn handle_client( - stream: UnixStream, - conn: Connection, - mut wrx: tokio::sync::watch::Receiver<Status>, - tx: Sender<Event>, -) -> Result<(), Error> { - let (reader, mut writer) = stream.into_split(); - - let mut reader = BufReader::new(reader); - let mut buf = Default::default(); - - reader.read_to_end(&mut buf).await?; - - tokio::spawn(async move { - while reader.read_to_end(&mut buf).await.is_ok() { - handle_click(conn.clone(), tx.clone(), serde_json::from_slice(&buf)?).await?; - } - Result::<(), Error>::Ok(()) - }); - - loop { - if wrx.changed().await.is_ok() { - let status = wrx.borrow_and_update(); - let Ok(v) = bincode::serialize(&*status) else { - continue; - }; - writer.write_all(&v).await; - } - } -} - -pub async fn handle_click( - conn: Connection, - tx: Sender<Event>, - click: Click, -) -> Result<(), Error> { - let Some(name) = click.name else { - return Ok(()); - }; - - let Some(instance) = click.instance else { - return Ok(()); - }; - - let player_proxy = PlayerProxy::builder(&conn) - .destination(name)? - .build() - .await?; - - let mpris_proxy = MediaPlayer2Proxy::builder(player_proxy.inner().connection()) - .destination(player_proxy.inner().destination())? - .build() - .await?; - - match (instance.as_ref(), click.button) { - ("mpris-icon", 1) if mpris_proxy.can_raise().await? => mpris_proxy.raise().await?, - ("mpris-title", _) => {} - ("mpris-prev", 1) => player_proxy.previous().await?, - ("mpris-playPause", 1) => player_proxy.play_pause().await?, - ("mpris-next", 1) => player_proxy.next().await?, - ("mpris-volume", 4) => { - player_proxy - .set_volume(player_proxy.volume().await? - 0.05) - .await? - } - ("mpris-volume", 5) => { - player_proxy - .set_volume(player_proxy.volume().await? + 0.05) - .await? - } - (_, 3) => tx.send(Event::Shift).await?, - _ => {} - } - - Ok(()) -} - -const IGNORED: [&str; 1] = ["playerctld"]; - -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 handle_name_owner_change(tx: Sender<Event>, conn: Connection) -> Result<(), Error> { - 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 mut stream = dbus_proxy.receive_name_owner_changed().await?; - - 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(()) -} - |