mirror of
				https://github.com/Alexays/Waybar.git
				synced 2025-10-29 19:46:32 +00:00 
			
		
		
		
	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)
			
			
This commit is contained in:
		
							parent
							
								
									833dcc1bb8
								
							
						
					
					
						commit
						c2f98d07ef
					
				| @ -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" | ||||
|  | ||||
							
								
								
									
										40
									
								
								include/modules/wireplumber.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								include/modules/wireplumber.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include <fmt/format.h> | ||||
| #include <wp/wp.h> | ||||
| 
 | ||||
| #include <algorithm> | ||||
| #include <array> | ||||
| 
 | ||||
| #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
 | ||||
							
								
								
									
										87
									
								
								man/waybar-wireplumber.5.scd
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								man/waybar-wireplumber.5.scd
									
									
									
									
									
										Normal file
									
								
							| @ -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* | ||||
| @ -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, | ||||
|  | ||||
| @ -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') | ||||
| @ -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; | ||||
|  | ||||
| @ -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]); | ||||
|  | ||||
							
								
								
									
										172
									
								
								src/modules/wireplumber.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/modules/wireplumber.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -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<WpPipewireObject *>(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<WpPlugin *>(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(); | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user