diff --git a/include/factory.hpp b/include/factory.hpp index d69930f92..688b9acc9 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -41,6 +41,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..a19ba91d8 --- /dev/null +++ b/include/modules/mpris/mpris.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include "gtkmm/box.h" +#include "gtkmm/label.h" + +extern "C" { + #include +} + +#include "ALabel.hpp" +#include "util/sleeper_thread.hpp" + +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 playerPause_cb(PlayerctlPlayer*, gpointer); + static void playerStop_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 = ""; + std::chrono::seconds interval = std::chrono::seconds(180); + + PlayerctlPlayerManager *manager; + std::string lastStatus; + std::string lastPlayer; + + util::SleeperThread thread_; +}; + +} // namespace waybar::modules::mpris diff --git a/man/waybar-mpris.5.scd b/man/waybar-mpris.5.scd new file mode 100644 index 000000000..4c7ab999e --- /dev/null +++ b/man/waybar-mpris.5.scd @@ -0,0 +1,84 @@ +waybar-upower(5) + +# NAME + +waybar - MPRIS module + +# DESCRIPTION + +The *mpris* module displays currently playing media via libplayerctl.++ + +# CONFIGURATION + +*interval*: ++ + typeof: integer ++ + Refresh MPRIS information on a timer + +*format*: ++ + typeof: string ++ + default: {player} ({status}) {dynamic} ++ + The text format. + +*format-[status]*: ++ + typeof: string ++ + The status-specific text format. + +*format-icons*: ++ + typeof: map[string]string + Allows setting "{status-icon}" and "{player-icon}" + +*on-click*: ++ + typeof: string ++ + default: play-pause ++ + Overwrite default action toggles + +*on-middle-click*: ++ + typeof: string ++ + default: previous track ++ + Overwrite default action toggles + +*on-right-click*: ++ + typeof: string ++ + default: next track ++ + 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/man/waybar.5.scd.in b/man/waybar.5.scd.in index 54340f21b..b1ed4c527 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -266,6 +266,7 @@ A module group is defined by specifying a module named "group/some-group-name". - *waybar-keyboard-state(5)* - *waybar-memory(5)* - *waybar-mpd(5)* +- *waybar-mpris(5)* - *waybar-network(5)* - *waybar-pulseaudio(5)* - *waybar-river-mode(5)* diff --git a/meson.build b/meson.build index 557a02dc6..8e735cca2 100644 --- a/meson.build +++ b/meson.build @@ -86,7 +86,10 @@ 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', version : ['>=1.9.2'], fallback : ['jsoncpp', 'jsoncpp_dep']) sigcpp = dependency('sigc++-2.0') libinotify = dependency('libinotify', required: false) @@ -95,6 +98,7 @@ libinput = dependency('libinput', required: get_option('libinput')) 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')) @@ -238,6 +242,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' @@ -334,6 +343,7 @@ executable( libnl, libnlgen, upower_glib, + playerctl, libpulse, libjack, libwireplumber, diff --git a/meson_options.txt b/meson_options.txt index 402912f48..98cd49493 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,6 +5,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 d16cb523f..3ccf25816 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -22,6 +22,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..6b3d13fe1 --- /dev/null +++ b/src/modules/mpris/mpris.cpp @@ -0,0 +1,288 @@ +#include "modules/mpris/mpris.hpp" + +#include + +#include + +extern "C" { +#include +} + +#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(); + } + if (config_["interval"].isUInt()) { + interval = std::chrono::seconds(config_["interval"].asUInt()); + } + + 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, "pause", G_CALLBACK(playerPause_cb), this); + g_signal_connect(player, "stop", G_CALLBACK(playerStop_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); + } + } + + // allow setting an interval count that triggers periodic refreshes + if (interval.count() > 0) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval); + }; + } +} + +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); + + spdlog::debug("mpris: name-appeared callback"); + // update widget + mpris->dp.emit(); +} +void Mpris::playerVanished_cb(PlayerctlPlayerManager* manager, PlayerctlPlayer* player, + gpointer data) { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: player-vanished callback"); + // 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); + + spdlog::debug("mpris: player-play callback"); + // update widget + mpris->dp.emit(); +} +void Mpris::playerPause_cb(PlayerctlPlayer* player, gpointer data) { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: player-pause callback"); + // update widget + mpris->dp.emit(); +} +void Mpris::playerStop_cb(PlayerctlPlayer* player, gpointer data) { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: player-stop callback"); + // update widget + mpris->dp.emit(); +} +void Mpris::playerMetadata_cb(PlayerctlPlayer* player, GVariant* metadata, gpointer data) { + Mpris* mpris = static_cast(data); + if (!mpris) return; + + spdlog::debug("mpris: player-metadata callback"); + // 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); + + if (playback_status == PLAYERCTL_PLAYBACK_STATUS_STOPPED) { + return; + } + + spdlog::debug("mpris: refreshing player: {}", player); + + std::string artist = ""; + std::string album = ""; + std::string title = ""; + std::string dynamic = ""; + + gchar* artist_ = playerctl_player_print_metadata_prop(top_player, "xesam:artist", &error); + if (artist_ != NULL) { + artist = Glib::Markup::escape_text(artist_); + dynamic += artist; + dynamic += " - "; + }; + + gchar* album_ = playerctl_player_print_metadata_prop(top_player, "xesam:album", &error); + if (album_ != NULL) { + album = Glib::Markup::escape_text(album_); + dynamic += album; + dynamic += " - "; + }; + + gchar* title_ = playerctl_player_print_metadata_prop(top_player, "xesam:title", &error); + if (title_ != NULL) { + title = Glib::Markup::escape_text(title_); + 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), + fmt::arg("title", title), fmt::arg("album", 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