Initial implementation

This commit is contained in:
Erik Reider
2023-10-26 23:08:57 +02:00
parent 9ecdbcc7bc
commit f7224d8459
18 changed files with 653 additions and 4 deletions

View File

@ -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<AModule> module_sp(module);

View File

@ -4,6 +4,7 @@
#include <iostream>
#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");

View File

@ -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]);

View File

@ -0,0 +1,138 @@
#include "modules/privacy/privacy.hpp"
#include <fmt/core.h>
#include <pipewire/pipewire.h>
#include <spdlog/spdlog.h>
#include <cstdio>
#include <cstring>
#include <string>
#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

View File

@ -0,0 +1,140 @@
#include "modules/privacy/privacy_item.hpp"
#include <fmt/core.h>
#include <pipewire/pipewire.h>
#include <spdlog/spdlog.h>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#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

View File

@ -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<PrivacyNodeInfo *>(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<PipewireBackend *>(_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<PipewireBackend *>(_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, &registry_listener, &registry_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> PipewireBackend::getInstance() {
private_constructor_tag tag;
return std::make_shared<PipewireBackend>(tag);
}
} // namespace waybar::util::PipewireBackend