From c2f98d07efc23bbe7d7dc91d06f6685abf2e625d Mon Sep 17 00:00:00 2001 From: Sasha Moak Date: Wed, 16 Nov 2022 12:23:12 -0800 Subject: [PATCH] feat: wireplumber support Adds basic support for showing volume via wireplumber. Allows specifying the node-id or falling back to the default Audio/Sink node id if node-id is not set. If tooltip on hover is enabled, will show `{node_name}` by default otherwise `tooltip-format`. Format replacements: `{volume}` - Volume in percentage `{node_name}` - The node's nickname (`node.nick` property) --- include/factory.hpp | 3 + include/modules/wireplumber.hpp | 40 ++++++++ man/waybar-wireplumber.5.scd | 87 ++++++++++++++++ meson.build | 7 ++ meson_options.txt | 1 + resources/style.css | 10 ++ src/factory.cpp | 5 + src/modules/wireplumber.cpp | 172 ++++++++++++++++++++++++++++++++ 8 files changed, 325 insertions(+) create mode 100644 include/modules/wireplumber.hpp create mode 100644 man/waybar-wireplumber.5.scd create mode 100644 src/modules/wireplumber.cpp diff --git a/include/factory.hpp b/include/factory.hpp index ca707a3..8ac015a 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -72,6 +72,9 @@ #ifdef HAVE_LIBJACK #include "modules/jack.hpp" #endif +#ifdef HAVE_LIBWIREPLUMBER +#include "modules/wireplumber.hpp" +#endif #include "bar.hpp" #include "modules/custom.hpp" #include "modules/temperature.hpp" diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp new file mode 100644 index 0000000..5aa9e22 --- /dev/null +++ b/include/modules/wireplumber.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include +#include + +#include "AButton.hpp" + +namespace waybar::modules { + +class Wireplumber : public AButton { + public: + Wireplumber(const std::string&, const Json::Value&); + ~Wireplumber(); + auto update() -> void; + + private: + void loadRequiredApiModules(); + void prepare(); + void activatePlugins(); + static void updateVolume(waybar::modules::Wireplumber* self); + static void updateNodeName(waybar::modules::Wireplumber* self); + static uint32_t getDefaultNodeId(waybar::modules::Wireplumber* self); + static void onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); + static void onObjectManagerInstalled(waybar::modules::Wireplumber* self); + + WpCore* wp_core_; + GPtrArray* apis_; + WpObjectManager* om_; + uint32_t pending_plugins_; + bool muted_; + double volume_; + uint32_t node_id_{0}; + std::string node_name_; + +}; + +} // namespace waybar::modules \ No newline at end of file diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd new file mode 100644 index 0000000..3cf5694 --- /dev/null +++ b/man/waybar-wireplumber.5.scd @@ -0,0 +1,87 @@ +waybar-wireplumber(5) + +# NAME + +waybar - WirePlumber module + +# DESCRIPTION + +The *wireplumber* module displays the current volume reported by WirePlumber. + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: *{volume}%* ++ + The format, how information should be displayed. This format is used when other formats aren't specified. + + *format-muted*: ++ + typeof: string ++ + This format is used when the sound is muted. + +*tooltip*: ++ + typeof: bool ++ + default: *true* ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: *{node_name}* ++ + The format of information displayed in the tooltip. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label. + +*states*: ++ + typeof: object ++ + A number of volume states which get activated on certain volume levels. See *waybar-states(5)*. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should take up. + +*align*: ++ + typeof: float ++ + The alignment of the text, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right clicked on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +# FORMAT REPLACEMENTS + +*{volume}*: Volume in percentage. + +*{node_name}*: The node's nickname as reported by WirePlumber (*node.nick* property) + +# EXAMPLES + +``` +"wireplumber": { + "format": "{volume}%", + "format-muted": "", + "on-click": "helvum" +} +``` + +# STYLE + +- *#wireplumber* +- *#wireplumber.muted* diff --git a/meson.build b/meson.build index 355a3c0..3f03310 100644 --- a/meson.build +++ b/meson.build @@ -101,6 +101,7 @@ libevdev = dependency('libevdev', required: get_option('libevdev')) libmpdclient = dependency('libmpdclient', required: get_option('mpd')) xkbregistry = dependency('xkbregistry') libjack = dependency('jack', required: get_option('jack')) +libwireplumber = dependency('wireplumber-0.4', required: get_option('wireplumber')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() @@ -247,6 +248,11 @@ if libjack.found() src_files += 'src/modules/jack.cpp' endif +if libwireplumber.found() + add_project_arguments('-DHAVE_LIBWIREPLUMBER', language: 'cpp') + src_files += 'src/modules/wireplumber.cpp' +endif + if dbusmenu_gtk.found() add_project_arguments('-DHAVE_DBUSMENU', language: 'cpp') src_files += files( @@ -330,6 +336,7 @@ executable( upower_glib, libpulse, libjack, + libwireplumber, libudev, libinotify, libepoll, diff --git a/meson_options.txt b/meson_options.txt index bd5eb81..402912f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -16,3 +16,4 @@ option('logind', type: 'feature', value: 'auto', description: 'Enable support fo option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('experimental', type : 'boolean', value : false, description: 'Enable experimental features') option('jack', type: 'feature', value: 'auto', description: 'Enable support for JACK') +option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') \ No newline at end of file diff --git a/resources/style.css b/resources/style.css index 40d870a..cf5c5fb 100644 --- a/resources/style.css +++ b/resources/style.css @@ -81,6 +81,7 @@ button:hover { #backlight, #network, #pulseaudio, +#wireplumber, #custom-media, #tray, #mode, @@ -176,6 +177,15 @@ label:focus { color: #2a5c45; } +#wireplumber { + background-color: #fff0f5; + color: #000000; +} + +#wireplumber.muted { + background-color: #f53c3c; +} + #custom-media { background-color: #66cc99; color: #2a5c45; diff --git a/src/factory.cpp b/src/factory.cpp index d00a7d4..d638a09 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -137,6 +137,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "jack") { return new waybar::modules::JACK(id, config_[name]); } +#endif +#ifdef HAVE_LIBWIREPLUMBER + if (ref == "wireplumber") { + return new waybar::modules::Wireplumber(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp new file mode 100644 index 0000000..81e639c --- /dev/null +++ b/src/modules/wireplumber.cpp @@ -0,0 +1,172 @@ +#include "modules/wireplumber.hpp" + +waybar::modules::Wireplumber::Wireplumber(const std::string &id, const Json::Value &config) + : AButton(config, "wireplumber", id, "{volume}%"), + wp_core_(nullptr), + apis_(nullptr), + om_(nullptr), + pending_plugins_(0), + muted_(false), + volume_(0.0), + node_id_(0) { + wp_init(WP_INIT_ALL); + wp_core_ = wp_core_new(NULL, NULL); + apis_ = g_ptr_array_new_with_free_func(g_object_unref); + om_ = wp_object_manager_new(); + + prepare(); + + loadRequiredApiModules(); + + if (!wp_core_connect(wp_core_)) { + throw std::runtime_error("Could not connect to PipeWire\n"); + } + + g_signal_connect_swapped(om_, "installed", (GCallback)onObjectManagerInstalled, this); + + activatePlugins(); + + dp.emit(); + } + +waybar::modules::Wireplumber::~Wireplumber() { + g_clear_pointer(&apis_, g_ptr_array_unref); + g_clear_object(&om_); + g_clear_object(&wp_core_); + +} + +uint32_t waybar::modules::Wireplumber::getDefaultNodeId(waybar::modules::Wireplumber* self) { + uint32_t id; + g_autoptr(WpPlugin) def_nodes_api = wp_plugin_find(self->wp_core_, "default-nodes-api"); + + if (!def_nodes_api) { + throw std::runtime_error("Default nodes API is not loaded\n"); + } + + g_signal_emit_by_name(def_nodes_api, "get-default-node", "Audio/Sink", &id); + + if (id <= 0 || id >= G_MAXUINT32) { + auto err = fmt::format("'{}' is not a valid ID (returned by default-nodes-api)\n", id); + throw std::runtime_error(err); + } + + return id; +} + +void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* self) { + auto proxy = static_cast(wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "object.id", "=u", self->node_id_, NULL)); + + if (!proxy) { + throw std::runtime_error(fmt::format("Object '{}' not found\n", self->node_id_)); + } + + g_autoptr(WpProperties) properties = wp_pipewire_object_get_properties(proxy); + properties = wp_properties_ensure_unique_owner(properties); + self->node_name_ = wp_properties_get(properties, "node.nick"); +} + +void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* self) { + double vol; + GVariant* variant = NULL; + g_autoptr(WpPlugin) mixer_api = wp_plugin_find(self->wp_core_, "mixer-api"); + g_signal_emit_by_name(mixer_api, "get-volume", self->node_id_, &variant); + if (!variant) { + auto err = fmt::format("Node {} does not support volume\n", self->node_id_); + throw std::runtime_error(err); + } + + g_variant_lookup(variant, "volume", "d", &vol); + g_variant_lookup(variant, "mute", "b", &self->muted_); + g_clear_pointer(&variant, g_variant_unref); + + self->volume_ = std::round(vol * 100.0F); + self->dp.emit(); +} + +void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wireplumber* self) { + self->node_id_ = self->config_["node-id"].isInt() ? self->config_["node-id"].asInt() : getDefaultNodeId(self); + + g_autoptr(WpPlugin) mixer_api = wp_plugin_find(self->wp_core_, "mixer-api"); + + updateVolume(self); + updateNodeName(self); + g_signal_connect_swapped(mixer_api, "changed", (GCallback)updateVolume, self); +} + +void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self) { + g_autoptr(GError) error = NULL; + + if (!wp_object_activate_finish(p, res, &error)) { + throw std::runtime_error(error->message); + } + + if (--self->pending_plugins_ == 0) { + wp_core_install_object_manager(self->wp_core_, self->om_); + } +} + +void waybar::modules::Wireplumber::activatePlugins() { + for (uint16_t i = 0; i < apis_->len; i++) { + WpPlugin* plugin = static_cast(g_ptr_array_index(apis_, i)); + pending_plugins_++; + wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, NULL, (GAsyncReadyCallback)onPluginActivated, this); + } +} + +void waybar::modules::Wireplumber::prepare() { + wp_object_manager_add_interest(om_, WP_TYPE_NODE, NULL); + wp_object_manager_request_object_features(om_, WP_TYPE_GLOBAL_PROXY, WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL); +} + +void waybar::modules::Wireplumber::loadRequiredApiModules() { + g_autoptr(GError) error = NULL; + + if (!wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", NULL, &error)) { + throw std::runtime_error(error->message); + } + + if (!wp_core_load_component(wp_core_, "libwireplumber-module-mixer-api", "module", NULL, &error)) { + throw std::runtime_error(error->message); + } + + g_ptr_array_add(apis_, wp_plugin_find(wp_core_, "default-nodes-api")); + g_ptr_array_add (apis_, ({ + WpPlugin *p = wp_plugin_find(wp_core_, "mixer-api"); + g_object_set (G_OBJECT (p), "scale", 1 /* cubic */, NULL); + p; + })); +} + +auto waybar::modules::Wireplumber::update() -> void { + auto format = format_; + std::string tooltip_format; + + if (muted_) { + format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; + button_.get_style_context()->add_class("muted"); + } else { + button_.get_style_context()->remove_class("muted"); + } + + std::string markup = fmt::format(format, fmt::arg("node_name", node_name_), fmt::arg("volume", volume_)); + label_->set_markup(markup); + + getState(volume_); + + if (tooltipEnabled()) { + if (tooltip_format.empty() && config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + + if (!tooltip_format.empty()) { + button_.set_tooltip_text(fmt::format( + tooltip_format, fmt::arg("node_name", node_name_), fmt::arg("volume", volume_))); + } else { + button_.set_tooltip_text(node_name_); + } + } + + // Call parent update + AButton::update(); +} \ No newline at end of file