Merge pull request #4504 from ErikReider/geoclue-privacy

Add GeoClue2 location privacy item
This commit is contained in:
Alexis Rouillard
2026-07-04 01:50:22 +02:00
committed by GitHub
15 changed files with 314 additions and 73 deletions
+69 -20
View File
@@ -10,10 +10,11 @@
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;
using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT;
using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT;
using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_LOCATION;
using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_NONE;
using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT;
Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientation orientation,
const std::string& pos)
@@ -21,7 +22,9 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat
nodes_screenshare(),
nodes_audio_in(),
nodes_audio_out(),
location_in_use(false),
visibility_conn(),
geoclue_timeout_conn(),
box_(orientation, 0) {
box_.set_name(name_);
@@ -45,20 +48,21 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat
// Initialize each privacy module
Json::Value modules = config_["modules"];
// Add Screenshare and Mic usage as default modules if none are specified
// Add Screenshare, Mic, and Location usage as default modules if none are specified
if (!modules.isArray() || modules.empty()) {
modules = Json::Value(Json::arrayValue);
for (const auto& type : {"screenshare", "audio-in"}) {
for (const auto& type : {"screenshare", "audio-in", "location"}) {
Json::Value obj = Json::Value(Json::objectValue);
obj["type"] = type;
modules.append(obj);
}
}
std::map<std::string, std::tuple<decltype(&nodes_audio_in), PrivacyNodeType> > typeMap = {
std::map<std::string, std::tuple<std::list<PWPrivacyNodeInfo*>*, PrivacyNodeType>> typeMap = {
{"screenshare", {&nodes_screenshare, PRIVACY_NODE_TYPE_VIDEO_INPUT}},
{"audio-in", {&nodes_audio_in, PRIVACY_NODE_TYPE_AUDIO_INPUT}},
{"audio-out", {&nodes_audio_out, PRIVACY_NODE_TYPE_AUDIO_OUTPUT}},
{"location", {nullptr, PRIVACY_NODE_TYPE_LOCATION}},
};
for (const auto& module : modules) {
@@ -68,8 +72,21 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat
auto iter = typeMap.find(type);
if (iter != typeMap.end()) {
auto& [nodePtr, nodeType] = iter->second;
auto* item = Gtk::make_managed<PrivacyItem>(module, nodeType, nodePtr, orientation, pos,
PrivacyItem* item = nullptr;
switch (nodeType) {
case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT:
case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT:
case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT:
item = Gtk::make_managed<PWPrivacyItem>(module, nodeType, nodePtr, orientation, pos,
iconSize, transition_duration);
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_LOCATION:
item = Gtk::make_managed<GeoCluePrivacyItem>(module, orientation, pos, iconSize,
transition_duration);
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE:
continue;
}
box_.add(*item);
modules_.push_back(item);
}
@@ -93,20 +110,24 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat
ignore_monitor = config_["ignore-monitor"].asBool();
}
backend = util::PipewireBackend::PipewireBackend::getInstance();
backend->privacy_nodes_changed_signal_event.connect(
sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged));
pw_backend = util::PipewireBackend::PipewireBackend::getInstance();
pw_backend->privacy_nodes_changed_signal_event.connect(
sigc::mem_fun(*this, &Privacy::onPWPrivacyNodesChanged));
geoclue_backend = util::GeoClueBackend::GeoClueBackend::getInstance();
geoclue_backend->in_use_changed_signal_event.connect(
sigc::mem_fun(*this, &Privacy::onGeoCluePrivacyNodesChanged));
dp.emit();
}
void Privacy::onPrivacyNodesChanged() {
void Privacy::onPWPrivacyNodesChanged() {
mutex_.lock();
nodes_audio_out.clear();
nodes_audio_in.clear();
nodes_screenshare.clear();
for (auto& node : backend->privacy_nodes) {
for (auto& node : pw_backend->privacy_nodes) {
if (ignore_monitor && node.second->is_monitor) continue;
auto iter = ignore.find(std::pair(node.second->type, node.second->node_name));
@@ -124,7 +145,7 @@ void Privacy::onPrivacyNodesChanged() {
case PRIVACY_NODE_TYPE_AUDIO_OUTPUT:
nodes_audio_out.push_back(node.second);
break;
case PRIVACY_NODE_TYPE_NONE:
default:
continue;
}
break;
@@ -137,36 +158,63 @@ void Privacy::onPrivacyNodesChanged() {
dp.emit();
}
bool Privacy::locationTimeout(bool in_use) {
location_in_use.store(geoclue_backend->location_in_use);
dp.emit();
return false;
}
void Privacy::onGeoCluePrivacyNodesChanged() {
geoclue_timeout_conn.disconnect();
const bool in_use = geoclue_backend->location_in_use;
if (in_use) {
locationTimeout(in_use);
} else {
// The GeoClue service is really fast, so wait before hiding, so that the
// widget is at least visible for 3 seconds.
geoclue_timeout_conn = Glib::signal_timeout().connect_seconds(
sigc::bind(sigc::mem_fun(*this, &Privacy::locationTimeout), in_use), 3);
}
}
auto Privacy::update() -> void {
// set in modules or not
bool setScreenshare = false;
bool setAudioIn = false;
bool setAudioOut = false;
bool setLocation = false;
// used or not
bool useScreenshare = false;
bool useAudioIn = false;
bool useAudioOut = false;
bool useLocation = false;
mutex_.lock();
for (PrivacyItem* module : modules_) {
switch (module->privacy_type) {
case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT:
case PRIVACY_NODE_TYPE_VIDEO_INPUT:
setScreenshare = true;
useScreenshare = !nodes_screenshare.empty();
module->set_in_use(useScreenshare);
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT:
case PRIVACY_NODE_TYPE_AUDIO_INPUT:
setAudioIn = true;
useAudioIn = !nodes_audio_in.empty();
module->set_in_use(useAudioIn);
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT:
case PRIVACY_NODE_TYPE_AUDIO_OUTPUT:
setAudioOut = true;
useAudioOut = !nodes_audio_out.empty();
module->set_in_use(useAudioOut);
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE:
case PRIVACY_NODE_TYPE_LOCATION:
setLocation = true;
useLocation = location_in_use;
module->set_in_use(useLocation);
break;
case PRIVACY_NODE_TYPE_NONE:
break;
}
}
@@ -174,7 +222,7 @@ auto Privacy::update() -> void {
// Hide the whole widget if none are in use
bool isVisible = (setScreenshare && useScreenshare) || (setAudioIn && useAudioIn) ||
(setAudioOut && useAudioOut);
(setAudioOut && useAudioOut) || (setLocation && useLocation);
if (isVisible != event_box_.get_visible()) {
// Disconnect any previous connection so that it doesn't get activated in
@@ -187,12 +235,13 @@ auto Privacy::update() -> void {
// have finished animating
visibility_conn = Glib::signal_timeout().connect(
sigc::track_obj(
[this, setScreenshare, setAudioOut, setAudioIn]() {
[this, setScreenshare, setAudioOut, setAudioIn, setLocation]() {
mutex_.lock();
bool visible = false;
visible |= setScreenshare && !nodes_screenshare.empty();
visible |= setAudioIn && !nodes_audio_in.empty();
visible |= setAudioOut && !nodes_audio_out.empty();
visible |= setLocation && location_in_use;
mutex_.unlock();
event_box_.set_visible(visible);
return false;
+30 -21
View File
@@ -8,19 +8,16 @@
#include "gtkmm/label.h"
#include "gtkmm/revealer.h"
#include "gtkmm/tooltip.h"
#include "util/pipewire/privacy_node_info.hpp"
namespace waybar::modules::privacy {
PrivacyItem::PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privacy_type_,
std::list<PrivacyNodeInfo*>* nodes_, Gtk::Orientation orientation,
const std::string& pos, const uint icon_size,
Gtk::Orientation orientation, 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),
signal_conn(),
box_(Gtk::ORIENTATION_HORIZONTAL, 0),
icon_() {
switch (privacy_type) {
@@ -36,7 +33,10 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privac
box_.get_style_context()->add_class("screenshare");
iconName = "waybar-privacy-screen-share-symbolic";
break;
default:
case util::PipewireBackend::PRIVACY_NODE_TYPE_LOCATION:
box_.get_style_context()->add_class("location");
iconName = "waybar-privacy-location-symbolic";
break;
case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE:
return;
}
@@ -81,7 +81,6 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privac
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<Gtk::Tooltip>& tooltip) {
tooltip->set_custom(tooltip_window);
@@ -103,21 +102,8 @@ void PrivacyItem::update_tooltip() {
// work differently in GTK4.
delete child;
}
for (auto* node : *nodes) {
auto* box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
// Set device icon
auto* node_icon = Gtk::make_managed<Gtk::Image>();
node_icon->set_pixel_size(tooltipIconSize);
node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID);
box->add(*node_icon);
// Set model
auto* nodeName = Gtk::make_managed<Gtk::Label>(node->getName());
box->add(*nodeName);
tooltip_window.add(*box);
}
set_tooltip();
tooltip_window.show_all();
}
@@ -161,4 +147,27 @@ void PrivacyItem::set_in_use(bool in_use) {
this->init = true;
}
void GeoCluePrivacyItem::set_tooltip() {
auto* nodeName = Gtk::make_managed<Gtk::Label>("Location in use");
tooltip_window.add(*nodeName);
}
void PWPrivacyItem::set_tooltip() {
for (auto* node : *nodes) {
auto* box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL, 4);
// Set device icon
auto* node_icon = Gtk::make_managed<Gtk::Image>();
node_icon->set_pixel_size(tooltipIconSize);
node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID);
box->add(*node_icon);
// Set model
auto* nodeName = Gtk::make_managed<Gtk::Label>(node->getName());
box->add(*nodeName);
tooltip_window.add(*box);
}
}
} // namespace waybar::modules::privacy
+88
View File
@@ -0,0 +1,88 @@
#include "util/geoclue_backend.hpp"
#include <giomm/dbuswatchname.h>
#include <string>
namespace waybar::util::GeoClueBackend {
GeoClueBackend::GeoClueBackend(PrivateConstructorTag tag)
: watcherID_(0), signal_conn(), connected(false), location_in_use(false) {
// Start watching DBUS
watcherID_ =
Gio::DBus::watch_name(Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.GeoClue2",
sigc::mem_fun(*this, &GeoClueBackend::onAppear),
sigc::mem_fun(*this, &GeoClueBackend::onVanished),
Gio::DBus::BusNameWatcherFlags::BUS_NAME_WATCHER_FLAGS_AUTO_START);
}
GeoClueBackend::~GeoClueBackend() {
if (watcherID_ > 0) {
Gio::DBus::unwatch_name(watcherID_);
watcherID_ = 0;
}
signal_conn.disconnect();
}
std::shared_ptr<GeoClueBackend> GeoClueBackend::getInstance() {
PrivateConstructorTag tag;
return std::make_shared<GeoClueBackend>(tag);
}
void GeoClueBackend::propertyChanged(
const Gio::DBus::Proxy::MapChangedProperties& changedProperties,
const std::vector<Glib::ustring>& invalidatedProperties) {
if (!connected) {
return;
}
if (auto in_use_variant = changedProperties.find("InUse");
in_use_variant != changedProperties.end()) {
bool in_use =
Glib::VariantBase::cast_dynamic<Glib::Variant<bool>>(in_use_variant->second).get();
location_in_use.store(in_use);
in_use_changed_signal_event.emit();
}
}
void GeoClueBackend::onAppear(const Glib::RefPtr<Gio::DBus::Connection>& conn,
const Glib::ustring& name, const Glib::ustring& name_owner) {
location_in_use.store(false);
signal_conn.disconnect();
connected = true;
proxy = Gio::DBus::Proxy::create_sync(conn, "org.freedesktop.GeoClue2",
"/org/freedesktop/GeoClue2/Manager",
"org.freedesktop.GeoClue2.Manager");
// Subscribe DBus events
signal_conn = proxy->signal_properties_changed().connect(
sigc::mem_fun(*this, &GeoClueBackend::propertyChanged));
// Get the initial value
auto callArgs = Glib::Variant<std::tuple<Glib::ustring, Glib::ustring>>::create(
std::make_tuple("org.freedesktop.GeoClue2.Manager", "InUse"));
Glib::VariantContainerBase base =
proxy->call_sync("org.freedesktop.DBus.Properties.Get", callArgs);
if (base.is_of_type(Glib::VariantType("(v)"))) {
Glib::Variant<std::tuple<bool>> variant_value;
base.get_child(variant_value, 0);
auto [value] = variant_value.get();
if (location_in_use != value) {
location_in_use.store(value);
}
}
in_use_changed_signal_event.emit();
}
void GeoClueBackend::onVanished(const Glib::RefPtr<Gio::DBus::Connection>& conn,
const Glib::ustring& name) {
signal_conn.disconnect();
connected = false;
location_in_use.store(false);
in_use_changed_signal_event.emit();
}
} // namespace waybar::util::GeoClueBackend
+7 -6
View File
@@ -5,7 +5,7 @@
namespace waybar::util::PipewireBackend {
static void getNodeInfo(void* data_, const struct pw_node_info* info) {
auto* pNodeInfo = static_cast<PrivacyNodeInfo*>(data_);
auto* pNodeInfo = static_cast<PWPrivacyNodeInfo*>(data_);
pNodeInfo->handleNodeEventInfo(info);
static_cast<PipewireBackend*>(pNodeInfo->data)->privacy_nodes_changed_signal_event.emit();
@@ -17,7 +17,7 @@ static const struct pw_node_events NODE_EVENTS = {
};
static void proxyDestroy(void* data) {
static_cast<PrivacyNodeInfo*>(data)->handleProxyEventDestroy();
static_cast<PWPrivacyNodeInfo*>(data)->handleProxyEventDestroy();
}
static const struct pw_proxy_events PROXY_EVENTS = {
@@ -127,12 +127,13 @@ void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permission
return;
}
auto* proxy = (pw_proxy*)pw_registry_bind(registry_, id, type, version, sizeof(PrivacyNodeInfo));
auto* proxy =
(pw_proxy*)pw_registry_bind(registry_, id, type, version, sizeof(PWPrivacyNodeInfo));
if (proxy == nullptr) return;
auto* pNodeInfo = (PrivacyNodeInfo*)pw_proxy_get_user_data(proxy);
new (pNodeInfo) PrivacyNodeInfo{};
auto* pNodeInfo = (PWPrivacyNodeInfo*)pw_proxy_get_user_data(proxy);
new (pNodeInfo) PWPrivacyNodeInfo{};
pNodeInfo->id = id;
pNodeInfo->data = this;
pNodeInfo->type = mediaType;
@@ -152,7 +153,7 @@ void PipewireBackend::handleRegistryEventGlobalRemove(uint32_t id) {
mutex_.lock();
auto iter = privacy_nodes.find(id);
if (iter != privacy_nodes.end()) {
privacy_nodes[id]->~PrivacyNodeInfo();
privacy_nodes[id]->~PWPrivacyNodeInfo();
privacy_nodes.erase(id);
}
mutex_.unlock();
+4 -4
View File
@@ -2,7 +2,7 @@
namespace waybar::util::PipewireBackend {
std::string PrivacyNodeInfo::getName() {
std::string PWPrivacyNodeInfo::getName() {
const std::vector<std::string*> names{&application_name, &node_name};
std::string name = "Unknown Application";
for (const auto& item : names) {
@@ -15,7 +15,7 @@ std::string PrivacyNodeInfo::getName() {
return name;
}
std::string PrivacyNodeInfo::getIconName() {
std::string PWPrivacyNodeInfo::getIconName() {
const std::vector<std::string*> names{&application_icon_name, &pipewire_access_portal_app_id,
&application_name, &node_name};
std::string name = "application-x-executable-symbolic";
@@ -27,12 +27,12 @@ std::string PrivacyNodeInfo::getIconName() {
return name;
}
void PrivacyNodeInfo::handleProxyEventDestroy() {
void PWPrivacyNodeInfo::handleProxyEventDestroy() {
spa_hook_remove(&proxy_listener);
spa_hook_remove(&object_listener);
}
void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info* info) {
void PWPrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info* info) {
state = info->state;
const struct spa_dict_item* item;