From 3666474bbf5d90c1d1f4515034d674121281ac1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20G=C3=BCnzler?= Date: Mon, 1 Nov 2021 19:17:29 +0100 Subject: [PATCH] Add mpris module --- include/factory.hpp | 3 + include/modules/mpris/mpris.hpp | 48 +++++++ man/waybar-mpris.5.scd | 74 ++++++++++ meson.build | 9 +- meson_options.txt | 1 + src/factory.cpp | 5 + src/modules/mpris/mpris.cpp | 237 ++++++++++++++++++++++++++++++++ 7 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 include/modules/mpris/mpris.hpp create mode 100644 man/waybar-mpris.5.scd create mode 100644 src/modules/mpris/mpris.cpp diff --git a/include/factory.hpp b/include/factory.hpp index b96669760..91f6b56d3 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -33,6 +33,9 @@ #ifdef HAVE_DBUSMENU #include "modules/sni/tray.hpp" #endif +#ifdef HAVE_MPRIS +#include "modules/mpris/mpris.hpp" +#endif #ifdef HAVE_LIBNL #include "modules/network.hpp" #endif diff --git a/include/modules/mpris/mpris.hpp b/include/modules/mpris/mpris.hpp new file mode 100644 index 000000000..be502cf7a --- /dev/null +++ b/include/modules/mpris/mpris.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include "ALabel.hpp" +#include "glibconfig.h" +#include "gtkmm/box.h" +#include "gtkmm/label.h" + +extern "C" { + #include +} + +namespace waybar::modules::mpris { + +class Mpris : public AModule { + public: + Mpris(const std::string&, const Json::Value&); + ~Mpris(); + auto update() -> void; + bool handleToggle(GdkEventButton* const&); + + private: + const std::string DEFAULT_FORMAT = "{player} ({status}): {dynamic}"; + + static void nameAppeared_cb(PlayerctlPlayerManager*, PlayerctlPlayerName*, gpointer); + static void playerVanished_cb(PlayerctlPlayerManager*, PlayerctlPlayer*, gpointer); + static void playerPlay_cb(PlayerctlPlayer*, gpointer); + static void playerMetadata_cb(PlayerctlPlayer*, GVariant*, gpointer); + PlayerctlPlayer* get_top_player(PlayerctlPlayerManager*); + std::string getIcon(const std::string &); + + Gtk::Box box_; + Gtk::Label label_; + + // config + std::string format = DEFAULT_FORMAT; + std::string format_playing = ""; + std::string format_paused = ""; + std::string format_stopped = ""; + + PlayerctlPlayerManager *manager; + std::string lastStatus; + std::string lastPlayer; +}; + +} // namespace waybar::modules::mpris diff --git a/man/waybar-mpris.5.scd b/man/waybar-mpris.5.scd new file mode 100644 index 000000000..28b69e9aa --- /dev/null +++ b/man/waybar-mpris.5.scd @@ -0,0 +1,74 @@ +waybar-upower(5) + +# NAME + +waybar - MPRIS module + +# DESCRIPTION + +The *mpris* module displays currently playing media via libplayerctl.++ +By default the following actions use libplayerctl to interact with the player: + +*on-click*: play-pause++ +*on-middle-click*: previous track++ +*on-right-click*: next track + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: {player} ({status}) {dynamic} ++ + The text format. + +*format-[status]*: ++ + typeof: string ++ + The text status-specific format. + +*format-icons*: ++ + typeof: map[string]string + Allows setting "{status-icon}" and "{player-icon}" + +*on-click*: ++ + typeof: string + Overwrite default action toggles + +# FORMAT REPLACEMENTS + +*{player}*: The name of the current media player + +*{status}*: The current status (playing, paused, stopped) + +*{artist}*: The artist of the current track + +*{album}*: The album title of the current track + +*{title}*: The title of the current track + +*{dynamic}*: Dynamically concatenates "{artist} - {album} - {title}", based++ +on if the values are populated by the player + +*{player-icon}*: Chooses an icon from "format-icons" based on "{player}" + +*{status-icon}*: Chooses an icon from "format-icons" based on "{status}" + +# EXAMPLES + +``` +"mpris": { + "format": "{player_icon} {dynamic}", + "format-paused": "{status_icon} {dynamic}", + "format-icons": { + "default": "♫", + + "paused": "⏸", + "playing": "⏵", + "stopped": "⏹", + } +} +``` + +# STYLE + +- *#mpris* +- *#mpris.[status]* +- *#mpris.[player]* diff --git a/meson.build b/meson.build index 73e74a510..5764253d5 100644 --- a/meson.build +++ b/meson.build @@ -86,13 +86,14 @@ wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols') gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0']) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk')) -giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or get_option('logind').enabled() or get_option('upower_glib').enabled())) +giounix = dependency('gio-unix-2.0', required: (get_option('dbusmenu-gtk').enabled() or get_option('logind').enabled() or get_option('upower_glib').enabled() or get_option('mpris').enabled())) jsoncpp = dependency('jsoncpp') sigcpp = dependency('sigc++-2.0') libepoll = dependency('epoll-shim', required: false) libnl = dependency('libnl-3.0', required: get_option('libnl')) libnlgen = dependency('libnl-genl-3.0', required: get_option('libnl')) upower_glib = dependency('upower-glib', required: get_option('upower_glib')) +playerctl = dependency('playerctl', version : ['>=2.0.0'], required: get_option('mpris')) libpulse = dependency('libpulse', required: get_option('pulseaudio')) libudev = dependency('libudev', required: get_option('libudev')) libevdev = dependency('libevdev', required: get_option('libevdev')) @@ -210,6 +211,11 @@ if (upower_glib.found() and giounix.found() and not get_option('logind').disable src_files += 'src/modules/upower/upower_tooltip.cpp' endif +if (playerctl.found() and giounix.found() and not get_option('logind').disabled()) + add_project_arguments('-DHAVE_MPRIS', language: 'cpp') + src_files += 'src/modules/mpris/mpris.cpp' +endif + if libpulse.found() add_project_arguments('-DHAVE_LIBPULSE', language: 'cpp') src_files += 'src/modules/pulseaudio.cpp' @@ -296,6 +302,7 @@ executable( libnl, libnlgen, upower_glib, + playerctl, libpulse, libudev, libepoll, diff --git a/meson_options.txt b/meson_options.txt index d2e984766..1e98b69db 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,6 +4,7 @@ option('libudev', type: 'feature', value: 'auto', description: 'Enable libudev s option('libevdev', type: 'feature', value: 'auto', description: 'Enable libevdev support for evdev related features') option('pulseaudio', type: 'feature', value: 'auto', description: 'Enable support for pulseaudio') option('upower_glib', type: 'feature', value: 'auto', description: 'Enable support for upower') +option('mpris', type: 'feature', value: 'auto', description: 'Enable support for mpris') option('systemd', type: 'feature', value: 'auto', description: 'Install systemd user service unit') option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable support for tray') option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') diff --git a/src/factory.cpp b/src/factory.cpp index 4aaa248e2..dea92b239 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -17,6 +17,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::upower::UPower(id, config_[name]); } #endif +#ifdef HAVE_MPRIS + if (ref == "mpris") { + return new waybar::modules::mpris::Mpris(id, config_[name]); + } +#endif #ifdef HAVE_SWAY if (ref == "sway/mode") { return new waybar::modules::sway::Mode(id, config_[name]); diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp new file mode 100644 index 000000000..2e5eeb4f0 --- /dev/null +++ b/src/modules/mpris/mpris.cpp @@ -0,0 +1,237 @@ +#include "modules/mpris/mpris.hpp" + +#include +#include + +extern "C" { + #include +} + +namespace waybar::modules::mpris { + +Mpris::Mpris(const std::string& id, const Json::Value& config) + : AModule(config, "mpris", id), + box_(Gtk::ORIENTATION_HORIZONTAL, 0), + // icon_(), + label_(), + manager() { + + // box_.pack_start(icon_); + box_.pack_start(label_); + box_.set_name(name_); + + event_box_.add(box_); + event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &Mpris::handleToggle)); + + if (config_["format"].isString()) { + format = config_["format"].asString(); + } + if (config_["format-playing"].isString()) { + format_playing = config_["format-playing"].asString(); + } + if (config_["format-paused"].isString()) { + format_paused = config_["format-paused"].asString(); + } + if (config_["format-stopped"].isString()) { + format_stopped = config_["format-stopped"].asString(); + } + + GError* error = NULL; + manager = playerctl_player_manager_new(&error); + if (manager == NULL) { + throw std::runtime_error("Unable to create MPRIS client!"); + } + + g_signal_connect(manager, "name-appeared", G_CALLBACK(nameAppeared_cb), this); + g_signal_connect(manager, "player-vanished", G_CALLBACK(playerVanished_cb), this); + + GList* player_list = playerctl_list_players(&error); + GList* l = NULL; + for (l = player_list; l != NULL; l = l->next) { + PlayerctlPlayerName* name = static_cast(l->data); + PlayerctlPlayer* player = playerctl_player_new_from_name(name, &error); + playerctl_player_manager_manage_player(manager, player); + + g_signal_connect(player, "play", G_CALLBACK(playerPlay_cb), this); + g_signal_connect(player, "metadata", G_CALLBACK(playerMetadata_cb), this); + + // make sure the player ends up on top if it's playing + PlayerctlPlaybackStatus status = PLAYERCTL_PLAYBACK_STATUS_STOPPED; + g_object_get(player, "playback-status", &status, NULL); + if (status == PLAYERCTL_PLAYBACK_STATUS_PLAYING) { + playerctl_player_manager_move_player_to_top(manager, player); + } + } + + dp.emit(); +} + +Mpris::~Mpris() { + if (manager != NULL) g_object_unref(manager); +} + +void Mpris::nameAppeared_cb(PlayerctlPlayerManager* manager, PlayerctlPlayerName* name, gpointer data) { + GError* error = NULL; + Mpris* mpris = static_cast(data); + if (!mpris) return; + + PlayerctlPlayer* player = playerctl_player_new_from_name(name, &error); + playerctl_player_manager_manage_player(mpris->manager, player); + g_object_unref(player); + // update widget + mpris->dp.emit(); +} +void Mpris::playerVanished_cb(PlayerctlPlayerManager* manager, PlayerctlPlayer* player, gpointer data) { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + // update widget + mpris->dp.emit(); +} +void Mpris::playerPlay_cb(PlayerctlPlayer* player, gpointer data) { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + playerctl_player_manager_move_player_to_top(mpris->manager, player); + // update widget + mpris->dp.emit(); +} +void Mpris::playerMetadata_cb(PlayerctlPlayer* player, GVariant *metadata, gpointer data) { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + // update widget + mpris->dp.emit(); +} + +PlayerctlPlayer* Mpris::get_top_player(PlayerctlPlayerManager* manager) { + GList* players = NULL; + g_object_get(manager, "players", &players, NULL); + auto p = g_list_first(players); + if (!p) return NULL; + return PLAYERCTL_PLAYER(p->data); +} + +std::string Mpris::getIcon(const std::string &key) { + auto format_icons = config_["format-icons"]; + if (format_icons.isObject()) { + if (format_icons[key].isString()) { + return format_icons[key].asString(); + } else if (format_icons["default"].isString()) { + return format_icons["default"].asString(); + } + } + return ""; +} + +bool Mpris::handleToggle(GdkEventButton* const& e) { + if (config_["on-click"].isString()) { + return AModule::handleToggle(e); + } + + GError* error = NULL; + PlayerctlPlayer* top_player = get_top_player(manager); + + if (e->type == GdkEventType::GDK_BUTTON_PRESS) { + switch (e->button) { + case 1: // left-click + playerctl_player_play_pause(top_player, &error); + break; + case 2: // middle-click + playerctl_player_previous(top_player, &error); + break; + case 3: // right-click + playerctl_player_next(top_player, &error); + break; + } + } + return true; +} + +auto Mpris::update() -> void { + GError* error = NULL; + + PlayerctlPlayer* top_player = get_top_player(manager); + + // if there is no top player, hide widget + if (top_player == NULL) { + event_box_.set_visible(false); + AModule::update(); + return; + } + + gchar* player = NULL; + gchar* status = NULL; + PlayerctlPlaybackStatus playback_status = PLAYERCTL_PLAYBACK_STATUS_STOPPED; + + g_object_get(top_player, + "player-name", &player, + "status", &status, + "playback-status", &playback_status, + NULL); + + gchar *artist = playerctl_player_print_metadata_prop(top_player, "xesam:artist", &error); + gchar *album = playerctl_player_print_metadata_prop(top_player, "xesam:album", &error); + gchar *title = playerctl_player_print_metadata_prop(top_player, "xesam:title", &error); + + std::string dynamic; + if (artist != NULL) { dynamic += artist; dynamic += " - "; }; + if (album != NULL) { dynamic += album; dynamic += " - "; }; + if (title != NULL) dynamic += title; + + // TODO art? length? + // gchar *length_s = playerctl_player_print_metadata_prop(top_player, "mpris:length", &error); + // auto length = std::strtol(length_s, nullptr, 10); + // gchar *url = playerctl_player_print_metadata_prop(top_player, "mpris:artUrl", &error); + + // make status lowercase + status[0] = std::tolower(status[0]); + + // set css class for player status + if (!lastStatus.empty() && box_.get_style_context()->has_class(lastStatus)) { + box_.get_style_context()->remove_class(lastStatus); + } + if (!box_.get_style_context()->has_class(status)) { + box_.get_style_context()->add_class(status); + } + lastStatus = status; + + // set css class for player name + if (!lastPlayer.empty() && box_.get_style_context()->has_class(lastPlayer)) { + box_.get_style_context()->remove_class(lastPlayer); + } + if (!box_.get_style_context()->has_class(player)) { + box_.get_style_context()->add_class(player); + } + lastPlayer = player; + + event_box_.set_visible(true); + + std::string formatstr = format; + switch (playback_status) { + case PLAYERCTL_PLAYBACK_STATUS_PLAYING: + if (!format_playing.empty()) formatstr = format_playing; + break; + case PLAYERCTL_PLAYBACK_STATUS_PAUSED: + if (!format_paused.empty()) formatstr = format_paused; + break; + case PLAYERCTL_PLAYBACK_STATUS_STOPPED: + if (!format_stopped.empty()) formatstr = format_stopped; + break; + } + std::string label_format = fmt::format(formatstr, + fmt::arg("player", player), + fmt::arg("status", status), + fmt::arg("artist", artist != NULL ? artist : ""), + fmt::arg("title", title != NULL ? title : ""), + fmt::arg("album", album != NULL ? album : ""), + fmt::arg("dynamic", dynamic), + fmt::arg("player_icon", getIcon(player)), + fmt::arg("status_icon", getIcon(status))); + label_.set_markup(label_format); + + // call parent update + AModule::update(); +} + +} // namespace waybar::modules::mpris