diff --git a/include/factory.hpp b/include/factory.hpp index b9666976..e9d99844 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -56,15 +56,11 @@ #endif #ifdef HAVE_GIO_UNIX #include "modules/inhibitor.hpp" +#include "modules/bluetooth.hpp" #endif #include "bar.hpp" #include "modules/custom.hpp" #include "modules/temperature.hpp" -#if defined(__linux__) -#ifdef WANT_RFKILL -#include "modules/bluetooth.hpp" -#endif -#endif namespace waybar { diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index 87845c95..0e7bbf39 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -1,18 +1,77 @@ #pragma once #include "ALabel.hpp" +#ifdef WANT_RFKILL #include "util/rfkill.hpp" +#endif +#include +#include +#include +#include namespace waybar::modules { class Bluetooth : public ALabel { + struct AdapterInfo + { + std::string path; + std::string address; + std::string address_type; + // std::string name; // just use alias instead + std::string alias; + bool powered; + bool discoverable; + bool pairable; + bool discovering; + }; + + // NOTE: there are some properties that not all devices provide + struct DeviceInfo + { + std::string path; + std::string paired_adapter; + std::string address; + std::string address_type; + // std::optional name; // just use alias instead + std::string alias; + std::optional icon; + bool paired; + bool trusted; + bool blocked; + bool connected; + bool services_resolved; + // TODO: make experimental in waybar as it is also a experimental feature in bluez + std::optional battery_percentage; + }; + public: Bluetooth(const std::string&, const Json::Value&); ~Bluetooth() = default; auto update() -> void; private: + static auto onInterfaceAddedOrRemoved(GDBusObjectManager*, GDBusObject*, GDBusInterface*, gpointer) -> void; + static auto onInterfaceProxyPropertiesChanged(GDBusObjectManagerClient*, GDBusObjectProxy*, GDBusProxy*, GVariant*, const gchar* const*, gpointer) -> void; + + auto getDeviceBatteryPercentage(GDBusObject*) -> std::optional; + auto getDeviceProperties(GDBusObject*, DeviceInfo&) -> bool; + auto getAdapterProperties(GDBusObject*, AdapterInfo&) -> bool; + + auto findCurAdapter(AdapterInfo&) -> bool; + auto findConnectedDevices(const std::string&, std::vector&) -> void; + + +#ifdef WANT_RFKILL util::Rfkill rfkill_; +#endif + const std::unique_ptr manager_; + + std::string state_; + AdapterInfo cur_adapter_; + std::vector connected_devices_; + DeviceInfo cur_focussed_device_; + + std::vector device_preference_; }; } // namespace waybar::modules diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index 1797e80c..71faaf85 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -6,21 +6,44 @@ waybar - bluetooth module # DESCRIPTION -The *bluetooth* module displays information about the status of the device's bluetooth device. +The *bluetooth* module displays information about the bluetooth adapter and its connections. # CONFIGURATION Addressed by *bluetooth* +*adapter-alias*: ++ + typeof: string ++ + Use the adapter with the defined alias (name). Otherwise select a random adapter to display. Recommended to define when there is more than 1 adapter available to the system (use ```bluetoothctl show``` to show all available adapters). + +*format-device-preference*: ++ + typeof: array ++ + A ranking of bluetooth devices, addressed by their alias (name). The order is *first displayed* to *last displayed*. If this config option is not defined or none of the devices in the list are connected, it will fall back to showing the last connected device. A devices alias can be manually changed using the ```bluetoothctl set-alias``` command. + *format*: ++ typeof: string ++ - default: *{icon}* ++ + default: * {status}* ++ The format, how information should be displayed. This format is used when other formats aren't specified. -*format-icons*: ++ - typeof: array/object ++ - Based on the device status, the corresponding icon gets selected. ++ - The order is *low* to *high*. Or by the state if it is an object. +*format-disabled*: ++ + typeof: string ++ + This format is used when the displayed adapter is disabled. + +*format-off*: ++ + typeof: string ++ + This format is used when the displayed adapter is turned off. + +*format-on*: ++ + typeof: string ++ + This format is used when the displayed adapter is turned on with no devices connected. + +*format-connected*: ++ + typeof: string ++ + This format is used when the displayed adapter is connected to at least 1 device. + +*format-connected-battery*: ++ + typeof: string ++ + This format is used when the selected connected device, defined by the config option *format-device-preference*, provides is battery percentage. This needs the experimental features of bluez to be enabled to work. *rotate*: ++ typeof: integer ++ @@ -71,23 +94,65 @@ Addressed by *bluetooth* typeof: string ++ The format, how information should be displayed in the tooltip. This format is used when other formats aren't specified. +*tooltip-format-disabled*: ++ + typeof: string ++ + This format is used when the displayed adapter is disabled. + +*tooltip-format-off*: ++ + typeof: string ++ + This format is used when the displayed adapter is turned off. + +*tooltip-format-on*: ++ + typeof: string ++ + This format is used when the displayed adapter is turned on with no devices connected. + +*tooltip-format-connected*: ++ + typeof: string ++ + This format is used when the displayed adapter is connected to at least 1 device. + +*tooltip-format-connected-battery*: ++ + typeof: string ++ + This format is used when the selected connected device, defined by the config option *format-device-preference*, provides is battery percentage. This needs the experimental features of bluez to be enabled to work. + # FORMAT REPLACEMENTS *{status}*: Status of the bluetooth device. -*{icon}*: Icon, as defined in *format-icons*. +*{num_connections}*: Number of connections the selected adapter has. + +*{adapter_address}*: Address of the selected adapter. + +*{adapter_address_type}*: Address type of the selected adapter. + +*{adapter_alias}*: Alias of the selected adapter. By default equal to the *adapter_name* but can be changed by the user when there are conflicts. + +*{device_address}*: Address of the current selected selected connected device. + +*{device_address_type}*: Address type of the current selected selected connected device. + +*{device_alias}*: Alias of the current selected connected device. By default equal to the *device_name* but can be changed by the user when there are conflicts. + +*{device_battery_percentage}*: Battery percentage of the current selected device if available. Only use in the *format-connected-battery* and *tooltip-format-connected-battery*. # EXAMPLES ``` "bluetooth": { - "format": "{icon}", - "format-alt": "bluetooth: {status}", - "format-icons": { - "enabled": "", - "disabled": "" - }, - "tooltip-format": "{}" + "format": " {status}", + "format-connected": " {device_alias}", + "format-connected-battery": " {device_battery_percentage}%", + "tooltip-format": "{adapter_alias} {adapter_address}" +} +``` + +``` +"bluetooth": { + // "adapter-alias": "adapter1", // specify the adapter alias (name) if there are more than 1 on the system + "format": " {status}", + "format-connected": " {num_connections} connected", + // TODO: make it so that it shows all connected devices in the tooltip + // "tooltip-format-connected": "{device_alias}", + // "tooltip-format-connected-battery": "{device_alias} {device_battery_percentage}%", } ``` @@ -95,3 +160,9 @@ Addressed by *bluetooth* - *#bluetooth* - *#bluetooth.disabled* +- *#bluetooth.off* +- *#bluetooth.on* +- *#bluetooth.connected* +- *#bluetooth.discoverable* +- *#bluetooth.discovering* +- *#bluetooth.pairable* diff --git a/meson.build b/meson.build index 73e74a51..b3a4a4cb 100644 --- a/meson.build +++ b/meson.build @@ -253,16 +253,14 @@ endif if (giounix.found() and not get_option('logind').disabled()) add_project_arguments('-DHAVE_GIO_UNIX', language: 'cpp') src_files += 'src/modules/inhibitor.cpp' + src_files += 'src/modules/bluetooth.cpp' endif -if get_option('rfkill').enabled() - if is_linux - add_project_arguments('-DWANT_RFKILL', language: 'cpp') - src_files += files( - 'src/modules/bluetooth.cpp', - 'src/util/rfkill.cpp' - ) - endif +if get_option('rfkill').enabled() and is_linux + add_project_arguments('-DWANT_RFKILL', language: 'cpp') + src_files += files( + 'src/util/rfkill.cpp' + ) endif if tz_dep.found() diff --git a/src/factory.cpp b/src/factory.cpp index 4aaa248e..e0d99f18 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -104,17 +104,13 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { if (ref == "inhibitor") { return new waybar::modules::Inhibitor(id, bar_, config_[name]); } -#endif - if (ref == "temperature") { - return new waybar::modules::Temperature(id, config_[name]); - } -#if defined(__linux__) -#ifdef WANT_RFKILL if (ref == "bluetooth") { return new waybar::modules::Bluetooth(id, config_[name]); } #endif -#endif + if (ref == "temperature") { + return new waybar::modules::Temperature(id, config_[name]); + } if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name]); } diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 371bc7d3..8981083e 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -1,30 +1,358 @@ #include "modules/bluetooth.hpp" +#include +#include #include +namespace { + +using GDBusManager = std::unique_ptr; + +auto generateManager() -> GDBusManager { + GError* error = nullptr; + GDBusObjectManager* manager = g_dbus_object_manager_client_new_for_bus_sync( + G_BUS_TYPE_SYSTEM, + GDBusObjectManagerClientFlags::G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, + "org.bluez", + "/", + NULL, + NULL, + NULL, + NULL, + &error + ); + + 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; +} + +auto getOptionalStringProperty(GDBusProxy* proxy, const char* property_name) -> std::optional { + 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) - : ALabel(config, "bluetooth", id, "{icon}", 10), rfkill_{RFKILL_TYPE_BLUETOOTH} { + : ALabel(config, "bluetooth", id, " {status}", 10), +#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(), + std::back_inserter(device_preference_), + [](auto x){ return x.asString(); }); + } + + // NOTE: assumption made that the adapter that is selcected stays unchanged + // for duration of the module + if (!findCurAdapter(cur_adapter_)) { + if (config_["adapter-alias"].isString()) { + spdlog::error("find_cur_adapter() failed: no bluetooth adapter found with alias '{}'", config_["adapter-alias"].asString()); + } else { + spdlog::error("find_cur_adapter() failed: no bluetooth adapter found"); + } + return; + } + findConnectedDevices(cur_adapter_.path, connected_devices_); + + g_signal_connect(manager_.get(), "interface-proxy-properties-changed", G_CALLBACK(onInterfaceProxyPropertiesChanged), this); + g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), this); + g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), this); +#ifdef WANT_RFKILL rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); +#endif + + dp.emit(); } auto waybar::modules::Bluetooth::update() -> void { - std::string status = rfkill_.getState() ? "disabled" : "enabled"; - - label_.set_markup( - fmt::format(format_, fmt::arg("status", status), fmt::arg("icon", getIcon(0, status)))); - if (status == "disabled") { - label_.get_style_context()->add_class("disabled"); - } else { - label_.get_style_context()->remove_class("disabled"); + // focussed device is either: + // - the first device in the device_preference_ list that is connected to the + // current adapter (if none fallback to last connected device) + // - it is the last device that connected to the current adapter + if (!connected_devices_.empty()) { + bool preferred_device_connected = false; + if (!device_preference_.empty()) { + for (const std::string& device_alias : device_preference_) { + auto it = std::find_if(connected_devices_.begin(), connected_devices_.end(), [device_alias](auto device){ return device_alias == device.alias; }); + if (it != connected_devices_.end()) { + preferred_device_connected = true; + cur_focussed_device_ = *it; + break; + } + } + } + if (!preferred_device_connected) { + cur_focussed_device_ = connected_devices_.back(); + } } - if (tooltipEnabled()) { - if (config_["tooltip-format"].isString()) { - auto tooltip_format = config_["tooltip-format"].asString(); - auto tooltip_text = fmt::format(tooltip_format, status, fmt::arg("status", status)); - label_.set_tooltip_text(tooltip_text); + std::string state; + std::string tooltip_format; + if (!cur_adapter_.powered) + state = "off"; + else if (!connected_devices_.empty()) + state = "connected"; + else + state = "on"; +#ifdef WANT_RFKILL + if (rfkill_.getState()) + state = "disabled"; +#endif + + if (!alt_) { + if (state == "connected" && cur_focussed_device_.battery_percentage.has_value() && config_["format-connected-battery"].isString()) { + format_ = config_["format-connected-battery"].asString(); + } else if (config_["format-" + state].isString()) { + format_ = config_["format-" + state].asString(); + } else if (config_["format"].isString()) { + format_ = config_["format"].asString(); } else { - label_.set_tooltip_text(status); + format_ = default_format_; + } + } + if (config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } else if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + + 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); + } + }; + update_style_context("discoverable", cur_adapter_.discoverable); + update_style_context("discovering", cur_adapter_.discovering); + update_style_context("pairable", cur_adapter_.pairable); + if (!state_.empty()) { + update_style_context(state_, false); + } + update_style_context(state, true); + state_ = state; + + label_.set_markup(fmt::format(format_, + fmt::arg("status", state_), + fmt::arg("num_connections", connected_devices_.size()), + fmt::arg("adapter_address", cur_adapter_.address), + fmt::arg("adapter_address_type", cur_adapter_.address_type), + fmt::arg("adapter_alias", cur_adapter_.alias), + fmt::arg("device_address", cur_focussed_device_.address), + fmt::arg("device_address_type", cur_focussed_device_.address_type), + fmt::arg("device_alias", cur_focussed_device_.alias), + fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)) + )); + + // TODO: make possible to show information about all connected devices in the tooltip + if (tooltipEnabled()) { + label_.set_tooltip_text(fmt::format(tooltip_format, + fmt::arg("status", state_), + fmt::arg("num_connections", connected_devices_.size()), + fmt::arg("adapter_address", cur_adapter_.address), + fmt::arg("adapter_address_type", cur_adapter_.address_type), + fmt::arg("adapter_alias", cur_adapter_.alias), + fmt::arg("device_address", cur_focussed_device_.address), + fmt::arg("device_address_type", cur_focussed_device_.address_type), + fmt::arg("device_alias", cur_focussed_device_.alias), + fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)) + )); + } + + // Call parent update + ALabel::update(); +} + +// 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 +{ + 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(user_data); + auto device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(), [object_path](auto d){ return d.path == object_path; }); + if (device != bt->connected_devices_.end()) { + device->battery_percentage = bt->getDeviceBatteryPercentage(object); + bt->dp.emit(); } } } + +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 +{ + 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(user_data); + if (interface_name == "org.bluez.Adapter1") { + if (object_path == bt->cur_adapter_.path) { + bt->getAdapterProperties(G_DBUS_OBJECT(object_proxy), bt->cur_adapter_); + bt->dp.emit(); + } + } else if (interface_name == "org.bluez.Device1" || + interface_name == "org.bluez.Battery1") { + DeviceInfo device; + bt->getDeviceProperties(G_DBUS_OBJECT(object_proxy), device); + auto cur_device = std::find_if(bt->connected_devices_.begin(), bt->connected_devices_.end(), [device](auto d){ return d.path == device.path; }); + if (cur_device == bt->connected_devices_.end()) { + if (device.connected) { + bt->connected_devices_.push_back(device); + bt->dp.emit(); + } + } else { + if (!device.connected) { + bt->connected_devices_.erase(cur_device); + } else { + *cur_device = device; + } + bt->dp.emit(); + } + } +} + +auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object) -> std::optional { + GDBusProxy* proxy_device_bat = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Battery1")); + 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; +} + +auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, DeviceInfo& device_info) -> bool { + 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_adapter = getStringProperty(proxy_device, "Adapter"); + 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; +} + +auto waybar::modules::Bluetooth::getAdapterProperties(GDBusObject* object, AdapterInfo& adapter_info) -> bool { + GDBusProxy* proxy_adapter = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Adapter1")); + + if (proxy_adapter != NULL) { + adapter_info.path = g_dbus_object_get_object_path(object); + adapter_info.address = getStringProperty(proxy_adapter, "Address"); + adapter_info.address_type = getStringProperty(proxy_adapter, "AddressType"); + adapter_info.alias = getStringProperty(proxy_adapter, "Alias"); + adapter_info.powered = getBoolProperty(proxy_adapter, "Powered"); + adapter_info.discoverable = getBoolProperty(proxy_adapter, "Discoverable"); + adapter_info.pairable = getBoolProperty(proxy_adapter, "Pairable"); + adapter_info.discovering = getBoolProperty(proxy_adapter, "Discovering"); + + g_object_unref(proxy_adapter); + + return true; + } + return false; +} + +auto waybar::modules::Bluetooth::findCurAdapter(AdapterInfo& adapter_info) -> bool { + bool found_adapter = false; + + 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); + if (getAdapterProperties(object, adapter_info) && (!config_["adapter-alias"].isString() || config_["adapter-alias"].asString() == adapter_info.alias)) { + found_adapter = true; + break; + } + } + g_list_free_full(objects, g_object_unref); + + return found_adapter; +} + +auto waybar::modules::Bluetooth::findConnectedDevices(const std::string& cur_adapter_path, std::vector& connected_devices) -> void { + 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); + DeviceInfo device; + if (getDeviceProperties(object, device) && device.connected && device.paired_adapter == cur_adapter_.path) { + connected_devices.push_back(device); + } + } + g_list_free_full(objects, g_object_unref); +}