diff --git a/README.md b/README.md index ac9718b5..6009d91a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ - Network - Bluetooth - Pulseaudio +- Privacy Info - Wireplumber - Disk - Memory 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..b8e76768 --- /dev/null +++ b/include/modules/privacy/privacy.hpp @@ -0,0 +1,41 @@ +#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 + + std::mutex mutex_; + sigc::connection visibility_conn; + + // Config + Gtk::Box box_; + uint iconSpacing = 4; + uint iconSize = 20; + uint transition_duration = 250; + + 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..a0e3038b --- /dev/null +++ b/include/modules/privacy/privacy_item.hpp @@ -0,0 +1,51 @@ +#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" + +using waybar::util::PipewireBackend::PrivacyNodeInfo; +using waybar::util::PipewireBackend::PrivacyNodeType; + +namespace waybar::modules::privacy { + +class PrivacyItem : public Gtk::Revealer { + public: + PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, + std::list *nodes, const std::string &pos, const uint icon_size, + const uint transition_duration); + + enum PrivacyNodeType privacy_type; + + void set_in_use(bool in_use); + + private: + std::list *nodes; + + sigc::connection signal_conn; + + Gtk::Box tooltip_window; + + bool init = false; + bool in_use = false; + + // Config + std::string iconName = "image-missing-symbolic"; + bool tooltip = true; + uint tooltipIconSize = 24; + + Gtk::Box box_; + Gtk::Image icon_; + + void update_tooltip(); +}; + +} // 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..4e23b282 --- /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::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..3b7f446d --- /dev/null +++ b/include/util/pipewire/privacy_node_info.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include + +#include "util/gtk_icon.hpp" + +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; + std::string application_name; + + std::string pipewire_access_portal_app_id; + std::string application_icon_name; + + struct spa_hook object_listener; + struct spa_hook proxy_listener; + + void *data; + + std::string get_name() { + const std::vector names{&application_name, &node_name}; + std::string name = "Unknown Application"; + for (auto &name_ : names) { + if (name_ != nullptr && name_->length() > 0) { + name = *name_; + name[0] = toupper(name[0]); + break; + } + } + return name; + } + + std::string get_icon_name() { + const std::vector names{&application_icon_name, &pipewire_access_portal_app_id, + &application_name, &node_name}; + const std::string name = "application-x-executable-symbolic"; + for (auto &name_ : names) { + if (name_ != nullptr && name_->length() > 0 && DefaultGtkIconThemeWrapper::has_icon(*name_)) { + return *name_; + } + } + return name; + } +}; +} // namespace waybar::util::PipewireBackend diff --git a/man/waybar-privacy.5.scd b/man/waybar-privacy.5.scd new file mode 100644 index 00000000..d13d8ed3 --- /dev/null +++ b/man/waybar-privacy.5.scd @@ -0,0 +1,85 @@ +waybar-privacy(5) + +# NAME + +waybar - privacy module + +# DESCRIPTION + +The *privacy* module displays if any application is capturing audio, sharing ++ +the screen or playing audio. + +# CONFIGURATION + +*icon-spacing*: ++ + typeof: integer ++ + default: 4 ++ + The spacing between each privacy icon. + +*icon-size*: ++ + typeof: integer ++ + default: 20 ++ + The size of each privacy icon. + +*transition-duration*: ++ + typeof: integer ++ + default: 250 ++ + Option to disable tooltip on hover. + +*modules* ++ + typeof: array of objects ++ + default: [{"type": "screenshare"}, {"type": "audio-in"}] ++ + Which privacy modules to monitor. See *MODULES CONFIGURATION* for++ + more information. + +# MODULES CONFIGURATION + +*type*: ++ + typeof: string ++ + values: "screenshare", "audio-in", "audio-out" ++ + Specifies which module to use and configure. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-icon-size*: ++ + typeof: integer ++ + default: 24 ++ + The size of each icon in the tooltip. + +# EXAMPLES + +``` +"privacy": { + "icon-spacing": 4, + "icon-size": 18, + "transition-duration": 250, + "modules": [ + { + "type": "screenshare", + "tooltip": true, + "tooltip-icon-size": 24 + }, + { + "type": "audio-out", + "tooltip": true, + "tooltip-icon-size": 24 + }, + { + "type": "audio-in", + "tooltip": true, + "tooltip-icon-size": 24 + } + ] +}, +``` + +# STYLE + +- *#privacy* +- *#privacy-item* +- *#privacy-item.screenshare* +- *#privacy-item.audio-in* +- *#privacy-item.audio-out* diff --git a/meson.build b/meson.build index de641b3b..676bef57 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, @@ -454,6 +467,7 @@ if scdoc.found() 'waybar-network.5.scd', 'waybar-pulseaudio.5.scd', 'waybar-pulseaudio-slider.5.scd', + 'waybar-privacy.5.scd', 'waybar-river-mode.5.scd', 'waybar-river-tags.5.scd', 'waybar-river-window.5.scd', 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/resources/style.css b/resources/style.css index cf5c5fb0..e6017fdb 100644 --- a/resources/style.css +++ b/resources/style.css @@ -278,3 +278,24 @@ label:focus { #scratchpad.empty { background-color: transparent; } + +#privacy { + padding: 0; +} + +#privacy-item { + padding: 0 5px; + color: white; +} + +#privacy-item.screenshare { + background-color: #cf5700; +} + +#privacy-item.audio-in { + background-color: #1ca000; +} + +#privacy-item.audio-out { + background-color: #0069d4; +} 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..e96f14fa --- /dev/null +++ b/src/modules/privacy/privacy.cpp @@ -0,0 +1,178 @@ +#include "modules/privacy/privacy.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include "AModule.hpp" +#include "gtkmm/image.h" +#include "modules/privacy/privacy_item.hpp" + +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(), + visibility_conn(), + box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + box_.set_name(name_); + + 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(); + } + + // Transition Duration + if (config_["transition-duration"].isUInt()) { + transition_duration = config_["transition-duration"].asUInt(); + } + + // Initialize each privacy module + Json::Value modules = config_["modules"]; + // Add Screenshare and Mic usage as default modules if none are specified + if (!modules.isArray() || modules.size() == 0) { + modules = Json::Value(Json::arrayValue); + for (auto& type : {"screenshare", "audio-in"}) { + Json::Value obj = Json::Value(Json::objectValue); + obj["type"] = type; + modules.append(obj); + } + } + for (uint i = 0; i < modules.size(); i++) { + const Json::Value& module_config = modules[i]; + if (!module_config.isObject() || !module_config["type"].isString()) continue; + const std::string type = module_config["type"].asString(); + if (type == "screenshare") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_VIDEO_INPUT, + &nodes_screenshare, pos, iconSize, transition_duration); + box_.add(*item); + } else if (type == "audio-in") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_AUDIO_INPUT, + &nodes_audio_in, pos, iconSize, transition_duration); + box_.add(*item); + } else if (type == "audio-out") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_AUDIO_OUTPUT, + &nodes_audio_out, pos, iconSize, transition_duration); + box_.add(*item); + } + } + + backend = util::PipewireBackend::PipewireBackend::getInstance(); + backend->privacy_nodes_changed_signal_event.connect( + sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); + + dp.emit(); +} + +void Privacy::onPrivacyNodesChanged() { + mutex_.lock(); + nodes_audio_out.clear(); + nodes_audio_in.clear(); + nodes_screenshare.clear(); + + for (auto& node : backend->privacy_nodes) { + switch (node.second->state) { + case PW_NODE_STATE_RUNNING: + switch (node.second->type) { + case PRIVACY_NODE_TYPE_VIDEO_INPUT: + nodes_screenshare.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_AUDIO_INPUT: + nodes_audio_in.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + nodes_audio_out.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_NONE: + continue; + } + break; + default: + break; + } + } + + mutex_.unlock(); + dp.emit(); +} + +auto Privacy::update() -> void { + mutex_.lock(); + bool screenshare = !nodes_screenshare.empty(); + bool audio_in = !nodes_audio_in.empty(); + bool audio_out = !nodes_audio_out.empty(); + + for (Gtk::Widget* widget : box_.get_children()) { + PrivacyItem* module = dynamic_cast(widget); + if (!module) continue; + switch (module->privacy_type) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + module->set_in_use(screenshare); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + module->set_in_use(audio_in); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + module->set_in_use(audio_out); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + break; + } + } + mutex_.unlock(); + + // 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 { + // Hides the widget when all of the privacy_item revealers animations + // have finished animating + visibility_conn = Glib::signal_timeout().connect( + sigc::track_obj( + [this] { + mutex_.lock(); + bool screenshare = !nodes_screenshare.empty(); + bool audio_in = !nodes_audio_in.empty(); + bool audio_out = !nodes_audio_out.empty(); + mutex_.unlock(); + event_box_.set_visible(screenshare || audio_in || audio_out); + 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..a0a2da57 --- /dev/null +++ b/src/modules/privacy/privacy_item.cpp @@ -0,0 +1,163 @@ +#include "modules/privacy/privacy_item.hpp" + +#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 PrivacyNodeType privacy_type_, + std::list *nodes_, const std::string &pos, + const uint icon_size, const uint transition_duration) + : Gtk::Revealer(), + privacy_type(privacy_type_), + nodes(nodes_), + signal_conn(), + tooltip_window(Gtk::ORIENTATION_VERTICAL, 0), + box_(Gtk::ORIENTATION_HORIZONTAL, 0), + icon_() { + switch (privacy_type) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + box_.get_style_context()->add_class("audio-in"); + iconName = "waybar-privacy-audio-input-symbolic"; + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + box_.get_style_context()->add_class("audio-out"); + iconName = "waybar-privacy-audio-output-symbolic"; + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + box_.get_style_context()->add_class("screenshare"); + iconName = "waybar-privacy-screen-share-symbolic"; + break; + default: + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + 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); + } + set_transition_duration(transition_duration); + + box_.set_name("privacy-item"); + box_.add(icon_); + icon_.set_pixel_size(icon_size); + add(box_); + + // Icon Name + if (config_["icon-name"].isString()) { + iconName = config_["icon-name"].asString(); + } + icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + + // Tooltip Icon Size + if (config_["tooltip-icon-size"].isUInt()) { + tooltipIconSize = config_["tooltip-icon-size"].asUInt(); + } + // Tooltip + if (config_["tooltip"].isString()) { + tooltip = config_["tooltip"].asBool(); + } + set_has_tooltip(tooltip); + if (tooltip) { + // Sets the window to use when showing the tooltip + update_tooltip(); + this->signal_query_tooltip().connect(sigc::track_obj( + [this](int x, int y, bool keyboard_tooltip, const Glib::RefPtr &tooltip) { + tooltip->set_custom(tooltip_window); + return true; + }, + *this)); + } + + // Don't show by default + set_reveal_child(true); + set_visible(false); +} + +void PrivacyItem::update_tooltip() { + // Removes all old nodes + for (auto child : tooltip_window.get_children()) { + delete child; + } + + for (auto *node : *nodes) { + Gtk::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4); + + // Set device icon + Gtk::Image *node_icon = new Gtk::Image(); + node_icon->set_pixel_size(tooltipIconSize); + node_icon->set_from_icon_name(node->get_icon_name(), Gtk::ICON_SIZE_INVALID); + box->add(*node_icon); + + // Set model + Gtk::Label *node_name = new Gtk::Label(node->get_name()); + box->add(*node_name); + + tooltip_window.add(*box); + } + + tooltip_window.show_all(); +} + +void PrivacyItem::set_in_use(bool in_use) { + if (in_use) { + update_tooltip(); + } + + if (this->in_use == in_use && init) 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; + guint duration = 0; + if (this->in_use) { + set_visible(true); + } else { + set_reveal_child(false); + duration = get_transition_duration(); + } + + signal_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + if (this->in_use) { + set_reveal_child(true); + } else { + set_visible(false); + } + return false; + }, + *this), + duration); + } else { + set_visible(false); + set_reveal_child(false); + } + this->init = true; +} + +} // namespace waybar::modules::privacy diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp new file mode 100644 index 00000000..5fe3ba62 --- /dev/null +++ b/src/util/pipewire_backend.cpp @@ -0,0 +1,155 @@ +#include "util/pipewire/pipewire_backend.hpp" + +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +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_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; + } else if (strcmp(item->key, PW_KEY_APP_NAME) == 0) { + p_node_info->application_name = item->value; + } else if (strcmp(item->key, "pipewire.access.portal.app_id") == 0) { + p_node_info->pipewire_access_portal_app_id = item->value; + } else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) { + p_node_info->application_icon_name = item->value; + } + } + + 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 proxy_destroy(void *data) { + PrivacyNodeInfo *node = (PrivacyNodeInfo *)data; + + spa_hook_remove(&node->proxy_listener); + spa_hook_remove(&node->object_listener); +} + +static const struct pw_proxy_events proxy_events = { + .version = PW_VERSION_PROXY_EVENTS, + .destroy = proxy_destroy, +}; + +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 (!props || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; + + const char *lookup_str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (!lookup_str) return; + std::string media_class = lookup_str; + enum PrivacyNodeType media_type = PRIVACY_NODE_TYPE_NONE; + if (media_class == "Stream/Input/Video") { + media_type = PRIVACY_NODE_TYPE_VIDEO_INPUT; + } else if (media_class == "Stream/Input/Audio") { + media_type = PRIVACY_NODE_TYPE_AUDIO_INPUT; + } else if (media_class == "Stream/Output/Audio") { + media_type = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; + } else { + return; + } + + PipewireBackend *backend = static_cast(_data); + struct pw_proxy *proxy = + (pw_proxy *)pw_registry_bind(backend->registry, id, type, version, sizeof(PrivacyNodeInfo)); + + if (!proxy) return; + + PrivacyNodeInfo *p_node_info = (PrivacyNodeInfo *)pw_proxy_get_user_data(proxy); + p_node_info->id = id; + p_node_info->data = backend; + p_node_info->type = media_type; + p_node_info->media_class = media_class; + + pw_proxy_add_listener(proxy, &p_node_info->proxy_listener, &proxy_events, p_node_info); + + pw_proxy_add_object_listener(proxy, &p_node_info->object_listener, &node_events, p_node_info); + + backend->privacy_nodes.insert_or_assign(id, p_node_info); +} + +static void registry_event_global_remove(void *_data, uint32_t id) { + auto backend = static_cast(_data); + + backend->mutex_.lock(); + auto iter = backend->privacy_nodes.find(id); + if (iter != backend->privacy_nodes.end()) { + 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() { + 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