From f1acea521bbacb034ae5bb7380e78c51853faa34 Mon Sep 17 00:00:00 2001 From: Lauri Niskanen Date: Sun, 7 Jul 2024 06:47:06 +0300 Subject: [PATCH 01/38] pulseaudio: Add 'sink-mapping' config option This commit adds a new optional config to the pulseaudio modules. It maps sink names to other sink names so that if the current sink is a key, the sink named by the value is considered to be the current sink instead of it. E.g. "sink-mapping": { "easyeffects_sink": "speakers_sink" } --- include/util/audio_backend.hpp | 4 +++- man/waybar-pulseaudio.5.scd | 4 ++++ src/modules/pulseaudio.cpp | 1 + src/modules/pulseaudio_slider.cpp | 3 ++- src/util/audio_backend.cpp | 17 +++++++++++++++++ 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp index 2f53103e..b9a9e0ff 100644 --- a/include/util/audio_backend.hpp +++ b/include/util/audio_backend.hpp @@ -48,6 +48,7 @@ class AudioBackend { std::string default_source_name_; std::vector ignored_sinks_; + std::map sink_mapping_; std::function on_updated_cb_ = NOOP; @@ -69,6 +70,7 @@ class AudioBackend { void changeVolume(ChangeType change_type, double step = 1, uint16_t max_volume = 100); void setIgnoredSinks(const Json::Value& config); + void setSinkMapping(const Json::Value& config); std::string getSinkPortName() const { return port_name_; } std::string getFormFactor() const { return form_factor_; } @@ -93,4 +95,4 @@ class AudioBackend { bool isBluetooth(); }; -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index 232e84a0..4f585d4e 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -113,6 +113,10 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c typeof: array ++ Sinks in this list will not be shown as active sink by Waybar. Entries should be the sink's description field. +*sink-mapping*: ++ + typeof: object ++ + Sinks named by the values of this mapping will be considered to be the current sink instead of the sinks named by the respective keys. + *menu*: ++ typeof: string ++ Action that popups the menu. diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index 255ca571..0189daaa 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -7,6 +7,7 @@ waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); backend->setIgnoredSinks(config_["ignored-sinks"]); + backend->setSinkMapping(config_["sink-mapping"]); } bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp index bf85584e..7e642d29 100644 --- a/src/modules/pulseaudio_slider.cpp +++ b/src/modules/pulseaudio_slider.cpp @@ -6,6 +6,7 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con : ASlider(config, "pulseaudio-slider", id) { backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); backend->setIgnoredSinks(config_["ignored-sinks"]); + backend->setSinkMapping(config_["sink-mapping"]); if (config_["target"].isString()) { std::string target = config_["target"].asString(); @@ -79,4 +80,4 @@ void PulseaudioSlider::onValueChanged() { backend->changeVolume(volume, min_, max_); } -} // namespace waybar::modules \ No newline at end of file +} // namespace waybar::modules diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 3d90b6d5..c267816f 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -155,6 +155,13 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i } } + if (const auto mapping = backend->sink_mapping_.find(backend->current_sink_name_); + mapping != backend->sink_mapping_.end()) { + if (i->name == mapping->second) { + backend->current_sink_name_ = i->name; + } + } + if (backend->current_sink_name_ == i->name) { backend->current_sink_running_ = i->state == PA_SINK_RUNNING; } @@ -292,4 +299,14 @@ void AudioBackend::setIgnoredSinks(const Json::Value &config) { } } +void AudioBackend::setSinkMapping(const Json::Value &config) { + if (config.isObject()) { + for (auto it = config.begin(); it != config.end(); ++it) { + if (it.key().isString() && it->isString()) { + sink_mapping_.emplace(it.key().asString(), it->asString()); + } + } + } +} + } // namespace waybar::util From 03eba632158fabcc17cd2847ad10a08e67ad6487 Mon Sep 17 00:00:00 2001 From: Theo Ratkin Date: Tue, 29 Jul 2025 14:04:00 -0400 Subject: [PATCH 02/38] feat(mpd): add playing-interval option --- include/modules/mpd/mpd.hpp | 3 +++ include/modules/mpd/state.hpp | 1 + include/modules/mpd/state.inl.hpp | 1 + src/modules/mpd/mpd.cpp | 5 +++++ src/modules/mpd/state.cpp | 6 +++--- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 32d526e9..699ff2cd 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -28,6 +28,8 @@ class MPD : public ALabel { unsigned timeout_; + unsigned playing_interval_; + detail::unique_connection connection_; detail::unique_status status_; @@ -59,6 +61,7 @@ class MPD : public ALabel { inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } inline bool paused() const { return connection_ && state_ == MPD_STATE_PAUSE; } + inline unsigned playing_interval() const { return playing_interval_; } }; #if !defined(MPD_NOINLINE) diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 2c9071b4..93246f9f 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -194,6 +194,7 @@ class Context { bool is_paused() const; bool is_stopped() const; constexpr std::size_t interval() const; + unsigned playing_interval() const; void tryConnect() const; void checkErrors(mpd_connection*) const; void do_update(); diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp index 895970e6..76d48bb4 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -8,6 +8,7 @@ inline bool Context::is_paused() const { return mpd_module_->paused(); } inline bool Context::is_stopped() const { return mpd_module_->stopped(); } constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_.count(); } +inline unsigned Context::playing_interval() const { return mpd_module_->playing_interval(); } inline void Context::tryConnect() const { mpd_module_->tryConnect(); } inline unique_connection& Context::connection() { return mpd_module_->connection_; } constexpr inline mpd_state Context::state() const { return mpd_module_->state_; } diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 192e6c1a..479eb09e 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -22,6 +22,7 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) port_(config_["port"].isUInt() ? config["port"].asUInt() : 0), password_(config_["password"].empty() ? "" : config_["password"].asString()), timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000), + playing_interval_(config_["playing-interval"].isUInt() ? config_["playing-interval"].asUInt() : 1'000), connection_(nullptr, &mpd_connection_free), status_(nullptr, &mpd_status_free), song_(nullptr, &mpd_song_free) { @@ -33,6 +34,10 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) spdlog::warn("{}: `timeout` configuration should be an unsigned int", module_name_); } + if (!config_["playing-interval"].isNull() && !config_["playing-interval"].isUInt()) { + spdlog::warn("{}: `playing-interval` configuration should be an unsigned int", module_name_); + } + if (!config["server"].isNull()) { if (!config_["server"].isString()) { spdlog::warn("{}:`server` configuration should be a string", module_name_); diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 3d7c8561..4f10f7c3 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -119,14 +119,14 @@ bool Idle::on_io(Glib::IOCondition const&) { void Playing::entry() noexcept { sigc::slot timer_slot = sigc::mem_fun(*this, &Playing::on_timer); - timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1); - spdlog::debug("mpd: Playing: enabled 1 second periodic timer."); + timer_connection_ = Glib::signal_timeout().connect(timer_slot, ctx_->playing_interval()); + spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval()); } void Playing::exit() noexcept { if (timer_connection_.connected()) { timer_connection_.disconnect(); - spdlog::debug("mpd: Playing: disabled 1 second periodic timer."); + spdlog::debug("mpd: Playing: disabled {}ms periodic timer.", ctx_->playing_interval()); } } From 929c817eb40f48bff74ce83fbef92e7d6627cf6f Mon Sep 17 00:00:00 2001 From: Theo Ratkin Date: Fri, 8 Aug 2025 07:55:28 -0400 Subject: [PATCH 03/38] chore(man): add playing-interval option to mpd man page --- man/waybar-mpd.5.scd | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index 84abc2e8..9ce01c4d 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -29,6 +29,11 @@ Addressed by *mpd* default: 5 ++ The interval in which the connection to the MPD server is retried +*playing-interval*: ++ + typeof: integer++ + default: 1000 ++ + The interval (in milliseconds) in which the playing state is updated. + *timeout*: ++ typeof: integer++ default: 30 ++ From e355b40c66279bd62411c388f26ffde26ebdee3c Mon Sep 17 00:00:00 2001 From: winkelnp <68015877+winkelnp@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:34:50 +0100 Subject: [PATCH 04/38] Add `format-bluetooth` support to wireplumber module --- src/modules/wireplumber.cpp | 44 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index a43ad29b..4b25f817 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -14,16 +14,16 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val mixer_api_(nullptr), def_nodes_api_(nullptr), default_node_name_(nullptr), - default_source_name_(nullptr), pending_plugins_(0), muted_(false), - source_muted_(false), volume_(0.0), - source_volume_(0.0), min_step_(0.0), node_id_(0), + type_(nullptr), source_node_id_(0), - type_(nullptr) { + source_muted_(false), + source_volume_(0.0), + default_source_name_(nullptr) { waybar::modules::Wireplumber::modules.push_back(this); wp_init(WP_INIT_PIPEWIRE); @@ -418,10 +418,33 @@ void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { auto waybar::modules::Wireplumber::update() -> void { auto format = format_; std::string tooltipFormat; + std::string format_name = "format"; + + // Handle sink bluetooth state + const std::string name = default_node_name_ != nullptr ? default_node_name_ : ""; + + auto bt = name.find("bluez") != std::string::npos || name.find("a2dp-sink") != std::string::npos; + if (bt) { + // format = + // config_["format-bluetooth"].isString() ? config_["format-bluetooth"].asString() : format; + format_name += "-bluetooth"; + label_.get_style_context()->add_class("bluetooth"); + } else { + label_.get_style_context()->remove_class("bluetooth"); + } // Handle sink mute state if (muted_) { - format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; + // if (bt) + // format = config_["format-bluetooth-muted"].isString() + // ? config_["format-bluetooth-muted"].asString() + // : format; + // else + // format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; + // Check muted bluetooth format exists, otherwise fall back to default muted format. + if (format_name != "format" && !config_[format_name + "-muted"].isString()) + format_name = "format"; + format_name += "-muted"; label_.get_style_context()->add_class("muted"); label_.get_style_context()->add_class("sink-muted"); } else { @@ -441,13 +464,10 @@ auto waybar::modules::Wireplumber::update() -> void { // Get the state and apply state-specific format if available auto state = getState(vol); - if (!state.empty()) { - std::string format_name = muted_ ? "format-muted" : "format"; - std::string state_format_name = format_name + "-" + state; - if (config_[state_format_name].isString()) { - format = config_[state_format_name].asString(); - } - } + if (!state.empty() && config_[format_name + "-" + state].isString()) + format = config_[format_name + "-" + state].asString(); + else if (config_[format_name].isString()) + format = config_[format_name].asString(); // Prepare source format string (similar to PulseAudio) std::string format_source = "{volume}%"; From 2e231472159e2b2a5dfc2e4bed69855b88e8fab3 Mon Sep 17 00:00:00 2001 From: ErrorNoInternet Date: Thu, 17 Apr 2025 16:45:47 -0400 Subject: [PATCH 05/38] feat(group): add reveal-by-default option --- man/waybar.5.scd.in | 5 +++++ src/group.cpp | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 5bb62724..e817e26a 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -322,6 +322,11 @@ A group may hide all but one element, showing them only on mouse hover. In order Defines the direction of the transition animation. If true, the hidden elements will slide from left to right. If false, they will slide from right to left. When the bar is vertical, it reads as top-to-bottom. +*reveal-by-default*: ++ + typeof: bool ++ + default: false ++ + Whether the child should be revealed when Waybar starts up. This has to be used with click-to-reveal to take effect. + ``` "group/power": { "orientation": "inherit", diff --git a/src/group.cpp b/src/group.cpp index 50841efd..31d60285 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -62,13 +62,22 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& const bool left_to_right = (drawer_config["transition-left-to-right"].isBool() ? drawer_config["transition-left-to-right"].asBool() : true); + const bool reveal_by_default = + (drawer_config["reveal-by-default"].isBool() ? drawer_config["reveal-by-default"].asBool() + : false); + click_to_reveal = drawer_config["click-to-reveal"].asBool(); auto transition_type = getPreferredTransitionType(vertical); revealer.set_transition_type(transition_type); revealer.set_transition_duration(transition_duration); - revealer.set_reveal_child(false); + if (click_to_reveal && reveal_by_default) { + box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + revealer.set_reveal_child(true); + } else { + revealer.set_reveal_child(false); + } revealer.get_style_context()->add_class("drawer"); From 644a3575416e457286ffb691d0ee941f52d5a175 Mon Sep 17 00:00:00 2001 From: winkelnp <68015877+winkelnp@users.noreply.github.com> Date: Mon, 13 Oct 2025 23:27:45 +0200 Subject: [PATCH 06/38] [wireplumber] Add support for device.form-factor --- include/modules/wireplumber.hpp | 2 + src/modules/wireplumber.cpp | 72 ++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index 0565704f..9b7d5a7b 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -33,6 +33,7 @@ class Wireplumber : public ALabel { static void onDefaultNodesApiChanged(waybar::modules::Wireplumber* self); bool handleScroll(GdkEventScroll* e) override; + std::vector getWPIcon(); static std::list modules; @@ -54,6 +55,7 @@ class Wireplumber : public ALabel { bool source_muted_; double source_volume_; gchar* default_source_name_; + std::string form_factor_; }; } // namespace waybar::modules diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 4b25f817..d4962e68 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -23,7 +23,8 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val source_node_id_(0), source_muted_(false), source_volume_(0.0), - default_source_name_(nullptr) { + default_source_name_(nullptr), + form_factor_("") { waybar::modules::Wireplumber::modules.push_back(this); wp_init(WP_INIT_PIPEWIRE); @@ -97,6 +98,36 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* : description != nullptr ? description : "Unknown node name"; spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_); + + // find form-factor only if sink + if (g_strcmp0(self->type_, "Audio/Sink") == 0) { + const auto* devid = wp_properties_get(properties, "device.id"); + spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); + + auto* devproxy = static_cast( + wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, + "bound-id", "=s", devid, nullptr)); + + if (devproxy == nullptr) { + auto err = fmt::format("Object '{}' not found\n", devid); + spdlog::error("[{}]: {}", self->name_, err); + throw std::runtime_error(err); + } + + g_autoptr(WpProperties) devprop = + WP_IS_PIPEWIRE_OBJECT(devproxy) != 0 + ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(devproxy)) + : wp_properties_new_empty(); + devprop = wp_properties_ensure_unique_owner(devprop); + + if (const auto* ff = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(devproxy), "device.form-factor")) { + self->form_factor_ = ff; + spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); + } else { + self->form_factor_ = ""; + } + } } void waybar::modules::Wireplumber::updateSourceName(waybar::modules::Wireplumber* self, @@ -358,6 +389,8 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { "=s", self->type_, nullptr); wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", "Audio/Source", nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", + "=s", "Audio/Device", nullptr); } void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, @@ -415,6 +448,33 @@ void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { this); } +static const std::array ports = { + "headphone", "speaker", "headset", "hands-free", "portable", "car", "hifi", +}; + +std::vector waybar::modules::Wireplumber::getWPIcon() { + std::vector res; + if (muted_) { + res.emplace_back(node_name_ + "-muted"); + } + res.push_back(node_name_); + res.push_back(source_name_); + std::transform(form_factor_.begin(), form_factor_.end(), form_factor_.begin(), ::tolower); + for (auto const& port : ports) { + if (form_factor_.find(port) != std::string::npos) { + if (muted_) { + res.emplace_back(port + "-muted"); + } + res.push_back(port); + break; + } + } + if (muted_) { + res.emplace_back("default-muted"); + } + return res; +} + auto waybar::modules::Wireplumber::update() -> void { auto format = format_; std::string tooltipFormat; @@ -485,10 +545,10 @@ auto waybar::modules::Wireplumber::update() -> void { std::string formatted_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_vol)); - std::string markup = - fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), - fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), - fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_)); + std::string markup = fmt::format( + fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), + fmt::arg("icon", getIcon(vol, getWPIcon())), fmt::arg("format_source", formatted_source), + fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_)); label_.set_markup(markup); if (tooltipEnabled()) { @@ -499,7 +559,7 @@ auto waybar::modules::Wireplumber::update() -> void { if (!tooltipFormat.empty()) { label_.set_tooltip_text(fmt::format( fmt::runtime(tooltipFormat), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), - fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), + fmt::arg("icon", getIcon(vol, getWPIcon())), fmt::arg("format_source", formatted_source), fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_))); } else { label_.set_tooltip_text(node_name_); From deb8a56eb1cc284925c1f238b04c1e011293a6a7 Mon Sep 17 00:00:00 2001 From: winkelnp <68015877+winkelnp@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:40:56 +0200 Subject: [PATCH 07/38] [wireplumber] remove old version (bluetooth) and unnecessary checks (form-factor) --- src/modules/wireplumber.cpp | 45 +++++++++---------------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index d4962e68..cec7968d 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -99,34 +99,19 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* : "Unknown node name"; spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_); - // find form-factor only if sink - if (g_strcmp0(self->type_, "Audio/Sink") == 0) { - const auto* devid = wp_properties_get(properties, "device.id"); - spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); + // find form-factor + const auto* devid = wp_properties_get(properties, "device.id"); + spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); - auto* devproxy = static_cast( - wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, - "bound-id", "=s", devid, nullptr)); + auto* dev = static_cast(wp_object_manager_lookup( + self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", devid, nullptr)); - if (devproxy == nullptr) { - auto err = fmt::format("Object '{}' not found\n", devid); - spdlog::error("[{}]: {}", self->name_, err); - throw std::runtime_error(err); - } - - g_autoptr(WpProperties) devprop = - WP_IS_PIPEWIRE_OBJECT(devproxy) != 0 - ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(devproxy)) - : wp_properties_new_empty(); - devprop = wp_properties_ensure_unique_owner(devprop); - - if (const auto* ff = - wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(devproxy), "device.form-factor")) { - self->form_factor_ = ff; - spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); - } else { - self->form_factor_ = ""; - } + if (const auto* ff = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), "device.form-factor")) { + self->form_factor_ = ff; + spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); + } else { + self->form_factor_ = ""; } } @@ -485,8 +470,6 @@ auto waybar::modules::Wireplumber::update() -> void { auto bt = name.find("bluez") != std::string::npos || name.find("a2dp-sink") != std::string::npos; if (bt) { - // format = - // config_["format-bluetooth"].isString() ? config_["format-bluetooth"].asString() : format; format_name += "-bluetooth"; label_.get_style_context()->add_class("bluetooth"); } else { @@ -495,12 +478,6 @@ auto waybar::modules::Wireplumber::update() -> void { // Handle sink mute state if (muted_) { - // if (bt) - // format = config_["format-bluetooth-muted"].isString() - // ? config_["format-bluetooth-muted"].asString() - // : format; - // else - // format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; // Check muted bluetooth format exists, otherwise fall back to default muted format. if (format_name != "format" && !config_[format_name + "-muted"].isString()) format_name = "format"; From c831a352f2dce8feb4b606d136f06e8d29c03e7e Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 18:11:50 -0500 Subject: [PATCH 08/38] Added signal support and wait-for-activity bool. --- include/modules/idle_inhibitor.hpp | 10 +++ man/waybar-idle-inhibitor.5.scd | 44 +++++++++- src/modules/idle_inhibitor.cpp | 130 +++++++++++++++++++++++++---- 3 files changed, 167 insertions(+), 17 deletions(-) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 22bd808f..4bc9124a 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -10,21 +10,31 @@ namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; + sigc::connection activity_timeout_; + sigc::connection motion_connection_; + sigc::connection key_connection_; public: IdleInhibitor(const std::string&, const waybar::Bar&, const Json::Value&); virtual ~IdleInhibitor(); auto update() -> void override; + auto refresh(int) -> void override; static std::list modules; static bool status; private: bool handleToggle(GdkEventButton* const& e) override; + bool handleMotion(GdkEventMotion* const& e); + bool handleKey(GdkEventKey* const& e); void toggleStatus(); + void resetActivityTimeout(); + void setupActivityMonitoring(); + void teardownActivityMonitoring(); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; int pid_; + bool wait_for_activity_; }; } // namespace waybar::modules diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 405c8fc5..cd6980f2 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -76,6 +76,17 @@ screensaver, also known as "presentation mode". typeof: double ++ The number of minutes the inhibition should last. +*wait-for-activity*: ++ + typeof: bool ++ + default: *false* ++ + When enabled, the idle inhibitor remains active as long as there is keyboard or mouse activity on the bar. If there is no activity for the duration specified in *timeout*, the inhibitor will automatically toggle off. This option requires *timeout* to be set. + +*signal*: ++ + typeof: integer ++ + The signal number used to toggle the idle inhibitor externally. ++ + The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ + Use `pkill -SIGRTMIN+N waybar` to toggle the idle inhibitor from scripts or keybindings. + *tooltip*: ++ typeof: bool ++ default: true ++ @@ -115,17 +126,46 @@ screensaver, also known as "presentation mode". # EXAMPLES +Basic usage with timeout: + ``` "idle_inhibitor": { "format": "{icon}", "format-icons": { - "activated": "", - "deactivated": "" + "activated": "", + "deactivated": "" }, "timeout": 30.5 } ``` +With external control via signals (can be toggled with `pkill -SIGRTMIN+8 waybar`): + +``` +"idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "", + "deactivated": "" + }, + "signal": 8 +} +``` + +With wait-for-activity feature: + +``` +"idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "", + "deactivated": "" + }, + "timeout": 5.0, + "wait-for-activity": true +} +``` + # STYLE - *#idle_inhibitor* diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index a5fc9ac7..6012bb4c 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -11,11 +11,17 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& : ALabel(config, "idle_inhibitor", id, "{status}", 0, false, true), bar_(bar), idle_inhibitor_(nullptr), - pid_(-1) { + pid_(-1), + wait_for_activity_(false) { if (waybar::Client::inst()->idle_inhibit_manager == nullptr) { throw std::runtime_error("idle-inhibit not available"); } + // Read the wait-for-activity config option + if (config_["wait-for-activity"].isBool()) { + wait_for_activity_ = config_["wait-for-activity"].asBool(); + } + if (waybar::modules::IdleInhibitor::modules.empty() && config_["start-activated"].isBool() && config_["start-activated"].asBool() != status) { toggleStatus(); @@ -32,6 +38,8 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& } waybar::modules::IdleInhibitor::~IdleInhibitor() { + teardownActivityMonitoring(); + if (idle_inhibitor_ != nullptr) { zwp_idle_inhibitor_v1_destroy(idle_inhibitor_); idle_inhibitor_ = nullptr; @@ -77,6 +85,17 @@ auto waybar::modules::IdleInhibitor::update() -> void { ALabel::update(); } +auto waybar::modules::IdleInhibitor::refresh(int sig) -> void { + if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { + toggleStatus(); + + // Make all other idle inhibitor modules update + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } + } +} + void waybar::modules::IdleInhibitor::toggleStatus() { status = !status; @@ -89,20 +108,30 @@ void waybar::modules::IdleInhibitor::toggleStatus() { auto timeoutMins = config_["timeout"].asDouble(); int timeoutSecs = timeoutMins * 60; - timeout_ = Glib::signal_timeout().connect_seconds( - []() { - /* intentionally not tied to a module instance lifetime - * as the output with `this` can be disconnected - */ - spdlog::info("deactivating idle_inhibitor by timeout"); - status = false; - for (auto const& module : waybar::modules::IdleInhibitor::modules) { - module->update(); - } - /* disconnect */ - return false; - }, - timeoutSecs); + // If wait-for-activity is enabled, set up activity monitoring + if (wait_for_activity_) { + setupActivityMonitoring(); + resetActivityTimeout(); + } else { + // Original behavior: simple timeout + timeout_ = Glib::signal_timeout().connect_seconds( + []() { + /* intentionally not tied to a module instance lifetime + * as the output with `this` can be disconnected + */ + spdlog::info("deactivating idle_inhibitor by timeout"); + status = false; + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } + /* disconnect */ + return false; + }, + timeoutSecs); + } + } else { + // When deactivated, tear down activity monitoring + teardownActivityMonitoring(); } } @@ -121,3 +150,74 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { ALabel::handleToggle(e); return true; } + +bool waybar::modules::IdleInhibitor::handleMotion(GdkEventMotion* const& e) { + if (wait_for_activity_ && status) { + resetActivityTimeout(); + } + return false; +} + +bool waybar::modules::IdleInhibitor::handleKey(GdkEventKey* const& e) { + if (wait_for_activity_ && status) { + resetActivityTimeout(); + } + return false; +} + +void waybar::modules::IdleInhibitor::resetActivityTimeout() { + if (!config_["timeout"].isNumeric()) { + return; + } + + if (activity_timeout_.connected()) { + activity_timeout_.disconnect(); + } + + auto timeoutMins = config_["timeout"].asDouble(); + int timeoutSecs = timeoutMins * 60; + + activity_timeout_ = Glib::signal_timeout().connect_seconds( + []() { + spdlog::info("deactivating idle_inhibitor due to inactivity"); + status = false; + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } + return false; + }, + timeoutSecs); +} + +void waybar::modules::IdleInhibitor::setupActivityMonitoring() { + // Don't set up if already connected + if (motion_connection_.connected() || key_connection_.connected()) { + return; + } + + // Enable motion and key event monitoring on the bar window + auto window = bar_.window.get_window(); + if (window) { + window->set_events(window->get_events() | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); + } + + // Connect to the bar window's event signals + motion_connection_ = bar_.window.signal_motion_notify_event().connect( + sigc::mem_fun(*this, &IdleInhibitor::handleMotion)); + key_connection_ = bar_.window.signal_key_press_event().connect( + sigc::mem_fun(*this, &IdleInhibitor::handleKey)); +} + +void waybar::modules::IdleInhibitor::teardownActivityMonitoring() { + if (activity_timeout_.connected()) { + activity_timeout_.disconnect(); + } + + if (motion_connection_.connected()) { + motion_connection_.disconnect(); + } + + if (key_connection_.connected()) { + key_connection_.disconnect(); + } +} From 262f8d96f31d1ac608beadee680e5f7b63d76353 Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 18:26:06 -0500 Subject: [PATCH 09/38] fixed the compilation errors in the `idle_inhibitor.cpp` file --- src/modules/idle_inhibitor.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 6012bb4c..619c52f1 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -195,16 +195,20 @@ void waybar::modules::IdleInhibitor::setupActivityMonitoring() { return; } + // Get non-const reference to the window to set up event monitoring + // This is safe because we're only setting up signal handlers, not modifying the Bar itself + auto& window = const_cast(bar_.window); + // Enable motion and key event monitoring on the bar window - auto window = bar_.window.get_window(); - if (window) { - window->set_events(window->get_events() | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); + auto gdk_window = window.get_window(); + if (gdk_window) { + gdk_window->set_events(gdk_window->get_events() | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); } // Connect to the bar window's event signals - motion_connection_ = bar_.window.signal_motion_notify_event().connect( + motion_connection_ = window.signal_motion_notify_event().connect( sigc::mem_fun(*this, &IdleInhibitor::handleMotion)); - key_connection_ = bar_.window.signal_key_press_event().connect( + key_connection_ = window.signal_key_press_event().connect( sigc::mem_fun(*this, &IdleInhibitor::handleKey)); } From 173e7306d0a968debedd934d24a9d9513df4c721 Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 19:42:53 -0500 Subject: [PATCH 10/38] fix using the `ext-idle-notify-v1` protocol --- include/client.hpp | 2 + include/modules/idle_inhibitor.hpp | 14 ++- protocol/ext-idle-notify-v1.xml | 131 +++++++++++++++++++++++++++++ protocol/meson.build | 1 + src/client.cpp | 4 + src/modules/idle_inhibitor.cpp | 116 ++++++++++++------------- 6 files changed, 197 insertions(+), 71 deletions(-) create mode 100644 protocol/ext-idle-notify-v1.xml diff --git a/include/client.hpp b/include/client.hpp index 0e68f002..a4bc5332 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -12,6 +12,7 @@ struct zwp_idle_inhibitor_v1; struct zwp_idle_inhibit_manager_v1; +struct ext_idle_notifier_v1; namespace waybar { @@ -27,6 +28,7 @@ class Client { struct wl_registry *registry = nullptr; struct zxdg_output_manager_v1 *xdg_output_manager = nullptr; struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; + struct ext_idle_notifier_v1 *idle_notifier = nullptr; std::vector> bars; Config config; std::string bar_id; diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 4bc9124a..523426c6 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -10,9 +10,8 @@ namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; - sigc::connection activity_timeout_; - sigc::connection motion_connection_; - sigc::connection key_connection_; + struct ext_idle_notification_v1* idle_notification_; + uint32_t idle_timeout_ms_; public: IdleInhibitor(const std::string&, const waybar::Bar&, const Json::Value&); @@ -24,12 +23,11 @@ class IdleInhibitor : public ALabel { private: bool handleToggle(GdkEventButton* const& e) override; - bool handleMotion(GdkEventMotion* const& e); - bool handleKey(GdkEventKey* const& e); void toggleStatus(); - void resetActivityTimeout(); - void setupActivityMonitoring(); - void teardownActivityMonitoring(); + void setupIdleNotification(); + void teardownIdleNotification(); + static void handleIdled(void* data, struct ext_idle_notification_v1* notification); + static void handleResumed(void* data, struct ext_idle_notification_v1* notification); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; diff --git a/protocol/ext-idle-notify-v1.xml b/protocol/ext-idle-notify-v1.xml new file mode 100644 index 00000000..db7d9c16 --- /dev/null +++ b/protocol/ext-idle-notify-v1.xml @@ -0,0 +1,131 @@ + + + + Copyright © 2015 Martin Gräßlin + Copyright © 2022 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This interface allows clients to monitor user idle status. + + After binding to this global, clients can create ext_idle_notification_v1 + objects to get notified when the user is idle for a given amount of time. + + + + + Destroy the manager object. All objects created via this interface + remain valid. + + + + + + Create a new idle notification object. + + The notification object has a minimum timeout duration and is tied to a + seat. The client will be notified if the seat is inactive for at least + the provided timeout. See ext_idle_notification_v1 for more details. + + A zero timeout is valid and means the client wants to be notified as + soon as possible when the seat is inactive. + + + + + + + + + + + Create a new idle notification object to track input from the + user, such as keyboard and mouse movement. Because this object is + meant to track user input alone, it ignores idle inhibitors. + + The notification object has a minimum timeout duration and is tied to a + seat. The client will be notified if the seat is inactive for at least + the provided timeout. See ext_idle_notification_v1 for more details. + + A zero timeout is valid and means the client wants to be notified as + soon as possible when the seat is inactive. + + + + + + + + + + + This interface is used by the compositor to send idle notification events + to clients. + + Initially the notification object is not idle. The notification object + becomes idle when no user activity has happened for at least the timeout + duration, starting from the creation of the notification object. User + activity may include input events or a presence sensor, but is + compositor-specific. + + How this notification responds to idle inhibitors depends on how + it was constructed. If constructed from the + get_idle_notification request, then if an idle inhibitor is + active (e.g. another client has created a zwp_idle_inhibitor_v1 + on a visible surface), the compositor must not make the + notification object idle. However, if constructed from the + get_input_idle_notification request, then idle inhibitors are + ignored, and only input from the user, e.g. from a keyboard or + mouse, counts as activity. + + When the notification object becomes idle, an idled event is sent. When + user activity starts again, the notification object stops being idle, + a resumed event is sent and the timeout is restarted. + + + + + Destroy the notification object. + + + + + + This event is sent when the notification object becomes idle. + + It's a compositor protocol error to send this event twice without a + resumed event in-between. + + + + + + This event is sent when the notification object stops being idle. + + It's a compositor protocol error to send this event twice without an + idled event in-between. It's a compositor protocol error to send this + event prior to any idled event. + + + + diff --git a/protocol/meson.build b/protocol/meson.build index b16113b2..3da6ee47 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -29,6 +29,7 @@ client_protocols = [ ['river-status-unstable-v1.xml'], ['river-control-unstable-v1.xml'], ['dwl-ipc-unstable-v2.xml'], + ['ext-idle-notify-v1.xml'], ] if wayland_protos.version().version_compare('>=1.39') diff --git a/src/client.cpp b/src/client.cpp index 946780db..a71d1c7a 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -7,6 +7,7 @@ #include #include "gtkmm/icontheme.h" +#include "ext-idle-notify-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" #include "util/format.hpp" @@ -26,6 +27,9 @@ void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint } else if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) { client->idle_inhibit_manager = static_cast( wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1)); + } else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) { + client->idle_notifier = static_cast( + wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, 1)); } } diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 619c52f1..7d5fdd01 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -1,5 +1,6 @@ #include "modules/idle_inhibitor.hpp" +#include "ext-idle-notify-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/command.hpp" @@ -11,6 +12,8 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& : ALabel(config, "idle_inhibitor", id, "{status}", 0, false, true), bar_(bar), idle_inhibitor_(nullptr), + idle_notification_(nullptr), + idle_timeout_ms_(0), pid_(-1), wait_for_activity_(false) { if (waybar::Client::inst()->idle_inhibit_manager == nullptr) { @@ -20,6 +23,11 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& // Read the wait-for-activity config option if (config_["wait-for-activity"].isBool()) { wait_for_activity_ = config_["wait-for-activity"].asBool(); + + // Check if ext-idle-notify protocol is available when wait-for-activity is enabled + if (wait_for_activity_ && waybar::Client::inst()->idle_notifier == nullptr) { + throw std::runtime_error("wait-for-activity requires ext-idle-notify-v1 protocol support"); + } } if (waybar::modules::IdleInhibitor::modules.empty() && config_["start-activated"].isBool() && @@ -38,7 +46,7 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& } waybar::modules::IdleInhibitor::~IdleInhibitor() { - teardownActivityMonitoring(); + teardownIdleNotification(); if (idle_inhibitor_ != nullptr) { zwp_idle_inhibitor_v1_destroy(idle_inhibitor_); @@ -107,11 +115,11 @@ void waybar::modules::IdleInhibitor::toggleStatus() { if (status && config_["timeout"].isNumeric()) { auto timeoutMins = config_["timeout"].asDouble(); int timeoutSecs = timeoutMins * 60; + idle_timeout_ms_ = timeoutSecs * 1000; - // If wait-for-activity is enabled, set up activity monitoring + // If wait-for-activity is enabled, set up idle notification if (wait_for_activity_) { - setupActivityMonitoring(); - resetActivityTimeout(); + setupIdleNotification(); } else { // Original behavior: simple timeout timeout_ = Glib::signal_timeout().connect_seconds( @@ -130,8 +138,8 @@ void waybar::modules::IdleInhibitor::toggleStatus() { timeoutSecs); } } else { - // When deactivated, tear down activity monitoring - teardownActivityMonitoring(); + // When deactivated, tear down idle notification + teardownIdleNotification(); } } @@ -151,77 +159,59 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { return true; } -bool waybar::modules::IdleInhibitor::handleMotion(GdkEventMotion* const& e) { - if (wait_for_activity_ && status) { - resetActivityTimeout(); +void waybar::modules::IdleInhibitor::handleIdled(void* data, + struct ext_idle_notification_v1* /*notification*/) { + spdlog::info("deactivating idle_inhibitor due to user inactivity"); + status = false; + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); } - return false; } -bool waybar::modules::IdleInhibitor::handleKey(GdkEventKey* const& e) { - if (wait_for_activity_ && status) { - resetActivityTimeout(); - } - return false; +void waybar::modules::IdleInhibitor::handleResumed(void* data, + struct ext_idle_notification_v1* /*notification*/) { + // User became active again - notification will continue monitoring + spdlog::debug("user activity detected, idle_inhibitor still active"); } -void waybar::modules::IdleInhibitor::resetActivityTimeout() { - if (!config_["timeout"].isNumeric()) { +void waybar::modules::IdleInhibitor::setupIdleNotification() { + // Don't set up if already exists + if (idle_notification_ != nullptr) { return; } - if (activity_timeout_.connected()) { - activity_timeout_.disconnect(); - } - - auto timeoutMins = config_["timeout"].asDouble(); - int timeoutSecs = timeoutMins * 60; - - activity_timeout_ = Glib::signal_timeout().connect_seconds( - []() { - spdlog::info("deactivating idle_inhibitor due to inactivity"); - status = false; - for (auto const& module : waybar::modules::IdleInhibitor::modules) { - module->update(); - } - return false; - }, - timeoutSecs); -} - -void waybar::modules::IdleInhibitor::setupActivityMonitoring() { - // Don't set up if already connected - if (motion_connection_.connected() || key_connection_.connected()) { + auto* client = waybar::Client::inst(); + if (client->idle_notifier == nullptr) { + spdlog::error("ext-idle-notify protocol not available"); return; } - // Get non-const reference to the window to set up event monitoring - // This is safe because we're only setting up signal handlers, not modifying the Bar itself - auto& window = const_cast(bar_.window); - - // Enable motion and key event monitoring on the bar window - auto gdk_window = window.get_window(); - if (gdk_window) { - gdk_window->set_events(gdk_window->get_events() | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); + // Get the wayland seat from the display + auto* gdk_seat = gdk_display_get_default_seat(client->gdk_display->gobj()); + if (gdk_seat == nullptr) { + spdlog::error("failed to get default seat"); + return; } + auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat); - // Connect to the bar window's event signals - motion_connection_ = window.signal_motion_notify_event().connect( - sigc::mem_fun(*this, &IdleInhibitor::handleMotion)); - key_connection_ = window.signal_key_press_event().connect( - sigc::mem_fun(*this, &IdleInhibitor::handleKey)); + // Create idle notification that monitors all input (not just when inhibitor is active) + // We use get_idle_notification instead of get_input_idle_notification to respect + // idle inhibitors from other applications + idle_notification_ = ext_idle_notifier_v1_get_idle_notification( + client->idle_notifier, idle_timeout_ms_, wl_seat); + + static const struct ext_idle_notification_v1_listener idle_notification_listener = { + .idled = &IdleInhibitor::handleIdled, + .resumed = &IdleInhibitor::handleResumed, + }; + + ext_idle_notification_v1_add_listener(idle_notification_, &idle_notification_listener, this); + wl_display_roundtrip(client->wl_display); } -void waybar::modules::IdleInhibitor::teardownActivityMonitoring() { - if (activity_timeout_.connected()) { - activity_timeout_.disconnect(); - } - - if (motion_connection_.connected()) { - motion_connection_.disconnect(); - } - - if (key_connection_.connected()) { - key_connection_.disconnect(); +void waybar::modules::IdleInhibitor::teardownIdleNotification() { + if (idle_notification_ != nullptr) { + ext_idle_notification_v1_destroy(idle_notification_); + idle_notification_ = nullptr; } } From d68f168e09cfbaf782bd5ef73ac737649a1014e6 Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 19:50:38 -0500 Subject: [PATCH 11/38] fixed the compilation error --- include/modules/idle_inhibitor.hpp | 6 +++--- src/modules/idle_inhibitor.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 523426c6..8ccb0d33 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -10,7 +10,7 @@ namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; - struct ext_idle_notification_v1* idle_notification_; + struct ::ext_idle_notification_v1* idle_notification_; uint32_t idle_timeout_ms_; public: @@ -26,8 +26,8 @@ class IdleInhibitor : public ALabel { void toggleStatus(); void setupIdleNotification(); void teardownIdleNotification(); - static void handleIdled(void* data, struct ext_idle_notification_v1* notification); - static void handleResumed(void* data, struct ext_idle_notification_v1* notification); + static void handleIdled(void* data, struct ::ext_idle_notification_v1* notification); + static void handleResumed(void* data, struct ::ext_idle_notification_v1* notification); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 7d5fdd01..78cc5ab8 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -160,7 +160,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } void waybar::modules::IdleInhibitor::handleIdled(void* data, - struct ext_idle_notification_v1* /*notification*/) { + struct ::ext_idle_notification_v1* /*notification*/) { spdlog::info("deactivating idle_inhibitor due to user inactivity"); status = false; for (auto const& module : waybar::modules::IdleInhibitor::modules) { @@ -169,7 +169,7 @@ void waybar::modules::IdleInhibitor::handleIdled(void* data, } void waybar::modules::IdleInhibitor::handleResumed(void* data, - struct ext_idle_notification_v1* /*notification*/) { + struct ::ext_idle_notification_v1* /*notification*/) { // User became active again - notification will continue monitoring spdlog::debug("user activity detected, idle_inhibitor still active"); } From 3e2ea1a8705b47e3967b5ae54bf5cd134ce24bfc Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 19:58:43 -0500 Subject: [PATCH 12/38] fixed the type declarations --- include/modules/idle_inhibitor.hpp | 6 +++--- src/modules/idle_inhibitor.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 8ccb0d33..515d7f5a 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -10,7 +10,7 @@ namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; - struct ::ext_idle_notification_v1* idle_notification_; + ext_idle_notification_v1* idle_notification_; uint32_t idle_timeout_ms_; public: @@ -26,8 +26,8 @@ class IdleInhibitor : public ALabel { void toggleStatus(); void setupIdleNotification(); void teardownIdleNotification(); - static void handleIdled(void* data, struct ::ext_idle_notification_v1* notification); - static void handleResumed(void* data, struct ::ext_idle_notification_v1* notification); + static void handleIdled(void* data, ext_idle_notification_v1* notification); + static void handleResumed(void* data, ext_idle_notification_v1* notification); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 78cc5ab8..95ef94bd 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -160,7 +160,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } void waybar::modules::IdleInhibitor::handleIdled(void* data, - struct ::ext_idle_notification_v1* /*notification*/) { + ext_idle_notification_v1* /*notification*/) { spdlog::info("deactivating idle_inhibitor due to user inactivity"); status = false; for (auto const& module : waybar::modules::IdleInhibitor::modules) { @@ -169,7 +169,7 @@ void waybar::modules::IdleInhibitor::handleIdled(void* data, } void waybar::modules::IdleInhibitor::handleResumed(void* data, - struct ::ext_idle_notification_v1* /*notification*/) { + ext_idle_notification_v1* /*notification*/) { // User became active again - notification will continue monitoring spdlog::debug("user activity detected, idle_inhibitor still active"); } From 26922c7fbc4296c27ae92384f0cfd5fd8283641c Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 20:07:28 -0500 Subject: [PATCH 13/38] Update idle_inhibitor.hpp --- include/modules/idle_inhibitor.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 515d7f5a..40c5227c 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -6,6 +6,8 @@ #include "bar.hpp" #include "client.hpp" +struct ext_idle_notification_v1; + namespace waybar::modules { class IdleInhibitor : public ALabel { From e1e99716802d40814964f3e3a4351cad18a9062f Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 20:49:24 -0500 Subject: [PATCH 14/38] fixed stale notification object issue --- src/modules/idle_inhibitor.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 95ef94bd..7ce192af 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -119,6 +119,9 @@ void waybar::modules::IdleInhibitor::toggleStatus() { // If wait-for-activity is enabled, set up idle notification if (wait_for_activity_) { + spdlog::debug("idle_inhibitor: wait-for-activity enabled, timeout: {} ms", idle_timeout_ms_); + // Tear down any existing notification first to ensure fresh setup + teardownIdleNotification(); setupIdleNotification(); } else { // Original behavior: simple timeout @@ -163,6 +166,13 @@ void waybar::modules::IdleInhibitor::handleIdled(void* data, ext_idle_notification_v1* /*notification*/) { spdlog::info("deactivating idle_inhibitor due to user inactivity"); status = false; + + // Clean up the notification since we're deactivating + auto* self = static_cast(data); + if (self != nullptr) { + self->teardownIdleNotification(); + } + for (auto const& module : waybar::modules::IdleInhibitor::modules) { module->update(); } @@ -175,9 +185,12 @@ void waybar::modules::IdleInhibitor::handleResumed(void* data, } void waybar::modules::IdleInhibitor::setupIdleNotification() { - // Don't set up if already exists + spdlog::debug("idle_inhibitor: setting up idle notification"); + + // Clean up any existing notification first if (idle_notification_ != nullptr) { - return; + spdlog::debug("idle_inhibitor: cleaning up existing notification before setup"); + teardownIdleNotification(); } auto* client = waybar::Client::inst(); @@ -197,9 +210,15 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { // Create idle notification that monitors all input (not just when inhibitor is active) // We use get_idle_notification instead of get_input_idle_notification to respect // idle inhibitors from other applications + spdlog::debug("idle_inhibitor: creating notification with timeout {} ms", idle_timeout_ms_); idle_notification_ = ext_idle_notifier_v1_get_idle_notification( client->idle_notifier, idle_timeout_ms_, wl_seat); + if (idle_notification_ == nullptr) { + spdlog::error("idle_inhibitor: failed to create idle notification"); + return; + } + static const struct ext_idle_notification_v1_listener idle_notification_listener = { .idled = &IdleInhibitor::handleIdled, .resumed = &IdleInhibitor::handleResumed, @@ -207,10 +226,12 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { ext_idle_notification_v1_add_listener(idle_notification_, &idle_notification_listener, this); wl_display_roundtrip(client->wl_display); + spdlog::debug("idle_inhibitor: idle notification setup complete"); } void waybar::modules::IdleInhibitor::teardownIdleNotification() { if (idle_notification_ != nullptr) { + spdlog::debug("idle_inhibitor: tearing down idle notification"); ext_idle_notification_v1_destroy(idle_notification_); idle_notification_ = nullptr; } From 19afad86751c022cc1a6690aea6be11b7da1ee21 Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 21:21:48 -0500 Subject: [PATCH 15/38] updated client to bind version 2 of the ext-idle-notifier protocol --- src/client.cpp | 6 +++++- src/modules/idle_inhibitor.cpp | 26 ++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index a71d1c7a..d4c7aca2 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -28,8 +29,11 @@ void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint client->idle_inhibit_manager = static_cast( wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1)); } else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) { + // Bind version 2 if available (for get_input_idle_notification), otherwise version 1 + auto bind_version = std::min(version, 2u); client->idle_notifier = static_cast( - wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, 1)); + wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, bind_version)); + spdlog::debug("Bound ext-idle-notifier-v1 at version {}", bind_version); } } diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 7ce192af..8dfe0b85 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -207,12 +207,26 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { } auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat); - // Create idle notification that monitors all input (not just when inhibitor is active) - // We use get_idle_notification instead of get_input_idle_notification to respect - // idle inhibitors from other applications - spdlog::debug("idle_inhibitor: creating notification with timeout {} ms", idle_timeout_ms_); - idle_notification_ = ext_idle_notifier_v1_get_idle_notification( - client->idle_notifier, idle_timeout_ms_, wl_seat); + // Check protocol version to determine which function to use + uint32_t version = wl_proxy_get_version(reinterpret_cast(client->idle_notifier)); + + spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})", + idle_timeout_ms_, version); + + if (version >= 2) { + // Version 2+: Use get_input_idle_notification which ignores idle inhibitors + // This allows us to detect actual user inactivity even while the inhibitor is active + spdlog::debug("idle_inhibitor: using get_input_idle_notification (ignores inhibitors)"); + idle_notification_ = ext_idle_notifier_v1_get_input_idle_notification( + client->idle_notifier, idle_timeout_ms_, wl_seat); + } else { + // Version 1: Fall back to get_idle_notification + // WARNING: This respects idle inhibitors, so it won't fire while inhibitor is active + spdlog::warn("idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support get_input_idle_notification, " + "wait-for-activity may not work correctly", version); + idle_notification_ = ext_idle_notifier_v1_get_idle_notification( + client->idle_notifier, idle_timeout_ms_, wl_seat); + } if (idle_notification_ == nullptr) { spdlog::error("idle_inhibitor: failed to create idle notification"); From 11533ecf0a5b78171a95bce304cd9f905b4d7ff6 Mon Sep 17 00:00:00 2001 From: Adam Schwalm Date: Fri, 26 Dec 2025 17:16:50 -0600 Subject: [PATCH 16/38] Add '-' support to wifi config Most modules support config fields like "format--", so the user can, for example, set a different format for batter when unplugged _and_ under a given level. Wifi didn't support this. Change the module to support format state based on signal strength. --- src/modules/network.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index d0b7970c..951b099e 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -291,19 +291,27 @@ auto waybar::modules::Network::update() -> void { bandwidth_up_total_ = up_octets; } + auto threshold_state = getState(signal_strength_); + if (!alt_) { auto state = getNetworkState(); if (!state_.empty() && label_.get_style_context()->has_class(state_)) { label_.get_style_context()->remove_class(state_); } - if (config_["format-" + state].isString()) { + if (!threshold_state.empty() && + config_["format-" + state + "-" + threshold_state].isString()) { + default_format_ = config_["format-" + state + "-" + threshold_state].asString(); + } else if (config_["format-" + state].isString()) { default_format_ = config_["format-" + state].asString(); } else if (config_["format"].isString()) { default_format_ = config_["format"].asString(); } else { default_format_ = DEFAULT_FORMAT; } - if (config_["tooltip-format-" + state].isString()) { + if (!threshold_state.empty() && + config_["tooltip-format-" + state + "-" + threshold_state].isString()) { + tooltip_format = config_["tooltip-format-" + state + "-" + threshold_state].asString(); + } else if (config_["tooltip-format-" + state].isString()) { tooltip_format = config_["tooltip-format-" + state].asString(); } if (!label_.get_style_context()->has_class(state)) { @@ -312,7 +320,6 @@ auto waybar::modules::Network::update() -> void { format_ = default_format_; state_ = state; } - getState(signal_strength_); std::string final_ipaddr_; if (addr_pref_ == ip_addr_pref::IPV4) { From 5870c4ba0e85911160b04d99793989de2586eefc Mon Sep 17 00:00:00 2001 From: Aksel Lunde Aase Date: Fri, 9 Jan 2026 13:22:48 +0100 Subject: [PATCH 17/38] feat(clock): Add action to execute external command --- include/modules/clock.hpp | 3 +++ man/waybar-clock.5.scd | 2 ++ src/modules/clock.cpp | 8 ++++++++ 3 files changed, 13 insertions(+) diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index c3548063..4ac91d61 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -80,6 +80,7 @@ class Clock final : public ALabel { void cldShift_reset(); void tz_up(); void tz_down(); + void action_exec(const std::string& action); // Module Action Map static inline std::map actionMap_{ {"mode", &waybar::modules::Clock::cldModeSwitch}, @@ -88,6 +89,8 @@ class Clock final : public ALabel { {"shift_reset", &waybar::modules::Clock::cldShift_reset}, {"tz_up", &waybar::modules::Clock::tz_up}, {"tz_down", &waybar::modules::Clock::tz_down}}; + static inline std::map actionWithArgsMap_{ + {"exec", &waybar::modules::Clock::action_exec}}; }; } // namespace waybar::modules diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index b4b5d5b7..440c3447 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -180,6 +180,8 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ Switch to the next calendar month/year |[ *shift_down* :[ Switch to the previous calendar month/year +|[ *exec * +:[ Execute the specified command # FORMAT REPLACEMENTS diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 5fe5407b..2bc99764 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -9,6 +9,7 @@ #include #include +#include "util/command.hpp" #include "util/ustring_clen.hpp" #ifdef HAVE_LANGINFO_1STDAY @@ -472,6 +473,8 @@ auto waybar::modules::Clock::local_zone() -> const time_zone* { auto waybar::modules::Clock::doAction(const std::string& name) -> void { if (actionMap_[name]) { (this->*actionMap_[name])(); + } else if (auto key = name.substr(0, name.find(" ")); actionWithArgsMap_[key]) { + (this->*actionWithArgsMap_[key])(name); } else spdlog::error("Clock. Unsupported action \"{0}\"", name); } @@ -498,6 +501,11 @@ void waybar::modules::Clock::tz_down() { if (tzSize == 1) return; tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1; } +void waybar::modules::Clock::action_exec(const std::string& action) { + auto cmd = action.substr(strlen("exec ")); + pid_children_.push_back(util::command::forkExec(cmd)); +} + #ifdef HAVE_LANGINFO_1STDAY template From 736c05335020d5e8165806d0bbcba9094021f91f Mon Sep 17 00:00:00 2001 From: eonphi <259135569+eonphi@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:37:35 +0100 Subject: [PATCH 18/38] feat(hyprland/workspaces): grouping icons --- include/modules/hyprland/workspaces.hpp | 4 ++ src/modules/hyprland/workspace.cpp | 51 ++++++++++++++++++++++--- src/modules/hyprland/workspaces.cpp | 10 +++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 8bf88888..a240549a 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -61,6 +61,8 @@ class Workspaces : public AModule, public EventHandler { std::string getRewrite(std::string window_class, std::string window_title); std::string& getWindowSeparator() { return m_formatWindowSeparator; } + auto windowRewriteGroupThreshold() const -> int { return m_windowRewriteGroupThreshold; } + auto const& getWindowRewriteGroupFormat() const { return m_windowRewriteGroupFormat; } bool isWorkspaceIgnored(std::string const& workspace_name); bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } @@ -172,6 +174,8 @@ class Workspaces : public AModule, public EventHandler { util::RegexCollection m_windowRewriteRules; bool m_anyWindowRewriteRuleUsesTitle = false; std::string m_formatWindowSeparator; + int m_windowRewriteGroupThreshold = 0; + std::string m_windowRewriteGroupFormat = "{icon}×{count}"; bool m_withIcon; uint64_t m_monitorId; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 87933ac0..01da287e 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -245,15 +246,53 @@ void Workspace::update(const std::string& workspace_icon) { // need to compute this if enableTaskbar() is true if (!m_workspaceManager.enableTaskbar()) { auto windowSeparator = m_workspaceManager.getWindowSeparator(); + auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold(); - bool isNotFirst = false; + if (groupThreshold > 0) { + // Build ordered counts of each unique icon (including singular ones when threshold set to 1) + std::vector> iconCounts; + for (const auto& window_repr : m_windowMap) { + auto it = std::ranges::find_if(iconCounts, [&](const auto& p) { + return p.first == window_repr.repr_rewrite; + }); + if (it != iconCounts.end()) { + it->second++; + } else { + iconCounts.emplace_back(window_repr.repr_rewrite, 1); + } + } - for (const auto& window_repr : m_windowMap) { - if (isNotFirst) { - windows.append(windowSeparator); + // Format the group string + auto groupFormat = m_workspaceManager.getWindowRewriteGroupFormat(); + bool isNotFirst = false; + for (const auto& [icon, count] : iconCounts) { + if (count >= groupThreshold) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + try { + windows.append(fmt::format(fmt::runtime(groupFormat), + fmt::arg("icon", icon), + fmt::arg("count", count))); + } catch (const fmt::format_error& e) { + spdlog::warn("Formatting window-rewrite-group-format error: {}", e.what()); + windows.append(icon); + } + } else { + for (int i = 0; i < count; ++i) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + windows.append(icon); + } + } + } + } else { + // Not grouping icons + bool isNotFirst = false; + for (const auto& window_repr : m_windowMap) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + windows.append(window_repr.repr_rewrite); } - isNotFirst = true; - windows.append(window_repr.repr_rewrite); } } diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8765d78b..bec503e9 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -649,6 +649,16 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { populateSortByConfig(config); populateIgnoreWorkspacesConfig(config); populateFormatWindowSeparatorConfig(config); + + const auto& groupThreshold = config["window-rewrite-group-threshold"]; + if (groupThreshold.isInt()) { + m_windowRewriteGroupThreshold = groupThreshold.asInt(); + } + const auto& groupFormat = config["window-rewrite-group-format"]; + if (groupFormat.isString()) { + m_windowRewriteGroupFormat = groupFormat.asString(); + } + populateWindowRewriteConfig(config); if (withWindows) { From 8a3a78676f9fb46889acc7fe379a4a596b55f529 Mon Sep 17 00:00:00 2001 From: eonphi <259135569+eonphi@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:01:47 +0100 Subject: [PATCH 19/38] docs(hyprland/workspaces): icon grouping man page entries --- man/waybar-hyprland-workspaces.5.scd | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 5284ce99..b495052d 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -41,6 +41,19 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. The separator to be used between windows in a workspace. ++ This setting is ignored if *workspace-taskbar.enable* is set to true. +*window-rewrite-group-threshold*: ++ + typeof: int ++ + default: 0 ++ + When a workspace contains at least this many windows with the same rewrite result, they are collapsed into a single one using *window-rewrite-group-format*. ++ + Set to 0 to disable grouping. ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + +*window-rewrite-group-format*: ++ + typeof: string ++ + default: "{icon}×{count}" ++ + The format used to represent a group of collapsed windows. Available placeholders are {icon} (the icon being grouped) and {count} (how many windows share it). ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + *workspace-taskbar*: ++ typeof: object ++ Contains settings for the workspace taskbar, an alternative mode for the workspaces module which displays the window icons as images instead of text. From efadb82c56b16699c01b029913af7474b61464f3 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Sat, 13 Sep 2025 00:00:20 +0200 Subject: [PATCH 20/38] feat: basic line graph component and cpu graph module --- include/AGraph.hpp | 50 +++++++++ include/modules/cpu_graph.hpp | 32 ++++++ man/waybar-cpu-graph.5.scd | 85 +++++++++++++++ meson.build | 3 + src/AGraph.cpp | 197 ++++++++++++++++++++++++++++++++++ src/factory.cpp | 4 + src/modules/cpu_graph.cpp | 47 ++++++++ 7 files changed, 418 insertions(+) create mode 100644 include/AGraph.hpp create mode 100644 include/modules/cpu_graph.hpp create mode 100644 man/waybar-cpu-graph.5.scd create mode 100644 src/AGraph.cpp create mode 100644 src/modules/cpu_graph.cpp diff --git a/include/AGraph.hpp b/include/AGraph.hpp new file mode 100644 index 00000000..3466e577 --- /dev/null +++ b/include/AGraph.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "AModule.hpp" + +namespace waybar { + +class AGraph : public AModule { + public: + AGraph(const Json::Value &, const std::string &, const std::string &, + uint16_t interval = 0, bool enable_click = false, + bool enable_scroll = false); + virtual ~AGraph() = default; + auto update() -> void override; + + protected: + Gtk::DrawingArea graph_; + std::deque values_; + uint16_t datapoints_ = 20; + uint16_t y_offset_ = 0; + + void addValue(const int n); + + const std::chrono::seconds interval_; + + bool onDraw(const Cairo::RefPtr &cr); + + std::map submenus_; + std::map menuActionsMap_; + static void handleGtkMenuEvent(GtkMenuItem *menuitem, gpointer data); + + private: + void drawFilledArea(const Cairo::RefPtr &cr, + const std::vector> &points, + double height, const Gdk::RGBA &bg_color); + + void drawLine(const Cairo::RefPtr &cr, + const std::vector> &points, const Gdk::RGBA &fg_color); + + void drawPath(const Cairo::RefPtr &cr, + const std::vector> &points); +}; + +} // namespace waybar diff --git a/include/modules/cpu_graph.hpp b/include/modules/cpu_graph.hpp new file mode 100644 index 00000000..df5f6ea3 --- /dev/null +++ b/include/modules/cpu_graph.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "AGraph.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CpuGraph : public AGraph { + public: + CpuGraph(const std::string&, const Json::Value&); + virtual ~CpuGraph() = default; + auto update() -> void override; + + private: + static constexpr const char *MODERATE_CLASS = "cpu-moderate"; + static constexpr const char *HIGH_CLASS = "cpu-high"; + static constexpr const char *INTENSIVE_CLASS = "cpu-intensive"; + + std::vector> prev_times_; + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/man/waybar-cpu-graph.5.scd b/man/waybar-cpu-graph.5.scd new file mode 100644 index 00000000..bfe9de42 --- /dev/null +++ b/man/waybar-cpu-graph.5.scd @@ -0,0 +1,85 @@ +waybar-cpu(5) + +# NAME + +waybar - cpu graph module + +# DESCRIPTION + +The *cpu graph* module displays a line graph with the CPU utilization. + +# CONFIGURATION + +*interval*: ++ + typeof: integer ++ + default: 10 ++ + The interval in which the information gets polled. + +*width*: ++ + typeof: integer ++ + The length in pixels the module should display. + +*y_offset*: ++ + typeof: integer ++ + The margin in pixels at the bottom of the module. + +*datapoints*: ++ + typeof: integer ++ + How many data points to show. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# EXAMPLES + +Basic configuration: + +``` +"cpu_graph": { + "interval": 2, + "width": 10, + "y_offset": 4 +} +``` + +# STYLE + +- *#cpu_graph* +- *.cpu-intensive* +- *.cpu-high* +- *.cpu-moderate* diff --git a/meson.build b/meson.build index 0c494eb2..713a723f 100644 --- a/meson.build +++ b/meson.build @@ -159,6 +159,7 @@ endif src_files = files( 'src/factory.cpp', + 'src/AGraph.cpp', 'src/AModule.cpp', 'src/ALabel.cpp', 'src/AIconLabel.cpp', @@ -210,6 +211,7 @@ if is_linux 'src/modules/bluetooth.cpp', 'src/modules/cffi.cpp', 'src/modules/cpu.cpp', + 'src/modules/cpu_graph.cpp', 'src/modules/cpu_frequency/common.cpp', 'src/modules/cpu_frequency/linux.cpp', 'src/modules/cpu_usage/common.cpp', @@ -234,6 +236,7 @@ elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd src_files += files( 'src/modules/cffi.cpp', 'src/modules/cpu.cpp', + 'src/modules/cpu_graph.cpp', 'src/modules/cpu_frequency/bsd.cpp', 'src/modules/cpu_frequency/common.cpp', 'src/modules/cpu_usage/bsd.cpp', diff --git a/src/AGraph.cpp b/src/AGraph.cpp new file mode 100644 index 00000000..529cdb66 --- /dev/null +++ b/src/AGraph.cpp @@ -0,0 +1,197 @@ +#include "AGraph.hpp" + +#include +#include + +#include +#include +#include + +#include "config.hpp" + +namespace waybar { + +AGraph::AGraph(const Json::Value& config, const std::string& name, const std::string& id, + uint16_t interval, bool enable_click, bool enable_scroll) + : AModule(config, name, id, + config["format-alt"].isString() || config["menu"].isString() || enable_click, + enable_scroll), + interval_(config_["interval"] == "once" + ? std::chrono::seconds::max() + : std::chrono::seconds( + config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)) { + graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw)); + graph_.set_name(name); + if (!id.empty()) { + graph_.get_style_context()->add_class(id); + } + graph_.get_style_context()->add_class(MODULE_CLASS); + if (config_["width"].isUInt()) { + graph_.set_size_request(config_["width"].asUInt(), -1); + } else { + graph_.set_size_request(100, -1); + } + + event_box_.add(graph_); + + if (config_["datapoints"].isUInt()) { + datapoints_ = config_["datapoints_"].asUInt(); + } + + if (config_["y_offset"].isUInt()) { + y_offset_ = config_["y_offset"].asUInt(); + } + + // If a GTKMenu is requested in the config + if (config_["menu"].isString()) { + // Create the GTKMenu widget + try { + // Check that the file exists + std::string menuFile = config_["menu-file"].asString(); + + // there might be "~" or "$HOME" in original path, try to expand it. + auto result = Config::tryExpandPath(menuFile, ""); + if (result.empty()) { + throw std::runtime_error("Failed to expand file: " + menuFile); + } + + menuFile = result.front(); + // Read the menu descriptor file + std::ifstream file(menuFile); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + menuFile); + } + std::stringstream fileContent; + fileContent << file.rdbuf(); + GtkBuilder* builder = gtk_builder_new(); + + // Make the GtkBuilder and check for errors in his parsing + if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) { + throw std::runtime_error("Error found in the file " + menuFile); + } + + menu_ = gtk_builder_get_object(builder, "menu"); + if (menu_ == nullptr) { + throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); + } + submenus_ = std::map(); + menuActionsMap_ = std::map(); + + // Linking actions to the GTKMenu based on + for (Json::Value::const_iterator it = config_["menu-actions"].begin(); + it != config_["menu-actions"].end(); ++it) { + std::string key = it.key().asString(); + submenus_[key] = GTK_MENU_ITEM(gtk_builder_get_object(builder, key.c_str())); + menuActionsMap_[key] = it->asString(); + g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), + (gpointer)menuActionsMap_[key].c_str()); + } + } catch (std::runtime_error& e) { + spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what()); + } + } +} + +auto AGraph::update() -> void { + graph_.queue_draw(); + AModule::update(); +} + +void AGraph::handleGtkMenuEvent(GtkMenuItem* /*menuitem*/, gpointer data) { + waybar::util::command::res res = waybar::util::command::exec((char*)data, "GtkMenu"); +} + +void AGraph::addValue(const int n) { + if (values_.size() >= datapoints_) { + values_.pop_front(); + } + values_.push_back(n); +} + +bool AGraph::onDraw(const Cairo::RefPtr& cr) { + const int width = graph_.get_allocated_width(); + const int height = graph_.get_allocated_height() - 1 - y_offset_; + + if (values_.empty() || width <= 0 || height <= 0) { + return false; + } + + auto style_context = graph_.get_style_context(); + Gdk::RGBA fg_color = style_context->get_color(Gtk::STATE_FLAG_NORMAL); + Gdk::RGBA bg_color = fg_color; + bg_color.set_alpha(0.3); + + cr->set_line_width(1.0); + + const double step_width = static_cast(width) / datapoints_; + const int values_count = values_.size(); + const int empty_space = datapoints_ - values_count; + + std::vector> points; + points.reserve(values_count); + + for (int i = empty_space; i < datapoints_; ++i) { + double x = i * step_width; + int value_index = i - empty_space; + int value = values_[value_index]; + double y = height - (static_cast(value) / 100.0 * height); + points.emplace_back(x, y); + } + + if (!points.empty()) { + + drawFilledArea(cr, points, height, bg_color); + + drawLine(cr, points, fg_color); + } + + return false; +} +void AGraph::drawFilledArea(const Cairo::RefPtr& cr, + const std::vector>& points, + double height, const Gdk::RGBA& bg_color) { + if (points.empty()) return; + + double first_x = points.front().first; + double last_x = points.back().first; + + drawPath(cr, points); + + cr->line_to(last_x, height); + cr->line_to(first_x, height); + cr->close_path(); + + cr->set_source_rgba(bg_color.get_red(), bg_color.get_green(), bg_color.get_blue(), + bg_color.get_alpha()); + cr->fill(); +} + +void AGraph::drawLine(const Cairo::RefPtr& cr, + const std::vector>& points, + const Gdk::RGBA& fg_color) { + if (points.empty()) return; + + cr->begin_new_path(); + drawPath(cr, points); + + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + fg_color.get_alpha()); + cr->stroke(); +} + +void AGraph::drawPath(const Cairo::RefPtr& cr, + const std::vector>& points) { + if (points.empty()) return; + + bool first_point = true; + for (const auto& point : points) { + if (first_point) { + cr->move_to(point.first, point.second); + first_point = false; + } else { + cr->line_to(point.first, point.second); + } + } +} + +} // namespace waybar diff --git a/src/factory.cpp b/src/factory.cpp index 2fd3e3b8..e82711b5 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -51,6 +51,7 @@ #endif #if defined(HAVE_CPU_LINUX) || defined(HAVE_CPU_BSD) #include "modules/cpu.hpp" +#include "modules/cpu_graph.hpp" #include "modules/cpu_frequency.hpp" #include "modules/cpu_usage.hpp" #include "modules/load.hpp" @@ -251,6 +252,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "cpu") { return new waybar::modules::Cpu(id, config_[name]); } + if (ref == "cpu_graph") { + return new waybar::modules::CpuGraph(id, config_[name]); + } #if defined(HAVE_CPU_LINUX) if (ref == "cpu_frequency") { return new waybar::modules::CpuFrequency(id, config_[name]); diff --git a/src/modules/cpu_graph.cpp b/src/modules/cpu_graph.cpp new file mode 100644 index 00000000..a9a29dd2 --- /dev/null +++ b/src/modules/cpu_graph.cpp @@ -0,0 +1,47 @@ +#include "modules/cpu_graph.hpp" + +#include "modules/cpu_frequency.hpp" +#include "modules/cpu_usage.hpp" +#include "modules/load.hpp" + +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + +waybar::modules::CpuGraph::CpuGraph(const std::string& id, const Json::Value& config) + : AGraph(config, "cpu_graph", id, 5) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; +} + +auto waybar::modules::CpuGraph::update() -> void { + // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both + auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); + if (tooltipEnabled()) { + graph_.set_tooltip_text(tooltip); + } + auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; + addValue(total_usage); + + graph_.get_style_context()->remove_class(MODERATE_CLASS); + graph_.get_style_context()->remove_class(HIGH_CLASS); + graph_.get_style_context()->remove_class(INTENSIVE_CLASS); + + if (total_usage > 90) { + graph_.get_style_context()->add_class(INTENSIVE_CLASS); + } else if (total_usage > 70) { + graph_.get_style_context()->add_class(HIGH_CLASS); + } else if (total_usage > 30) { + graph_.get_style_context()->add_class(MODERATE_CLASS); + } + + // Call parent update + AGraph::update(); +} From 4d6354af48e5fba70d522ce75d8a0eb20cd661c6 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Sat, 11 Oct 2025 01:52:10 +0200 Subject: [PATCH 21/38] feat: add gauge and stacked bar graph type --- include/AGraph.hpp | 19 ++- include/modules/custom_graph.hpp | 49 ++++++ meson.build | 1 + src/AGraph.cpp | 122 ++++++++++++- src/factory.cpp | 4 + src/modules/custom_graph.cpp | 283 +++++++++++++++++++++++++++++++ 6 files changed, 466 insertions(+), 12 deletions(-) create mode 100644 include/modules/custom_graph.hpp create mode 100644 src/modules/custom_graph.cpp diff --git a/include/AGraph.hpp b/include/AGraph.hpp index 3466e577..a49b215d 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -11,11 +11,12 @@ namespace waybar { +enum class GraphType { LINE, BAR, GAUGE }; + class AGraph : public AModule { public: - AGraph(const Json::Value &, const std::string &, const std::string &, - uint16_t interval = 0, bool enable_click = false, - bool enable_scroll = false); + AGraph(const Json::Value &, const std::string &, const std::string &, uint16_t interval = 0, + bool enable_click = false, bool enable_scroll = false); virtual ~AGraph() = default; auto update() -> void override; @@ -24,6 +25,7 @@ class AGraph : public AModule { std::deque values_; uint16_t datapoints_ = 20; uint16_t y_offset_ = 0; + GraphType graph_type_ = GraphType::LINE; void addValue(const int n); @@ -37,14 +39,21 @@ class AGraph : public AModule { private: void drawFilledArea(const Cairo::RefPtr &cr, - const std::vector> &points, - double height, const Gdk::RGBA &bg_color); + const std::vector> &points, double height, + const Gdk::RGBA &bg_color); void drawLine(const Cairo::RefPtr &cr, const std::vector> &points, const Gdk::RGBA &fg_color); void drawPath(const Cairo::RefPtr &cr, const std::vector> &points); + + void drawBars(const Cairo::RefPtr &cr, + double width, double height, int current_value, + const Gdk::RGBA &fg_color); + + void drawGauge(const Cairo::RefPtr &cr, double width, double height, + int current_value, const Gdk::RGBA &fg_color); }; } // namespace waybar diff --git a/include/modules/custom_graph.hpp b/include/modules/custom_graph.hpp new file mode 100644 index 00000000..a081f376 --- /dev/null +++ b/include/modules/custom_graph.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include + +#include "AGraph.hpp" +#include "util/command.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CustomGraph : public AGraph { + public: + CustomGraph(const std::string&, const std::string&, const Json::Value&, const std::string&); + virtual ~CustomGraph(); + auto update() -> void override; + void refresh(int /*signal*/) override; + + private: + void delayWorker(); + void continuousWorker(); + void waitingWorker(); + void parseOutputRaw(); + void parseOutputJson(); + void handleEvent(); + bool handleScroll(GdkEventScroll* e) override; + bool handleToggle(GdkEventButton* const& e) override; + + const std::string name_; + const std::string output_name_; + std::string text_; + std::string id_; + std::string alt_; + std::string tooltip_; + const bool tooltip_format_enabled_; + std::vector class_; + int percentage_; + FILE* fp_; + int pid_; + util::command::res output_; + util::JsonParser parser_; + + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/meson.build b/meson.build index 713a723f..8413dbe1 100644 --- a/meson.build +++ b/meson.build @@ -165,6 +165,7 @@ src_files = files( 'src/AIconLabel.cpp', 'src/AAppIconLabel.cpp', 'src/modules/custom.cpp', + 'src/modules/custom_graph.cpp', 'src/modules/disk.cpp', 'src/modules/idle_inhibitor.cpp', 'src/modules/image.cpp', diff --git a/src/AGraph.cpp b/src/AGraph.cpp index 529cdb66..b8076ecc 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,17 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st y_offset_ = config_["y_offset"].asUInt(); } + if (config_["graph_type"].isString()) { + std::string type = config_["graph_type"].asString(); + if (type == "line") { + graph_type_ = GraphType::LINE; + } else if (type == "bar") { + graph_type_ = GraphType::BAR; + } else if (type == "gauge") { + graph_type_ = GraphType::GAUGE; + } + } + // If a GTKMenu is requested in the config if (config_["menu"].isString()) { // Create the GTKMenu widget @@ -137,19 +149,26 @@ bool AGraph::onDraw(const Cairo::RefPtr& cr) { double y = height - (static_cast(value) / 100.0 * height); points.emplace_back(x, y); } - if (!points.empty()) { - - drawFilledArea(cr, points, height, bg_color); - - drawLine(cr, points, fg_color); + switch (graph_type_) { + case GraphType::LINE: + drawFilledArea(cr, points, height, bg_color); + drawLine(cr, points, fg_color); + break; + case GraphType::BAR: + drawBars(cr, width, height, values_.empty() ? 0 : values_.back(), fg_color); + break; + case GraphType::GAUGE: + drawGauge(cr, width, height, values_.empty() ? 0 : values_.back(), fg_color); + break; + } } return false; } void AGraph::drawFilledArea(const Cairo::RefPtr& cr, - const std::vector>& points, - double height, const Gdk::RGBA& bg_color) { + const std::vector>& points, double height, + const Gdk::RGBA& bg_color) { if (points.empty()) return; double first_x = points.front().first; @@ -194,4 +213,93 @@ void AGraph::drawPath(const Cairo::RefPtr& cr, } } +void AGraph::drawBars(const Cairo::RefPtr& cr, + double width, double height, int current_value, + const Gdk::RGBA& fg_color) { + + current_value = std::min(100, std::max(0, current_value)); + + double green_height = height * (std::min(current_value, 40) / 100.0); + cr->set_source_rgba(0.0, 1.0, 0.0, 1.0); + cr->rectangle(0, height - green_height, width, green_height); + cr->fill(); + + if (current_value > 40) { + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + cr->set_source_rgba(1.0, 1.0, 0.0, 1.0); + cr->rectangle(0, height - green_height - yellow_height, width, yellow_height); + cr->fill(); + } + + if (current_value > 75) { + double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; + cr->set_source_rgba(1.0, 0.5, 0.0, 1.0); + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + cr->rectangle(0, height - green_height - yellow_height - orange_height, width, + orange_height); + cr->fill(); + } + + if (current_value > 85) { + double red_height = height * (current_value - 85) / 100.0; + cr->set_source_rgba(1.0, 0.0, 0.0, 1.0); + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; + cr->rectangle(0, height - green_height - yellow_height - orange_height - red_height, width, + red_height); + cr->fill(); + } + + double value_height = height * (current_value / 100.0); + cr->set_source_rgba(0.2, 0.2, 0.2, 0.8); + cr->rectangle(0, height - value_height, width, 2); + cr->fill(); +} + +void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color) { + double center_x = width / 2.0; + double center_y = height; + double radius = height / 2.0; + + cr->set_line_width(10.0); + + double angle1 = M_PI; + double angle2 = angle1 + 0.3 * angle1; + + // Green section (0-33%) + cr->set_source_rgba(0.0, 1.0, 0.0, 1.0); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Yellow section (33-66%) + angle1 = angle2; + angle2 = angle1 + 0.3 * angle1; + cr->set_source_rgba(1.0, 1.0, 0.0, 1.0); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Red section (66-100%) + angle1 = angle2; + angle2 = 0.0; + cr->set_source_rgba(1.0, 0.0, 0.0, 0.8); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Draw needle + double percentage = std::min(100, std::max(0, current_value)) / 100.0; + double needle_angle = M_PI * percentage; + double needle_length = radius; + + double needle_x = center_x - needle_length * cos(needle_angle); + double needle_y = center_y - needle_length * sin(needle_angle); + + cr->set_source_rgba(1.0, 1.0, 1.0, 1.0); + cr->set_line_width(2.0); + cr->begin_new_path(); + cr->move_to(center_x, center_y); + cr->line_to(needle_x, needle_y); + cr->stroke(); +} + } // namespace waybar diff --git a/src/factory.cpp b/src/factory.cpp index e82711b5..dfb5caf4 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -118,6 +118,7 @@ #include "modules/cava/cava_frontend.hpp" #include "modules/cffi.hpp" #include "modules/custom.hpp" +#include "modules/custom_graph.hpp" #include "modules/image.hpp" #include "modules/temperature.hpp" #include "modules/user.hpp" @@ -362,6 +363,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name], bar_.output->name); } + if (ref.compare(0, 13, "custom-graph/") == 0 && ref.size() > 7) { + return new waybar::modules::CustomGraph(ref.substr(7), id, config_[name], bar_.output->name); + } if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) { return new waybar::modules::CFFI(ref.substr(5), id, config_[name]); } diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp new file mode 100644 index 00000000..288d97f2 --- /dev/null +++ b/src/modules/custom_graph.cpp @@ -0,0 +1,283 @@ +#include "modules/custom_graph.hpp" + +#include + +#include "util/scope_guard.hpp" + +waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, + const Json::Value& config, const std::string& output_name) + : AGraph(config, "custom-graph" + name, id), + name_(name), + output_name_(output_name), + id_(id), + tooltip_format_enabled_{config_["tooltip-format"].isString()}, + percentage_(0), + fp_(nullptr), + pid_(-1) { + if (config.isNull()) { + spdlog::warn("There is no configuration for 'custom-graph/{}', element will be hidden", name); + } + dp.emit(); + if (!config_["signal"].empty() && config_["interval"].empty() && + config_["restart-interval"].empty()) { + waitingWorker(); + } else if (interval_.count() > 0) { + delayWorker(); + } else if (config_["exec"].isString()) { + continuousWorker(); + } +} + +waybar::modules::CustomGraph::~CustomGraph() { + if (pid_ != -1) { + killpg(pid_, SIGTERM); + waitpid(pid_, NULL, 0); + pid_ = -1; + } +} + +void waybar::modules::CustomGraph::delayWorker() { + thread_ = [this] { + for (int i : this->pid_children_) { + int status; + waitpid(i, &status, 0); + } + + this->pid_children_.clear(); + + bool can_update = true; + if (config_["exec-if"].isString()) { + output_ = util::command::execNoRead(config_["exec-if"].asString()); + if (output_.exit_code != 0) { + can_update = false; + dp.emit(); + } + } + if (can_update) { + if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString(), output_name_); + } + dp.emit(); + } + thread_.sleep_for(interval_); + }; +} + +void waybar::modules::CustomGraph::continuousWorker() { + auto cmd = config_["exec"].asString(); + pid_ = -1; + fp_ = util::command::open(cmd, pid_, output_name_); + if (!fp_) { + throw std::runtime_error("Unable to open " + cmd); + } + thread_ = [this, cmd] { + char* buff = nullptr; + waybar::util::ScopeGuard buff_deleter([&buff]() { + if (buff) { + free(buff); + } + }); + size_t len = 0; + if (getline(&buff, &len, fp_) == -1) { + int exit_code = 1; + if (fp_) { + exit_code = WEXITSTATUS(util::command::close(fp_, pid_)); + fp_ = nullptr; + } + if (exit_code != 0) { + output_ = {exit_code, ""}; + dp.emit(); + spdlog::error("{} stopped unexpectedly, is it endless?", name_); + } + if (config_["restart-interval"].isUInt()) { + pid_ = -1; + thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt())); + fp_ = util::command::open(cmd, pid_, output_name_); + if (!fp_) { + throw std::runtime_error("Unable to open " + cmd); + } + } else { + thread_.stop(); + return; + } + } else { + std::string output = buff; + + // Remove last newline + if (!output.empty() && output[output.length() - 1] == '\n') { + output.erase(output.length() - 1); + } + output_ = {0, output}; + dp.emit(); + } + }; +} + +void waybar::modules::CustomGraph::waitingWorker() { + thread_ = [this] { + bool can_update = true; + if (config_["exec-if"].isString()) { + output_ = util::command::execNoRead(config_["exec-if"].asString()); + if (output_.exit_code != 0) { + can_update = false; + dp.emit(); + } + } + if (can_update) { + if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString(), output_name_); + } + dp.emit(); + } + thread_.sleep(); + }; +} + +void waybar::modules::CustomGraph::refresh(int sig) { + if (sig == SIGRTMIN + config_["signal"].asInt()) { + thread_.wake_up(); + } +} + +void waybar::modules::CustomGraph::handleEvent() { + if (!config_["exec-on-event"].isBool() || config_["exec-on-event"].asBool()) { + thread_.wake_up(); + } +} + +bool waybar::modules::CustomGraph::handleScroll(GdkEventScroll* e) { + auto ret = AGraph::handleScroll(e); + handleEvent(); + return ret; +} + +bool waybar::modules::CustomGraph::handleToggle(GdkEventButton* const& e) { + auto ret = AGraph::handleToggle(e); + handleEvent(); + return ret; +} + +auto waybar::modules::CustomGraph::update() -> void { + // Hide label if output is empty + if ((config_["exec"].isString() || config_["exec-if"].isString()) && + (output_.out.empty() || output_.exit_code != 0)) { + event_box_.hide(); + } else { + if (config_["return-type"].asString() == "json") { + parseOutputJson(); + } else { + parseOutputRaw(); + } + + try { + addValue(percentage_); + + if (tooltipEnabled()) { + if (tooltip_format_enabled_) { + auto tooltip = config_["tooltip-format"].asString(); + tooltip = fmt::format( + fmt::runtime(tooltip), fmt::arg("text", text_), fmt::arg("alt", alt_), + fmt::arg("percentage", percentage_)); + graph_.set_tooltip_markup(tooltip); + } else { + if (graph_.get_tooltip_markup() != tooltip_) { + graph_.set_tooltip_markup(tooltip_); + } + } + } + auto style = graph_.get_style_context(); + auto classes = style->list_classes(); + for (auto const& c : classes) { + if (c == id_) continue; + style->remove_class(c); + } + for (auto const& c : class_) { + style->add_class(c); + } + style->add_class("flat"); + style->add_class(MODULE_CLASS); + event_box_.show(); + } catch (const fmt::format_error& e) { + if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) + throw; + + throw fmt::format_error( + "mixing manual and automatic argument indexing is no longer supported; " + "try replacing \"{}\" with \"{text}\" in your format specifier"); + } + } + // Call parent update + AGraph::update(); +} + +void waybar::modules::CustomGraph::parseOutputRaw() { + std::istringstream output(output_.out); + std::string line; + int i = 0; + while (getline(output, line)) { + Glib::ustring validated_line = line; + if (!validated_line.validate()) { + validated_line = validated_line.make_valid(); + } + + if (i == 0) { + if (config_["escape"].isBool() && config_["escape"].asBool()) { + text_ = Glib::Markup::escape_text(validated_line); + tooltip_ = Glib::Markup::escape_text(validated_line); + } else { + text_ = validated_line; + tooltip_ = validated_line; + } + tooltip_ = validated_line; + class_.clear(); + } else if (i == 1) { + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(validated_line); + } else { + tooltip_ = validated_line; + } + } else if (i == 2) { + class_.push_back(validated_line); + } else { + break; + } + i++; + } +} + +void waybar::modules::CustomGraph::parseOutputJson() { + std::istringstream output(output_.out); + std::string line; + class_.clear(); + while (getline(output, line)) { + auto parsed = parser_.parse(line); + if (config_["escape"].isBool() && config_["escape"].asBool()) { + text_ = Glib::Markup::escape_text(parsed["text"].asString()); + } else { + text_ = parsed["text"].asString(); + } + if (config_["escape"].isBool() && config_["escape"].asBool()) { + alt_ = Glib::Markup::escape_text(parsed["alt"].asString()); + } else { + alt_ = parsed["alt"].asString(); + } + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString()); + } else { + tooltip_ = parsed["tooltip"].asString(); + } + if (parsed["class"].isString()) { + class_.push_back(parsed["class"].asString()); + } else if (parsed["class"].isArray()) { + for (auto const& c : parsed["class"]) { + class_.push_back(c.asString()); + } + } + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { + percentage_ = (int)lround(parsed["percentage"].asFloat()); + } else { + percentage_ = 0; + } + break; + } +} From dfc26364e15dfe202990e598426e9f5535f52fde Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Fri, 24 Oct 2025 19:47:43 +0200 Subject: [PATCH 22/38] feat: use the foreground color for cleaner look --- include/AGraph.hpp | 1 - man/waybar-cpu-graph.5.scd | 7 +------ src/AGraph.cpp | 26 +++++++++++++++----------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/include/AGraph.hpp b/include/AGraph.hpp index a49b215d..69045473 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -24,7 +24,6 @@ class AGraph : public AModule { Gtk::DrawingArea graph_; std::deque values_; uint16_t datapoints_ = 20; - uint16_t y_offset_ = 0; GraphType graph_type_ = GraphType::LINE; void addValue(const int n); diff --git a/man/waybar-cpu-graph.5.scd b/man/waybar-cpu-graph.5.scd index bfe9de42..c93aaf06 100644 --- a/man/waybar-cpu-graph.5.scd +++ b/man/waybar-cpu-graph.5.scd @@ -19,10 +19,6 @@ The *cpu graph* module displays a line graph with the CPU utilization. typeof: integer ++ The length in pixels the module should display. -*y_offset*: ++ - typeof: integer ++ - The margin in pixels at the bottom of the module. - *datapoints*: ++ typeof: integer ++ How many data points to show. @@ -72,8 +68,7 @@ Basic configuration: ``` "cpu_graph": { "interval": 2, - "width": 10, - "y_offset": 4 + "width": 10 } ``` diff --git a/src/AGraph.cpp b/src/AGraph.cpp index b8076ecc..1899b1b2 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -39,10 +39,6 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st datapoints_ = config_["datapoints_"].asUInt(); } - if (config_["y_offset"].isUInt()) { - y_offset_ = config_["y_offset"].asUInt(); - } - if (config_["graph_type"].isString()) { std::string type = config_["graph_type"].asString(); if (type == "line") { @@ -122,7 +118,7 @@ void AGraph::addValue(const int n) { bool AGraph::onDraw(const Cairo::RefPtr& cr) { const int width = graph_.get_allocated_width(); - const int height = graph_.get_allocated_height() - 1 - y_offset_; + const int height = graph_.get_allocated_height() - 1; if (values_.empty() || width <= 0 || height <= 0) { return false; @@ -220,20 +216,23 @@ void AGraph::drawBars(const Cairo::RefPtr& cr, current_value = std::min(100, std::max(0, current_value)); double green_height = height * (std::min(current_value, 40) / 100.0); - cr->set_source_rgba(0.0, 1.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.5); cr->rectangle(0, height - green_height, width, green_height); cr->fill(); if (current_value > 40) { double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; - cr->set_source_rgba(1.0, 1.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.7); cr->rectangle(0, height - green_height - yellow_height, width, yellow_height); cr->fill(); } if (current_value > 75) { double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; - cr->set_source_rgba(1.0, 0.5, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.85); double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; cr->rectangle(0, height - green_height - yellow_height - orange_height, width, orange_height); @@ -242,7 +241,8 @@ void AGraph::drawBars(const Cairo::RefPtr& cr, if (current_value > 85) { double red_height = height * (current_value - 85) / 100.0; - cr->set_source_rgba(1.0, 0.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 1.0); double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; cr->rectangle(0, height - green_height - yellow_height - orange_height - red_height, width, @@ -268,14 +268,16 @@ void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, do double angle2 = angle1 + 0.3 * angle1; // Green section (0-33%) - cr->set_source_rgba(0.0, 1.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.5); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); // Yellow section (33-66%) angle1 = angle2; angle2 = angle1 + 0.3 * angle1; - cr->set_source_rgba(1.0, 1.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.75); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); @@ -283,6 +285,8 @@ void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, do angle1 = angle2; angle2 = 0.0; cr->set_source_rgba(1.0, 0.0, 0.0, 0.8); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 1.0); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); From 01c6ebdf9e56b943821dbbccbe8599976589dd04 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Sat, 25 Oct 2025 00:13:15 +0200 Subject: [PATCH 23/38] feat: update man pages --- man/waybar-cpu-graph.5.scd | 2 +- man/waybar-custom-graph.5.scd | 189 ++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 man/waybar-custom-graph.5.scd diff --git a/man/waybar-cpu-graph.5.scd b/man/waybar-cpu-graph.5.scd index c93aaf06..1877aeeb 100644 --- a/man/waybar-cpu-graph.5.scd +++ b/man/waybar-cpu-graph.5.scd @@ -1,4 +1,4 @@ -waybar-cpu(5) +waybar-cpu-graph(5) # NAME diff --git a/man/waybar-custom-graph.5.scd b/man/waybar-custom-graph.5.scd new file mode 100644 index 00000000..4f020a6c --- /dev/null +++ b/man/waybar-custom-graph.5.scd @@ -0,0 +1,189 @@ +waybar-custom-graph(5) +# NAME + +waybar - custom graph module + +# DESCRIPTION + +The *custom-graph* module displays a graph with the percentage output of a script. + +# CONFIGURATION + +Addressed by *custom-graph/* + +*exec*: ++ + typeof: string ++ + The path to the script, which should be executed. + +*exec-if*: ++ + typeof: string ++ + The path to a script, which determines if the script in *exec* should be executed. ++ + *exec* will be executed if the exit code of *exec-if* equals 0. + +*exec-on-event*: ++ + typeof: bool ++ + default: true ++ + If an event command is set (e.g. *on-click* or *on-scroll-up*) then re-execute the script after executing the event command. + +*return-type*: ++ + typeof: string ++ + See *return-type* + +*interval*: ++ + typeof: integer or float ++ + The interval (in seconds) in which the information gets polled. ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + Use *once* if you want to execute the module only on startup. ++ + You can update it manually with a signal. If no *interval* or *signal* is defined, it is assumed that the out script loops itself. ++ + If a *signal* is defined then the script will run once on startup and will only update with a signal. + +*restart-interval*: ++ + typeof: integer or float ++ + The restart interval (in seconds). ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + Can't be used with the *interval* option, so only with continuous scripts. ++ + Once the script exits, it'll be re-executed after the *restart-interval*. + +*signal*: ++ + typeof: integer ++ + The signal number used to update the module. ++ + The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ + If no interval is defined then a signal will be the only way to update the module. + +*format*: ++ + typeof: string ++ + default: {text} ++ + The format, how information should be displayed. On {text} data gets inserted. + +*format-icons*: ++ + typeof: array ++ + Based on the set percentage, the corresponding icon gets selected. The order is *low* to *high*. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label (in 90 degree increments). + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + The tooltip format. If specified, overrides any tooltip output from the script in *exec*. ++ + Uses the same format replacements as *format*. + +*escape*: ++ + typeof: bool ++ + default: false ++ + Option to enable escaping of script output. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# RETURN-TYPE + +When *return-type* is set to *json*, Waybar expects the *exec*-script to output its data in JSON format. +This should look like this: + +``` +{"text": "$text", "tooltip": "$tooltip", "class": "$class", "percentage": $percentage } +``` + +The *class* parameter also accepts an array of strings. + +If nothing or an invalid option is specified, Waybar expects i3blocks style output. Values are *newline* separated. +This should look like this: + +``` +$text\\n$tooltip\\n$class* +``` + +*class* is a CSS class, to apply different styles in *style.css* + +# FORMAT REPLACEMENTS + +*{text}*: Output of the script. + +*{percentage}* Percentage which can be set via a json return type. + +*{icon}*: An icon from 'format-icons' according to percentage. + +# EXAMPLES + +## Memory: + +``` +"custom-graph/memory": { + "interval": 60, + "graph_type": "gauge", + "width": 52, + "exec": "/path/mem.sh", + "signal": 8, + "return-type": "json" +}, +``` + +mem.sh: + +``` +#!/bin/bash + +mem_info=$(cat /proc/meminfo) +mem_total=$(echo "$mem_info" | grep '^MemTotal:' | awk '{print $2}') +mem_available=$(echo "$mem_info" | grep '^MemAvailable:' | awk '{print $2}') + +mem_used=$((mem_total - mem_available)) +mem_percent=$((mem_used * 100 / mem_total)) + +echo "{\"text\": \"${mem_percent}%\", \"percentage\": ${mem_percent},\"tooltip\": \"Memory: ${mem_used}KB used / ${mem_total}KB total\"}'" +``` + +# STYLE + +- *#custom-graph-* +- *#custom-graph-.* +- ** can be set by the script. For more information see *return-type* From 283515901e1b48a11d1e6af58a1776ea6e95fb64 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Sat, 8 Nov 2025 01:46:31 +0100 Subject: [PATCH 24/38] fix: id read and name set --- src/factory.cpp | 4 ++-- src/modules/custom_graph.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/factory.cpp b/src/factory.cpp index dfb5caf4..b0ac2e8c 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -363,8 +363,8 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name], bar_.output->name); } - if (ref.compare(0, 13, "custom-graph/") == 0 && ref.size() > 7) { - return new waybar::modules::CustomGraph(ref.substr(7), id, config_[name], bar_.output->name); + if (ref.compare(0, 13, "custom-graph/") == 0 && ref.size() > 13) { + return new waybar::modules::CustomGraph(ref.substr(13), id, config_[name], bar_.output->name); } if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) { return new waybar::modules::CFFI(ref.substr(5), id, config_[name]); diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index 288d97f2..f8660c5f 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -6,7 +6,7 @@ waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, const Json::Value& config, const std::string& output_name) - : AGraph(config, "custom-graph" + name, id), + : AGraph(config, "custom-graph-" + name, id), name_(name), output_name_(output_name), id_(id), From dbf1cfb0f11b687cbaafa65f73acaf5ce2eeb282 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Mon, 23 Feb 2026 16:20:58 +0100 Subject: [PATCH 25/38] fix: typo reading datapoints --- src/AGraph.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AGraph.cpp b/src/AGraph.cpp index 1899b1b2..139bddb2 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -36,7 +36,7 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st event_box_.add(graph_); if (config_["datapoints"].isUInt()) { - datapoints_ = config_["datapoints_"].asUInt(); + datapoints_ = config_["datapoints"].asUInt(); } if (config_["graph_type"].isString()) { @@ -110,7 +110,7 @@ void AGraph::handleGtkMenuEvent(GtkMenuItem* /*menuitem*/, gpointer data) { } void AGraph::addValue(const int n) { - if (values_.size() >= datapoints_) { + if (datapoints_ > 0 && values_.size() >= datapoints_) { values_.pop_front(); } values_.push_back(n); From 132ca3ac456c374afba1a2d913d6bcf5aae2d418 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Fri, 6 Mar 2026 23:48:12 +0100 Subject: [PATCH 26/38] fix: datapoints should be positive --- src/AGraph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AGraph.cpp b/src/AGraph.cpp index 139bddb2..bd64410f 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -35,7 +35,7 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st event_box_.add(graph_); - if (config_["datapoints"].isUInt()) { + if (config_["datapoints"].isUInt() && config_["datapoints"].asUInt() > 0) { datapoints_ = config_["datapoints"].asUInt(); } From 7d965a874f736854516f18320787ede9c6c47ef4 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Fri, 6 Mar 2026 23:57:22 +0100 Subject: [PATCH 27/38] pr fixes --- src/AGraph.cpp | 31 ++++++++++--------------------- src/modules/custom_graph.cpp | 1 - 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/AGraph.cpp b/src/AGraph.cpp index bd64410f..f79e7218 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -209,40 +209,33 @@ void AGraph::drawPath(const Cairo::RefPtr& cr, } } -void AGraph::drawBars(const Cairo::RefPtr& cr, - double width, double height, int current_value, - const Gdk::RGBA& fg_color) { - +void AGraph::drawBars(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color) { current_value = std::min(100, std::max(0, current_value)); double green_height = height * (std::min(current_value, 40) / 100.0); - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.5); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.5); cr->rectangle(0, height - green_height, width, green_height); cr->fill(); if (current_value > 40) { double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.7); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.7); cr->rectangle(0, height - green_height - yellow_height, width, yellow_height); cr->fill(); } if (current_value > 75) { double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.85); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.85); double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; - cr->rectangle(0, height - green_height - yellow_height - orange_height, width, - orange_height); + cr->rectangle(0, height - green_height - yellow_height - orange_height, width, orange_height); cr->fill(); } if (current_value > 85) { double red_height = height * (current_value - 85) / 100.0; - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 1.0); double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; cr->rectangle(0, height - green_height - yellow_height - orange_height - red_height, width, @@ -268,25 +261,21 @@ void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, do double angle2 = angle1 + 0.3 * angle1; // Green section (0-33%) - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.5); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.5); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); // Yellow section (33-66%) angle1 = angle2; angle2 = angle1 + 0.3 * angle1; - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.75); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.75); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); // Red section (66-100%) angle1 = angle2; angle2 = 0.0; - cr->set_source_rgba(1.0, 0.0, 0.0, 0.8); - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 1.0); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index f8660c5f..c23ed0d6 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -228,7 +228,6 @@ void waybar::modules::CustomGraph::parseOutputRaw() { text_ = validated_line; tooltip_ = validated_line; } - tooltip_ = validated_line; class_.clear(); } else if (i == 1) { if (config_["escape"].isBool() && config_["escape"].asBool()) { From d1d501450d27eeb31504ff5379537f27ff7373dc Mon Sep 17 00:00:00 2001 From: nonamescm Date: Mon, 4 May 2026 03:38:17 -0300 Subject: [PATCH 28/38] Add static river tags --- man/waybar-river-tags.5.scd | 1 + src/modules/river/tags.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index 64621229..7481b974 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -50,6 +50,7 @@ Addressed by *river/tags* - *#tags button.occupied* - *#tags button.focused* - *#tags button.urgent* +- *#tags button.tag-N* Note that occupied/focused/urgent status may overlap. That is, a tag may be both occupied and focused at the same time. diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index f06565f6..5381bf9f 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -149,6 +149,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con button.signal_button_press_event().connect( sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), (1 << tag))); } + button.get_style_context()->add_class("tag-" + std::to_string(tag + 1)); button.show(); } From 3971bb6c00cc437dab6fc92c86ea4b8be63d42e7 Mon Sep 17 00:00:00 2001 From: Antoine Gaudreau Simard Date: Sat, 20 Dec 2025 18:07:52 -0500 Subject: [PATCH 29/38] feat: add support for multiple batteries in a single bluetooth device --- include/modules/bluetooth.hpp | 6 ++ man/waybar-bluetooth.5.scd | 14 ++++ src/modules/bluetooth.cpp | 151 +++++++++++++++++++++++++++++++++- 3 files changed, 168 insertions(+), 3 deletions(-) diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index b89383a0..a06d4645 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -41,6 +41,7 @@ class Bluetooth : public ALabel { bool services_resolved; // NOTE: experimental feature in bluez std::optional battery_percentage; + std::optional battery_percentage_peripheral; }; public: @@ -59,6 +60,11 @@ class Bluetooth : public ALabel { gpointer) -> void; auto getDeviceBatteryPercentage(GDBusObject*) -> std::optional; + auto getDeviceGattBatteryLevels(GDBusObject*, std::optional&, + std::optional&) -> void; + static auto processBatteryServiceCharacteristics(GList*, const std::string&, const std::string&, + const std::string&, std::optional&, + std::optional&) -> void; auto getDeviceProperties(GDBusObject*, DeviceInfo&) -> bool; auto getControllerProperties(GDBusObject*, ControllerInfo&) -> bool; diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index fd7d5fb5..36c6443a 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -178,6 +178,9 @@ At the time of writing, the experimental features of BlueZ need to be turned on, *{device_battery_percentage}*: Battery percentage of the displayed device if available. Use only in the config options defined below. +*{device_battery_percentage_peripheral}*: Battery percentage of the peripheral half of a split keyboard (e.g., ZMK keyboards with separate central and peripheral batteries). ++ +This is read from GATT Battery Service characteristics that have a User Description descriptor. Use only in the config options defined below. + ## CONFIGURATION *format-connected-battery*: ++ @@ -220,6 +223,17 @@ At the time of writing, the experimental features of BlueZ need to be turned on, } ``` +Split keyboard with separate central/peripheral batteries (e.g., ZMK): + +``` +"bluetooth": { + "format-device-preference": [ "Keyball44" ], + "format": "", + "format-connected-battery": " {device_battery_percentage}%|{device_battery_percentage_peripheral}%", + "tooltip-format-connected": "{device_alias}\\nCentral: {device_battery_percentage}%\\nPeripheral: {device_battery_percentage_peripheral}%" +} +``` + # STYLE - *#bluetooth* diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index c59af3b5..97aad8b2 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -83,6 +83,66 @@ auto getUcharProperty(GDBusProxy* proxy, const char* property_name) -> unsigned return 0; } +auto isChildPath(const std::string& child, const std::string& parent) -> bool { + return child.starts_with(parent); +} + +auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional { + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + + GError* error = nullptr; + GVariant* gvar = g_dbus_proxy_call_sync(proxy_char, "ReadValue", g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (error != nullptr) { + g_error_free(error); + return std::nullopt; + } + if (gvar == nullptr) { + return std::nullopt; + } + + GVariant* value_array = g_variant_get_child_value(gvar, 0); + gsize n_elements; + const auto* data = + static_cast(g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar))); + + std::optional result; + if (data != nullptr && n_elements > 0) { + result = data[0]; + } + + g_variant_unref(value_array); + g_variant_unref(gvar); + return result; +} + +auto hasUserDescriptionDescriptor(GList* objects, const std::string& char_path, + const std::string& user_description_uuid) -> bool { + for (GList* n = objects; n != nullptr; n = n->next) { + GDBusObject* desc_object = G_DBUS_OBJECT(n->data); + std::string desc_path = g_dbus_object_get_object_path(desc_object); + + if (!isChildPath(desc_path, char_path)) { + continue; + } + + GDBusProxy* proxy_desc = + G_DBUS_PROXY(g_dbus_object_get_interface(desc_object, "org.bluez.GattDescriptor1")); + if (proxy_desc == nullptr) { + continue; + } + + auto desc_uuid = getOptionalStringProperty(proxy_desc, "UUID"); + g_object_unref(proxy_desc); + + if (desc_uuid.has_value() && desc_uuid.value().find(user_description_uuid) != std::string::npos) { + return true; + } + } + return false; +} + } // namespace waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& config) @@ -232,8 +292,9 @@ auto waybar::modules::Bluetooth::update() -> void { 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("icon", icon_label), - fmt::arg("device_battery_percentage", - cur_focussed_device_.battery_percentage.value_or(0)))); + fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + cur_focussed_device_.battery_percentage_peripheral.value_or(0)))); } if (tooltipEnabled()) { @@ -258,7 +319,9 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::runtime(enumerate_format), fmt::arg("device_address", dev.address), fmt::arg("device_address_type", dev.address_type), fmt::arg("device_alias", dev.alias), fmt::arg("icon", enumerate_icon), - fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0))); + fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + dev.battery_percentage_peripheral.value_or(0))); } } device_enumerate_ = ss.str(); @@ -278,6 +341,8 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::arg("device_address_type", cur_focussed_device_.address_type), fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_tooltip), fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + cur_focussed_device_.battery_percentage_peripheral.value_or(0)), fmt::arg("device_enumerate", device_enumerate_))); } @@ -398,6 +463,84 @@ auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object) return std::nullopt; } +auto waybar::modules::Bluetooth::getDeviceGattBatteryLevels( + GDBusObject* device_object, std::optional& central_battery, + std::optional& peripheral_battery) -> void { + const std::string BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb"; + const std::string BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb"; + const std::string USER_DESCRIPTION_UUID = "00002901-0000-1000-8000-00805f9b34fb"; + + GList* objects = g_dbus_object_manager_get_objects(manager_.get()); + std::string device_path = g_dbus_object_get_object_path(device_object); + + for (GList* l = objects; l != nullptr; l = l->next) { + GDBusObject* service_object = G_DBUS_OBJECT(l->data); + std::string service_path = g_dbus_object_get_object_path(service_object); + + if (!isChildPath(service_path, device_path)) { + continue; + } + + GDBusProxy* proxy_service = + G_DBUS_PROXY(g_dbus_object_get_interface(service_object, "org.bluez.GattService1")); + if (proxy_service == nullptr) { + continue; + } + + auto service_uuid = getOptionalStringProperty(proxy_service, "UUID"); + g_object_unref(proxy_service); + + if (!service_uuid.has_value() || + service_uuid.value().find(BATTERY_SERVICE_UUID) == std::string::npos) { + continue; + } + + processBatteryServiceCharacteristics(objects, service_path, BATTERY_LEVEL_UUID, + USER_DESCRIPTION_UUID, central_battery, peripheral_battery); + } + + g_list_free_full(objects, g_object_unref); +} + +auto waybar::modules::Bluetooth::processBatteryServiceCharacteristics( + GList* objects, const std::string& service_path, const std::string& battery_level_uuid, + const std::string& user_description_uuid, std::optional& central_battery, + std::optional& peripheral_battery) -> void { + for (GList* m = objects; m != nullptr; m = m->next) { + GDBusObject* char_object = G_DBUS_OBJECT(m->data); + std::string char_path = g_dbus_object_get_object_path(char_object); + + if (!isChildPath(char_path, service_path)) { + continue; + } + + GDBusProxy* proxy_char = + G_DBUS_PROXY(g_dbus_object_get_interface(char_object, "org.bluez.GattCharacteristic1")); + if (proxy_char == nullptr) { + continue; + } + + auto char_uuid = getOptionalStringProperty(proxy_char, "UUID"); + if (!char_uuid.has_value() || char_uuid.value().find(battery_level_uuid) == std::string::npos) { + g_object_unref(proxy_char); + continue; + } + + auto battery_value = readBatteryCharacteristicValue(proxy_char); + g_object_unref(proxy_char); + + if (!battery_value.has_value()) { + continue; + } + + if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) { + peripheral_battery = battery_value.value(); + } else { + central_battery = battery_value.value(); + } + } +} + 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")); @@ -418,6 +561,8 @@ auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, Device g_object_unref(proxy_device); device_info.battery_percentage = getDeviceBatteryPercentage(object); + getDeviceGattBatteryLevels(object, device_info.battery_percentage, + device_info.battery_percentage_peripheral); return true; } From 51becf2fbcf19bca2e7079657ca1d1a40647a43e Mon Sep 17 00:00:00 2001 From: Jack Barnes Date: Sat, 16 May 2026 13:03:53 +0800 Subject: [PATCH 30/38] feat(niri/workspaces): Add `enable-bar-scroll` option --- include/modules/niri/workspaces.hpp | 1 + man/waybar-niri-workspaces.5.scd | 5 ++++ src/modules/niri/workspaces.cpp | 45 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index 08986412..01840b33 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -20,6 +20,7 @@ class Workspaces : public AModule, public EventHandler { void doUpdate(); Gtk::Button& addButton(const Json::Value& ws); std::string getIcon(const std::string& value, const Json::Value& ws); + bool handleScroll(GdkEventScroll* /*unused*/) override; const Bar& bar_; Gtk::Box box_; diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 4be85eb3..ec329404 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -31,6 +31,11 @@ Addressed by *niri/workspaces* default: false ++ If set to false, you can click to change workspace. If set to true this behaviour is disabled. +*enable-bar-scroll*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can't scroll to cycle throughout workspaces from the entire bar. If set to true this behaviour is enabled. + *disable-markup*: ++ typeof: bool ++ default: false ++ diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 97d15215..97472260 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -22,6 +22,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); gIPC->registerForIPC("WorkspaceUrgencyChanged", this); + if (config["enable-bar-scroll"].asBool()) { + auto& window = const_cast(bar_).window; + window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); + } + dp.emit(); } @@ -198,4 +204,43 @@ std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) return value; } +bool Workspaces::handleScroll(GdkEventScroll* e) { + if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) { + /** + * Ignore emulated scroll events on window + */ + return false; + } + + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + + std::string action_name; + + if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) { + action_name = "FocusWorkspaceDown"; + } else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) { + action_name = "FocusWorkspaceUp"; + } else { + return true; + } + + action[action_name] = Json::Value(Json::objectValue); + + IPC::send(request); + + } catch (const std::exception& e) { + spdlog::error("Workspaces: {}", e.what()); + return false; + } + + return true; +} + } // namespace waybar::modules::niri From 822071f36efbf828781e99ea3ab36a97d9a55cb7 Mon Sep 17 00:00:00 2001 From: stalker Date: Mon, 25 May 2026 08:18:12 +0400 Subject: [PATCH 31/38] feat(custom): add image --- include/modules/custom.hpp | 7 +++++-- src/modules/custom.cpp | 32 +++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index a345a33b..442b1c37 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -5,14 +5,14 @@ #include #include -#include "ALabel.hpp" +#include "AIconLabel.hpp" #include "util/command.hpp" #include "util/json.hpp" #include "util/sleeper_thread.hpp" namespace waybar::modules { -class Custom : public ALabel { +class Custom : public AIconLabel { public: Custom(const std::string&, const std::string&, const Json::Value&, const std::string&); virtual ~Custom(); @@ -36,6 +36,9 @@ class Custom : public ALabel { std::string alt_; std::string tooltip_; std::string last_tooltip_markup_; + std::string image_path_; + std::string image_name_; + unsigned app_icon_size_{24}; const bool tooltip_format_enabled_; std::vector class_; int percentage_; diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 28def8c9..22465802 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -8,7 +8,7 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, const Json::Value& config, const std::string& output_name) - : ALabel(config, "custom-" + name, id, "{}"), + : AIconLabel(config, "custom-" + name, id, "{}"), name_(name), output_name_(output_name), id_(id), @@ -28,6 +28,15 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, } else if (config_["exec"].isString()) { continuousWorker(); } + if (config_["image-path"].isString()) { + image_path_ = config_["image-path"].asString(); + } + if (config_["image-name"].isString()) { + image_name_ = config_["image-name"].asString(); + } + if (config["icon-size"].isUInt()) { + app_icon_size_ = config["icon-size"].asUInt(); + } } waybar::modules::Custom::~Custom() { @@ -177,7 +186,7 @@ auto waybar::modules::Custom::update() -> void { auto str = fmt::format(fmt::runtime(format_), fmt::arg("text", text_), fmt::arg("alt", alt_), fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); - if ((config_["hide-empty-text"].asBool() && text_.empty()) || str.empty()) { + if ((config_["hide-empty-text"].asBool() && text_.empty()) || (str.empty() && image_path_.empty() && image_name_.empty())) { event_box_.hide(); } else { label_.set_markup(str); @@ -212,7 +221,20 @@ auto waybar::modules::Custom::update() -> void { style->add_class("flat"); style->add_class("text-button"); style->add_class(MODULE_CLASS); + auto image_style = image_.get_style_context(); + image_style->add_class("image-button"); event_box_.show(); + if (!image_path_.empty()) { + auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); + image_.set(pixbuf); + } else if (!image_name_.empty()) { + image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID); + image_.set_pixel_size(app_icon_size_); + } + + image_.set_visible(!image_name_.empty() || !image_path_.empty()); + label_.set_visible(!str.empty()); + } } catch (const fmt::format_error& e) { if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) @@ -222,9 +244,12 @@ auto waybar::modules::Custom::update() -> void { "mixing manual and automatic argument indexing is no longer supported; " "try replacing \"{}\" with \"{text}\" in your format specifier"); } + + + } // Call parent update - ALabel::update(); + AIconLabel::update(); } void waybar::modules::Custom::parseOutputRaw() { @@ -290,6 +315,7 @@ void waybar::modules::Custom::parseOutputJson() { class_.push_back(c.asString()); } } + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { percentage_ = (int)lround(parsed["percentage"].asFloat()); } else { From f7b9d8c1238db93325ed1f50681fbc3f9a98ccc9 Mon Sep 17 00:00:00 2001 From: Zuodong Yu Date: Sun, 7 Jun 2026 17:35:37 +0800 Subject: [PATCH 32/38] modules/niri: add {total} format token to workspaces --- man/waybar-niri-workspaces.5.scd | 2 ++ src/modules/niri/workspaces.cpp | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 4be85eb3..80cc7eb5 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -63,6 +63,8 @@ as defined by niri. *{output}*: Output where the workspace is located. +*{total}*: The total number of workspaces. + # ICONS Additional to workspace name matching, the following *format-icons* can be set. diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 97d15215..ce0d2376 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -100,7 +100,8 @@ void Workspaces::doUpdate() { name = fmt::format(fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name), fmt::arg("name", ws["name"].asString()), fmt::arg("index", ws["idx"].asUInt()), - fmt::arg("output", ws["output"].asString())); + fmt::arg("output", ws["output"].asString()), + fmt::arg("total", my_workspaces.size())); } if (!config_["disable-markup"].asBool()) { static_cast(button.get_children()[0])->set_markup(name); From 2ac1f1c437456b6cdc39e76be661d7ac1c88eb56 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:51:10 +0200 Subject: [PATCH 33/38] Fix clang-format --- src/modules/network.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index e7cdfcaf..0981d571 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -322,8 +322,7 @@ auto waybar::modules::Network::update() -> void { if (!state_.empty() && label_.get_style_context()->has_class(state_)) { label_.get_style_context()->remove_class(state_); } - if (!threshold_state.empty() && - config_["format-" + state + "-" + threshold_state].isString()) { + if (!threshold_state.empty() && config_["format-" + state + "-" + threshold_state].isString()) { default_format_ = config_["format-" + state + "-" + threshold_state].asString(); } else if (config_["format-" + state].isString()) { default_format_ = config_["format-" + state].asString(); From 913cde915abbff9e70827e9d57cd440d87c62361 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:51:45 +0200 Subject: [PATCH 34/38] Fix clang-format --- include/modules/bluetooth.hpp | 3 ++- src/modules/bluetooth.cpp | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index a06d4645..5e03b118 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -63,7 +63,8 @@ class Bluetooth : public ALabel { auto getDeviceGattBatteryLevels(GDBusObject*, std::optional&, std::optional&) -> void; static auto processBatteryServiceCharacteristics(GList*, const std::string&, const std::string&, - const std::string&, std::optional&, + const std::string&, + std::optional&, std::optional&) -> void; auto getDeviceProperties(GDBusObject*, DeviceInfo&) -> bool; auto getControllerProperties(GDBusObject*, ControllerInfo&) -> bool; diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 97aad8b2..d54c4d07 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -92,8 +92,9 @@ auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional std::optional(g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar))); + const auto* data = static_cast( + g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar))); std::optional result; if (data != nullptr && n_elements > 0) { @@ -136,7 +137,8 @@ auto hasUserDescriptionDescriptor(GList* objects, const std::string& char_path, auto desc_uuid = getOptionalStringProperty(proxy_desc, "UUID"); g_object_unref(proxy_desc); - if (desc_uuid.has_value() && desc_uuid.value().find(user_description_uuid) != std::string::npos) { + if (desc_uuid.has_value() && + desc_uuid.value().find(user_description_uuid) != std::string::npos) { return true; } } @@ -496,7 +498,8 @@ auto waybar::modules::Bluetooth::getDeviceGattBatteryLevels( } processBatteryServiceCharacteristics(objects, service_path, BATTERY_LEVEL_UUID, - USER_DESCRIPTION_UUID, central_battery, peripheral_battery); + USER_DESCRIPTION_UUID, central_battery, + peripheral_battery); } g_list_free_full(objects, g_object_unref); From c4c9345fef30a5a29f5ab44ad16b1d3c6413ad86 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:52:02 +0200 Subject: [PATCH 35/38] Fix clang-format --- include/AGraph.hpp | 31 +++++++++++++++---------------- include/modules/cpu_graph.hpp | 6 +++--- src/factory.cpp | 6 +++--- src/modules/custom_graph.cpp | 7 +++---- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/include/AGraph.hpp b/include/AGraph.hpp index 69045473..e2ada835 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -15,7 +15,7 @@ enum class GraphType { LINE, BAR, GAUGE }; class AGraph : public AModule { public: - AGraph(const Json::Value &, const std::string &, const std::string &, uint16_t interval = 0, + AGraph(const Json::Value&, const std::string&, const std::string&, uint16_t interval = 0, bool enable_click = false, bool enable_scroll = false); virtual ~AGraph() = default; auto update() -> void override; @@ -30,29 +30,28 @@ class AGraph : public AModule { const std::chrono::seconds interval_; - bool onDraw(const Cairo::RefPtr &cr); + bool onDraw(const Cairo::RefPtr& cr); - std::map submenus_; + std::map submenus_; std::map menuActionsMap_; - static void handleGtkMenuEvent(GtkMenuItem *menuitem, gpointer data); + static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data); private: - void drawFilledArea(const Cairo::RefPtr &cr, - const std::vector> &points, double height, - const Gdk::RGBA &bg_color); + void drawFilledArea(const Cairo::RefPtr& cr, + const std::vector>& points, double height, + const Gdk::RGBA& bg_color); - void drawLine(const Cairo::RefPtr &cr, - const std::vector> &points, const Gdk::RGBA &fg_color); + void drawLine(const Cairo::RefPtr& cr, + const std::vector>& points, const Gdk::RGBA& fg_color); - void drawPath(const Cairo::RefPtr &cr, - const std::vector> &points); + void drawPath(const Cairo::RefPtr& cr, + const std::vector>& points); - void drawBars(const Cairo::RefPtr &cr, - double width, double height, int current_value, - const Gdk::RGBA &fg_color); + void drawBars(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color); - void drawGauge(const Cairo::RefPtr &cr, double width, double height, - int current_value, const Gdk::RGBA &fg_color); + void drawGauge(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color); }; } // namespace waybar diff --git a/include/modules/cpu_graph.hpp b/include/modules/cpu_graph.hpp index df5f6ea3..cf74a3fe 100644 --- a/include/modules/cpu_graph.hpp +++ b/include/modules/cpu_graph.hpp @@ -21,9 +21,9 @@ class CpuGraph : public AGraph { auto update() -> void override; private: - static constexpr const char *MODERATE_CLASS = "cpu-moderate"; - static constexpr const char *HIGH_CLASS = "cpu-high"; - static constexpr const char *INTENSIVE_CLASS = "cpu-intensive"; + static constexpr const char* MODERATE_CLASS = "cpu-moderate"; + static constexpr const char* HIGH_CLASS = "cpu-high"; + static constexpr const char* INTENSIVE_CLASS = "cpu-intensive"; std::vector> prev_times_; util::SleeperThread thread_; diff --git a/src/factory.cpp b/src/factory.cpp index cd29554c..6969da6b 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -43,11 +43,11 @@ #include "modules/niri/workspaces.hpp" #endif #ifdef HAVE_MANGO -#include "modules/mango/language.hpp" #include "modules/mango/keymode.hpp" +#include "modules/mango/language.hpp" +#include "modules/mango/layout.hpp" #include "modules/mango/window.hpp" #include "modules/mango/workspaces.hpp" -#include "modules/mango/layout.hpp" #endif #ifdef HAVE_WAYFIRE #include "modules/wayfire/window.hpp" @@ -58,8 +58,8 @@ #endif #if defined(HAVE_CPU_LINUX) || defined(HAVE_CPU_BSD) #include "modules/cpu.hpp" -#include "modules/cpu_graph.hpp" #include "modules/cpu_frequency.hpp" +#include "modules/cpu_graph.hpp" #include "modules/cpu_usage.hpp" #include "modules/load.hpp" #endif diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index c23ed0d6..2a12f0c1 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -5,7 +5,7 @@ #include "util/scope_guard.hpp" waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, - const Json::Value& config, const std::string& output_name) + const Json::Value& config, const std::string& output_name) : AGraph(config, "custom-graph-" + name, id), name_(name), output_name_(output_name), @@ -175,9 +175,8 @@ auto waybar::modules::CustomGraph::update() -> void { if (tooltipEnabled()) { if (tooltip_format_enabled_) { auto tooltip = config_["tooltip-format"].asString(); - tooltip = fmt::format( - fmt::runtime(tooltip), fmt::arg("text", text_), fmt::arg("alt", alt_), - fmt::arg("percentage", percentage_)); + tooltip = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_), + fmt::arg("alt", alt_), fmt::arg("percentage", percentage_)); graph_.set_tooltip_markup(tooltip); } else { if (graph_.get_tooltip_markup() != tooltip_) { From 885ced58fca19cb7400969ba1b5ed5ced622ffbb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:52:13 +0200 Subject: [PATCH 36/38] Fix clang-format --- src/modules/custom.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 9b60eeb1..37198e3c 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -193,7 +193,8 @@ auto waybar::modules::Custom::update() -> void { auto str = fmt::format(fmt::runtime(format_), fmt::arg("text", text_), fmt::arg("alt", alt_), fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); - if ((config_["hide-empty-text"].asBool() && text_.empty()) || (str.empty() && image_path_.empty() && image_name_.empty())) { + if ((config_["hide-empty-text"].asBool() && text_.empty()) || + (str.empty() && image_path_.empty() && image_name_.empty())) { event_box_.hide(); } else { label_.set_markup(str); @@ -231,17 +232,16 @@ auto waybar::modules::Custom::update() -> void { auto image_style = image_.get_style_context(); image_style->add_class("image-button"); event_box_.show(); - if (!image_path_.empty()) { - auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); - image_.set(pixbuf); - } else if (!image_name_.empty()) { - image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID); - image_.set_pixel_size(app_icon_size_); - } + if (!image_path_.empty()) { + auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); + image_.set(pixbuf); + } else if (!image_name_.empty()) { + image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID); + image_.set_pixel_size(app_icon_size_); + } image_.set_visible(!image_name_.empty() || !image_path_.empty()); label_.set_visible(!str.empty()); - } } catch (const fmt::format_error& e) { if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) @@ -251,9 +251,6 @@ auto waybar::modules::Custom::update() -> void { "mixing manual and automatic argument indexing is no longer supported; " "try replacing \"{}\" with \"{text}\" in your format specifier"); } - - - } // Call parent update AIconLabel::update(); @@ -322,7 +319,7 @@ void waybar::modules::Custom::parseOutputJson() { class_.push_back(c.asString()); } } - + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { percentage_ = (int)lround(parsed["percentage"].asFloat()); } else { From 331fe0e963ea867eb60dde2ebb6c98de6b0a1080 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:52:24 +0200 Subject: [PATCH 37/38] Fix clang-format --- include/modules/clock.hpp | 5 +++-- src/modules/clock.cpp | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index 4ac91d61..5fd9df54 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -89,8 +89,9 @@ class Clock final : public ALabel { {"shift_reset", &waybar::modules::Clock::cldShift_reset}, {"tz_up", &waybar::modules::Clock::tz_up}, {"tz_down", &waybar::modules::Clock::tz_down}}; - static inline std::map actionWithArgsMap_{ - {"exec", &waybar::modules::Clock::action_exec}}; + static inline std::map + actionWithArgsMap_{{"exec", &waybar::modules::Clock::action_exec}}; }; } // namespace waybar::modules diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index b92f2264..6050df31 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -550,7 +550,6 @@ void waybar::modules::Clock::action_exec(const std::string& action) { pid_children_.push_back(util::command::forkExec(cmd)); } - #ifdef HAVE_LANGINFO_1STDAY template using deleter_from_fn = std::integral_constant; From 66ecba4e99fcbb3a91acd197c6c552d730a608a7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:58:46 +0200 Subject: [PATCH 38/38] idle_inhibitor: apply clang-format --- src/modules/idle_inhibitor.cpp | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 8dfe0b85..9638d0fc 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -23,7 +23,7 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& // Read the wait-for-activity config option if (config_["wait-for-activity"].isBool()) { wait_for_activity_ = config_["wait-for-activity"].asBool(); - + // Check if ext-idle-notify protocol is available when wait-for-activity is enabled if (wait_for_activity_ && waybar::Client::inst()->idle_notifier == nullptr) { throw std::runtime_error("wait-for-activity requires ext-idle-notify-v1 protocol support"); @@ -162,31 +162,31 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { return true; } -void waybar::modules::IdleInhibitor::handleIdled(void* data, - ext_idle_notification_v1* /*notification*/) { +void waybar::modules::IdleInhibitor::handleIdled(void* data, + ext_idle_notification_v1* /*notification*/) { spdlog::info("deactivating idle_inhibitor due to user inactivity"); status = false; - + // Clean up the notification since we're deactivating auto* self = static_cast(data); if (self != nullptr) { self->teardownIdleNotification(); } - + for (auto const& module : waybar::modules::IdleInhibitor::modules) { module->update(); } } void waybar::modules::IdleInhibitor::handleResumed(void* data, - ext_idle_notification_v1* /*notification*/) { + ext_idle_notification_v1* /*notification*/) { // User became active again - notification will continue monitoring spdlog::debug("user activity detected, idle_inhibitor still active"); } void waybar::modules::IdleInhibitor::setupIdleNotification() { spdlog::debug("idle_inhibitor: setting up idle notification"); - + // Clean up any existing notification first if (idle_notification_ != nullptr) { spdlog::debug("idle_inhibitor: cleaning up existing notification before setup"); @@ -208,11 +208,12 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat); // Check protocol version to determine which function to use - uint32_t version = wl_proxy_get_version(reinterpret_cast(client->idle_notifier)); - - spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})", + uint32_t version = + wl_proxy_get_version(reinterpret_cast(client->idle_notifier)); + + spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})", idle_timeout_ms_, version); - + if (version >= 2) { // Version 2+: Use get_input_idle_notification which ignores idle inhibitors // This allows us to detect actual user inactivity even while the inhibitor is active @@ -222,10 +223,13 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { } else { // Version 1: Fall back to get_idle_notification // WARNING: This respects idle inhibitors, so it won't fire while inhibitor is active - spdlog::warn("idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support get_input_idle_notification, " - "wait-for-activity may not work correctly", version); - idle_notification_ = ext_idle_notifier_v1_get_idle_notification( - client->idle_notifier, idle_timeout_ms_, wl_seat); + spdlog::warn( + "idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support " + "get_input_idle_notification, " + "wait-for-activity may not work correctly", + version); + idle_notification_ = ext_idle_notifier_v1_get_idle_notification(client->idle_notifier, + idle_timeout_ms_, wl_seat); } if (idle_notification_ == nullptr) {