diff --git a/Cargo.lock b/Cargo.lock index e89f8ce8..51040417 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,6 +763,7 @@ dependencies = [ "chrono", "gdk4-x11", "gtk4", + "once_cell", "toml", "x11", "zbus", diff --git a/Cargo.toml b/Cargo.toml index efdac025..3ab33917 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ cascade = "1" chrono = "0.4" gdk4-x11 = "0.2" gtk4 = "0.2" +once_cell = "1" toml = "0.5" x11 = { version = "2", features = ["xlib"] } zbus = "1" diff --git a/src/deref_cell.rs b/src/deref_cell.rs new file mode 100644 index 00000000..dfdd4936 --- /dev/null +++ b/src/deref_cell.rs @@ -0,0 +1,31 @@ +use once_cell::unsync::OnceCell; + +/// Wrapper around `OnceCell` implementing `Deref`, and thus also panicking +/// when not set (or set twice). +/// +/// To be used in place of `gtk::TemplateChild`, but without xml. +pub struct DerefCell(OnceCell); + +impl DerefCell { + #[track_caller] + pub fn set(&self, value: T) { + if self.0.set(value).is_err() { + panic!("Initialized twice"); + } + } +} + +impl Default for DerefCell { + fn default() -> Self { + Self(OnceCell::default()) + } +} + +impl std::ops::Deref for DerefCell { + type Target = T; + + #[track_caller] + fn deref(&self) -> &T { + self.0.get().unwrap() + } +} diff --git a/src/main.rs b/src/main.rs index cd866df7..3afbc48f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ use gtk4::{gdk, glib, prelude::*}; +mod deref_cell; +mod mpris; mod window; mod x; diff --git a/src/mpris.rs b/src/mpris.rs new file mode 100644 index 00000000..3b3453c7 --- /dev/null +++ b/src/mpris.rs @@ -0,0 +1,205 @@ +use cascade::cascade; +use gtk4::{ + gio, + glib::{self, clone}, + prelude::*, + subclass::prelude::*, +}; +use std::cell::RefCell; + +use crate::deref_cell::DerefCell; + +#[derive(Default)] +pub struct MprisControlsInner { + box_: DerefCell, + backward_button: DerefCell, + play_pause_button: DerefCell, + forward_button: DerefCell, +} + +#[glib::object_subclass] +impl ObjectSubclass for MprisControlsInner { + const NAME: &'static str = "S76MprisControls"; + type ParentType = gtk4::Widget; + type Type = MprisControls; + + fn class_init(klass: &mut Self::Class) { + klass.set_layout_manager_type::(); + } +} + +impl ObjectImpl for MprisControlsInner { + fn constructed(&self, obj: &MprisControls) { + let backward_button = cascade! { + gtk4::Button::from_icon_name(Some("media-skip-backward-symbolic")); + ..connect_clicked(|_| {}); + }; + + let play_pause_button = cascade! { + gtk4::Button::from_icon_name(Some("media-playback-start-symbolic")); + ..connect_clicked(|_| {}); + }; + + let forward_button = cascade! { + gtk4::Button::from_icon_name(Some("media-skip-forward-symbolic")); + ..connect_clicked(|_| {}); + }; + + let box_ = cascade! { + gtk4::Box::new(gtk4::Orientation::Horizontal, 0); + ..set_parent(obj); + ..append(&backward_button); + ..append(&play_pause_button); + ..append(&forward_button); + }; + + /* + glib::MainContext::default().spawn_local(clone!(@strong obj => async move { + // XXX unwrap + let dbus = DBus::new().await.unwrap(); + dbus.connect_name_owner_changed(|a, b, c| { + println!("{:?}", (a, b, c)); + }); + for name in dbus.list_names().await.unwrap() { + if !name.starts_with("org.mpris.MediaPlayer2.") { + continue; + } + let player = Player::new(&name).await.unwrap(); + + println!("{}", name); + let status = player.playback_status(); + let metadata = player.metadata().unwrap(); + let title = metadata.title(); + let album = metadata.album(); + let artist = metadata.artist(); + let art = metadata.arturl(); + println!("{:?}", (title, art)); + } + std::mem::forget(dbus); + })); + */ + + self.box_.set(box_); + self.backward_button.set(backward_button); + self.play_pause_button.set(play_pause_button); + self.forward_button.set(forward_button); + } + + fn dispose(&self, obj: &MprisControls) { + self.box_.unparent(); + } +} + +impl WidgetImpl for MprisControlsInner {} + +glib::wrapper! { + pub struct MprisControls(ObjectSubclass) + @extends gtk4::Widget; +} + +impl MprisControls { + pub fn new() -> Self { + glib::Object::new(&[]).unwrap() + } +} + +fn dict_lookup(dict: &glib::VariantDict, key: &str) -> Option { + dict.lookup_value(key, None)?.get() +} + +fn property(proxy: &gio::DBusProxy, prop: &str) -> Option { + proxy.cached_property(prop)?.get() +} + +struct Metadata(glib::VariantDict); + +impl Metadata { + fn title(&self) -> Option { + dict_lookup(&self.0, "xesam:title") + } + + fn album(&self) -> Option { + dict_lookup(&self.0, "xesam:album") + } + + fn artist(&self) -> Option> { + dict_lookup(&self.0, "xesam:artist") + } + + fn arturl(&self) -> Option { + dict_lookup(&self.0, "mpris:artUrl") + } +} + +struct Player(gio::DBusProxy); + +impl Player { + async fn new(name: &str) -> Result { + let proxy = gio::DBusProxy::for_bus_future( + gio::BusType::Session, + gio::DBusProxyFlags::NONE, + None, + name, + "/org/mpris/MediaPlayer2", + "org.mpris.MediaPlayer2.Player", + ) + .await?; + Ok(Self(proxy)) + } + + fn connect_properties_changed(&self, f: F) { + self.0 + .connect_local("g-properties-changed", false, move |_| { + f(); + None + }) + .unwrap(); + } + + fn playback_status(&self) -> Option { + property(&self.0, "PlayBackStatus") + } + + fn metadata(&self) -> Option { + Some(Metadata(property(&self.0, "Metadata")?)) + } +} + +struct DBus(gio::DBusProxy); + +impl DBus { + async fn new() -> Result { + let proxy = gio::DBusProxy::for_bus_future( + gio::BusType::Session, + gio::DBusProxyFlags::NONE, + None, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + ) + .await?; + Ok(Self(proxy)) + } + + async fn list_names(&self) -> Result, glib::Error> { + Ok(self + .0 + .call_future("ListNames", None, gio::DBusCallFlags::NONE, 1000) + .await? + .child_value(0) + .iter() + .filter_map(|x| x.get::())) + } + + fn connect_name_owner_changed(&self, f: F) { + self.0 + .connect_local("g-signal", false, move |args| { + if &args[2].get::().unwrap() == "NameOwnerChanged" { + let (name, old, new) = args[3].get::().unwrap().get().unwrap(); + f(name, old, new); + } + None + }) + .unwrap(); + } +} diff --git a/src/window.rs b/src/window.rs index 0d9c9c05..e548d683 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,6 +2,7 @@ use cascade::cascade; use glib::clone; use gtk4::{gdk, glib, prelude::*}; +use crate::mpris::MprisControls; use crate::x; pub fn window(monitor: gdk::Monitor) -> gtk4::Window { @@ -11,7 +12,11 @@ pub fn window(monitor: gdk::Monitor) -> gtk4::Window { ..set_popover(Some(&cascade! { gtk4::Popover::new(); ..set_child(Some(&cascade! { - gtk4::Calendar::new(); + gtk4::Box::new(gtk4::Orientation::Horizontal, 0); + ..append(&MprisControls::new()); + ..append(&cascade! { + gtk4::Calendar::new(); + }); })); })); };