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