diff --git a/include/factory.hpp b/include/factory.hpp index cb25078d..fea5ba99 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -68,6 +68,9 @@ #ifdef HAVE_UPOWER #include "modules/upower/upower.hpp" #endif +#ifdef HAVE_PIPEWIRE +#include "modules/privacy/privacy.hpp" +#endif #ifdef HAVE_LIBPULSE #include "modules/pulseaudio.hpp" #endif @@ -101,7 +104,7 @@ namespace waybar { class Factory { public: Factory(const Bar& bar, const Json::Value& config); - AModule* makeModule(const std::string& name) const; + AModule* makeModule(const std::string& name, const std::string& pos) const; private: const Bar& bar_; diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp new file mode 100644 index 00000000..3ec767a6 --- /dev/null +++ b/include/modules/privacy/privacy.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "ALabel.hpp" +#include "gtkmm/box.h" +#include "modules/privacy/privacy_item.hpp" +#include "util/pipewire/pipewire_backend.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +using waybar::util::PipewireBackend::PrivacyNodeInfo; + +namespace waybar::modules::privacy { + +class Privacy : public AModule { + public: + Privacy(const std::string &, const Json::Value &, const std::string &pos); + auto update() -> void override; + + void onPrivacyNodesChanged(); + + private: + std::list nodes_screenshare; // Screen is being shared + std::list nodes_audio_in; // Application is using the microphone + std::list nodes_audio_out; // Application is outputting audio + + PrivacyItem privacy_item_screenshare; + PrivacyItem privacy_item_audio_input; + PrivacyItem privacy_item_audio_output; + + sigc::connection visibility_conn; + + // Config + Gtk::Box box_; + uint iconSpacing = 4; + uint iconSize = 20; + uint transition_duration = 500; + + std::shared_ptr backend = nullptr; +}; + +} // namespace waybar::modules::privacy diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp new file mode 100644 index 00000000..56d17acf --- /dev/null +++ b/include/modules/privacy/privacy_item.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "gtkmm/box.h" +#include "gtkmm/image.h" +#include "gtkmm/revealer.h" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::modules::privacy { + +class PrivacyItem : public Gtk::Revealer { + public: + PrivacyItem(const Json::Value&, enum util::PipewireBackend::PrivacyNodeType privacy_type_, + const std::string& pos); + + bool is_enabled(); + + void set_in_use(bool in_use); + + void set_icon_size(uint size); + + private: + enum util::PipewireBackend::PrivacyNodeType privacy_type; + + std::mutex mutex_; + sigc::connection signal_conn; + + bool init = false; + bool in_use = false; + std::string lastStatus; + + // Config + bool enabled = true; + std::string iconName = "image-missing-symbolic"; + + Gtk::Box box_; + Gtk::Image icon_; +}; + +} // namespace waybar::modules::privacy diff --git a/include/util/pipewire/pipewire_backend.hpp b/include/util/pipewire/pipewire_backend.hpp new file mode 100644 index 00000000..fc622567 --- /dev/null +++ b/include/util/pipewire/pipewire_backend.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "util/backend_common.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +class PipewireBackend { + private: + pw_thread_loop* mainloop_; + pw_context* context_; + pw_core* core_; + + spa_hook registry_listener; + + /* Hack to keep constructor inaccessible but still public. + * This is required to be able to use std::make_shared. + * It is important to keep this class only accessible via a reference-counted + * pointer because the destructor will manually free memory, and this could be + * a problem with C++20's copy and move semantics. + */ + struct private_constructor_tag {}; + + public: + std::mutex mutex_; + + pw_registry* registry; + + sigc::signal privacy_nodes_changed_signal_event; + + std::unordered_map privacy_nodes; + + static std::shared_ptr getInstance(); + + PipewireBackend(private_constructor_tag tag); + ~PipewireBackend(); +}; +} // namespace waybar::util::pipewire::PipewireBackend diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp new file mode 100644 index 00000000..c645ade5 --- /dev/null +++ b/include/util/pipewire/privacy_node_info.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +namespace waybar::util::PipewireBackend { + +enum PrivacyNodeType { + PRIVACY_NODE_TYPE_NONE, + PRIVACY_NODE_TYPE_VIDEO_INPUT, + PRIVACY_NODE_TYPE_AUDIO_INPUT, + PRIVACY_NODE_TYPE_AUDIO_OUTPUT +}; + +class PrivacyNodeInfo { + public: + PrivacyNodeType type = PRIVACY_NODE_TYPE_NONE; + uint32_t id; + uint32_t client_id; + enum pw_node_state state = PW_NODE_STATE_IDLE; + std::string media_class; + std::string media_name; + std::string node_name; + + struct spa_hook node_listener; + + bool changed = false; + + void* data; + + PrivacyNodeInfo(uint32_t id_, void* data_) : id(id_), data(data_) {} + + ~PrivacyNodeInfo() { spa_hook_remove(&node_listener); } +}; +} // namespace waybar::util::pipewire::PipewireBackend diff --git a/meson.build b/meson.build index bb9abdeb..8bea0ce3 100644 --- a/meson.build +++ b/meson.build @@ -98,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')) +pipewire = dependency('libpipewire-0.3', required: get_option('pipewire')) 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')) @@ -273,6 +274,14 @@ if (upower_glib.found() and giounix.found() and not get_option('logind').disable src_files += 'src/modules/upower/upower_tooltip.cpp' endif + +if (pipewire.found()) + add_project_arguments('-DHAVE_PIPEWIRE', language: 'cpp') + src_files += 'src/modules/privacy/privacy.cpp' + src_files += 'src/modules/privacy/privacy_item.cpp' + src_files += 'src/util/pipewire_backend.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' @@ -373,9 +382,12 @@ endif subdir('protocol') +app_resources = [] +subdir('resources/icons') + executable( 'waybar', - src_files, + [src_files, app_resources], dependencies: [ thread_dep, client_protos, @@ -392,6 +404,7 @@ executable( libnl, libnlgen, upower_glib, + pipewire, playerctl, libpulse, libjack, diff --git a/meson_options.txt b/meson_options.txt index 7dacf087..827f9ac1 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('pipewire', type: 'feature', value: 'auto', description: 'Enable support for pipewire') 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') diff --git a/resources/icons/meson.build b/resources/icons/meson.build new file mode 100644 index 00000000..05532d3d --- /dev/null +++ b/resources/icons/meson.build @@ -0,0 +1,6 @@ +gnome = import('gnome') + +app_resources += gnome.compile_resources('icon-resources', + 'waybar_icons.gresource.xml', + c_name: 'waybar_icons' +) diff --git a/resources/icons/waybar-privacy-audio-input-symbolic.svg b/resources/icons/waybar-privacy-audio-input-symbolic.svg new file mode 100644 index 00000000..61356891 --- /dev/null +++ b/resources/icons/waybar-privacy-audio-input-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/waybar-privacy-audio-output-symbolic.svg b/resources/icons/waybar-privacy-audio-output-symbolic.svg new file mode 100644 index 00000000..10ad4f9d --- /dev/null +++ b/resources/icons/waybar-privacy-audio-output-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/icons/waybar-privacy-screen-share-symbolic.svg b/resources/icons/waybar-privacy-screen-share-symbolic.svg new file mode 100644 index 00000000..9738c571 --- /dev/null +++ b/resources/icons/waybar-privacy-screen-share-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/icons/waybar_icons.gresource.xml b/resources/icons/waybar_icons.gresource.xml new file mode 100644 index 00000000..077049bf --- /dev/null +++ b/resources/icons/waybar_icons.gresource.xml @@ -0,0 +1,8 @@ + + + + waybar-privacy-audio-input-symbolic.svg + waybar-privacy-audio-output-symbolic.svg + waybar-privacy-screen-share-symbolic.svg + + diff --git a/src/bar.cpp b/src/bar.cpp index d0a187c6..1ffe2ef6 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -760,7 +760,7 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, getModules(factory, ref, group_module); module = group_module; } else { - module = factory.makeModule(ref); + module = factory.makeModule(ref, pos); } std::shared_ptr module_sp(module); diff --git a/src/client.cpp b/src/client.cpp index cd0fa55b..066247e7 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -4,6 +4,7 @@ #include +#include "gtkmm/icontheme.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" #include "util/format.hpp" @@ -244,6 +245,11 @@ int waybar::Client::main(int argc, char *argv[]) { } gtk_app = Gtk::Application::create(argc, argv, "fr.arouillard.waybar", Gio::APPLICATION_HANDLES_COMMAND_LINE); + + // Initialize Waybars GTK resources with our custom icons + auto theme = Gtk::IconTheme::get_default(); + theme->add_resource_path("/fr/arouillard/waybar/icons"); + gdk_display = Gdk::Display::get_default(); if (!gdk_display) { throw std::runtime_error("Can't find display"); diff --git a/src/factory.cpp b/src/factory.cpp index aaf46036..91a882b0 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -10,7 +10,8 @@ waybar::Factory::Factory(const Bar& bar, const Json::Value& config) : bar_(bar), config_(config) {} -waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { +waybar::AModule* waybar::Factory::makeModule(const std::string& name, + const std::string& pos) const { try { auto hash_pos = name.find('#'); auto ref = name.substr(0, hash_pos); @@ -30,6 +31,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::upower::UPower(id, config_[name]); } #endif +#ifdef HAVE_PIPEWIRE + if (ref == "privacy") { + return new waybar::modules::privacy::Privacy(id, config_[name], pos); + } +#endif #ifdef HAVE_MPRIS if (ref == "mpris") { return new waybar::modules::mpris::Mpris(id, config_[name]); diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp new file mode 100644 index 00000000..ea97b352 --- /dev/null +++ b/src/modules/privacy/privacy.cpp @@ -0,0 +1,138 @@ +#include "modules/privacy/privacy.hpp" + +#include +#include +#include + +#include +#include +#include + +#include "AModule.hpp" +#include "gtkmm/image.h" + +namespace waybar::modules::privacy { + +using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT; +using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT; +using util::PipewireBackend::PRIVACY_NODE_TYPE_NONE; +using util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT; + +Privacy::Privacy(const std::string& id, const Json::Value& config, const std::string& pos) + : AModule(config, "privacy", id), + nodes_screenshare(), + nodes_audio_in(), + nodes_audio_out(), + privacy_item_screenshare(config["screenshare"], PRIVACY_NODE_TYPE_VIDEO_INPUT, pos), + privacy_item_audio_input(config["audio-in"], PRIVACY_NODE_TYPE_AUDIO_INPUT, pos), + privacy_item_audio_output(config["audio-out"], PRIVACY_NODE_TYPE_AUDIO_OUTPUT, pos), + visibility_conn(), + box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + box_.set_name(name_); + box_.add(privacy_item_screenshare); + box_.add(privacy_item_audio_output); + box_.add(privacy_item_audio_input); + + event_box_.add(box_); + + // Icon Spacing + if (config_["icon-spacing"].isUInt()) { + iconSpacing = config_["icon-spacing"].asUInt(); + } + box_.set_spacing(iconSpacing); + + // Icon Size + if (config_["icon-size"].isUInt()) { + iconSize = config_["icon-size"].asUInt(); + } + privacy_item_screenshare.set_icon_size(iconSize); + privacy_item_audio_output.set_icon_size(iconSize); + privacy_item_audio_input.set_icon_size(iconSize); + + // Transition Duration + if (config_["transition-duration"].isUInt()) { + transition_duration = config_["transition-duration"].asUInt(); + } + privacy_item_screenshare.set_transition_duration(transition_duration); + privacy_item_audio_output.set_transition_duration(transition_duration); + privacy_item_audio_input.set_transition_duration(transition_duration); + + if (!privacy_item_screenshare.is_enabled() && !privacy_item_audio_input.is_enabled() && + !privacy_item_audio_output.is_enabled()) { + throw std::runtime_error("No privacy modules enabled"); + } + backend = util::PipewireBackend::PipewireBackend::getInstance(); + backend->privacy_nodes_changed_signal_event.connect( + sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); +} + +void Privacy::onPrivacyNodesChanged() { + nodes_audio_out.clear(); + nodes_audio_in.clear(); + nodes_screenshare.clear(); + + bool screenshare = false; + bool audio_in = false; + bool audio_out = false; + for (auto& node : backend->privacy_nodes) { + if (screenshare && audio_in && audio_out) break; + switch (node.second->state) { + case PW_NODE_STATE_RUNNING: + switch (node.second->type) { + case PRIVACY_NODE_TYPE_VIDEO_INPUT: + screenshare = true; + nodes_screenshare.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_AUDIO_INPUT: + audio_in = true; + nodes_audio_in.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + audio_out = true; + nodes_audio_out.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_NONE: + continue; + } + break; + default: + break; + } + } + + dp.emit(); +} + +auto Privacy::update() -> void { + bool screenshare = !nodes_screenshare.empty(); + bool audio_in = !nodes_audio_in.empty(); + bool audio_out = !nodes_audio_out.empty(); + + privacy_item_screenshare.set_in_use(screenshare); + privacy_item_audio_input.set_in_use(audio_in); + privacy_item_audio_output.set_in_use(audio_out); + + // Hide the whole widget if none are in use + bool is_visible = screenshare || audio_in || audio_out; + if (is_visible != event_box_.get_visible()) { + // Disconnect any previous connection so that it doesn't get activated in + // the future, hiding the module when it should be visible + visibility_conn.disconnect(); + if (is_visible) { + event_box_.set_visible(true); + } else { + visibility_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + event_box_.set_visible(false); + return false; + }, + *this), + transition_duration); + } + } + + // Call parent update + AModule::update(); +} + +} // namespace waybar::modules::privacy diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp new file mode 100644 index 00000000..943dfdbf --- /dev/null +++ b/src/modules/privacy/privacy_item.cpp @@ -0,0 +1,140 @@ +#include "modules/privacy/privacy_item.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "AModule.hpp" +#include "glibmm/main.h" +#include "glibmm/priorities.h" +#include "gtkmm/enums.h" +#include "gtkmm/label.h" +#include "gtkmm/revealer.h" +#include "gtkmm/tooltip.h" +#include "sigc++/adaptors/bind.h" +#include "util/gtk_icon.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::modules::privacy { + +PrivacyItem::PrivacyItem(const Json::Value& config_, + enum util::PipewireBackend::PrivacyNodeType privacy_type_, + const std::string& pos) + : Gtk::Revealer(), + privacy_type(privacy_type_), + mutex_(), + signal_conn(), + box_(Gtk::ORIENTATION_HORIZONTAL, 0), + icon_() { + switch (privacy_type) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + get_style_context()->add_class("audio-in"); + iconName = "waybar-privacy-audio-input-symbolic"; + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + get_style_context()->add_class("audio-out"); + iconName = "waybar-privacy-audio-output-symbolic"; + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + get_style_context()->add_class("screenshare"); + iconName = "waybar-privacy-screen-share-symbolic"; + break; + default: + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + enabled = false; + return; + } + + // Set the reveal transition to not look weird when sliding in + if (pos == "modules-left") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_RIGHT); + } else if (pos == "modules-center") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_CROSSFADE); + } else if (pos == "modules-right") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT); + } + + box_.set_name("privacy-item"); + box_.add(icon_); + add(box_); + + // Icon Name + if (config_["enabled"].isBool()) { + enabled = config_["enabled"].asBool(); + } + + // Icon Name + if (config_["icon-name"].isString()) { + iconName = config_["icon-name"].asString(); + } + icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + + // Don't show by default + set_reveal_child(true); + set_visible(false); +} + +bool PrivacyItem::is_enabled() { return enabled; } + +void PrivacyItem::set_in_use(bool in_use) { + mutex_.lock(); + if (this->in_use == in_use && init) { + mutex_.unlock(); + return; + } + + if (init) { + // Disconnect any previous connection so that it doesn't get activated in + // the future, hiding the module when it should be visible + signal_conn.disconnect(); + + this->in_use = in_use; + if (this->in_use) { + set_visible(true); + signal_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + set_reveal_child(true); + return false; + }, + *this), + 0); + } else { + set_reveal_child(false); + signal_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + set_visible(false); + return false; + }, + *this), + get_transition_duration()); + } + } else { + set_visible(false); + set_reveal_child(false); + } + this->init = true; + + // CSS status class + const std::string status = this->in_use ? "in-use" : ""; + // Remove last status if it exists + if (!lastStatus.empty() && get_style_context()->has_class(lastStatus)) { + get_style_context()->remove_class(lastStatus); + } + // Add the new status class to the Box + if (!status.empty() && !get_style_context()->has_class(status)) { + get_style_context()->add_class(status); + } + lastStatus = status; + + mutex_.unlock(); +} + +void PrivacyItem::set_icon_size(uint size) { icon_.set_pixel_size(size); } + +} // namespace waybar::modules::privacy diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp new file mode 100644 index 00000000..a2ac64a1 --- /dev/null +++ b/src/util/pipewire_backend.cpp @@ -0,0 +1,149 @@ +#include "util/pipewire/pipewire_backend.hpp" + +namespace waybar::util::PipewireBackend { + +// TODO: Refresh on suspend wake +static void get_node_info(void *data_, const struct pw_node_info *info) { + PrivacyNodeInfo *p_node_info = static_cast(data_); + PipewireBackend *backend = (PipewireBackend *)p_node_info->data; + + p_node_info->state = info->state; + + const struct spa_dict_item *item; + spa_dict_for_each(item, info->props) { + if (strcmp(item->key, PW_KEY_CLIENT_ID) == 0) { + p_node_info->client_id = strtoul(item->value, NULL, 10); + } else if (strcmp(item->key, PW_KEY_MEDIA_CLASS) == 0) { + p_node_info->media_class = item->value; + if (strcmp(p_node_info->media_class.c_str(), "Stream/Input/Video") == 0) { + p_node_info->type = PRIVACY_NODE_TYPE_VIDEO_INPUT; + } else if (strcmp(p_node_info->media_class.c_str(), "Stream/Input/Audio") == 0) { + p_node_info->type = PRIVACY_NODE_TYPE_AUDIO_INPUT; + } else if (strcmp(p_node_info->media_class.c_str(), "Stream/Output/Audio") == 0) { + p_node_info->type = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; + } + } else if (strcmp(item->key, PW_KEY_MEDIA_NAME) == 0) { + p_node_info->media_name = item->value; + } else if (strcmp(item->key, PW_KEY_NODE_NAME) == 0) { + p_node_info->node_name = item->value; + } + } + + if (p_node_info->type != PRIVACY_NODE_TYPE_NONE) { + backend->mutex_.lock(); + p_node_info->changed = true; + backend->privacy_nodes.insert_or_assign(info->id, p_node_info); + backend->mutex_.unlock(); + + backend->privacy_nodes_changed_signal_event.emit(); + } else { + if (p_node_info->changed) { + backend->mutex_.lock(); + PrivacyNodeInfo *node = backend->privacy_nodes.at(info->id); + delete node; + backend->privacy_nodes.erase(info->id); + backend->mutex_.unlock(); + + backend->privacy_nodes_changed_signal_event.emit(); + } + } +} + +static const struct pw_node_events node_events = { + .version = PW_VERSION_NODE_EVENTS, + .info = get_node_info, +}; + +static void registry_event_global(void *_data, uint32_t id, uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) { + if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; + + PipewireBackend *backend = static_cast(_data); + struct pw_proxy *proxy = (pw_proxy *)pw_registry_bind(backend->registry, id, type, version, 0); + if (proxy) { + PrivacyNodeInfo *p_node_info; + backend->mutex_.lock(); + if (backend->privacy_nodes.contains(id)) { + p_node_info = backend->privacy_nodes.at(id); + } else { + p_node_info = new PrivacyNodeInfo(id, backend); + } + backend->mutex_.unlock(); + pw_proxy_add_object_listener(proxy, &p_node_info->node_listener, &node_events, p_node_info); + } +} + +static void registry_event_global_remove(void *_data, uint32_t id) { + auto backend = static_cast(_data); + + backend->mutex_.lock(); + if (backend->privacy_nodes.contains(id)) { + PrivacyNodeInfo *node_info = backend->privacy_nodes.at(id); + delete node_info; + backend->privacy_nodes.erase(id); + } + backend->mutex_.unlock(); + + backend->privacy_nodes_changed_signal_event.emit(); +} + +static const struct pw_registry_events registry_events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +PipewireBackend::PipewireBackend(private_constructor_tag tag) + : mainloop_(nullptr), context_(nullptr), core_(nullptr) { + pw_init(nullptr, nullptr); + mainloop_ = pw_thread_loop_new("waybar", nullptr); + if (mainloop_ == nullptr) { + throw std::runtime_error("pw_thread_loop_new() failed."); + } + context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0); + if (context_ == nullptr) { + throw std::runtime_error("pa_context_new() failed."); + } + core_ = pw_context_connect(context_, nullptr, 0); + if (core_ == nullptr) { + throw std::runtime_error("pw_context_connect() failed"); + } + registry = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0); + + spa_zero(registry_listener); + pw_registry_add_listener(registry, ®istry_listener, ®istry_events, this); + if (pw_thread_loop_start(mainloop_) < 0) { + throw std::runtime_error("pw_thread_loop_start() failed."); + } +} + +PipewireBackend::~PipewireBackend() { + for (auto &node : privacy_nodes) { + delete node.second; + } + + if (registry != nullptr) { + pw_proxy_destroy((struct pw_proxy *)registry); + } + + spa_zero(registry_listener); + + if (core_ != nullptr) { + pw_core_disconnect(core_); + } + + if (context_ != nullptr) { + pw_context_destroy(context_); + } + + if (mainloop_ != nullptr) { + pw_thread_loop_stop(mainloop_); + pw_thread_loop_destroy(mainloop_); + } +} + +std::shared_ptr PipewireBackend::getInstance() { + private_constructor_tag tag; + return std::make_shared(tag); +} +} // namespace waybar::util::PipewireBackend