diff options
-rw-r--r-- | Cargo.lock | 22 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/color.rs | 118 | ||||
-rw-r--r-- | src/error.rs | 22 | ||||
-rw-r--r-- | src/i3bar.rs | 80 | ||||
-rw-r--r-- | src/lib.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 99 | ||||
-rw-r--r-- | src/mpris.rs | 101 |
8 files changed, 352 insertions, 98 deletions
@@ -456,6 +456,8 @@ version = "0.1.0" dependencies = [ "anyhow", "futures-util", + "serde", + "thiserror", "tokio", "zbus", ] @@ -791,6 +793,26 @@ dependencies = [ ] [[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "tokio" version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6,5 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0.86" futures-util = "0.3.30" +serde = { version = "1.0.203", features = ["derive"] } +thiserror = "1.0.61" tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] } zbus = { version = "4.3.0", default-features = false, features = ["tokio"] } diff --git a/src/color.rs b/src/color.rs new file mode 100644 index 0000000..d8b79e7 --- /dev/null +++ b/src/color.rs @@ -0,0 +1,118 @@ +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 + .strip_prefix('#') + .ok_or_else(|| Error::Color(String::from("Missing '#' prefix")))?; + + 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/error.rs b/src/error.rs new file mode 100644 index 0000000..26593f2 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,22 @@ +pub type Result<T, E = Error> = std::result::Result<T, E>; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + + #[error("Join error: {0}")] + Join(#[from] tokio::task::JoinError), + + #[error("Invalid color format: {0}")] + Color(String), + + #[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()) + } +} diff --git a/src/i3bar.rs b/src/i3bar.rs new file mode 100644 index 0000000..8cf5f0c --- /dev/null +++ b/src/i3bar.rs @@ -0,0 +1,80 @@ +use serde::Serialize; + +use crate::color::Color; + +/// Represent block as described in <https://i3wm.org/docs/i3bar-protocol.html> +#[derive(Serialize, Debug, Clone)] +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>, + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option<Color>, + #[serde(skip_serializing_if = "Option::is_none")] + pub border: Option<Color>, + #[serde(skip_serializing_if = "Option::is_none")] + pub border_top: Option<usize>, + #[serde(skip_serializing_if = "Option::is_none")] + pub border_right: Option<usize>, + #[serde(skip_serializing_if = "Option::is_none")] + pub border_bottom: Option<usize>, + #[serde(skip_serializing_if = "Option::is_none")] + pub border_left: Option<usize>, + #[serde(skip_serializing_if = "Option::is_none")] + pub min_width: Option<MinWidth>, + #[serde(skip_serializing_if = "Option::is_none")] + 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 urgent: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] + pub separator: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] + pub separator_block_width: Option<usize>, + #[serde(skip_serializing_if = "Option::is_none")] + 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 { + Center, + Right, + Left, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(untagged)] +pub enum MinWidth { + Pixels(usize), + Text(String), +} @@ -1 +1,7 @@ +pub use error::{Error, Result}; + +pub mod color; pub mod dbus; +pub mod error; +pub mod i3bar; +pub mod mpris; diff --git a/src/main.rs b/src/main.rs index 7e4058a..263c9d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,102 +1,5 @@ -use futures_util::StreamExt; -use tokio::{ - sync::mpsc::{self, Sender}, - task::JoinSet, -}; -use zbus::{ - fdo::{DBusProxy, NameOwnerChangedArgs, NameOwnerChangedStream}, - names::OwnedBusName, - Connection, -}; - -use i3blocks::dbus::player::PlayerProxy; - -const IGNORED: [&str; 1] = ["playerctld"]; -const PREFIX: &str = "org.mpris.MediaPlayer2."; - #[tokio::main] async fn main() -> 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?; - } - } - + println!("Hello, World!"); Ok(()) } diff --git a/src/mpris.rs b/src/mpris.rs new file mode 100644 index 0000000..3cb29af --- /dev/null +++ b/src/mpris.rs @@ -0,0 +1,101 @@ +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(()) +} |