summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock22
-rw-r--r--Cargo.toml2
-rw-r--r--src/color.rs118
-rw-r--r--src/error.rs22
-rw-r--r--src/i3bar.rs80
-rw-r--r--src/lib.rs6
-rw-r--r--src/main.rs99
-rw-r--r--src/mpris.rs101
8 files changed, 352 insertions, 98 deletions
diff --git a/Cargo.lock b/Cargo.lock
index fd3e48f..85bce40 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 0bd4284..d593174 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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),
+}
diff --git a/src/lib.rs b/src/lib.rs
index b390f4b..22661ed 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(())
+}