diff options
-rw-r--r-- | src/error.rs | 3 | ||||
-rw-r--r-- | src/i3bar.rs | 25 | ||||
-rw-r--r-- | src/lib.rs | 104 | ||||
-rw-r--r-- | src/listener.rs | 95 | ||||
-rw-r--r-- | src/main.rs | 154 |
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)] @@ -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(()) +} |