summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2024-08-03 16:33:20 -0500
committerToby Vincent <tobyv@tobyvin.dev>2024-08-03 16:33:32 -0500
commitd8fb83aad6254f1a4136a561479310af7b176d9b (patch)
tree7cb5e3dd3403c989a08ae9760b78703353f153c2
parent1aa216f21d29b0aa92a738d43c572c33799dfef5 (diff)
feat: impl click handling
-rw-r--r--src/error.rs3
-rw-r--r--src/i3bar.rs25
-rw-r--r--src/lib.rs104
-rw-r--r--src/listener.rs95
-rw-r--r--src/main.rs154
5 files changed, 277 insertions, 104 deletions
diff --git a/src/error.rs b/src/error.rs
index ef596d2..f4158a4 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -19,4 +19,7 @@ pub enum Error {
#[error("Json serde error: {0}")]
Json(#[from] serde_json::Error),
+
+ #[error("Invalid block kind: {0}")]
+ InvalidKind(String),
}
diff --git a/src/i3bar.rs b/src/i3bar.rs
index 28306e0..b69095e 100644
--- a/src/i3bar.rs
+++ b/src/i3bar.rs
@@ -38,6 +38,9 @@ pub struct Block {
pub separator_block_width: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub markup: Option<String>,
+
+ #[serde(skip)]
+ pub enabled: bool,
}
impl Block {
@@ -64,11 +67,29 @@ impl Align {
}
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(untagged)]
pub enum MinWidth {
Pixels(usize),
- Text(String),
+ #[serde(with = "min_width_serde")]
+ Text(usize),
+}
+mod min_width_serde {
+ use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+ pub(super) fn serialize<S>(value: &usize, s: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ "x".repeat(*value).serialize(s)
+ }
+
+ pub(super) fn deserialize<'de, D>(d: D) -> Result<usize, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ Ok(String::deserialize(d)?.len())
+ }
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
diff --git a/src/lib.rs b/src/lib.rs
index 6306546..e00640e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,4 @@
-use std::sync::Arc;
+use std::{fmt::Display, str::FromStr, sync::Arc};
use i3bar::Block;
use tokio::sync::{Notify, RwLock};
@@ -9,8 +9,69 @@ pub mod dbus;
pub mod error;
pub mod i3bar;
pub mod listener;
+pub mod handler {
+ use zbus::Connection;
-pub type Blocks = Arc<Watcher<[Option<Block>; 6]>>;
+ use crate::{
+ dbus::{media_player2::MediaPlayer2Proxy, player::PlayerProxy},
+ BlockKind, Error,
+ };
+
+ pub async fn handlers(
+ conn: Connection,
+ mut rx: tokio::sync::mpsc::Receiver<(BlockKind, u8)>,
+ ) -> Result<(), Error> {
+ let player_proxy = PlayerProxy::builder(&conn)
+ .destination("org.mpris.MediaPlayer2.playerctld")?
+ .build()
+ .await?;
+
+ let mpris_proxy = MediaPlayer2Proxy::builder(&conn)
+ .destination("org.mpris.MediaPlayer2.playerctld")?
+ .build()
+ .await?;
+
+ while let Some(click) = rx.recv().await {
+ if let Err(err) = handle_click(&player_proxy, &mpris_proxy, click).await {
+ eprintln!("{err}")
+ }
+ }
+
+ Ok(())
+ }
+
+ pub async fn handle_click(
+ player: &PlayerProxy<'_>,
+ mpris: &MediaPlayer2Proxy<'_>,
+ click: (BlockKind, u8),
+ ) -> Result<(), Error> {
+ // TODO: Show notification with metadata on `(BlockKind::Title, 1)`
+
+ match click {
+ (BlockKind::Icon, 1) if mpris.can_raise().await? => mpris.raise().await?,
+ (BlockKind::Prev, 1) if player.can_go_previous().await? => player.previous().await?,
+ (BlockKind::Prev, 3) if player.can_seek().await? => player.seek(-5_000_000).await?,
+ (BlockKind::Play, 1) if player.can_pause().await? => player.play_pause().await?,
+ (BlockKind::Next, 1) if player.can_go_next().await? => player.next().await?,
+ (BlockKind::Next, 3) if player.can_seek().await? => player.seek(5_000_000).await?,
+ (BlockKind::Volume, 4) => update_volume(player, -0.05).await?,
+ (BlockKind::Volume, 5) => update_volume(player, 0.05).await?,
+ _ => {}
+ }
+
+ Ok(())
+ }
+
+ pub async fn update_volume(proxy: &PlayerProxy<'_>, delta: f64) -> Result<(), Error> {
+ if let Ok(vol) = proxy.volume().await {
+ proxy.set_volume(vol + delta).await?
+ }
+
+ Ok(())
+ }
+}
+
+pub type State = Arc<Watcher<[Block; 6]>>;
#[derive(Default)]
pub struct Watcher<T> {
@@ -26,3 +87,42 @@ impl<T> Watcher<T> {
}
}
}
+
+#[repr(u8)]
+pub enum BlockKind {
+ Icon,
+ Title,
+ Prev,
+ Play,
+ Next,
+ Volume,
+}
+
+impl Display for BlockKind {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ BlockKind::Icon => write!(f, "icon"),
+ BlockKind::Title => write!(f, "title"),
+ BlockKind::Prev => write!(f, "prev"),
+ BlockKind::Play => write!(f, "play"),
+ BlockKind::Next => write!(f, "next"),
+ BlockKind::Volume => write!(f, "volume"),
+ }
+ }
+}
+
+impl FromStr for BlockKind {
+ type Err = Error;
+
+ fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
+ match s {
+ "icon" => Ok(Self::Icon),
+ "title" => Ok(Self::Title),
+ "prev" => Ok(Self::Prev),
+ "play" => Ok(Self::Play),
+ "next" => Ok(Self::Next),
+ "volume" => Ok(Self::Volume),
+ s => Err(Error::InvalidKind(s.to_owned())),
+ }
+ }
+}
diff --git a/src/listener.rs b/src/listener.rs
index 6414fb3..88efb6c 100644
--- a/src/listener.rs
+++ b/src/listener.rs
@@ -6,8 +6,8 @@ use zbus::{proxy::PropertyChanged, Connection};
use crate::{
dbus::player::{Metadata, PlaybackStatus, PlayerProxy},
- i3bar::{Align, Block, MinWidth},
- Blocks, Error,
+ i3bar::MinWidth,
+ BlockKind, Error, State,
};
const TICK_RATE: Duration = Duration::from_millis(500);
@@ -21,9 +21,7 @@ static COLORS: LazyLock<[String; 3]> = LazyLock::new(|| {
]
});
-pub async fn listeners(blocks: Blocks) -> Result<(), Error> {
- let conn = Connection::session().await?;
-
+pub async fn listeners(conn: Connection, blocks: State) -> Result<(), Error> {
let proxy = PlayerProxy::builder(&conn)
.destination("org.mpris.MediaPlayer2.playerctld")?
.build()
@@ -41,9 +39,9 @@ pub async fn listeners(blocks: Blocks) -> Result<(), Error> {
loop {
tokio::select! {
Some(prop) = metadata.next() => handle_metadata(prop, blocks.clone(), &mut old_title, &mut rotator).await,
- Some(prop) = prev.next() => handle_prev_next(prop, blocks.clone(), '󰒮', "mpris-prev").await,
+ Some(prop) = prev.next() => handle_prev_next(prop, blocks.clone(), BlockKind::Prev).await,
Some(prop) = play.next() => handle_play(prop, blocks.clone()).await,
- Some(prop) = next.next() => handle_prev_next(prop, blocks.clone(), '󰒭', "mpris-next").await,
+ Some(prop) = next.next() => handle_prev_next(prop, blocks.clone(), BlockKind::Next).await,
Some(prop) = volume.next() => handle_volume(prop, blocks.clone()).await,
else => {
eprintln!("Failed to get next property");
@@ -58,34 +56,28 @@ pub async fn listeners(blocks: Blocks) -> Result<(), Error> {
Ok(())
}
-async fn title_rotator(blocks: Blocks, title: String) -> Result<(), Error> {
+async fn title_rotator(blocks: State, title: String) -> Result<(), Error> {
let mut interval = tokio::time::interval(TICK_RATE);
let mut chars = title.chars().collect::<Vec<char>>();
chars.push(' ');
let full_text = String::from_iter(chars[0..10].iter());
- blocks.value.write().await[1] = Some(Block {
- name: Some("mpris-title".into()),
- min_width: Some(MinWidth::Text(format!("x{}", full_text))),
- full_text,
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- });
+ let block = &mut blocks.value.write().await[BlockKind::Title as usize];
+ block.min_width = Some(MinWidth::Text(full_text.len() + 1));
+ block.full_text = full_text;
+ block.enabled = false;
loop {
interval.tick().await;
- let mut value = blocks.value.write().await;
- if let Some(block) = &mut value[1] {
- block.full_text = String::from_iter(chars[0..10].iter());
- };
+ let block = &mut blocks.value.write().await[BlockKind::Title as usize];
+ block.full_text = String::from_iter(chars[0..10].iter());
chars.rotate_left(1);
}
}
async fn handle_metadata(
prop: PropertyChanged<'_, Metadata>,
- blocks: Blocks,
+ blocks: State,
old_title: &mut String,
rotator: &mut Option<JoinHandle<Result<(), Error>>>,
) -> ControlFlow<Result<(), Error>> {
@@ -100,14 +92,10 @@ async fn handle_metadata(
if title.len() >= 10 {
*rotator = Some(tokio::spawn(title_rotator(blocks.clone(), title)));
} else {
- blocks.value.write().await[1] = Some(Block {
- name: Some("mpris-title".into()),
- min_width: Some(MinWidth::Text(format!("x{}", title))),
- full_text: title,
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- });
+ let block = &mut blocks.value.write().await[BlockKind::Title as usize];
+ block.min_width = Some(MinWidth::Text(title.len() + 1));
+ block.full_text = title;
+ block.enabled = true;
}
ControlFlow::Continue(())
@@ -115,27 +103,19 @@ async fn handle_metadata(
async fn handle_prev_next(
prop: PropertyChanged<'_, bool>,
- blocks: Blocks,
- icon: char,
- name: &'static str,
+ blocks: State,
+ kind: BlockKind,
) -> ControlFlow<Result<(), Error>> {
if let Ok(value) = prop.get().await {
- blocks.value.write().await[2] = value.then_some(Block {
- name: Some(name.into()),
- full_text: icon.into(),
- min_width: Some(MinWidth::Text("xx".to_string())),
- align: Align::Center,
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- });
- };
+ blocks.value.write().await[kind as usize].enabled = value
+ }
+
ControlFlow::Continue(())
}
async fn handle_play(
prop: PropertyChanged<'_, PlaybackStatus>,
- blocks: Blocks,
+ blocks: State,
) -> ControlFlow<Result<(), Error>> {
let (icon, color, bg) = match prop.get().await {
Ok(PlaybackStatus::Playing) => ('󰏤', Some(COLORS[0].clone()), Some(COLORS[1].clone())),
@@ -145,17 +125,11 @@ async fn handle_play(
};
let mut blocks = blocks.value.write().await;
- blocks[3] = Some(Block {
- name: Some("mpris-next".into()),
- full_text: icon.into(),
- min_width: Some(MinWidth::Text("xx".to_string())),
- align: Align::Center,
- separator: Some(false),
- separator_block_width: Some(0),
- ..Default::default()
- });
-
- for block in blocks.iter_mut().flatten() {
+ let block = &mut blocks[BlockKind::Play as usize];
+ block.full_text = icon.into();
+ block.enabled = true;
+
+ for block in blocks.iter_mut() {
block.color.clone_from(&color);
block.background.clone_from(&bg);
}
@@ -165,7 +139,7 @@ async fn handle_play(
async fn handle_volume(
prop: PropertyChanged<'_, f64>,
- blocks: Blocks,
+ blocks: State,
) -> ControlFlow<Result<(), Error>> {
let full_text = match prop.get().await.map(|v| (v * 100_f64) as u32) {
Ok(v @ 66..) => format!("󰕾 {v}%"),
@@ -174,13 +148,10 @@ async fn handle_volume(
Err(_) => return ControlFlow::Continue(()),
};
- blocks.value.write().await[5] = Some(Block {
- name: Some("mpris-volume".into()),
- min_width: Some(MinWidth::Text(format!("x{}", full_text))),
- full_text,
- align: Align::Center,
- ..Default::default()
- });
+ let block = &mut blocks.value.write().await[BlockKind::Volume as usize];
+ block.min_width = Some(MinWidth::Text(full_text.len() + 1));
+ block.full_text = full_text;
+ block.enabled = true;
ControlFlow::Continue(())
}
diff --git a/src/main.rs b/src/main.rs
index 26b5784..cd17ec2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,11 +1,17 @@
-use std::process::Stdio;
+use std::{process::Stdio, sync::Arc};
use i3blocks_mpris::{
- i3bar::{Block, Click},
+ handler::handlers,
+ i3bar::{Align, Block, Click, MinWidth},
listener::listeners,
- Blocks,
+ BlockKind, Watcher,
};
-use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
+use tokio::{
+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
+ process::ChildStdin,
+ sync::mpsc::Sender,
+};
+use zbus::Connection;
#[tokio::main]
async fn main() -> Result<(), main_error::MainError> {
@@ -22,54 +28,126 @@ async fn main() -> Result<(), main_error::MainError> {
let mut child_stdin = child.stdin.take().unwrap();
let mut child_stdout = BufReader::new(child.stdout.take().unwrap()).lines();
- let blocks: Blocks = Blocks::default();
+ let align = Align::Center;
+ let separator = Some(false);
+ let separator_block_width = Some(0);
+ let min_width = Some(MinWidth::Text(2));
- let blocks2 = blocks.clone();
- tokio::spawn(async move {
- blocks2.value.write().await[0] = Some(Block {
- name: Some("mpris-icon".into()),
+ let blocks = Arc::new(Watcher::new([
+ Block {
full_text: " 󰝚 ".into(),
- separator: Some(false),
- separator_block_width: Some(0),
+ align,
+ name: Some("mpris".to_owned()),
+ instance: Some(BlockKind::Icon.to_string()),
+ separator,
+ separator_block_width,
+ ..Default::default()
+ },
+ Block {
+ align,
+ name: Some("mpris".to_owned()),
+ instance: Some(BlockKind::Title.to_string()),
+ separator,
+ separator_block_width,
+ ..Default::default()
+ },
+ Block {
+ full_text: '󰒮'.into(),
+ min_width,
+ align,
+ name: Some("mpris".to_owned()),
+ instance: Some(BlockKind::Prev.to_string()),
+ separator,
+ separator_block_width,
+ ..Default::default()
+ },
+ Block {
+ instance: Some(BlockKind::Play.to_string()),
+ min_width,
+ align,
+ separator,
+ separator_block_width,
..Default::default()
- });
- blocks2.notify.notify_one();
- });
+ },
+ Block {
+ full_text: '󰒭'.into(),
+ min_width,
+ align,
+ name: Some("mpris".to_owned()),
+ instance: Some(BlockKind::Play.to_string()),
+ separator,
+ separator_block_width,
+ ..Default::default()
+ },
+ Block {
+ align,
+ name: Some("mpris".to_owned()),
+ instance: Some(BlockKind::Volume.to_string()),
+ ..Default::default()
+ },
+ ]));
+
+ blocks.notify.notify_one();
+
+ let (tx, rx) = tokio::sync::mpsc::channel(128);
+ let conn = Connection::session().await?;
- tokio::spawn(listeners(blocks.clone()));
+ tokio::spawn(listeners(conn.clone(), blocks.clone()));
+ tokio::spawn(handlers(conn, rx));
let mut mpris_blocks = String::new();
let mut other_blocks = String::new();
loop {
tokio::select! {
- _ = blocks.notify.notified() => {
- mpris_blocks = blocks
- .value
- .read()
- .await
- .iter()
- .map(serde_json::to_string)
- .collect::<Result<Vec<_>, _>>()?
- .join(",");
- }
- Ok(Some(line)) = child_stdout.next_line() => other_blocks = line,
- Ok(Some(line)) = stdin.next_line() => match serde_json::from_str::<Click>(&line) {
- Err(_) => continue,
- Ok(Click { name: Some(name), button, .. }) if name.starts_with("mpris-") => println!("{name}, {button}"),
- Ok(click) => {
- let mut s = serde_json::to_vec(&click)?;
- s.push(b'\n');
- child_stdin.write_all(&s).await?;
+ _ = blocks.notify.notified() => {
+ mpris_blocks = blocks
+ .value
+ .read()
+ .await
+ .iter()
+ .filter(|b| b.enabled)
+ .map(serde_json::to_string)
+ .collect::<Result<Vec<_>, _>>()?
+ .join(",");
}
- },
- else => break,
+ Ok(Some(line)) = child_stdout.next_line() => other_blocks = line,
+ Ok(Some(line)) = stdin.next_line() => {
+ handle_stdin(&line, tx.clone(), &mut child_stdin).await?;
+ continue;
+ }
+ else => break,
}
- let mut output = other_blocks.replace(r#"{"name":"mpris","full_text":""}"#, &mpris_blocks);
- output.push('\n');
- stdout.write_all(output.as_bytes()).await?;
+ let output = other_blocks.replace(r#"{"name":"mpris","full_text":""},"#, &mpris_blocks);
+ if !output.is_empty() {
+ stdout.write_all(output.as_bytes()).await?;
+ stdout.write_u8(b'\n').await?;
+ stdout.flush().await?;
+ }
}
Ok(())
}
+
+async fn handle_stdin(
+ line: &str,
+ tx: Sender<(BlockKind, u8)>,
+ child_stdin: &mut ChildStdin,
+) -> Result<(), main_error::MainError> {
+ match serde_json::from_str::<Click>(line) {
+ Ok(Click {
+ name: Some(name),
+ instance: Some(instance),
+ button,
+ ..
+ }) if name == "mpris" => tx.send((instance.parse()?, button)).await?,
+ _ => {
+ child_stdin.write_all(line.as_bytes()).await?;
+ child_stdin.write_u8(b'\n').await?;
+ child_stdin.flush().await?;
+ }
+ };
+
+ Ok(())
+}