1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 10:41:03 +02:00

chore: initial commit

This commit is contained in:
Jake Stanger 2022-08-14 14:30:13 +01:00
commit e37d8f2b14
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
36 changed files with 4948 additions and 0 deletions

58
src/modules/mpd/client.rs Normal file
View file

@ -0,0 +1,58 @@
use mpd_client::commands::responses::Status;
use mpd_client::raw::MpdProtocolError;
use mpd_client::{Client, Connection};
use std::path::PathBuf;
use tokio::net::{TcpStream, UnixStream};
fn is_unix_socket(host: &String) -> bool {
PathBuf::from(host).is_file()
}
pub async fn get_connection(host: &String) -> Result<Connection, MpdProtocolError> {
if is_unix_socket(host) {
connect_unix(host).await
} else {
connect_tcp(host).await
}
}
async fn connect_unix(host: &String) -> Result<Connection, MpdProtocolError> {
let connection = UnixStream::connect(host)
.await
.unwrap_or_else(|_| panic!("Error connecting to unix socket: {}", host));
Client::connect(connection).await
}
async fn connect_tcp(host: &String) -> Result<Connection, MpdProtocolError> {
let connection = TcpStream::connect(host)
.await
.unwrap_or_else(|_| panic!("Error connecting to unix socket: {}", host));
Client::connect(connection).await
}
// /// Gets MPD server status.
// /// Panics on error.
// pub async fn get_status(client: &Client) -> Status {
// client
// .command(commands::Status)
// .await
// .expect("Failed to get MPD server status")
// }
/// Gets the duration of the current song
pub fn get_duration(status: &Status) -> u64 {
status
.duration
.expect("Failed to get duration from MPD status")
.as_secs()
}
/// Gets the elapsed time of the current song
pub fn get_elapsed(status: &Status) -> u64 {
status
.elapsed
.expect("Failed to get elapsed time from MPD status")
.as_secs()
}

232
src/modules/mpd/mod.rs Normal file
View file

@ -0,0 +1,232 @@
mod client;
mod popup;
use self::popup::Popup;
use crate::modules::mpd::client::{get_connection, get_duration, get_elapsed};
use crate::modules::mpd::popup::{MpdPopup, PopupEvent};
use crate::modules::{Module, ModuleInfo};
use crate::popup::PopupAlignment;
use dirs::home_dir;
use glib::Continue;
use gtk::prelude::*;
use gtk::{Button, Orientation};
use mpd_client::commands::responses::{PlayState, Song, Status};
use mpd_client::{commands, Tag};
use regex::Regex;
use serde::Deserialize;
use std::path::PathBuf;
use tokio::spawn;
use tokio::sync::mpsc;
use tokio::time::sleep;
#[derive(Debug, Deserialize, Clone)]
pub struct MpdModule {
#[serde(default = "default_socket")]
host: String,
#[serde(default = "default_format")]
format: String,
#[serde(default = "default_icon_play")]
icon_play: Option<String>,
#[serde(default = "default_icon_pause")]
icon_pause: Option<String>,
#[serde(default = "default_music_dir")]
music_dir: PathBuf,
}
fn default_socket() -> String {
String::from("localhost:6600")
}
fn default_format() -> String {
String::from("{icon} {title} / {artist}")
}
fn default_icon_play() -> Option<String> {
Some(String::from(""))
}
fn default_icon_pause() -> Option<String> {
Some(String::from(""))
}
fn default_music_dir() -> PathBuf {
home_dir().unwrap().join("Music")
}
/// Attempts to read the first value for a tag
/// (since the MPD client returns a vector of tags, or None)
pub fn try_get_first_tag(vec: Option<&Vec<String>>) -> Option<&str> {
match vec {
Some(vec) => vec.first().map(String::as_str),
None => None,
}
}
/// Formats a duration given in seconds
/// in hh:mm format
fn format_time(time: u64) -> String {
let minutes = (time / 60) % 60;
let seconds = time % 60;
format!("{:0>2}:{:0>2}", minutes, seconds)
}
/// Extracts the formatting tokens from a formatting string
fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
re.captures_iter(format_string)
.map(|caps| caps[1].to_string())
.collect::<Vec<_>>()
}
enum Event {
Open(f64),
Update(Box<Option<(Song, Status, String)>>),
}
impl Module<Button> for MpdModule {
fn into_widget(self, info: &ModuleInfo) -> Button {
let re = Regex::new(r"\{([\w-]+)}").unwrap();
let tokens = get_tokens(&re, self.format.as_str());
let button = Button::new();
let (ui_tx, mut ui_rx) = mpsc::channel(32);
let popup = Popup::new("popup-mpd", info.app, Orientation::Horizontal);
let mpd_popup = MpdPopup::new(popup, ui_tx);
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let click_tx = tx.clone();
let music_dir = self.music_dir.clone();
button.connect_clicked(move |button| {
let button_w = button.allocation().width();
let (button_x, _) = button
.translate_coordinates(&button.toplevel().unwrap(), 0, 0)
.unwrap();
click_tx
.send(Event::Open(f64::from(button_x + button_w)))
.unwrap();
});
let host = self.host.clone();
let host2 = self.host.clone();
spawn(async move {
let (client, _) = get_connection(&host).await.unwrap(); // TODO: Handle connecting properly
loop {
let current_song = client.command(commands::CurrentSong).await;
let status = client.command(commands::Status).await;
if let (Ok(Some(song)), Ok(status)) = (current_song, status) {
let string = self
.replace_tokens(self.format.as_str(), &tokens, &song.song, &status)
.await;
tx.send(Event::Update(Box::new(Some((song.song, status, string)))))
.unwrap();
} else {
tx.send(Event::Update(Box::new(None))).unwrap();
}
sleep(tokio::time::Duration::from_secs(1)).await;
}
});
spawn(async move {
let (client, _) = get_connection(&host2).await.unwrap(); // TODO: Handle connecting properly
while let Some(event) = ui_rx.recv().await {
match event {
PopupEvent::Previous => client.command(commands::Previous).await,
PopupEvent::Toggle => {
let status = client.command(commands::Status).await.unwrap();
match status.state {
PlayState::Playing => client.command(commands::SetPause(true)).await,
PlayState::Paused => client.command(commands::SetPause(false)).await,
PlayState::Stopped => Ok(())
}
}
PopupEvent::Next => client.command(commands::Next).await
}.unwrap();
}
});
{
let button = button.clone();
rx.attach(None, move |event| {
match event {
Event::Open(pos) => {
mpd_popup.popup.show();
mpd_popup.popup.set_pos(pos, PopupAlignment::Right);
}
Event::Update(mut msg) => {
if let Some((song, status, string)) = msg.take() {
mpd_popup.update(&song, &status, music_dir.as_path());
button.set_label(&string);
button.show();
} else {
button.hide();
}
}
}
Continue(true)
});
};
button
}
}
impl MpdModule {
/// Replaces each of the formatting tokens in the formatting string
/// with actual data pulled from MPD
async fn replace_tokens(
&self,
format_string: &str,
tokens: &Vec<String>,
song: &Song,
status: &Status,
) -> String {
let mut compiled_string = format_string.to_string();
for token in tokens {
let value = self.get_token_value(song, status, token).await;
compiled_string =
compiled_string.replace(format!("{{{}}}", token).as_str(), value.as_str());
}
compiled_string
}
/// Converts a string format token value
/// into its respective MPD value.
pub async fn get_token_value(&self, song: &Song, status: &Status, token: &str) -> String {
let s = match token {
"icon" => {
let icon = match status.state {
PlayState::Stopped => None,
PlayState::Playing => self.icon_play.as_ref(),
PlayState::Paused => self.icon_pause.as_ref(),
};
icon.map(|i| i.as_str())
}
"title" => song.title(),
"album" => try_get_first_tag(song.tags.get(&Tag::Album)),
"artist" => try_get_first_tag(song.tags.get(&Tag::Artist)),
"date" => try_get_first_tag(song.tags.get(&Tag::Date)),
"disc" => try_get_first_tag(song.tags.get(&Tag::Disc)),
"genre" => try_get_first_tag(song.tags.get(&Tag::Genre)),
"track" => try_get_first_tag(song.tags.get(&Tag::Track)),
"duration" => return format_time(get_duration(status)),
"elapsed" => return format_time(get_elapsed(status)),
_ => return token.to_string(),
};
s.unwrap_or_default().to_string()
}
}

164
src/modules/mpd/popup.rs Normal file
View file

@ -0,0 +1,164 @@
pub use crate::popup::Popup;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::prelude::*;
use gtk::{Button, Image, Label, Orientation};
use mpd_client::commands::responses::{PlayState, Song, Status};
use std::path::Path;
use tokio::sync::mpsc;
#[derive(Clone)]
struct IconLabel {
label: Label,
container: gtk::Box,
}
impl IconLabel {
fn new(icon: &str, label: Option<&str>) -> Self {
let container = gtk::Box::new(Orientation::Horizontal, 5);
let icon = Label::new(Some(icon));
let label = Label::new(label);
icon.style_context().add_class("icon");
label.style_context().add_class("label");
container.add(&icon);
container.add(&label);
Self { label, container }
}
}
#[derive(Clone)]
pub struct MpdPopup {
pub popup: Popup,
cover: Image,
title: IconLabel,
album: IconLabel,
artist: IconLabel,
btn_prev: Button,
btn_play_pause: Button,
btn_next: Button,
}
#[derive(Debug)]
pub enum PopupEvent {
Previous,
Toggle,
Next,
}
impl MpdPopup {
pub fn new(popup: Popup, tx: mpsc::Sender<PopupEvent>) -> Self {
let album_image = Image::builder()
.width_request(128)
.height_request(128)
.name("album-art")
.build();
let info_box = gtk::Box::new(Orientation::Vertical, 10);
let title_label = IconLabel::new("\u{f886}", None);
let album_label = IconLabel::new("\u{f524}", None);
let artist_label = IconLabel::new("\u{fd01}", None);
title_label.container.set_widget_name("title");
album_label.container.set_widget_name("album");
artist_label.container.set_widget_name("label");
info_box.add(&title_label.container);
info_box.add(&album_label.container);
info_box.add(&artist_label.container);
let controls_box = gtk::Box::builder().name("controls").build();
let btn_prev = Button::builder().label("\u{f9ad}").name("btn-prev").build();
let btn_play_pause = Button::builder().label("").name("btn-play-pause").build();
let btn_next = Button::builder().label("\u{f9ac}").name("btn-next").build();
controls_box.add(&btn_prev);
controls_box.add(&btn_play_pause);
controls_box.add(&btn_next);
info_box.add(&controls_box);
popup.container.add(&album_image);
popup.container.add(&info_box);
let tx_prev = tx.clone();
btn_prev.connect_clicked(move |_| {
tx_prev.try_send(PopupEvent::Previous).unwrap();
});
let tx_toggle = tx.clone();
btn_play_pause.connect_clicked(move |_| {
tx_toggle.try_send(PopupEvent::Toggle).unwrap();
});
let tx_next = tx;
btn_next.connect_clicked(move |_| {
tx_next.try_send(PopupEvent::Next).unwrap();
});
Self {
popup,
cover: album_image,
artist: artist_label,
album: album_label,
title: title_label,
btn_prev,
btn_play_pause,
btn_next,
}
}
pub fn update(&self, song: &Song, status: &Status, path: &Path) {
let prev_album = self.album.label.text();
let curr_album = song.album().unwrap_or_default();
// only update art when album changes
if prev_album != curr_album {
let cover_path = path.join(song.file_path().parent().unwrap().join("cover.jpg"));
if let Ok(pixbuf) = Pixbuf::from_file_at_scale(cover_path, 128, 128, true) {
self.cover.set_from_pixbuf(Some(&pixbuf));
}
}
self.title.label.set_text(song.title().unwrap_or_default());
self.album.label.set_text(song.album().unwrap_or_default());
self.artist
.label
.set_text(song.artists().first().unwrap_or(&String::new()));
match status.state {
PlayState::Stopped => {
self.btn_play_pause.set_sensitive(false);
}
PlayState::Playing => {
self.btn_play_pause.set_sensitive(true);
self.btn_play_pause.set_label("");
}
PlayState::Paused => {
self.btn_play_pause.set_sensitive(true);
self.btn_play_pause.set_label("");
}
}
let enable_prev = match status.current_song {
Some((pos, _)) => pos.0 > 0,
None => false,
};
let enable_next = match status.current_song {
Some((pos, _)) => pos.0 < status.playlist_length,
None => false,
};
self.btn_prev.set_sensitive(enable_prev);
self.btn_next.set_sensitive(enable_next);
}
}