Waybar/src/modules/bluetooth.cpp

411 lines
16 KiB
C++
Raw Normal View History

#include "modules/bluetooth.hpp"
2020-01-22 10:37:47 +00:00
2022-05-06 15:01:43 +00:00
#include <fmt/format.h>
#include <spdlog/spdlog.h>
2022-05-02 16:11:21 +00:00
#include <algorithm>
#include <sstream>
2022-05-02 16:11:21 +00:00
namespace {
using GDBusManager = std::unique_ptr<GDBusObjectManager, void (*)(GDBusObjectManager*)>;
auto generateManager() -> GDBusManager {
GError* error = nullptr;
GDBusObjectManager* manager = g_dbus_object_manager_client_new_for_bus_sync(
2022-05-06 15:01:43 +00:00
G_BUS_TYPE_SYSTEM,
GDBusObjectManagerClientFlags::G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START,
"org.bluez", "/", NULL, NULL, NULL, NULL, &error);
2022-05-02 16:11:21 +00:00
if (error) {
spdlog::error("g_dbus_object_manager_client_new_for_bus_sync() failed: {}", error->message);
g_error_free(error);
}
auto destructor = [](GDBusObjectManager* manager) {
if (manager) {
g_object_unref(manager);
}
};
return GDBusManager{manager, destructor};
}
auto getBoolProperty(GDBusProxy* proxy, const char* property_name) -> bool {
auto gvar = g_dbus_proxy_get_cached_property(proxy, property_name);
if (gvar) {
bool property_value = g_variant_get_boolean(gvar);
g_variant_unref(gvar);
return property_value;
}
spdlog::error("getBoolProperty() failed: doesn't have property {}", property_name);
return false;
}
2022-05-06 15:01:43 +00:00
auto getOptionalStringProperty(GDBusProxy* proxy, const char* property_name)
-> std::optional<std::string> {
2022-05-02 16:11:21 +00:00
auto gvar = g_dbus_proxy_get_cached_property(proxy, property_name);
if (gvar) {
std::string property_value = g_variant_get_string(gvar, NULL);
g_variant_unref(gvar);
return property_value;
}
return std::nullopt;
}
auto getStringProperty(GDBusProxy* proxy, const char* property_name) -> std::string {
auto property_value = getOptionalStringProperty(proxy, property_name);
if (!property_value.has_value()) {
spdlog::error("getStringProperty() failed: doesn't have property {}", property_name);
}
return property_value.value_or("");
}
auto getUcharProperty(GDBusProxy* proxy, const char* property_name) -> unsigned char {
auto gvar = g_dbus_proxy_get_cached_property(proxy, property_name);
if (gvar) {
unsigned char property_value;
g_variant_get(gvar, "y", &property_value);
g_variant_unref(gvar);
return property_value;
}
spdlog::error("getUcharProperty() failed: doesn't have property {}", property_name);
return 0;
}
} // namespace
waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& config)
: AButton(config, "bluetooth", id, " {status}", 10),
2022-05-02 16:11:21 +00:00
#ifdef WANT_RFKILL
rfkill_{RFKILL_TYPE_BLUETOOTH},
#endif
manager_(generateManager()) {
if (config_["format-device-preference"].isArray()) {
std::transform(config_["format-device-preference"].begin(),
config_["format-device-preference"].end(),
2022-05-06 15:01:43 +00:00
std::back_inserter(device_preference_), [](auto x) { return x.asString(); });
2022-05-02 16:11:21 +00:00
}
// NOTE: assumption made that the controller that is selcected stays unchanged
2022-05-02 16:11:21 +00:00
// for duration of the module
if (!findCurController(cur_controller_)) {
if (config_["controller-alias"].isString()) {
2022-05-06 15:01:43 +00:00
spdlog::error("findCurController() failed: no bluetooth controller found with alias '{}'",
config_["controller-alias"].asString());
2022-05-02 16:11:21 +00:00
} else {
spdlog::error("findCurController() failed: no bluetooth controller found");
2022-05-02 16:11:21 +00:00
}
return;
}
findConnectedDevices(cur_controller_.path, connected_devices_);
2022-05-02 16:11:21 +00:00
2022-05-06 15:01:43 +00:00
g_signal_connect(manager_.get(), "interface-proxy-properties-changed",
G_CALLBACK(onInterfaceProxyPropertiesChanged), this);
2022-05-02 16:11:21 +00:00
g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), this);
2022-05-06 15:01:43 +00:00
g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved),
this);
2022-05-02 16:11:21 +00:00
#ifdef WANT_RFKILL
rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update)));
2022-05-02 16:11:21 +00:00
#endif
dp.emit();
}
auto waybar::modules::Bluetooth::update() -> void {
2022-05-02 16:11:21 +00:00
// focussed device is either:
// - the first device in the device_preference_ list that is connected to the
// current controller (if none fallback to last connected device)
// - it is the last device that connected to the current controller
2022-05-02 16:11:21 +00:00
if (!connected_devices_.empty()) {
bool preferred_device_connected = false;
if (!device_preference_.empty()) {
for (const std::string& device_alias : device_preference_) {
2022-05-06 15:01:43 +00:00
auto it =
std::find_if(connected_devices_.begin(), connected_devices_.end(),
[device_alias](auto device) { return device_alias == device.alias; });
2022-05-02 16:11:21 +00:00
if (it != connected_devices_.end()) {
preferred_device_connected = true;
cur_focussed_device_ = *it;
break;
}
}
}
if (!preferred_device_connected) {
cur_focussed_device_ = connected_devices_.back();
}
}
std::string state;
std::string tooltip_format;
if (!cur_controller_.powered)
2022-05-02 16:11:21 +00:00
state = "off";
else if (!connected_devices_.empty())
state = "connected";
else
state = "on";
#ifdef WANT_RFKILL
2022-05-06 15:01:43 +00:00
if (rfkill_.getState()) state = "disabled";
2022-05-02 16:11:21 +00:00
#endif
2022-05-28 10:58:37 +00:00
bool battery_available =
state == "connected" && cur_focussed_device_.battery_percentage.has_value();
2022-05-28 10:35:08 +00:00
#ifdef WANT_RFKILL
// also adds enabled icon if icon for state is not defined
2022-05-28 10:58:37 +00:00
std::vector<std::string> states = {state, rfkill_.getState() ? "disabled" : "enabled"};
2022-05-28 10:35:08 +00:00
std::string icon = getIcon(0, states);
#else
std::string icon = getIcon(0, state);
#endif
std::string icon_label = icon;
std::string icon_tooltip = icon;
2022-05-02 16:11:21 +00:00
if (!alt_) {
2022-05-28 10:35:08 +00:00
if (battery_available && config_["format-connected-battery"].isString()) {
2022-05-02 16:11:21 +00:00
format_ = config_["format-connected-battery"].asString();
2022-05-28 10:35:08 +00:00
icon_label = getIcon(cur_focussed_device_.battery_percentage.value_or(0));
2022-05-02 16:11:21 +00:00
} else if (config_["format-" + state].isString()) {
format_ = config_["format-" + state].asString();
} else if (config_["format"].isString()) {
format_ = config_["format"].asString();
} else {
format_ = default_format_;
}
}
if (battery_available && config_["tooltip-format-connected-battery"].isString()) {
tooltip_format = config_["tooltip-format-connected-battery"].asString();
2022-05-28 10:35:08 +00:00
icon_tooltip = getIcon(cur_focussed_device_.battery_percentage.value_or(0));
} else if (config_["tooltip-format-" + state].isString()) {
2022-05-02 16:11:21 +00:00
tooltip_format = config_["tooltip-format-" + state].asString();
} else if (config_["tooltip-format"].isString()) {
tooltip_format = config_["tooltip-format"].asString();
}
2022-05-07 11:42:27 +00:00
format_.empty() ? event_box_.hide() : event_box_.show();
2022-05-02 16:11:21 +00:00
auto update_style_context = [this](const std::string& style_class, bool in_next_state) {
if (in_next_state && !label_->get_style_context()->has_class(style_class)) {
label_->get_style_context()->add_class(style_class);
} else if (!in_next_state && label_->get_style_context()->has_class(style_class)) {
label_->get_style_context()->remove_class(style_class);
2022-05-02 16:11:21 +00:00
}
};
update_style_context("discoverable", cur_controller_.discoverable);
update_style_context("discovering", cur_controller_.discovering);
update_style_context("pairable", cur_controller_.pairable);
2022-05-02 16:11:21 +00:00
if (!state_.empty()) {
update_style_context(state_, false);
}
2022-05-02 16:11:21 +00:00
update_style_context(state, true);
state_ = state;
label_->set_markup(fmt::format(
2022-05-06 15:01:43 +00:00
format_, fmt::arg("status", state_), fmt::arg("num_connections", connected_devices_.size()),
fmt::arg("controller_address", cur_controller_.address),
fmt::arg("controller_address_type", cur_controller_.address_type),
fmt::arg("controller_alias", cur_controller_.alias),
fmt::arg("device_address", cur_focussed_device_.address),
fmt::arg("device_address_type", cur_focussed_device_.address_type),
2022-05-28 10:58:37 +00:00
fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_label),
2022-05-06 15:01:43 +00:00
fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0))));
2022-05-02 16:11:21 +00:00
if (tooltipEnabled()) {
bool tooltip_enumerate_connections_ = config_["tooltip-format-enumerate-connected"].isString();
2022-05-06 15:01:43 +00:00
bool tooltip_enumerate_connections_battery_ =
config_["tooltip-format-enumerate-connected-battery"].isString();
if (tooltip_enumerate_connections_ || tooltip_enumerate_connections_battery_) {
std::stringstream ss;
for (DeviceInfo dev : connected_devices_) {
2022-05-06 15:01:43 +00:00
if ((tooltip_enumerate_connections_battery_ && dev.battery_percentage.has_value()) ||
tooltip_enumerate_connections_) {
ss << "\n";
2022-05-28 10:35:08 +00:00
std::string enumerate_format;
std::string enumerate_icon;
if (tooltip_enumerate_connections_battery_ && dev.battery_percentage.has_value()) {
enumerate_format = config_["tooltip-format-enumerate-connected-battery"].asString();
enumerate_icon = getIcon(dev.battery_percentage.value_or(0));
} else {
enumerate_format = config_["tooltip-format-enumerate-connected"].asString();
}
2022-05-06 15:01:43 +00:00
ss << fmt::format(
enumerate_format, fmt::arg("device_address", dev.address),
fmt::arg("device_address_type", dev.address_type),
2022-05-28 10:58:37 +00:00
fmt::arg("device_alias", dev.alias), fmt::arg("icon", enumerate_icon),
2022-05-06 15:01:43 +00:00
fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)));
}
}
device_enumerate_ = ss.str();
// don't start the connected devices text with a new line
if (!device_enumerate_.empty()) {
device_enumerate_.erase(0, 1);
}
}
label_->set_tooltip_text(fmt::format(
2022-05-06 15:01:43 +00:00
tooltip_format, fmt::arg("status", state_),
fmt::arg("num_connections", connected_devices_.size()),
fmt::arg("controller_address", cur_controller_.address),
fmt::arg("controller_address_type", cur_controller_.address_type),
fmt::arg("controller_alias", cur_controller_.alias),
fmt::arg("device_address", cur_focussed_device_.address),
fmt::arg("device_address_type", cur_focussed_device_.address_type),
2022-05-28 10:58:37 +00:00
fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_tooltip),
2022-05-06 15:01:43 +00:00
fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)),
fmt::arg("device_enumerate", device_enumerate_)));
2022-05-02 16:11:21 +00:00
}
// Call parent update
AButton::update();
2022-05-02 16:11:21 +00:00
}
2022-05-06 15:01:43 +00:00
// NOTE: only for when the org.bluez.Battery1 interface is added/removed after/before a device is
// connected/disconnected
auto waybar::modules::Bluetooth::onInterfaceAddedOrRemoved(GDBusObjectManager* manager,
GDBusObject* object,
GDBusInterface* interface,
gpointer user_data) -> void {
2022-05-02 16:11:21 +00:00
std::string interface_name = g_dbus_proxy_get_interface_name(G_DBUS_PROXY(interface));
std::string object_path = g_dbus_proxy_get_object_path(G_DBUS_PROXY(interface));
if (interface_name == "org.bluez.Battery1") {
Bluetooth* bt = static_cast<Bluetooth*>(user_data);
2022-05-06 15:01:43 +00:00
auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(),
[object_path](auto d) { return d.path == object_path; });
2022-05-02 16:11:21 +00:00
if (device != bt->connected_devices_.end()) {
device->battery_percentage = bt->getDeviceBatteryPercentage(object);
bt->dp.emit();
}
}
}
2022-05-06 15:01:43 +00:00
auto waybar::modules::Bluetooth::onInterfaceProxyPropertiesChanged(
GDBusObjectManagerClient* manager, GDBusObjectProxy* object_proxy, GDBusProxy* interface_proxy,
GVariant* changed_properties, const gchar* const* invalidated_properties, gpointer user_data)
-> void {
2022-05-02 16:11:21 +00:00
std::string interface_name = g_dbus_proxy_get_interface_name(interface_proxy);
std::string object_path = g_dbus_object_get_object_path(G_DBUS_OBJECT(object_proxy));
Bluetooth* bt = static_cast<Bluetooth*>(user_data);
if (interface_name == "org.bluez.Adapter1") {
if (object_path == bt->cur_controller_.path) {
bt->getControllerProperties(G_DBUS_OBJECT(object_proxy), bt->cur_controller_);
2022-05-02 16:11:21 +00:00
bt->dp.emit();
}
2022-05-06 15:01:43 +00:00
} else if (interface_name == "org.bluez.Device1" || interface_name == "org.bluez.Battery1") {
2022-05-02 16:11:21 +00:00
DeviceInfo device;
bt->getDeviceProperties(G_DBUS_OBJECT(object_proxy), device);
2022-05-06 15:01:43 +00:00
auto cur_device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(),
[device](auto d) { return d.path == device.path; });
2022-05-02 16:11:21 +00:00
if (cur_device == bt->connected_devices_.end()) {
if (device.connected) {
bt->connected_devices_.push_back(device);
bt->dp.emit();
}
} else {
2022-05-02 16:11:21 +00:00
if (!device.connected) {
bt->connected_devices_.erase(cur_device);
} else {
*cur_device = device;
}
bt->dp.emit();
}
}
}
2022-05-06 15:01:43 +00:00
auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object)
-> std::optional<unsigned char> {
GDBusProxy* proxy_device_bat =
G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Battery1"));
2022-05-02 16:11:21 +00:00
if (proxy_device_bat != NULL) {
unsigned char battery_percentage = getUcharProperty(proxy_device_bat, "Percentage");
g_object_unref(proxy_device_bat);
return battery_percentage;
}
return std::nullopt;
}
2022-05-06 15:01:43 +00:00
auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, DeviceInfo& device_info)
-> bool {
2022-05-02 16:11:21 +00:00
GDBusProxy* proxy_device = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Device1"));
if (proxy_device != NULL) {
device_info.path = g_dbus_object_get_object_path(object);
device_info.paired_controller = getStringProperty(proxy_device, "Adapter");
2022-05-02 16:11:21 +00:00
device_info.address = getStringProperty(proxy_device, "Address");
device_info.address_type = getStringProperty(proxy_device, "AddressType");
device_info.alias = getStringProperty(proxy_device, "Alias");
device_info.icon = getOptionalStringProperty(proxy_device, "Icon");
device_info.paired = getBoolProperty(proxy_device, "Paired");
device_info.trusted = getBoolProperty(proxy_device, "Trusted");
device_info.blocked = getBoolProperty(proxy_device, "Blocked");
device_info.connected = getBoolProperty(proxy_device, "Connected");
device_info.services_resolved = getBoolProperty(proxy_device, "ServicesResolved");
g_object_unref(proxy_device);
device_info.battery_percentage = getDeviceBatteryPercentage(object);
return true;
}
return false;
}
2022-05-06 15:01:43 +00:00
auto waybar::modules::Bluetooth::getControllerProperties(GDBusObject* object,
ControllerInfo& controller_info) -> bool {
GDBusProxy* proxy_controller =
G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Adapter1"));
2022-05-02 16:11:21 +00:00
if (proxy_controller != NULL) {
controller_info.path = g_dbus_object_get_object_path(object);
controller_info.address = getStringProperty(proxy_controller, "Address");
controller_info.address_type = getStringProperty(proxy_controller, "AddressType");
controller_info.alias = getStringProperty(proxy_controller, "Alias");
controller_info.powered = getBoolProperty(proxy_controller, "Powered");
controller_info.discoverable = getBoolProperty(proxy_controller, "Discoverable");
controller_info.pairable = getBoolProperty(proxy_controller, "Pairable");
controller_info.discovering = getBoolProperty(proxy_controller, "Discovering");
2022-05-02 16:11:21 +00:00
g_object_unref(proxy_controller);
2022-05-02 16:11:21 +00:00
return true;
}
return false;
}
auto waybar::modules::Bluetooth::findCurController(ControllerInfo& controller_info) -> bool {
bool found_controller = false;
2022-05-02 16:11:21 +00:00
GList* objects = g_dbus_object_manager_get_objects(manager_.get());
for (GList* l = objects; l != NULL; l = l->next) {
GDBusObject* object = G_DBUS_OBJECT(l->data);
2022-05-06 15:01:43 +00:00
if (getControllerProperties(object, controller_info) &&
(!config_["controller-alias"].isString() ||
config_["controller-alias"].asString() == controller_info.alias)) {
found_controller = true;
2022-05-02 16:11:21 +00:00
break;
}
}
g_list_free_full(objects, g_object_unref);
return found_controller;
2022-05-02 16:11:21 +00:00
}
2022-05-06 15:01:43 +00:00
auto waybar::modules::Bluetooth::findConnectedDevices(const std::string& cur_controller_path,
std::vector<DeviceInfo>& connected_devices)
-> void {
2022-05-02 16:11:21 +00:00
GList* objects = g_dbus_object_manager_get_objects(manager_.get());
2022-05-06 15:01:43 +00:00
for (GList* l = objects; l != NULL; l = l->next) {
2022-05-02 16:11:21 +00:00
GDBusObject* object = G_DBUS_OBJECT(l->data);
DeviceInfo device;
2022-05-06 15:01:43 +00:00
if (getDeviceProperties(object, device) && device.connected &&
device.paired_controller == cur_controller_.path) {
2022-05-02 16:11:21 +00:00
connected_devices.push_back(device);
}
}
2022-05-02 16:11:21 +00:00
g_list_free_full(objects, g_object_unref);
}