From 0cbb280de635aa3ddcdbb416d8ebd55a1fb82b15 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 24 Feb 2024 15:00:39 +0100 Subject: [PATCH 001/167] fix title not showing for special workspaces (seperate outputs: true) --- src/modules/hyprland/window.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 1af02b55..eb659b13 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -99,7 +99,8 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{-1, 0, "", ""}; } - const int id = (*monitor)["activeWorkspace"]["id"].asInt(); + const int special_id = (*monitor)["specialWorkspace"]["id"].asInt(); + const int id = special_id != 0 ? special_id : (*monitor)["activeWorkspace"]["id"].asInt(); const auto workspaces = gIPC->getSocket1JsonReply("workspaces"); assert(workspaces.isArray()); From 9f5c27c6e06e51853b55f68043521d1677e4d048 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 24 Feb 2024 18:16:43 +0100 Subject: [PATCH 002/167] fix title not showing for special workspaces (seperate outputs: false --- include/modules/hyprland/window.hpp | 1 - src/modules/hyprland/window.cpp | 13 ++++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index ea4d83b2..4cc16ffb 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -42,7 +42,6 @@ class Window : public waybar::AAppIconLabel, public EventHandler { }; auto getActiveWorkspace(const std::string&) -> Workspace; - auto getActiveWorkspace() -> Workspace; void onEvent(const std::string&) override; void queryActiveWorkspace(); void setClass(const std::string&, bool enable); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index eb659b13..fca8b9ab 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -84,17 +84,12 @@ auto Window::update() -> void { AAppIconLabel::update(); } -auto Window::getActiveWorkspace() -> Workspace { - const auto workspace = gIPC->getSocket1JsonReply("activeworkspace"); - assert(workspace.isObject()); - return Workspace::parse(workspace); -} - -auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { +auto Window::getActiveWorkspace(const std::string& monitorName = "") -> Workspace { const auto monitors = gIPC->getSocket1JsonReply("monitors"); assert(monitors.isArray()); - auto monitor = std::find_if(monitors.begin(), monitors.end(), - [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + auto monitor = std::find_if(monitors.begin(), monitors.end(), [&](Json::Value monitor) { + return monitorName == "" ? monitor["focused"].asBool() : monitor["name"] == monitorName; + }); if (monitor == std::end(monitors)) { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{-1, 0, "", ""}; From 696f7e1542ee2786d27e6a6e4e86bf2359132673 Mon Sep 17 00:00:00 2001 From: Victor Perez Cano Date: Fri, 18 Oct 2024 20:41:13 +0200 Subject: [PATCH 003/167] tooltip functionality on hyprland/language module --- include/modules/hyprland/language.hpp | 1 + src/modules/hyprland/language.cpp | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index 47a4d69c..ce5b9262 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -35,6 +35,7 @@ class Language : public waybar::ALabel, public EventHandler { std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; + const bool tooltip_format_enabled_; Layout layout_; }; diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index d86393af..901749a0 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -10,7 +10,9 @@ namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { + : ALabel(config, "language", id, "{}", 0, true), + bar_(bar), + tooltip_format_enabled_{config_["tooltip-format"].isString()} { modulesReady = true; if (!gIPC) { @@ -57,9 +59,27 @@ auto Language::update() -> void { spdlog::debug("hyprland language formatted layout name {}", layoutName); + std::string tooltipContent = std::string{}; + if (tooltipEnabled()) { + if (tooltip_format_enabled_) { + auto tooltip_format = config_["tooltip-format"].asString(); + tooltipContent = fmt::format(fmt::runtime(tooltip_format), + fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant)); + + } else { + tooltipContent = layoutName; + } + } + + spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); + if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); + label_.set_tooltip_markup(tooltipContent); } else { label_.hide(); } From 550d108a03a4d86dced4eef8e2db095a294b23f1 Mon Sep 17 00:00:00 2001 From: Victor Perez Cano Date: Sat, 19 Oct 2024 17:30:23 +0200 Subject: [PATCH 004/167] added tooltip-format- on hyprland/language tooltip --- include/modules/hyprland/language.hpp | 1 - src/modules/hyprland/language.cpp | 37 +++++++++++++++++---------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index ce5b9262..47a4d69c 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -35,7 +35,6 @@ class Language : public waybar::ALabel, public EventHandler { std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; - const bool tooltip_format_enabled_; Layout layout_; }; diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 901749a0..ecb3ea2c 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -11,8 +11,7 @@ namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "language", id, "{}", 0, true), - bar_(bar), - tooltip_format_enabled_{config_["tooltip-format"].isString()} { + bar_(bar) { modulesReady = true; if (!gIPC) { @@ -56,30 +55,40 @@ auto Language::update() -> void { fmt::arg("shortDescription", layout_.short_description), fmt::arg("variant", layout_.variant))); } - spdlog::debug("hyprland language formatted layout name {}", layoutName); std::string tooltipContent = std::string{}; - if (tooltipEnabled()) { - if (tooltip_format_enabled_) { + bool tooltip_enabled = tooltipEnabled(); + if (tooltip_enabled) { + if (config_.isMember("tooltip-format")) { auto tooltip_format = config_["tooltip-format"].asString(); - tooltipContent = fmt::format(fmt::runtime(tooltip_format), - fmt::arg("long", layout_.full_name), - fmt::arg("short", layout_.short_name), - fmt::arg("shortDescription", layout_.short_description), - fmt::arg("variant", layout_.variant)); - - } else { + if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) { + const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant; + tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); + } else if (config_.isMember("tooltip-format-" + layout_.short_description)) { + const auto propName = "tooltip-format-" + layout_.short_description; + tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); + } else { + tooltipContent = trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); + } + } + else { + // if no tooltip format is provided, use the same text as the module tooltipContent = layoutName; } + spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); } - spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); - label_.set_tooltip_markup(tooltipContent); + if (tooltip_enabled) { + label_.set_tooltip_markup(tooltipContent); + } } else { label_.hide(); } From cb0ecfbddc6d4e2d305608b01fd03945e167706b Mon Sep 17 00:00:00 2001 From: Victor Perez Cano Date: Sat, 19 Oct 2024 18:04:07 +0200 Subject: [PATCH 005/167] updated man for hyprland/language --- man/waybar-hyprland-language.5.scd | 41 +++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index 33b28ae4..9ebc8801 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -38,6 +38,19 @@ Addressed by *hyprland/language* typeof: array ++ The actions corresponding to the buttons of the menu. +*tooltip*: ++ + typeof: boolean ++ + default: true ++ + Enables or disables the tooltip for the language module. By default, the tooltip is enabled. Set to *false* to disable. + +*tooltip-format*: ++ + typeof: string ++ + default: {long} ++ + Specifies the format of the tooltip when it is enabled. It follows the same format replacement rules as the *format* key. + +*tooltip-format-*: ++ + typeof: string ++ + Allows specifying a different tooltip format for each language. The ** should be replaced with the language code. This can be used to provide a custom tooltip for each language. # FORMAT REPLACEMENTS @@ -54,13 +67,35 @@ Addressed by *hyprland/language* ``` "hyprland/language": { - "format": "Lang: {long}" - "format-en": "AMERICA, HELL YEAH!" - "format-tr": "As bayrakları" + "format": "Lang: {long}", + "format-en": "AMERICA, HELL YEAH!", + "format-tr": "As bayrakları", "keyboard-name": "at-translated-set-2-keyboard" } ``` +``` +"hyprland/language": { + "format": "{}", + "format-en": "US", + "format-es": "ES", + "tooltip": true, + "tooltip-format": "{long}" +} +``` + +``` +"hyprland/language": { + "format": "{}", + "format-en": "US", + "format-es": "ES", + "tooltip": true, + "tooltip-format": "{}", + "tooltip-format-es": "{Español}", + "tooltip-format-en": "{English (american)}" +} +``` + # STYLE - *#language* From 38cf981f78896888a51d4785af05b75c83015ec2 Mon Sep 17 00:00:00 2001 From: Victor Perez Cano Date: Fri, 25 Oct 2024 18:58:55 +0200 Subject: [PATCH 006/167] fixed clang format for language.cpp --- src/modules/hyprland/language.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index ecb3ea2c..26985fcb 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -10,8 +10,7 @@ namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "language", id, "{}", 0, true), - bar_(bar) { + : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { modulesReady = true; if (!gIPC) { @@ -69,20 +68,19 @@ auto Language::update() -> void { const auto propName = "tooltip-format-" + layout_.short_description; tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); } else { - tooltipContent = trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), - fmt::arg("short", layout_.short_name), - fmt::arg("shortDescription", layout_.short_description), - fmt::arg("variant", layout_.variant))); + tooltipContent = + trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); } - } - else { + } else { // if no tooltip format is provided, use the same text as the module tooltipContent = layoutName; } spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); } - if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); From 2faad27cadb6d86c064debc82fcea90dcaf1d0a2 Mon Sep 17 00:00:00 2001 From: fkobi Date: Mon, 7 Apr 2025 14:45:31 +0000 Subject: [PATCH 007/167] meson_options: improve sndio's description Correct me if I am wrong but I think that is it's only use. If that is the case the description would be less generic and thus better. --- meson_options.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson_options.txt b/meson_options.txt index d83fe01f..42827c02 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -12,7 +12,7 @@ option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable supp option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') option('mpd', type: 'feature', value: 'auto', description: 'Enable support for the Music Player Daemon') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') -option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio') +option('sndio', type: 'feature', value: 'auto', description: 'Enable support for volume control via sndio') option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind') option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('experimental', type : 'boolean', value : false, description: 'Enable experimental features') From ca8f51a39f67a0a5c90be2d12fd24b1110899b76 Mon Sep 17 00:00:00 2001 From: shibjyoti555 Date: Sat, 9 Aug 2025 03:11:21 +0530 Subject: [PATCH 008/167] added a "fallback" option to hyprland/window when no window in focus --- src/modules/hyprland/window.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 815fbad8..13c4a924 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -13,6 +13,8 @@ #include "util/rewrite_string.hpp" #include "util/sanitize_str.hpp" +#include + namespace waybar::modules::hyprland { std::shared_mutex windowIpcSmtx; @@ -54,8 +56,15 @@ auto Window::update() -> void { std::string label_text; if (!format_.empty()) { label_.show(); + + // If the focused window name is empty and fallback is configured, use fallback text + std::string displayTitle = windowName; + if (displayTitle.empty() && config_["fallback"].isString()) { + displayTitle = config_["fallback"].asString(); + } + label_text = waybar::util::rewriteString( - fmt::format(fmt::runtime(format_), fmt::arg("title", windowName), + fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle), fmt::arg("initialTitle", windowData_.initial_title), fmt::arg("class", windowData_.class_name), fmt::arg("initialClass", windowData_.initial_class_name)), From 4a7da0cab2747981bdab15acb0fc3af07c48a6e3 Mon Sep 17 00:00:00 2001 From: Steffen Kothe Date: Fri, 15 Aug 2025 10:55:21 +0000 Subject: [PATCH 009/167] fix: Fail early when neither user nor system is requested The module can be configured to neither request the user nor the system session. In fact does this display non-valuable information to the user and can be considered as abuse case of the configuration. Therefore throw an exception when neither user nor system session are requested. Signed-off-by: Steffen Kothe --- src/modules/systemd_failed_units.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 90f33be7..ac389257 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -47,6 +47,9 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& user_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); } + if (!user_proxy && !system_proxy) + throw std::runtime_error("Neither system nor user status is requested."); + updateData(); /* Always update for the first time. */ dp.emit(); From 3060141a65fad5a79c7f8375c1e363d1e6e34f58 Mon Sep 17 00:00:00 2001 From: Steffen Kothe Date: Fri, 15 Aug 2025 10:58:23 +0000 Subject: [PATCH 010/167] fix: Fix hide_on_ok when user or system session is ignored In case of an user ignoring the failed system or user units, hiding of the waybar module did not work. Ensure that the ignore case of a system or user bus is reflected correctly by determining the configuration. Fixes [1] where regression was introduced in [2]. Links: [1] https://github.com/Alexays/Waybar/issues/4241 [2] https://github.com/Alexays/Waybar/issues/4240 Signed-off-by: Steffen Kothe --- src/modules/systemd_failed_units.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index ac389257..9e3f16f3 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -74,7 +74,7 @@ auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, void SystemdFailedUnits::RequestSystemState() { auto load = [](const char* kind, Glib::RefPtr& proxy) -> std::string { try { - if (!proxy) return "unknown"; + if (!proxy) return "ignored"; auto parameters = Glib::VariantContainerBase( g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState")); Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); @@ -93,7 +93,9 @@ void SystemdFailedUnits::RequestSystemState() { system_state = load("systemwide", system_proxy); user_state = load("user", user_proxy); - if (system_state == "running" && user_state == "running") + + if ((system_state == "running" || system_state == "ignored") && + (user_state == "running" || user_state == "ignored")) overall_state = "ok"; else overall_state = "degraded"; From eee0362367be8a574b2e9fcd9faa7d594248527e Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Wed, 17 Sep 2025 22:09:49 +0000 Subject: [PATCH 011/167] Fix clang build error --- src/modules/clock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index a7d57437..e4c48207 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -375,7 +375,7 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea } } os << Glib::ustring::format( - (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(L' '), + (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(' '), std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), calendarLine); From 41c34b15d6f4bdd77d650c9adc5a59b368eb9b13 Mon Sep 17 00:00:00 2001 From: Aleksi Hannula Date: Fri, 3 Oct 2025 09:15:47 +0300 Subject: [PATCH 012/167] Hide niri/language widget if formatted string is empty --- src/modules/niri/language.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp index 496e5404..64b8c99d 100644 --- a/src/modules/niri/language.cpp +++ b/src/modules/niri/language.cpp @@ -84,7 +84,7 @@ void Language::doUpdate() { spdlog::debug("niri language formatted layout name {}", layoutName); - if (!format_.empty()) { + if (!layoutName.empty()) { label_.show(); label_.set_markup(layoutName); } else { From 5b5b80a03146751174572aaa5a2023ae731fcce3 Mon Sep 17 00:00:00 2001 From: OlivierNeyret Date: Sat, 22 Nov 2025 17:47:03 +0100 Subject: [PATCH 013/167] Fix indentation formatting in power_menu.xml --- resources/custom_modules/power_menu.xml | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/resources/custom_modules/power_menu.xml b/resources/custom_modules/power_menu.xml index aa2a42ca..972499d7 100644 --- a/resources/custom_modules/power_menu.xml +++ b/resources/custom_modules/power_menu.xml @@ -2,27 +2,27 @@ - - Suspend - - - - - Hibernate - - + + Suspend + + - - Shutdown - + + Hibernate + + + + + Shutdown + - - Reboot - + + Reboot + From b277b639991318e1b99d146fa5c9059ae1664bb0 Mon Sep 17 00:00:00 2001 From: Mental-Vortex <206187961+Mental-Vortex@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:31:51 +0800 Subject: [PATCH 014/167] fix: Return when ID doesn't exist, we only care about the interest list. --- src/modules/wireplumber.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index a43ad29b..b30961cb 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -200,6 +200,7 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* return; } } + return; } spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_, From 268e33e8927537498f37b6b4b34c82eab192ef72 Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Tue, 9 Dec 2025 19:13:24 -0500 Subject: [PATCH 015/167] Unique icons for active and named workspaces Allows you to set active icons for named workspaces: "active:" = "" --- src/modules/hyprland/workspace.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index ae66bc84..298ba590 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -161,6 +161,13 @@ std::string &Workspace::selectIcon(std::map &icons_map } } + if (isActive() && isSpecial()) { + auto activeIconIt = icons_map.find("active:" + name()); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; + } + } + if (isActive()) { auto activeIconIt = icons_map.find("active"); if (activeIconIt != icons_map.end()) { From 55e1c4c4a93061ce7ed5ad7b2922e91b89848676 Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Tue, 9 Dec 2025 20:47:04 -0500 Subject: [PATCH 016/167] Adds name of hyprland workspace to button class Allows you to target named hyprland special workspaces\n #workspaces button.special.\ {} --- src/modules/hyprland/workspace.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 298ba590..d84b973b 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -241,6 +241,7 @@ void Workspace::update(const std::string &workspace_icon) { auto styleContext = m_button.get_style_context(); addOrRemoveClass(styleContext, isActive(), "active"); addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isSpecial(), name()); addOrRemoveClass(styleContext, isEmpty(), "empty"); addOrRemoveClass(styleContext, isPersistent(), "persistent"); addOrRemoveClass(styleContext, isUrgent(), "urgent"); From a28fe0a9c437f1c591144e16d1a21b1849ca0da8 Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Tue, 9 Dec 2025 23:45:45 -0500 Subject: [PATCH 017/167] bugfix(hyprland/workspaces): Special workspaces being opened on different monitors now updates correctly Previously, if you were to open a special workspace on a different monitor than it was created, the display for workspaces wouldn't update properly\nI've implemented a somewhat hacky solution by just reloading hyprland everytime onSpecialWorkspaceActivated is called. --- src/modules/hyprland/workspaces.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8360137f..670b1b11 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -366,6 +366,7 @@ void Workspaces::onWorkspaceActivated(std::string const &payload) { void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) { std::string name(begin(payload), begin(payload) + payload.find_first_of(',')); m_activeSpecialWorkspaceName = (!name.starts_with("special:") ? name : name.substr(8)); + m_ipc.getSocket1JsonReply("reload"); } void Workspaces::onWorkspaceDestroyed(std::string const &payload) { From a2f277a0b1efd94fc191cad3e06e60dc4e69ce55 Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Thu, 11 Dec 2025 12:04:10 -0500 Subject: [PATCH 018/167] Active special workspaces fallback on non-active icon before default --- src/modules/hyprland/workspace.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index d84b973b..82247020 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -166,6 +166,10 @@ std::string &Workspace::selectIcon(std::map &icons_map if (activeIconIt != icons_map.end()) { return activeIconIt->second; } + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; + } } if (isActive()) { From 9dfcd206d70c2e3be5785e262c39871aca430f39 Mon Sep 17 00:00:00 2001 From: Hallvard Lavik Date: Sat, 27 Dec 2025 12:11:43 +0100 Subject: [PATCH 019/167] Allow tooltip formatting the CPU module. --- src/modules/cpu.cpp | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 0703eaf7..b0519f8d 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -26,15 +26,17 @@ auto waybar::modules::Cpu::update() -> void { auto [load1, load5, load15] = Load::getLoad(); auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); - if (tooltipEnabled()) { - label_.set_tooltip_text(tooltip); - } + auto format = format_; + std::string tooltip_format; auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; auto state = getState(total_usage); if (!state.empty() && config_["format-" + state].isString()) { format = config_["format-" + state].asString(); } + if (config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } if (format.empty()) { event_box_.hide(); @@ -58,6 +60,38 @@ auto waybar::modules::Cpu::update() -> void { label_.set_markup(fmt::vformat(format, store)); } + if (tooltipEnabled()) { + if (tooltip_format.empty() && config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + tooltip_format = config_["tooltip-format"].asString(); + + auto icons = std::vector{state}; + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("load", load1)); + store.push_back(fmt::arg("usage", total_usage)); + store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); + store.push_back(fmt::arg("max_frequency", max_frequency)); + store.push_back(fmt::arg("min_frequency", min_frequency)); + store.push_back(fmt::arg("avg_frequency", avg_frequency)); + for (size_t i = 1; i < cpu_usage.size(); ++i) { + auto core_i = i - 1; + auto core_format = fmt::format("usage{}", core_i); + store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i])); + auto icon_format = fmt::format("icon{}", core_i); + store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); + } + auto tooltip_text = fmt::vformat(tooltip_format, store); + + if (label_.get_tooltip_text() != tooltip_text) { + label_.set_tooltip_markup(tooltip_text); + } + } else { + label_.set_tooltip_text(tooltip); + } + } + // Call parent update ALabel::update(); } From 0a5da13fbf5fa5e0c30de4d6d42d23c574b4450a Mon Sep 17 00:00:00 2001 From: Mathis Raguin Date: Wed, 31 Dec 2025 14:44:49 +0100 Subject: [PATCH 020/167] fix(workspaces/sway): handle workspaces with quotes --- include/modules/sway/workspaces.hpp | 2 +- src/modules/sway/workspaces.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index d8a9e18a..fcc91ea1 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -23,7 +23,7 @@ class Workspaces : public AModule, public sigc::trackable { auto update() -> void override; private: - static constexpr std::string_view workspace_switch_cmd_ = "workspace {} \"{}\""; + static constexpr std::string_view workspace_switch_cmd_ = "workspace {} {}"; static constexpr std::string_view persistent_workspace_switch_cmd_ = R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 182bf631..cb29b587 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -369,7 +369,7 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { node["name"].asString(), node["target_output"].asString(), "--no-auto-back-and-forth", node["name"].asString())); } else { - ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"", + ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, config_["disable-auto-back-and-forth"].asBool() ? "--no-auto-back-and-forth" : "", From da4367df2f37fd30eaee5f4bc57c7c9179a5917b Mon Sep 17 00:00:00 2001 From: Mathis Raguin Date: Wed, 31 Dec 2025 15:35:38 +0100 Subject: [PATCH 021/167] fix(sway/workspaces): use number when available to avoid parsing issues --- include/modules/sway/workspaces.hpp | 3 ++- src/modules/sway/workspaces.cpp | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index fcc91ea1..f68223be 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -23,7 +23,8 @@ class Workspaces : public AModule, public sigc::trackable { auto update() -> void override; private: - static constexpr std::string_view workspace_switch_cmd_ = "workspace {} {}"; + static constexpr std::string_view workspace_switch_cmd_ = "workspace {} \"{}\""; + static constexpr std::string_view workspace_switch_number_cmd_ = "workspace {} number {}"; static constexpr std::string_view persistent_workspace_switch_cmd_ = R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index cb29b587..4efbeb74 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -369,11 +369,14 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { node["name"].asString(), node["target_output"].asString(), "--no-auto-back-and-forth", node["name"].asString())); } else { - ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, - config_["disable-auto-back-and-forth"].asBool() - ? "--no-auto-back-and-forth" - : "", - node["name"].asString())); + std::string flag = config_["disable-auto-back-and-forth"].asBool() + ? "--no-auto-back-and-forth" + : ""; + if (node["num"].asInt() >= 0) { + ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_number_cmd_, flag, node["num"].asInt())); + } else { + ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, flag, node["name"].asString())); + } } } catch (const std::exception &e) { spdlog::error("Workspaces: {}", e.what()); From c2a038c222d4961b6f5f9ba1d6df9dc5682df45d Mon Sep 17 00:00:00 2001 From: Mathis Raguin Date: Wed, 31 Dec 2025 16:15:54 +0100 Subject: [PATCH 022/167] fix(sway/workspaces): escape markup in workspace if enabled --- src/modules/sway/workspaces.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 182bf631..450f08a9 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -329,7 +329,12 @@ auto Workspaces::update() -> void { } else { button.get_style_context()->remove_class("current_output"); } - std::string output = (*it)["name"].asString(); + std::string output; + if (!config_["disable-markup"].asBool()) { + output = g_markup_escape_text((*it)["name"].asString().c_str(), -1); + } else { + output = (*it)["name"].asString(); + } std::string windows = ""; if (config_["window-rewrite"].isObject()) { updateWindows((*it), windows); From 49083131d18fc0d11e315b7c046fa1cb167149a1 Mon Sep 17 00:00:00 2001 From: Ansari Date: Sat, 3 Jan 2026 11:16:03 +0530 Subject: [PATCH 023/167] fix(AModule): improve cursor configuration handling --- src/AModule.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/AModule.cpp b/src/AModule.cpp index c6fdff3e..f4a9470e 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -72,8 +72,12 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: // Respect user configuration of cursor if (config_.isMember("cursor")) { - if (config_["cursor"].isBool() && config_["cursor"].asBool()) { - setCursor(Gdk::HAND2); + if (config_["cursor"].isBool()) { + if (config_["cursor"].asBool()) { + setCursor(Gdk::HAND2); + } else { + setCursor(Gdk::ARROW); + } } else if (config_["cursor"].isInt()) { setCursor(Gdk::CursorType(config_["cursor"].asInt())); } else { From a8a41bd17cf68ff0ae83910147f7785f82da687d Mon Sep 17 00:00:00 2001 From: Ansari Date: Sat, 3 Jan 2026 11:20:03 +0530 Subject: [PATCH 024/167] fix(workspaces): skip ignored workspaces in persistent rules --- src/modules/hyprland/workspaces.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8360137f..4a2c0e17 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -306,6 +306,10 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &c // 2. the rule's monitor is the current monitor // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { + // => skip ignore-workspaces even if its a persistent + if(isWorkspaceIgnored(workspace)) { + continue; + } // => persistent workspace should be shown on this monitor auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); workspaceData["persistent-rule"] = true; From b192244f50ab34885f011cadec327400c12b07cb Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Fri, 23 Jan 2026 11:27:38 -0500 Subject: [PATCH 025/167] Commit Squash --- src/modules/hyprland/workspace.cpp | 12 ++++++++++++ src/modules/hyprland/workspaces.cpp | 1 + 2 files changed, 13 insertions(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index ae66bc84..82247020 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -161,6 +161,17 @@ std::string &Workspace::selectIcon(std::map &icons_map } } + if (isActive() && isSpecial()) { + auto activeIconIt = icons_map.find("active:" + name()); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; + } + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; + } + } + if (isActive()) { auto activeIconIt = icons_map.find("active"); if (activeIconIt != icons_map.end()) { @@ -234,6 +245,7 @@ void Workspace::update(const std::string &workspace_icon) { auto styleContext = m_button.get_style_context(); addOrRemoveClass(styleContext, isActive(), "active"); addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isSpecial(), name()); addOrRemoveClass(styleContext, isEmpty(), "empty"); addOrRemoveClass(styleContext, isPersistent(), "persistent"); addOrRemoveClass(styleContext, isUrgent(), "urgent"); diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8360137f..670b1b11 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -366,6 +366,7 @@ void Workspaces::onWorkspaceActivated(std::string const &payload) { void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) { std::string name(begin(payload), begin(payload) + payload.find_first_of(',')); m_activeSpecialWorkspaceName = (!name.starts_with("special:") ? name : name.substr(8)); + m_ipc.getSocket1JsonReply("reload"); } void Workspaces::onWorkspaceDestroyed(std::string const &payload) { From e67b230dbe5d0f58546a0b052e2c752f5dc53586 Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Tue, 10 Feb 2026 01:52:59 +0530 Subject: [PATCH 026/167] fix: prioritize plugged status over default when battery is full --- src/modules/battery.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index c706eb3c..bb1bb6fa 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -685,13 +685,20 @@ auto waybar::modules::Battery::update() -> void { if (status == "Unknown") { status = getAdapterStatus(capacity); } + auto state = getState(capacity, true); + bool adapter_online = false; + if (!adapter_.empty()) { + std::ifstream(adapter_ / "online") >> adapter_online; + } + if (status == "Full" && adapter_online) { + status = "Plugged"; + } auto status_pretty = status; puts(status.c_str()); // Transform to lowercase and replace space with dash std::ranges::transform(status.begin(), status.end(), status.begin(), [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); auto format = format_; - auto state = getState(capacity, true); processEvents(state, status, capacity); setBarClass(state); auto time_remaining_formatted = formatTimeRemaining(time_remaining); From 55fa8cf185e64041dfaea09048c46a4479acc359 Mon Sep 17 00:00:00 2001 From: BBaoVanC Date: Mon, 2 Feb 2026 14:03:51 -0600 Subject: [PATCH 027/167] Make CssReloadHelper watch CSS new style on appearance change If you have reload_style_on_change enabled, and use dynamic appearance styling (style-dark.css and style-light.css), then changing the appearance doesn't update the files that are being watched for reload. Add a method to CssReloadHelper to change the CSS file being watched, and also provide the CSS file as a parameter to the callback so setupCss can be called on the right file when the file watcher triggers. --- include/util/css_reload_helper.hpp | 6 ++++-- src/client.cpp | 3 ++- src/util/css_reload_helper.cpp | 11 +++++++++-- test/utils/css_reload_helper.cpp | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/include/util/css_reload_helper.hpp b/include/util/css_reload_helper.hpp index 032b2382..e198a5b0 100644 --- a/include/util/css_reload_helper.hpp +++ b/include/util/css_reload_helper.hpp @@ -14,12 +14,14 @@ struct pollfd; namespace waybar { class CssReloadHelper { public: - CssReloadHelper(std::string cssFile, std::function callback); + CssReloadHelper(std::string cssFile, std::function callback); virtual ~CssReloadHelper() = default; virtual void monitorChanges(); + virtual void changeCssFile(const std::string& newCssFile); + protected: std::vector parseImports(const std::string& cssFile); @@ -42,7 +44,7 @@ class CssReloadHelper { private: std::string m_cssFile; - std::function m_callback; + std::function m_callback; std::vector>> m_fileMonitors; }; diff --git a/src/client.cpp b/src/client.cpp index 103b093f..ce178f0a 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -289,9 +289,10 @@ int waybar::Client::main(int argc, char* argv[]) { } m_cssFile = getStyle(style_opt); setupCss(m_cssFile); - m_cssReloadHelper = std::make_unique(m_cssFile, [&]() { setupCss(m_cssFile); }); + m_cssReloadHelper = std::make_unique(m_cssFile, [&](const std::string& css_file) { setupCss(css_file); }); portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) { auto css_file = getStyle(style_opt, appearance); + m_cssReloadHelper->changeCssFile(css_file); setupCss(css_file); }); diff --git a/src/util/css_reload_helper.cpp b/src/util/css_reload_helper.cpp index 06381d60..dc686a4c 100644 --- a/src/util/css_reload_helper.cpp +++ b/src/util/css_reload_helper.cpp @@ -23,7 +23,8 @@ namespace { const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)"); } -waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, std::function callback) +waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, + std::function callback) : m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {} std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) { @@ -88,6 +89,12 @@ void waybar::CssReloadHelper::monitorChanges() { } } +void waybar::CssReloadHelper::changeCssFile(const std::string& newCssFile) { + m_fileMonitors.clear(); + m_cssFile = newCssFile; + monitorChanges(); +} + void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr const& file, Glib::RefPtr const& other_type, Gio::FileMonitorEvent event_type) { @@ -95,7 +102,7 @@ void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr const& fi // fire for one if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { spdlog::debug("Reloading style, file changed: {}", file->get_path()); - m_callback(); + m_callback(m_cssFile); } } diff --git a/test/utils/css_reload_helper.cpp b/test/utils/css_reload_helper.cpp index f3888a83..0ba0bde4 100644 --- a/test/utils/css_reload_helper.cpp +++ b/test/utils/css_reload_helper.cpp @@ -10,7 +10,7 @@ class CssReloadHelperTest : public waybar::CssReloadHelper { public: - CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this]() { callback(); }) {} + CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this](const std::string&) { callback(); }) {} void callback() { m_callbackCounter++; } From c504b7f4377fee7ca77b175d83032189daedf306 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:00:25 +0000 Subject: [PATCH 028/167] Initial plan From d5297bc42444f22a7266415d9f3042566c101043 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:27:22 +0000 Subject: [PATCH 029/167] fix: resolve PulseAudio/WirePlumber deadlock and freeze issues - Fix AudioBackend destructor: properly lock the PA mainloop before disconnecting the context to prevent race conditions with PA callbacks - Fix context leak on reconnect: call pa_context_unref() when the old context is replaced after PA_CONTEXT_FAILED to avoid resource leaks - Fix PA mainloop killed on reconnect (critical): PA_CONTEXT_TERMINATED was unconditionally calling quit() on the mainloop, even during reconnection when the old context fires TERMINATED after the new one was created. This was killing the new context and preventing successful reconnection, causing Waybar to appear frozen. The fix only quits the mainloop when the terminating context is still the active one. - Fix Wireplumber use-after-free: explicitly disconnect GObject signal handlers for mixer_api_, def_nodes_api_, and om_ before clearing the object references in the destructor to prevent callbacks from firing with a destroyed self pointer. - Fix GVariant memory leak in Wireplumber::handleScroll: unref the GVariant created for the set-volume signal after the emit call. Co-authored-by: Alexays <13947260+Alexays@users.noreply.github.com> --- src/modules/wireplumber.cpp | 10 ++++++++++ src/util/audio_backend.cpp | 25 +++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index e9cb7206..0c0d1dde 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -54,6 +54,15 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val waybar::modules::Wireplumber::~Wireplumber() { waybar::modules::Wireplumber::modules.remove(this); + if (mixer_api_ != nullptr) { + g_signal_handlers_disconnect_by_data(mixer_api_, this); + } + if (def_nodes_api_ != nullptr) { + g_signal_handlers_disconnect_by_data(def_nodes_api_, this); + } + if (om_ != nullptr) { + g_signal_handlers_disconnect_by_data(om_, this); + } wp_core_disconnect(wp_core_); g_clear_pointer(&apis_, g_ptr_array_unref); g_clear_object(&om_); @@ -528,6 +537,7 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { GVariant* variant = g_variant_new_double(newVol); gboolean ret; g_signal_emit_by_name(mixer_api_, "set-volume", node_id_, variant, &ret); + g_variant_unref(variant); } return true; } diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index f61ee945..4087e096 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -40,12 +40,16 @@ AudioBackend::AudioBackend(std::function on_updated_cb, private_construc } AudioBackend::~AudioBackend() { - if (context_ != nullptr) { - pa_context_disconnect(context_); - } - if (mainloop_ != nullptr) { - mainloop_api_->quit(mainloop_api_, 0); + // Lock the mainloop so we can safely disconnect the context. + // This must be done before stopping the thread. + pa_threaded_mainloop_lock(mainloop_); + if (context_ != nullptr) { + pa_context_disconnect(context_); + pa_context_unref(context_); + context_ = nullptr; + } + pa_threaded_mainloop_unlock(mainloop_); pa_threaded_mainloop_stop(mainloop_); pa_threaded_mainloop_free(mainloop_); } @@ -73,7 +77,14 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { auto* backend = static_cast(data); switch (pa_context_get_state(c)) { case PA_CONTEXT_TERMINATED: - backend->mainloop_api_->quit(backend->mainloop_api_, 0); + // Only quit the mainloop if this is still the active context. + // During reconnection, the old context fires TERMINATED after the new one + // has already been created; quitting in that case would kill the new context. + // Note: context_ is only written from PA callbacks (while the mainloop lock is + // held), so this comparison is safe within any PA callback. + if (backend->context_ == nullptr || backend->context_ == c) { + backend->mainloop_api_->quit(backend->mainloop_api_, 0); + } break; case PA_CONTEXT_READY: pa_context_get_server_info(c, serverInfoCb, data); @@ -93,6 +104,8 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { // So there is no need to lock it again. if (backend->context_ != nullptr) { pa_context_disconnect(backend->context_); + pa_context_unref(backend->context_); + backend->context_ = nullptr; } backend->connectContext(); break; From dbbad059f7384fdbe88c570014b96f27ab284468 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 13:39:23 -0600 Subject: [PATCH 030/167] fix(sleeper-thread): stop and join before worker reassignment Reassigning SleeperThread could replace a joinable std::thread and trigger std::terminate. I now stop and join any existing worker before reassignment, then reset control state before starting the replacement worker. Signed-off-by: Austin Horstman --- include/util/sleeper_thread.hpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 62d12931..183083cf 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -42,6 +42,15 @@ class SleeperThread { } SleeperThread& operator=(std::function func) { + if (thread_.joinable()) { + stop(); + thread_.join(); + } + { + std::lock_guard lck(mutex_); + do_run_ = true; + signal_ = false; + } thread_ = std::thread([this, func] { while (do_run_) { signal_ = false; @@ -92,7 +101,7 @@ class SleeperThread { condvar_.notify_all(); } - auto stop() { + void stop() { { std::lock_guard lck(mutex_); signal_ = true; From 1c61ecf864ae637f4ed649dd8e61e92c1f6e9095 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 13:39:26 -0600 Subject: [PATCH 031/167] test(utils): add SleeperThread reassignment regression We needed a regression test for reassignment safety after lifecycle fixes. I added a subprocess test that reassigns SleeperThread workers and verifies the process exits normally instead of terminating. Signed-off-by: Austin Horstman --- test/utils/meson.build | 1 + test/utils/sleeper_thread.cpp | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 test/utils/sleeper_thread.cpp diff --git a/test/utils/meson.build b/test/utils/meson.build index b7b3665a..050af262 100644 --- a/test/utils/meson.build +++ b/test/utils/meson.build @@ -13,6 +13,7 @@ test_src = files( '../../src/config.cpp', 'JsonParser.cpp', 'SafeSignal.cpp', + 'sleeper_thread.cpp', 'css_reload_helper.cpp', '../../src/util/css_reload_helper.cpp', ) diff --git a/test/utils/sleeper_thread.cpp b/test/utils/sleeper_thread.cpp new file mode 100644 index 00000000..2556a106 --- /dev/null +++ b/test/utils/sleeper_thread.cpp @@ -0,0 +1,42 @@ +#if __has_include() +#include +#else +#include +#endif + +#include +#include +#include +#include + +#include "util/sleeper_thread.hpp" + +namespace waybar::util { +SafeSignal& prepare_for_sleep() { + static SafeSignal signal; + return signal; +} +} // namespace waybar::util + +namespace { +int run_reassignment_regression() { + waybar::util::SleeperThread thread; + thread = [] { std::this_thread::sleep_for(std::chrono::milliseconds(10)); }; + thread = [] { std::this_thread::sleep_for(std::chrono::milliseconds(1)); }; + return 0; +} +} // namespace + +TEST_CASE("SleeperThread reassignment does not terminate process", "[util][sleeper_thread]") { + const auto pid = fork(); + REQUIRE(pid >= 0); + + if (pid == 0) { + _exit(run_reassignment_regression()); + } + + int status = -1; + REQUIRE(waitpid(pid, &status, 0) == pid); + REQUIRE(WIFEXITED(status)); + REQUIRE(WEXITSTATUS(status) == 0); +} From 44eed7afea7ab251a8aee554a360dbb6fbed6c6d Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 13:44:26 -0600 Subject: [PATCH 032/167] fix(sleeper-thread): synchronize control flags with atomics SleeperThread control flags were shared across threads without consistent synchronization. I converted the run/signal flags to atomics and updated wait predicates and lifecycle transitions to use explicit atomic loads/stores. Signed-off-by: Austin Horstman --- include/util/sleeper_thread.hpp | 40 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 183083cf..966772a2 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -31,8 +32,8 @@ class SleeperThread { SleeperThread(std::function func) : thread_{[this, func] { - while (do_run_) { - signal_ = false; + while (do_run_.load(std::memory_order_relaxed)) { + signal_.store(false, std::memory_order_relaxed); func(); } }} { @@ -48,12 +49,12 @@ class SleeperThread { } { std::lock_guard lck(mutex_); - do_run_ = true; - signal_ = false; + do_run_.store(true, std::memory_order_relaxed); + signal_.store(false, std::memory_order_relaxed); } thread_ = std::thread([this, func] { - while (do_run_) { - signal_ = false; + while (do_run_.load(std::memory_order_relaxed)) { + signal_.store(false, std::memory_order_relaxed); func(); } }); @@ -65,12 +66,15 @@ class SleeperThread { return *this; } - bool isRunning() const { return do_run_; } + bool isRunning() const { return do_run_.load(std::memory_order_relaxed); } auto sleep() { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; - return condvar_.wait(lk, [this] { return signal_ || !do_run_; }); + return condvar_.wait(lk, [this] { + return signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); } auto sleep_for(std::chrono::system_clock::duration dur) { @@ -82,7 +86,10 @@ class SleeperThread { if (now < max_time_point - dur) { wait_end = now + dur; } - return condvar_.wait_until(lk, wait_end, [this] { return signal_ || !do_run_; }); + return condvar_.wait_until(lk, wait_end, [this] { + return signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); } auto sleep_until( @@ -90,13 +97,16 @@ class SleeperThread { time_point) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; - return condvar_.wait_until(lk, time_point, [this] { return signal_ || !do_run_; }); + return condvar_.wait_until(lk, time_point, [this] { + return signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); } void wake_up() { { std::lock_guard lck(mutex_); - signal_ = true; + signal_.store(true, std::memory_order_relaxed); } condvar_.notify_all(); } @@ -104,8 +114,8 @@ class SleeperThread { void stop() { { std::lock_guard lck(mutex_); - signal_ = true; - do_run_ = false; + signal_.store(true, std::memory_order_relaxed); + do_run_.store(false, std::memory_order_relaxed); } condvar_.notify_all(); auto handle = thread_.native_handle(); @@ -127,8 +137,8 @@ class SleeperThread { std::thread thread_; std::condition_variable condvar_; std::mutex mutex_; - bool do_run_ = true; - bool signal_ = false; + std::atomic do_run_ = true; + std::atomic signal_ = false; sigc::connection connection_; }; From 864523772d427fea72f99a6fe9dcc100dc708acc Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 13:44:28 -0600 Subject: [PATCH 033/167] test(utils): stress SleeperThread wake and stop control flow SleeperThread concurrency paths needed stress coverage around wake/stop races. I added a subprocess stress test that repeatedly interleaves wake_up() and stop() and verifies the worker exits cleanly. Signed-off-by: Austin Horstman --- test/utils/sleeper_thread.cpp | 57 +++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/test/utils/sleeper_thread.cpp b/test/utils/sleeper_thread.cpp index 2556a106..7a9a3ca1 100644 --- a/test/utils/sleeper_thread.cpp +++ b/test/utils/sleeper_thread.cpp @@ -19,24 +19,61 @@ SafeSignal& prepare_for_sleep() { } // namespace waybar::util namespace { +int run_in_subprocess(int (*task)()) { + const auto pid = fork(); + if (pid < 0) { + return -1; + } + if (pid == 0) { + alarm(5); + _exit(task()); + } + + int status = -1; + if (waitpid(pid, &status, 0) != pid) { + return -1; + } + if (!WIFEXITED(status)) { + return -1; + } + return WEXITSTATUS(status); +} + int run_reassignment_regression() { waybar::util::SleeperThread thread; thread = [] { std::this_thread::sleep_for(std::chrono::milliseconds(10)); }; thread = [] { std::this_thread::sleep_for(std::chrono::milliseconds(1)); }; return 0; } + +int run_control_flag_stress() { + for (int i = 0; i < 200; ++i) { + waybar::util::SleeperThread thread; + thread = [&thread] { thread.sleep_for(std::chrono::milliseconds(1)); }; + + std::thread waker([&thread] { + for (int j = 0; j < 100; ++j) { + thread.wake_up(); + std::this_thread::yield(); + } + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(2)); + thread.stop(); + waker.join(); + if (thread.isRunning()) { + return 1; + } + } + return 0; +} } // namespace TEST_CASE("SleeperThread reassignment does not terminate process", "[util][sleeper_thread]") { - const auto pid = fork(); - REQUIRE(pid >= 0); + REQUIRE(run_in_subprocess(run_reassignment_regression) == 0); +} - if (pid == 0) { - _exit(run_reassignment_regression()); - } - - int status = -1; - REQUIRE(waitpid(pid, &status, 0) == pid); - REQUIRE(WIFEXITED(status)); - REQUIRE(WEXITSTATUS(status) == 0); +TEST_CASE("SleeperThread control flags are stable under concurrent wake and stop", + "[util][sleeper_thread]") { + REQUIRE(run_in_subprocess(run_control_flag_stress) == 0); } From e4ff024fa87af9ad63b6a9fc9da8f0beb5afa7c6 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 14:06:27 -0600 Subject: [PATCH 034/167] fix(util): bound SafeSignal queue growth under burst load SafeSignal could queue events forever when worker threads emitted faster than the main loop could consume, which risks memory growth and stale updates. I added a queue cap with a drop-oldest policy so growth stays bounded under burst load, plus a regression test that validates bounded delivery. Signed-off-by: Austin Horstman --- include/util/SafeSignal.hpp | 20 ++++++++++++++++++++ test/utils/SafeSignal.cpp | 31 +++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp index 340f74ee..1e901792 100644 --- a/include/util/SafeSignal.hpp +++ b/include/util/SafeSignal.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,12 @@ struct SafeSignal : sigc::signal...)> { public: SafeSignal() { dp_.connect(sigc::mem_fun(*this, &SafeSignal::handle_event)); } + void set_max_queued_events(std::size_t max_queued_events) { + std::unique_lock lock(mutex_); + max_queued_events_ = max_queued_events; + trim_queue_locked(); + } + template void emit(EmitArgs&&... args) { if (main_tid_ == std::this_thread::get_id()) { @@ -41,6 +48,9 @@ struct SafeSignal : sigc::signal...)> { } else { { std::unique_lock lock(mutex_); + if (max_queued_events_ != 0 && queue_.size() >= max_queued_events_) { + queue_.pop(); + } queue_.emplace(std::forward(args)...); } dp_.emit(); @@ -60,6 +70,15 @@ struct SafeSignal : sigc::signal...)> { using signal_t::emit_reverse; using signal_t::make_slot; + void trim_queue_locked() { + if (max_queued_events_ == 0) { + return; + } + while (queue_.size() > max_queued_events_) { + queue_.pop(); + } + } + void handle_event() { for (std::unique_lock lock(mutex_); !queue_.empty(); lock.lock()) { auto args = queue_.front(); @@ -72,6 +91,7 @@ struct SafeSignal : sigc::signal...)> { Glib::Dispatcher dp_; std::mutex mutex_; std::queue queue_; + std::size_t max_queued_events_ = 4096; const std::thread::id main_tid_ = std::this_thread::get_id(); // cache functor for signal emission to avoid recreating it on each event const slot_t cached_fn_ = make_slot(); diff --git a/test/utils/SafeSignal.cpp b/test/utils/SafeSignal.cpp index e7e096b0..1502b7d9 100644 --- a/test/utils/SafeSignal.cpp +++ b/test/utils/SafeSignal.cpp @@ -9,6 +9,7 @@ #endif #include #include +#include #include "fixtures/GlibTestsFixture.hpp" @@ -141,3 +142,33 @@ TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal copy/move counter", "[signal][thr producer.join(); REQUIRE(count == NUM_EVENTS); } + +TEST_CASE_METHOD(GlibTestsFixture, "SafeSignal queue stays bounded under burst load", + "[signal][thread][util][perf]") { + constexpr int NUM_EVENTS = 200; + constexpr std::size_t MAX_QUEUED_EVENTS = 8; + std::vector received; + + SafeSignal test_signal; + test_signal.set_max_queued_events(MAX_QUEUED_EVENTS); + + setTimeout(500); + + test_signal.connect([&](auto value) { received.push_back(value); }); + + run([&]() { + std::thread producer([&]() { + for (int i = 1; i <= NUM_EVENTS; ++i) { + test_signal.emit(i); + } + }); + producer.join(); + + Glib::signal_timeout().connect_once([this]() { this->quit(); }, 50); + }); + + REQUIRE(received.size() <= MAX_QUEUED_EVENTS); + REQUIRE_FALSE(received.empty()); + REQUIRE(received.back() == NUM_EVENTS); + REQUIRE(received.front() == NUM_EVENTS - static_cast(received.size()) + 1); +} From d0363313b879c7c7fb1a3cf36a10c943ff2b9c5a Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 13:48:28 -0600 Subject: [PATCH 035/167] fix(hyprland-ipc): harden fd lifecycle and listener loop Hyprland IPC had fd lifecycle risks on failure/shutdown paths and used a spin-sleep listener model. I initialized fd state defensively, tightened connect/close/shutdown handling, moved to blocking read with newline framing, and added RAII-style fd cleanup in socket1 reply paths. Signed-off-by: Austin Horstman --- include/modules/hyprland/backend.hpp | 8 +- src/modules/hyprland/backend.cpp | 139 ++++++++++++++++++--------- 2 files changed, 101 insertions(+), 46 deletions(-) diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index 2e0ef657..a6ebd191 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -43,10 +44,11 @@ class IPC { std::thread ipcThread_; std::mutex callbackMutex_; + std::mutex socketMutex_; util::JsonParser parser_; std::list> callbacks_; - int socketfd_; // the hyprland socket file descriptor - pid_t socketOwnerPid_; - bool running_ = true; // the ipcThread will stop running when this is false + int socketfd_ = -1; // the hyprland socket file descriptor + pid_t socketOwnerPid_ = -1; + std::atomic running_ = true; // the ipcThread will stop running when this is false }; }; // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 7060d304..4022d408 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -9,11 +9,35 @@ #include #include +#include +#include +#include #include #include namespace waybar::modules::hyprland { +namespace { +class ScopedFd { + public: + explicit ScopedFd(int fd) : fd_(fd) {} + ~ScopedFd() { + if (fd_ != -1) { + close(fd_); + } + } + + // ScopedFd is non-copyable + ScopedFd(const ScopedFd&) = delete; + ScopedFd& operator=(const ScopedFd&) = delete; + + int get() const { return fd_; } + + private: + int fd_; +}; +} // namespace + std::filesystem::path IPC::socketFolder_; std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { @@ -45,8 +69,8 @@ std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { IPC::IPC() { // will start IPC and relay events to parseIPC - ipcThread_ = std::thread([this]() { socketListener(); }); socketOwnerPid_ = getpid(); + ipcThread_ = std::thread([this]() { socketListener(); }); } IPC::~IPC() { @@ -54,19 +78,20 @@ IPC::~IPC() { // failed exec()) exits. if (getpid() != socketOwnerPid_) return; - running_ = false; + running_.store(false, std::memory_order_relaxed); spdlog::info("Hyprland IPC stopping..."); - if (socketfd_ != -1) { - spdlog::trace("Shutting down socket"); - if (shutdown(socketfd_, SHUT_RDWR) == -1) { - spdlog::error("Hyprland IPC: Couldn't shutdown socket"); - } - spdlog::trace("Closing socket"); - if (close(socketfd_) == -1) { - spdlog::error("Hyprland IPC: Couldn't close socket"); + { + std::lock_guard lock(socketMutex_); + if (socketfd_ != -1) { + spdlog::trace("Shutting down socket"); + if (shutdown(socketfd_, SHUT_RDWR) == -1 && errno != ENOTCONN) { + spdlog::error("Hyprland IPC: Couldn't shutdown socket"); + } } } - ipcThread_.join(); + if (ipcThread_.joinable()) { + ipcThread_.join(); + } } IPC& IPC::inst() { @@ -86,9 +111,9 @@ void IPC::socketListener() { spdlog::info("Hyprland IPC starting"); struct sockaddr_un addr; - socketfd_ = socket(AF_UNIX, SOCK_STREAM, 0); + const int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); - if (socketfd_ == -1) { + if (socketfd == -1) { spdlog::error("Hyprland IPC: socketfd failed"); return; } @@ -102,38 +127,67 @@ void IPC::socketListener() { int l = sizeof(struct sockaddr_un); - if (connect(socketfd_, (struct sockaddr*)&addr, l) == -1) { - spdlog::error("Hyprland IPC: Unable to connect?"); + if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { + spdlog::error("Hyprland IPC: Unable to connect? {}", std::strerror(errno)); + close(socketfd); return; } - auto* file = fdopen(socketfd_, "r"); - if (file == nullptr) { - spdlog::error("Hyprland IPC: Couldn't open file descriptor"); - return; + + { + std::lock_guard lock(socketMutex_); + socketfd_ = socketfd; } - while (running_) { + + std::string pending; + while (running_.load(std::memory_order_relaxed)) { std::array buffer; // Hyprland socket2 events are max 1024 bytes + const ssize_t bytes_read = read(socketfd, buffer.data(), buffer.size()); - auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file); - - if (receivedCharPtr == nullptr) { - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - continue; + if (bytes_read == 0) { + if (running_.load(std::memory_order_relaxed)) { + spdlog::warn("Hyprland IPC: Socket closed by peer"); + } + break; } - std::string messageReceived(buffer.data()); - messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n')); - spdlog::debug("hyprland IPC received {}", messageReceived); - - try { - parseIPC(messageReceived); - } catch (std::exception& e) { - spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what()); - } catch (...) { - throw; + if (bytes_read < 0) { + if (errno == EINTR) { + continue; + } + if (!running_.load(std::memory_order_relaxed)) { + break; + } + spdlog::error("Hyprland IPC: read failed: {}", std::strerror(errno)); + break; } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + pending.append(buffer.data(), static_cast(bytes_read)); + for (auto newline_pos = pending.find('\n'); newline_pos != std::string::npos; + newline_pos = pending.find('\n')) { + std::string messageReceived = pending.substr(0, newline_pos); + pending.erase(0, newline_pos + 1); + if (messageReceived.empty()) { + continue; + } + spdlog::debug("hyprland IPC received {}", messageReceived); + + try { + parseIPC(messageReceived); + } catch (std::exception& e) { + spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what()); + } catch (...) { + throw; + } + } + } + { + std::lock_guard lock(socketMutex_); + if (socketfd_ != -1) { + if (close(socketfd_) == -1) { + spdlog::error("Hyprland IPC: Couldn't close socket"); + } + socketfd_ = -1; + } } spdlog::debug("Hyprland IPC stopped"); } @@ -178,9 +232,9 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) { std::string IPC::getSocket1Reply(const std::string& rq) { // basically hyprctl - const auto serverSocket = socket(AF_UNIX, SOCK_STREAM, 0); + ScopedFd serverSocket(socket(AF_UNIX, SOCK_STREAM, 0)); - if (serverSocket < 0) { + if (serverSocket.get() < 0) { throw std::runtime_error("Hyprland IPC: Couldn't open a socket (1)"); } @@ -203,12 +257,13 @@ std::string IPC::getSocket1Reply(const std::string& rq) { throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)"); } - if (connect(serverSocket, reinterpret_cast(&serverAddress), sizeof(serverAddress)) < + if (connect(serverSocket.get(), reinterpret_cast(&serverAddress), + sizeof(serverAddress)) < 0) { throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); } - auto sizeWritten = write(serverSocket, rq.c_str(), rq.length()); + auto sizeWritten = write(serverSocket.get(), rq.c_str(), rq.length()); if (sizeWritten < 0) { spdlog::error("Hyprland IPC: Couldn't write (4)"); @@ -219,17 +274,15 @@ std::string IPC::getSocket1Reply(const std::string& rq) { std::string response; do { - sizeWritten = read(serverSocket, buffer.data(), 8192); + sizeWritten = read(serverSocket.get(), buffer.data(), 8192); if (sizeWritten < 0) { spdlog::error("Hyprland IPC: Couldn't read (5)"); - close(serverSocket); return ""; } response.append(buffer.data(), sizeWritten); } while (sizeWritten > 0); - close(serverSocket); return response; } From 87a5b7ed0fd683bc565e89ece3f36412dbd0f648 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 13:48:30 -0600 Subject: [PATCH 036/167] test(hyprland): add failure-path fd-leak coverage Hyprland tests did not explicitly verify descriptor behavior on key failure paths. I added focused tests for missing instance signature and connect-failure paths that assert file descriptor counts stay stable across repeated attempts. Signed-off-by: Austin Horstman --- test/hyprland/backend.cpp | 80 +++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp index b83b839c..cc7295ec 100644 --- a/test/hyprland/backend.cpp +++ b/test/hyprland/backend.cpp @@ -4,56 +4,114 @@ #include #endif -#include "fixtures/IPCTestFixture.hpp" +#include + +#include "modules/hyprland/backend.hpp" namespace fs = std::filesystem; namespace hyprland = waybar::modules::hyprland; -TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirExists", "[getSocketFolder]") { +namespace { +class IPCTestHelper : public hyprland::IPC { + public: + static void resetSocketFolder() { socketFolder_.clear(); } +}; + +std::size_t countOpenFds() { +#if defined(__linux__) + std::size_t count = 0; + for (const auto& _ : fs::directory_iterator("/proc/self/fd")) { + (void)_; + ++count; + } + return count; +#else + return 0; +#endif +} +} // namespace + +TEST_CASE("XDGRuntimeDirExists", "[getSocketFolder]") { // Test case: XDG_RUNTIME_DIR exists and contains "hypr" directory // Arrange - tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + constexpr auto instanceSig = "instance_sig"; + const fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + std::error_code ec; + fs::remove_all(tempDir, ec); fs::path expectedPath = tempDir / "hypr" / instanceSig; - fs::create_directories(tempDir / "hypr" / instanceSig); + fs::create_directories(expectedPath); setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); + IPCTestHelper::resetSocketFolder(); // Act - fs::path actualPath = getSocketFolder(instanceSig); + fs::path actualPath = hyprland::IPC::getSocketFolder(instanceSig); // Assert expected result REQUIRE(actualPath == expectedPath); + fs::remove_all(tempDir, ec); } -TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirDoesNotExist", "[getSocketFolder]") { +TEST_CASE("XDGRuntimeDirDoesNotExist", "[getSocketFolder]") { // Test case: XDG_RUNTIME_DIR does not exist // Arrange + constexpr auto instanceSig = "instance_sig"; unsetenv("XDG_RUNTIME_DIR"); fs::path expectedPath = fs::path("/tmp") / "hypr" / instanceSig; + IPCTestHelper::resetSocketFolder(); // Act - fs::path actualPath = getSocketFolder(instanceSig); + fs::path actualPath = hyprland::IPC::getSocketFolder(instanceSig); // Assert expected result REQUIRE(actualPath == expectedPath); } -TEST_CASE_METHOD(IPCTestFixture, "XDGRuntimeDirExistsNoHyprDir", "[getSocketFolder]") { +TEST_CASE("XDGRuntimeDirExistsNoHyprDir", "[getSocketFolder]") { // Test case: XDG_RUNTIME_DIR exists but does not contain "hypr" directory // Arrange + constexpr auto instanceSig = "instance_sig"; fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + std::error_code ec; + fs::remove_all(tempDir, ec); fs::create_directories(tempDir); setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); fs::path expectedPath = fs::path("/tmp") / "hypr" / instanceSig; + IPCTestHelper::resetSocketFolder(); // Act - fs::path actualPath = getSocketFolder(instanceSig); + fs::path actualPath = hyprland::IPC::getSocketFolder(instanceSig); // Assert expected result REQUIRE(actualPath == expectedPath); + fs::remove_all(tempDir, ec); } -TEST_CASE_METHOD(IPCTestFixture, "getSocket1Reply throws on no socket", "[getSocket1Reply]") { +TEST_CASE("getSocket1Reply throws on no socket", "[getSocket1Reply]") { + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + IPCTestHelper::resetSocketFolder(); std::string request = "test_request"; - CHECK_THROWS(getSocket1Reply(request)); + CHECK_THROWS(hyprland::IPC::getSocket1Reply(request)); } + +#if defined(__linux__) +TEST_CASE("getSocket1Reply failure paths do not leak fds", "[getSocket1Reply][fd-leak]") { + const auto baseline = countOpenFds(); + + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + for (int i = 0; i < 16; ++i) { + IPCTestHelper::resetSocketFolder(); + CHECK_THROWS(hyprland::IPC::getSocket1Reply("test_request")); + } + const auto after_missing_signature = countOpenFds(); + REQUIRE(after_missing_signature == baseline); + + setenv("HYPRLAND_INSTANCE_SIGNATURE", "definitely-not-running", 1); + for (int i = 0; i < 16; ++i) { + IPCTestHelper::resetSocketFolder(); + CHECK_THROWS(hyprland::IPC::getSocket1Reply("test_request")); + } + const auto after_connect_failures = countOpenFds(); + REQUIRE(after_connect_failures == baseline); +} +#endif From 04ddc5fd23cfad281f725eda3160f9c5ef4a0ee5 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 14:01:45 -0600 Subject: [PATCH 037/167] chore(test-hyprland): remove unused IPC test fixture The hyprland IPC fixture was no longer used by the current test setup. I removed the dead fixture so the test code reflects the actual execution path and is easier to maintain. Signed-off-by: Austin Horstman --- test/hyprland/fixtures/IPCTestFixture.hpp | 25 ----------------------- 1 file changed, 25 deletions(-) delete mode 100644 test/hyprland/fixtures/IPCTestFixture.hpp diff --git a/test/hyprland/fixtures/IPCTestFixture.hpp b/test/hyprland/fixtures/IPCTestFixture.hpp deleted file mode 100644 index caa92975..00000000 --- a/test/hyprland/fixtures/IPCTestFixture.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "modules/hyprland/backend.hpp" - -namespace fs = std::filesystem; -namespace hyprland = waybar::modules::hyprland; - -class IPCTestFixture : public hyprland::IPC { - public: - IPCTestFixture() : IPC() { IPC::socketFolder_ = ""; } - ~IPCTestFixture() { fs::remove_all(tempDir); } - - protected: - const char* instanceSig = "instance_sig"; - fs::path tempDir = fs::temp_directory_path() / "hypr_test"; - - private: -}; - -class IPCMock : public IPCTestFixture { - public: - // Mock getSocket1Reply to return an empty string - static std::string getSocket1Reply(const std::string& rq) { return ""; } - - protected: - const char* instanceSig = "instance_sig"; -}; From 6dfe1c31111602fe2f92ca81e0907ec1f5903639 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 08:21:12 -0600 Subject: [PATCH 038/167] feat(util): add ScopedFd RAII utility for file descriptors Signed-off-by: Austin Horstman --- include/util/scoped_fd.hpp | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 include/util/scoped_fd.hpp diff --git a/include/util/scoped_fd.hpp b/include/util/scoped_fd.hpp new file mode 100644 index 00000000..e970109e --- /dev/null +++ b/include/util/scoped_fd.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +namespace waybar::util { + +class ScopedFd { + public: + explicit ScopedFd(int fd = -1) : fd_(fd) {} + ~ScopedFd() { + if (fd_ != -1) { + close(fd_); + } + } + + // ScopedFd is non-copyable + ScopedFd(const ScopedFd&) = delete; + ScopedFd& operator=(const ScopedFd&) = delete; + + // ScopedFd is moveable + ScopedFd(ScopedFd&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; } + ScopedFd& operator=(ScopedFd&& other) noexcept { + if (this != &other) { + if (fd_ != -1) { + close(fd_); + } + fd_ = other.fd_; + other.fd_ = -1; + } + return *this; + } + + int get() const { return fd_; } + + operator int() const { return fd_; } + + void reset(int fd = -1) { + if (fd_ != -1) { + close(fd_); + } + fd_ = fd; + } + + int release() { + int fd = fd_; + fd_ = -1; + return fd; + } + + private: + int fd_; +}; + +} // namespace waybar::util From e83ab7609c7e6f5eb3eb038c905ba37ea1ed22b8 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 08:21:15 -0600 Subject: [PATCH 039/167] refactor(hyprland): use shared ScopedFd utility Signed-off-by: Austin Horstman --- src/modules/hyprland/backend.cpp | 34 +++++++------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 4022d408..0f02b919 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -15,29 +15,10 @@ #include #include +#include "util/scoped_fd.hpp" + namespace waybar::modules::hyprland { -namespace { -class ScopedFd { - public: - explicit ScopedFd(int fd) : fd_(fd) {} - ~ScopedFd() { - if (fd_ != -1) { - close(fd_); - } - } - - // ScopedFd is non-copyable - ScopedFd(const ScopedFd&) = delete; - ScopedFd& operator=(const ScopedFd&) = delete; - - int get() const { return fd_; } - - private: - int fd_; -}; -} // namespace - std::filesystem::path IPC::socketFolder_; std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { @@ -232,9 +213,9 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) { std::string IPC::getSocket1Reply(const std::string& rq) { // basically hyprctl - ScopedFd serverSocket(socket(AF_UNIX, SOCK_STREAM, 0)); + util::ScopedFd serverSocket(socket(AF_UNIX, SOCK_STREAM, 0)); - if (serverSocket.get() < 0) { + if (serverSocket < 0) { throw std::runtime_error("Hyprland IPC: Couldn't open a socket (1)"); } @@ -257,13 +238,12 @@ std::string IPC::getSocket1Reply(const std::string& rq) { throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)"); } - if (connect(serverSocket.get(), reinterpret_cast(&serverAddress), - sizeof(serverAddress)) < + if (connect(serverSocket, reinterpret_cast(&serverAddress), sizeof(serverAddress)) < 0) { throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); } - auto sizeWritten = write(serverSocket.get(), rq.c_str(), rq.length()); + auto sizeWritten = write(serverSocket, rq.c_str(), rq.length()); if (sizeWritten < 0) { spdlog::error("Hyprland IPC: Couldn't write (4)"); @@ -274,7 +254,7 @@ std::string IPC::getSocket1Reply(const std::string& rq) { std::string response; do { - sizeWritten = read(serverSocket.get(), buffer.data(), 8192); + sizeWritten = read(serverSocket, buffer.data(), 8192); if (sizeWritten < 0) { spdlog::error("Hyprland IPC: Couldn't read (5)"); From 2ff77fb73dd2e14d399c22fa77242aea1fbf8a8f Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 08:21:17 -0600 Subject: [PATCH 040/167] refactor(niri): use shared ScopedFd utility Signed-off-by: Austin Horstman --- src/modules/niri/backend.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 68bb1724..c23aaa47 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -13,6 +13,7 @@ #include #include +#include "util/scoped_fd.hpp" #include "giomm/datainputstream.h" #include "giomm/dataoutputstream.h" #include "giomm/unixinputstream.h" @@ -30,7 +31,7 @@ int IPC::connectToSocket() { } struct sockaddr_un addr; - int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); + util::ScopedFd socketfd(socket(AF_UNIX, SOCK_STREAM, 0)); if (socketfd == -1) { throw std::runtime_error("socketfd failed"); @@ -45,11 +46,10 @@ int IPC::connectToSocket() { int l = sizeof(struct sockaddr_un); if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { - close(socketfd); throw std::runtime_error("unable to connect"); } - return socketfd; + return socketfd.release(); } void IPC::startIPC() { @@ -235,7 +235,7 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) { } Json::Value IPC::send(const Json::Value& request) { - int socketfd = connectToSocket(); + util::ScopedFd socketfd(connectToSocket()); auto unix_istream = Gio::UnixInputStream::create(socketfd, true); auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false); From 39e09118f9943c62000542cd35af735b5f9826a1 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 08:21:20 -0600 Subject: [PATCH 041/167] refactor(wayfire): replace custom Sock with shared ScopedFd Signed-off-by: Austin Horstman --- include/modules/wayfire/backend.hpp | 20 +++----------------- src/modules/wayfire/backend.cpp | 11 +++++------ 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/include/modules/wayfire/backend.hpp b/include/modules/wayfire/backend.hpp index d3173269..e1f259cc 100644 --- a/include/modules/wayfire/backend.hpp +++ b/include/modules/wayfire/backend.hpp @@ -12,6 +12,8 @@ #include #include +#include "util/scoped_fd.hpp" + namespace waybar::modules::wayfire { using EventHandler = std::function; @@ -71,23 +73,7 @@ struct State { auto update_view(const Json::Value& view) -> void; }; -struct Sock { - int fd; - - Sock(int fd) : fd{fd} {} - ~Sock() { close(fd); } - Sock(const Sock&) = delete; - auto operator=(const Sock&) = delete; - Sock(Sock&& rhs) noexcept { - fd = rhs.fd; - rhs.fd = -1; - } - auto& operator=(Sock&& rhs) noexcept { - fd = rhs.fd; - rhs.fd = -1; - return *this; - } -}; +using Sock = util::ScopedFd; class IPC : public std::enable_shared_from_this { static std::weak_ptr instance; diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 545aaa89..42976d20 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -27,14 +27,14 @@ inline auto byteswap(uint32_t x) -> uint32_t { auto pack_and_write(Sock& sock, std::string&& buf) -> void { uint32_t len = buf.size(); if constexpr (std::endian::native != std::endian::little) len = byteswap(len); - (void)write(sock.fd, &len, 4); - (void)write(sock.fd, buf.data(), buf.size()); + (void)write(sock, &len, 4); + (void)write(sock, buf.data(), buf.size()); } auto read_exact(Sock& sock, size_t n) -> std::string { auto buf = std::string(n, 0); for (size_t i = 0; i < n;) { - auto r = read(sock.fd, &buf[i], n - i); + auto r = read(sock, &buf[i], n - i); if (r <= 0) { throw std::runtime_error("Wayfire IPC: read failed"); } @@ -111,7 +111,7 @@ auto IPC::connect() -> Sock { throw std::runtime_error{"Wayfire IPC: ipc not available"}; } - auto sock = socket(AF_UNIX, SOCK_STREAM, 0); + util::ScopedFd sock(socket(AF_UNIX, SOCK_STREAM, 0)); if (sock == -1) { throw std::runtime_error{"Wayfire IPC: socket() failed"}; } @@ -121,11 +121,10 @@ auto IPC::connect() -> Sock { addr.sun_path[sizeof(addr.sun_path) - 1] = 0; if (::connect(sock, (const sockaddr*)&addr, sizeof(addr)) == -1) { - close(sock); throw std::runtime_error{"Wayfire IPC: connect() failed"}; } - return {sock}; + return sock; } auto IPC::receive(Sock& sock) -> Json::Value { From 8d22d3e07a629470114edcebabe6d6fb6afe85cf Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 08:23:06 -0600 Subject: [PATCH 042/167] refactor(sway): use shared ScopedFd for IPC sockets Signed-off-by: Austin Horstman --- include/modules/sway/ipc/client.hpp | 5 +++-- src/modules/sway/ipc/client.cpp | 12 ++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/include/modules/sway/ipc/client.hpp b/include/modules/sway/ipc/client.hpp index 281df7ab..eb0f32f9 100644 --- a/include/modules/sway/ipc/client.hpp +++ b/include/modules/sway/ipc/client.hpp @@ -14,6 +14,7 @@ #include "ipc.hpp" #include "util/SafeSignal.hpp" #include "util/sleeper_thread.hpp" +#include "util/scoped_fd.hpp" namespace waybar::modules::sway { @@ -45,8 +46,8 @@ class Ipc { struct ipc_response send(int fd, uint32_t type, const std::string& payload = ""); struct ipc_response recv(int fd); - int fd_; - int fd_event_; + util::ScopedFd fd_; + util::ScopedFd fd_event_; std::mutex mutex_; util::SleeperThread thread_; }; diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index 4139a53b..3ebccccd 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -9,8 +9,8 @@ namespace waybar::modules::sway { Ipc::Ipc() { const std::string& socketPath = getSocketPath(); - fd_ = open(socketPath); - fd_event_ = open(socketPath); + fd_ = util::ScopedFd(open(socketPath)); + fd_event_ = util::ScopedFd(open(socketPath)); } Ipc::~Ipc() { @@ -21,15 +21,11 @@ Ipc::~Ipc() { if (write(fd_, "close-sway-ipc", 14) == -1) { spdlog::error("Failed to close sway IPC"); } - close(fd_); - fd_ = -1; } if (fd_event_ > 0) { if (write(fd_event_, "close-sway-ipc", 14) == -1) { spdlog::error("Failed to close sway IPC event handler"); } - close(fd_event_); - fd_event_ = -1; } } @@ -64,7 +60,7 @@ const std::string Ipc::getSocketPath() const { } int Ipc::open(const std::string& socketPath) const { - int32_t fd = socket(AF_UNIX, SOCK_STREAM, 0); + util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0)); if (fd == -1) { throw std::runtime_error("Unable to open Unix socket"); } @@ -78,7 +74,7 @@ int Ipc::open(const std::string& socketPath) const { if (::connect(fd, reinterpret_cast(&addr), l) == -1) { throw std::runtime_error("Unable to connect to Sway"); } - return fd; + return fd.release(); } struct Ipc::ipc_response Ipc::recv(int fd) { From 5e7dbf171542a3e1a0457488a23c7562cbec0fd1 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 13:12:42 -0600 Subject: [PATCH 043/167] fix(command): return non-zero when child exec fails Child exec failure paths were returning success, which masked command launch errors from callers. I switched the child-side failure exits to _exit(127) and added errno-specific logging so failures propagate with actionable diagnostics. Signed-off-by: Austin Horstman --- include/util/command.hpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/include/util/command.hpp b/include/util/command.hpp index b1adcd7c..f6d2cabf 100644 --- a/include/util/command.hpp +++ b/include/util/command.hpp @@ -20,6 +20,8 @@ extern std::list reap; namespace waybar::util::command { +constexpr int kExecFailureExitCode = 127; + struct res { int exit_code; std::string out; @@ -114,7 +116,9 @@ inline FILE* open(const std::string& cmd, int& pid, const std::string& output_na setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); } execlp("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0); - exit(0); + const int saved_errno = errno; + spdlog::error("execlp(/bin/sh) failed in open: {}", strerror(saved_errno)); + _exit(kExecFailureExitCode); } else { ::close(fd[1]); } @@ -162,7 +166,9 @@ inline int32_t forkExec(const std::string& cmd, const std::string& output_name) setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); } execl("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0); - exit(0); + const int saved_errno = errno; + spdlog::error("execl(/bin/sh) failed in forkExec: {}", strerror(saved_errno)); + _exit(kExecFailureExitCode); } else { reap_mtx.lock(); reap.push_back(pid); From 79fb1d9f58d15cb3704a62359a7457451cbc0164 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 13:12:45 -0600 Subject: [PATCH 044/167] test(command): cover exec failure paths Command tests did not assert behavior when exec fails in child processes. I added deterministic regression coverage that forces execl/execlp failure and verifies non-zero exit status propagation for both open() and forkExec paths. Signed-off-by: Austin Horstman --- test/utils/command.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++ test/utils/meson.build | 1 + 2 files changed, 57 insertions(+) create mode 100644 test/utils/command.cpp diff --git a/test/utils/command.cpp b/test/utils/command.cpp new file mode 100644 index 00000000..2ccb3383 --- /dev/null +++ b/test/utils/command.cpp @@ -0,0 +1,56 @@ +#if __has_include() +#include +#else +#include +#endif + +#include +#include +#include +#include +#include + +std::mutex reap_mtx; +std::list reap; + +extern "C" int waybar_test_execl(const char* path, const char* arg, ...); +extern "C" int waybar_test_execlp(const char* file, const char* arg, ...); + +#define execl waybar_test_execl +#define execlp waybar_test_execlp +#include "util/command.hpp" +#undef execl +#undef execlp + +extern "C" int waybar_test_execl(const char* path, const char* arg, ...) { + (void)path; + (void)arg; + errno = ENOENT; + return -1; +} + +extern "C" int waybar_test_execlp(const char* file, const char* arg, ...) { + (void)file; + (void)arg; + errno = ENOENT; + return -1; +} + +TEST_CASE("command::execNoRead returns 127 when shell exec fails", "[util][command]") { + const auto result = waybar::util::command::execNoRead("echo should-not-run"); + REQUIRE(result.exit_code == waybar::util::command::kExecFailureExitCode); + REQUIRE(result.out.empty()); +} + +TEST_CASE("command::forkExec child exits 127 when shell exec fails", "[util][command]") { + const auto pid = waybar::util::command::forkExec("echo should-not-run", "test-output"); + REQUIRE(pid > 0); + + int status = -1; + REQUIRE(waitpid(pid, &status, 0) == pid); + REQUIRE(WIFEXITED(status)); + REQUIRE(WEXITSTATUS(status) == waybar::util::command::kExecFailureExitCode); + + std::scoped_lock lock(reap_mtx); + reap.remove(pid); +} diff --git a/test/utils/meson.build b/test/utils/meson.build index 050af262..e8dd37fa 100644 --- a/test/utils/meson.build +++ b/test/utils/meson.build @@ -14,6 +14,7 @@ test_src = files( 'JsonParser.cpp', 'SafeSignal.cpp', 'sleeper_thread.cpp', + 'command.cpp', 'css_reload_helper.cpp', '../../src/util/css_reload_helper.cpp', ) From d929f1a62c764a4cff0897540bc00bf24beb0d5e Mon Sep 17 00:00:00 2001 From: Bryce Gust Date: Mon, 2 Mar 2026 17:58:00 -0600 Subject: [PATCH 045/167] Fix 4894 - float interval cast --- src/ALabel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 0d92c372..fe87e098 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -26,7 +26,7 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st : std::chrono::milliseconds( (config_["interval"].isNumeric() ? std::max(1L, // Minimum 1ms due to millisecond precision - static_cast(config_["interval"].asDouble()) * 1000) + static_cast(config_["interval"].asDouble() * 1000)) : 1000 * (long)interval))), default_format_(format_) { label_.set_name(name); From 25089b24567c853ad79d09175f2de9dcfe4127f9 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 21:22:53 -0600 Subject: [PATCH 046/167] fix(network): align tooltip and tooltip text Closes https://github.com/Alexays/Waybar/issues/4867 Signed-off-by: Austin Horstman --- src/modules/network.cpp | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 34dcc03c..e0866011 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -373,18 +373,26 @@ auto waybar::modules::Network::update() -> void { fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", - pow_format(bandwidth_down * 8ull / interval_.count(), "b/s")), - fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / interval_.count(), "b/s")), + pow_format(bandwidth_down * 8ull / (interval_.count() / 1000.0), "b/s")), + fmt::arg("bandwidthUpBits", + pow_format(bandwidth_up * 8ull / (interval_.count() / 1000.0), "b/s")), fmt::arg("bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / interval_.count(), "b/s")), - fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / interval_.count(), "o/s")), - fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / interval_.count(), "o/s")), - fmt::arg("bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "o/s")), - fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / interval_.count(), "B/s")), - fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / interval_.count(), "B/s")), - fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / interval_.count(), "B/s"))); + pow_format((bandwidth_up + bandwidth_down) * 8ull / (interval_.count() / 1000.0), + "b/s")), + fmt::arg("bandwidthDownOctets", + pow_format(bandwidth_down / (interval_.count() / 1000.0), "o/s")), + fmt::arg("bandwidthUpOctets", + pow_format(bandwidth_up / (interval_.count() / 1000.0), "o/s")), + fmt::arg( + "bandwidthTotalOctets", + pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "o/s")), + fmt::arg("bandwidthDownBytes", + pow_format(bandwidth_down / (interval_.count() / 1000.0), "B/s")), + fmt::arg("bandwidthUpBytes", + pow_format(bandwidth_up / (interval_.count() / 1000.0), "B/s")), + fmt::arg( + "bandwidthTotalBytes", + pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "B/s"))); if (label_.get_tooltip_text() != tooltip_text) { label_.set_tooltip_markup(tooltip_text); } From 4c71b2bf9f2636d2ff3c5702eadcc0c02aba3b3d Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 21:51:32 -0600 Subject: [PATCH 047/167] perf(memory): optimize C++ string operations to reduce heap fragmentation - Replaced pass-by-value std::string parameters with const std::string& or std::string_view to prevent SSO overallocations. - Refactored static mapping functions in UPower to return std::string_view instead of constructing std::string literals, enabling perfect cache locality. - Optimized string concatenation in hot loops (network IPs, inhibitor lists, sway window marks) by using std::string::append() and pre-reserving capacity instead of overloaded operator+ which produces temporary heap instances. These optimizations reduce high-frequency memory churn and overall heap fragmentation within the main rendering loops. Signed-off-by: Austin Horstman --- heaptrack.waybar.2711357.zst | Bin 0 -> 12780 bytes include/modules/disk.hpp | 2 +- .../modules/hyprland/windowcreationpayload.hpp | 7 ++++--- include/modules/hyprland/workspaces.hpp | 2 +- include/modules/mpd/mpd.hpp | 2 +- include/modules/sni/host.hpp | 2 +- include/modules/sway/bar.hpp | 2 +- include/modules/sway/language.hpp | 2 +- include/modules/sway/workspaces.hpp | 4 ++-- include/util/SafeSignal.hpp | 2 +- include/util/sleeper_thread.hpp | 9 +++------ src/modules/disk.cpp | 2 +- src/modules/hyprland/windowcreationpayload.cpp | 9 +++++---- src/modules/hyprland/workspaces.cpp | 3 ++- src/modules/inhibitor.cpp | 3 ++- src/modules/mpd/mpd.cpp | 3 ++- src/modules/network.cpp | 1 + src/modules/sni/host.cpp | 2 +- src/modules/sway/bar.cpp | 2 +- src/modules/sway/language.cpp | 2 +- src/modules/sway/window.cpp | 4 ++-- src/modules/sway/workspaces.cpp | 4 ++-- src/modules/upower.cpp | 7 ++++--- test/utils/sleeper_thread.cpp | 5 +++-- 24 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 heaptrack.waybar.2711357.zst diff --git a/heaptrack.waybar.2711357.zst b/heaptrack.waybar.2711357.zst new file mode 100644 index 0000000000000000000000000000000000000000..88921cba1ce1ab95825bb77f62a6f1a72e958a87 GIT binary patch literal 12780 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 literal 0 HcmV?d00001 diff --git a/include/modules/disk.hpp b/include/modules/disk.hpp index 1b4f3176..b576e19f 100644 --- a/include/modules/disk.hpp +++ b/include/modules/disk.hpp @@ -22,7 +22,7 @@ class Disk : public ALabel { std::string path_; std::string unit_; - float calc_specific_divisor(const std::string divisor); + float calc_specific_divisor(const std::string& divisor); }; } // namespace waybar::modules diff --git a/include/modules/hyprland/windowcreationpayload.hpp b/include/modules/hyprland/windowcreationpayload.hpp index 226a7c41..906be805 100644 --- a/include/modules/hyprland/windowcreationpayload.hpp +++ b/include/modules/hyprland/windowcreationpayload.hpp @@ -40,10 +40,11 @@ struct WindowRepr { class WindowCreationPayload { public: - WindowCreationPayload(std::string workspace_name, WindowAddress window_address, + WindowCreationPayload(const std::string& workspace_name, WindowAddress window_address, WindowRepr window_repr); - WindowCreationPayload(std::string workspace_name, WindowAddress window_address, - std::string window_class, std::string window_title, bool is_active); + WindowCreationPayload(const std::string& workspace_name, WindowAddress window_address, + const std::string& window_class, const std::string& window_title, + bool is_active); WindowCreationPayload(Json::Value const& client_data); int incrementTimeSpentUncreated(); diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 8bf88888..0cec3c1d 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -59,7 +59,7 @@ class Workspaces : public AModule, public EventHandler { enum class ActiveWindowPosition { NONE, FIRST, LAST }; auto activeWindowPosition() const -> ActiveWindowPosition { return m_activeWindowPosition; } - std::string getRewrite(std::string window_class, std::string window_title); + std::string getRewrite(const std::string& window_class, const std::string& window_title); std::string& getWindowSeparator() { return m_formatWindowSeparator; } bool isWorkspaceIgnored(std::string const& workspace_name); diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 32d526e9..9d81638e 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -44,7 +44,7 @@ class MPD : public ALabel { std::string getFilename() const; void setLabel(); std::string getStateIcon() const; - std::string getOptionIcon(std::string optionName, bool activated) const; + std::string getOptionIcon(const std::string& optionName, bool activated) const; // GUI-side methods bool handlePlayPause(GdkEventButton* const&); diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index 6c62ac31..7248ad2f 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -30,7 +30,7 @@ class Host { static void itemUnregistered(SnWatcher*, const gchar*, gpointer); std::tuple getBusNameAndObjectPath(const std::string); - void addRegisteredItem(std::string service); + void addRegisteredItem(const std::string& service); std::vector> items_; const std::string bus_name_; diff --git a/include/modules/sway/bar.hpp b/include/modules/sway/bar.hpp index fd48e5a3..381c6c37 100644 --- a/include/modules/sway/bar.hpp +++ b/include/modules/sway/bar.hpp @@ -37,7 +37,7 @@ class BarIpcClient { void onModeUpdate(bool visible_by_modifier); void onUrgencyUpdate(bool visible_by_urgency); void update(); - bool isModuleEnabled(std::string name); + bool isModuleEnabled(const std::string& name); Bar& bar_; util::JsonParser parser_; diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index 91aa181d..470b9879 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -47,7 +47,7 @@ class Language : public ALabel, public sigc::trackable { void onEvent(const struct Ipc::ipc_response&); void onCmd(const struct Ipc::ipc_response&); - auto set_current_layout(std::string current_layout) -> void; + auto set_current_layout(const std::string& current_layout) -> void; auto init_layouts_map(const std::vector& used_layouts) -> void; const static std::string XKB_LAYOUT_NAMES_KEY; diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index d8a9e18a..14528841 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -27,7 +27,7 @@ class Workspaces : public AModule, public sigc::trackable { static constexpr std::string_view persistent_workspace_switch_cmd_ = R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; - static int convertWorkspaceNameToNum(std::string name); + static int convertWorkspaceNameToNum(const std::string& name); static int windowRewritePriorityFunction(std::string const& window_rule); void onCmd(const struct Ipc::ipc_response&); @@ -40,7 +40,7 @@ class Workspaces : public AModule, public sigc::trackable { std::string getIcon(const std::string&, const Json::Value&); std::string getCycleWorkspace(std::vector::iterator, bool prev) const; uint16_t getWorkspaceIndex(const std::string& name) const; - static std::string trimWorkspaceName(std::string); + static std::string trimWorkspaceName(const std::string&); bool handleScroll(GdkEventScroll* /*unused*/) override; const Bar& bar_; diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp index 1e901792..afb0893e 100644 --- a/include/util/SafeSignal.hpp +++ b/include/util/SafeSignal.hpp @@ -3,10 +3,10 @@ #include #include +#include #include #include #include -#include #include #include #include diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 966772a2..1f3a372d 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -72,8 +72,7 @@ class SleeperThread { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; return condvar_.wait(lk, [this] { - return signal_.load(std::memory_order_relaxed) || - !do_run_.load(std::memory_order_relaxed); + return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); }); } @@ -87,8 +86,7 @@ class SleeperThread { wait_end = now + dur; } return condvar_.wait_until(lk, wait_end, [this] { - return signal_.load(std::memory_order_relaxed) || - !do_run_.load(std::memory_order_relaxed); + return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); }); } @@ -98,8 +96,7 @@ class SleeperThread { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; return condvar_.wait_until(lk, time_point, [this] { - return signal_.load(std::memory_order_relaxed) || - !do_run_.load(std::memory_order_relaxed); + return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); }); } diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index fd7ef817..462940b2 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -92,7 +92,7 @@ auto waybar::modules::Disk::update() -> void { ALabel::update(); } -float waybar::modules::Disk::calc_specific_divisor(std::string divisor) { +float waybar::modules::Disk::calc_specific_divisor(const std::string& divisor) { if (divisor == "kB") { return 1000.0; } else if (divisor == "kiB") { diff --git a/src/modules/hyprland/windowcreationpayload.cpp b/src/modules/hyprland/windowcreationpayload.cpp index 61ea606f..5ace5b32 100644 --- a/src/modules/hyprland/windowcreationpayload.cpp +++ b/src/modules/hyprland/windowcreationpayload.cpp @@ -19,7 +19,7 @@ WindowCreationPayload::WindowCreationPayload(Json::Value const& client_data) clearWorkspaceName(); } -WindowCreationPayload::WindowCreationPayload(std::string workspace_name, +WindowCreationPayload::WindowCreationPayload(const std::string& workspace_name, WindowAddress window_address, WindowRepr window_repr) : m_window(std::move(window_repr)), m_windowAddress(std::move(window_address)), @@ -28,9 +28,10 @@ WindowCreationPayload::WindowCreationPayload(std::string workspace_name, clearWorkspaceName(); } -WindowCreationPayload::WindowCreationPayload(std::string workspace_name, - WindowAddress window_address, std::string window_class, - std::string window_title, bool is_active) +WindowCreationPayload::WindowCreationPayload(const std::string& workspace_name, + WindowAddress window_address, + const std::string& window_class, + const std::string& window_title, bool is_active) : m_window(std::make_pair(std::move(window_class), std::move(window_title))), m_windowAddress(std::move(window_address)), m_workspaceName(std::move(workspace_name)), diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 88b01223..dd891673 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -155,7 +155,8 @@ void Workspaces::extendOrphans(int workspaceId, Json::Value const& clientsJson) } } -std::string Workspaces::getRewrite(std::string window_class, std::string window_title) { +std::string Workspaces::getRewrite(const std::string& window_class, + const std::string& window_title) { std::string windowReprKey; if (windowRewriteConfigUsesTitle()) { windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title); diff --git a/src/modules/inhibitor.cpp b/src/modules/inhibitor.cpp index 170d0508..43c1f83e 100644 --- a/src/modules/inhibitor.cpp +++ b/src/modules/inhibitor.cpp @@ -85,7 +85,8 @@ auto getInhibitors(const Json::Value& config) -> std::string { if (config["what"].isArray()) { inhibitors = checkInhibitor(config["what"][0].asString()); for (decltype(config["what"].size()) i = 1; i < config["what"].size(); ++i) { - inhibitors += ":" + checkInhibitor(config["what"][i].asString()); + inhibitors.append(":"); + inhibitors.append(checkInhibitor(config["what"][i].asString())); } return inhibitors; } diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 1e28a58a..9874f458 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -239,7 +239,8 @@ std::string waybar::modules::MPD::getStateIcon() const { } } -std::string waybar::modules::MPD::getOptionIcon(std::string optionName, bool activated) const { +std::string waybar::modules::MPD::getOptionIcon(const std::string& optionName, + bool activated) const { if (!config_[optionName + "-icons"].isObject()) { return ""; } diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 34dcc03c..868a06f2 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -321,6 +321,7 @@ auto waybar::modules::Network::update() -> void { } else if (addr_pref_ == ip_addr_pref::IPV6) { final_ipaddr_ = ipaddr6_; } else if (addr_pref_ == ip_addr_pref::IPV4_6) { + final_ipaddr_.reserve(ipaddr_.length() + ipaddr6_.length() + 1); final_ipaddr_ = ipaddr_; final_ipaddr_ += '\n'; final_ipaddr_ += ipaddr6_; diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 75501207..6bd1154a 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -132,7 +132,7 @@ std::tuple Host::getBusNameAndObjectPath(const std::st return {service, "/StatusNotifierItem"}; } -void Host::addRegisteredItem(std::string service) { +void Host::addRegisteredItem(const std::string& service) { std::string bus_name, object_path; std::tie(bus_name, object_path) = getBusNameAndObjectPath(service); auto it = std::find_if(items_.begin(), items_.end(), [&bus_name, &object_path](const auto& item) { diff --git a/src/modules/sway/bar.cpp b/src/modules/sway/bar.cpp index f28b0502..f374f1c1 100644 --- a/src/modules/sway/bar.cpp +++ b/src/modules/sway/bar.cpp @@ -60,7 +60,7 @@ BarIpcClient::BarIpcClient(waybar::Bar& bar) : bar_{bar} { }); } -bool BarIpcClient::isModuleEnabled(std::string name) { +bool BarIpcClient::isModuleEnabled(const std::string& name) { for (const auto& section : {"modules-left", "modules-center", "modules-right"}) { if (const auto& modules = bar_.config.get(section, {}); modules.isArray()) { for (const auto& module : modules) { diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index f4cfa6c3..2e1103d3 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -124,7 +124,7 @@ auto Language::update() -> void { ALabel::update(); } -auto Language::set_current_layout(std::string current_layout) -> void { +auto Language::set_current_layout(const std::string& current_layout) -> void { label_.get_style_context()->remove_class(layout_.short_name); layout_ = layouts_map_[current_layout]; label_.get_style_context()->add_class(layout_.short_name); diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index 830a4120..2908b85c 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -184,9 +184,9 @@ std::tuple getWindowInfo( continue; } if (!marks.empty()) { - marks += ','; + marks.append(","); } - marks += m.asString(); + marks.append(m.asString()); } } return {app_id, app_class, shell, marks}; diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index e0255893..6abe5383 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -10,7 +10,7 @@ namespace waybar::modules::sway { // Helper function to assign a number to a workspace, just like sway. In fact // this is taken quite verbatim from `sway/ipc-json.c`. -int Workspaces::convertWorkspaceNameToNum(std::string name) { +int Workspaces::convertWorkspaceNameToNum(const std::string& name) { if (isdigit(name[0]) != 0) { errno = 0; char* endptr = nullptr; @@ -487,7 +487,7 @@ std::string Workspaces::getCycleWorkspace(std::vector::iterator it, return (*it)["name"].asString(); } -std::string Workspaces::trimWorkspaceName(std::string name) { +std::string Workspaces::trimWorkspaceName(const std::string& name) { std::size_t found = name.find(':'); if (found != std::string::npos) { return name.substr(found + 1); diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp index 8202b718..d260ba83 100644 --- a/src/modules/upower.cpp +++ b/src/modules/upower.cpp @@ -95,7 +95,7 @@ UPower::~UPower() { removeDevices(); } -static const std::string getDeviceStatus(UpDeviceState& state) { +static std::string_view getDeviceStatus(UpDeviceState& state) { switch (state) { case UP_DEVICE_STATE_CHARGING: case UP_DEVICE_STATE_PENDING_CHARGE: @@ -112,7 +112,7 @@ static const std::string getDeviceStatus(UpDeviceState& state) { } } -static const std::string getDeviceIcon(UpDeviceKind& kind) { +static std::string_view getDeviceIcon(UpDeviceKind& kind) { switch (kind) { case UP_DEVICE_KIND_LINE_POWER: return "ac-adapter-symbolic"; @@ -212,7 +212,8 @@ auto UPower::update() -> void { // Remove last status if it exists if (!lastStatus_.empty() && box_.get_style_context()->has_class(lastStatus_)) box_.get_style_context()->remove_class(lastStatus_); - if (!box_.get_style_context()->has_class(status)) box_.get_style_context()->add_class(status); + if (!box_.get_style_context()->has_class(std::string(status))) + box_.get_style_context()->add_class(std::string(status)); lastStatus_ = status; if (devices_.size() == 0 && !upDeviceValid && hideIfEmpty_) { diff --git a/test/utils/sleeper_thread.cpp b/test/utils/sleeper_thread.cpp index 7a9a3ca1..9e1a1ef3 100644 --- a/test/utils/sleeper_thread.cpp +++ b/test/utils/sleeper_thread.cpp @@ -4,11 +4,12 @@ #include #endif -#include #include -#include #include +#include +#include + #include "util/sleeper_thread.hpp" namespace waybar::util { From e684e701df0515efd207b2f70ca766acc9770075 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 22:27:41 -0600 Subject: [PATCH 048/167] perf(sni): eliminate redundant g_memdup2 allocation for D-Bus pixbufs Memory profiling via Valgrind Massif indicated that 10-20% of peak memory allocations within the SNI loop resulted from copying DBus image data payloads via g_memdup2 before modifying them from ARGB to RGBA. This commit optimizes the pixel conversion by directly allocating the final array via g_malloc and running the ARGB->RGBA transposition in a single pass while copying from the read-only GVariant buffer, entirely eliminating the intermediate g_memdup stage. Signed-off-by: Austin Horstman --- src/modules/sni/item.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index d33765d2..7ce95faf 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -336,11 +336,20 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { if (array != nullptr) { g_free(array); } -#if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 68 - array = static_cast(g_memdup2(data, size)); -#else - array = static_cast(g_memdup(data, size)); -#endif + // We must allocate our own array because the data from GVariant is read-only + // and we need to modify it to convert ARGB to RGBA. + array = static_cast(g_malloc(size)); + + // Copy and convert ARGB to RGBA in one pass to avoid g_memdup2 overhead + const guchar* src = static_cast(data); + for (gsize i = 0; i < size; i += 4) { + guchar alpha = src[i]; + array[i] = src[i + 1]; + array[i + 1] = src[i + 2]; + array[i + 2] = src[i + 3]; + array[i + 3] = alpha; + } + lwidth = width; lheight = height; } @@ -349,14 +358,6 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { } g_variant_iter_free(it); if (array != nullptr) { - /* argb to rgba */ - for (uint32_t i = 0; i < 4U * lwidth * lheight; i += 4) { - guchar alpha = array[i]; - array[i] = array[i + 1]; - array[i + 1] = array[i + 2]; - array[i + 2] = array[i + 3]; - array[i + 3] = alpha; - } return Gdk::Pixbuf::create_from_data(array, Gdk::Colorspace::COLORSPACE_RGB, true, 8, lwidth, lheight, 4 * lwidth, &pixbuf_data_deleter); } From 7d8be29f97b17061404c1ba01938b9481499e2be Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 22:36:55 -0600 Subject: [PATCH 049/167] perf(sni): eliminate icon theme rescanning from system tray hotpath Valgrind Massif profiling revealed that invoking Gtk::IconTheme::rescan_if_needed() inside SNI updateImage() and getIconByName() loops caused considerable memory churn and potential filesystem stat overhead whenever a system tray app pushed a metadata update. This commit removes the rescan polling from the SNI proxy callback pipeline and the DefaultGtkIconThemeWrapper, restricting icon theme caching to load boundaries. Signed-off-by: Austin Horstman --- src/modules/sni/item.cpp | 2 -- src/util/gtk_icon.cpp | 1 - 2 files changed, 3 deletions(-) diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 7ce95faf..1428bd8e 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -423,8 +423,6 @@ Glib::RefPtr Item::getIconPixbuf() { } Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { - icon_theme->rescan_if_needed(); - if (!icon_theme_path.empty()) { auto icon_info = icon_theme->lookup_icon(name.c_str(), request_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); diff --git a/src/util/gtk_icon.cpp b/src/util/gtk_icon.cpp index 73f77284..1b2fca57 100644 --- a/src/util/gtk_icon.cpp +++ b/src/util/gtk_icon.cpp @@ -21,7 +21,6 @@ Glib::RefPtr DefaultGtkIconThemeWrapper::load_icon( const std::lock_guard lock(default_theme_mutex); auto default_theme = Gtk::IconTheme::get_default(); - default_theme->rescan_if_needed(); auto icon_info = default_theme->lookup_icon(name, tmp_size, flags); From fe03dfaa3bb32622139a1a3b124d31af631e7837 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 22:40:14 -0600 Subject: [PATCH 050/167] perf(memory): eliminate deep copies in range-based for loops and lambdas This commit addresses memory churn caused by implicit deep copies during traversal and allocation of complex structures: - Replaced pass-by-value 'Json::Value' in std::ranges and range-based for loops with 'const auto&' or 'const Json::Value&' in Hyprland modules, preventing large JSON tree duplications on every update. - Fixed implicit string and pair copies in UPower and CPU Frequency loops by converting 'auto' to 'const auto&' where possible. - Added 'std::vector::reserve' calls before 'push_back' loops in MPRIS, Niri, and CFFI modules to prevent exponential vector reallocation during initialization. Signed-off-by: Austin Horstman --- src/modules/cffi.cpp | 1 + src/modules/cpu_frequency/linux.cpp | 2 +- src/modules/hyprland/window.cpp | 16 ++++++++-------- src/modules/hyprland/windowcount.cpp | 4 ++-- src/modules/hyprland/workspace.cpp | 2 +- src/modules/hyprland/workspaces.cpp | 6 +++--- src/modules/mpris/mpris.cpp | 1 + src/modules/niri/language.cpp | 1 + 8 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp index 930c4d47..c3f1398f 100644 --- a/src/modules/cffi.cpp +++ b/src/modules/cffi.cpp @@ -71,6 +71,7 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co // Prepare config_entries array std::vector config_entries; + config_entries.reserve(keys.size()); for (size_t i = 0; i < keys.size(); i++) { config_entries.push_back({keys[i].c_str(), config_entries_stringstor[i].c_str()}); } diff --git a/src/modules/cpu_frequency/linux.cpp b/src/modules/cpu_frequency/linux.cpp index 1f368789..83f06aa5 100644 --- a/src/modules/cpu_frequency/linux.cpp +++ b/src/modules/cpu_frequency/linux.cpp @@ -26,7 +26,7 @@ std::vector waybar::modules::CpuFrequency::parseCpuFrequencies() { if (std::filesystem::exists(cpufreq_dir)) { std::vector frequency_files = {"/cpuinfo_min_freq", "/cpuinfo_max_freq"}; for (auto& p : std::filesystem::directory_iterator(cpufreq_dir)) { - for (auto freq_file : frequency_files) { + for (const auto& freq_file : frequency_files) { std::string freq_file_path = p.path().string() + freq_file; if (std::filesystem::exists(freq_file_path)) { std::string freq_value; diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 1fddb45b..97d49eaf 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -124,7 +124,7 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { const auto monitors = IPC::inst().getSocket1JsonReply("monitors"); if (monitors.isArray()) { auto monitor = std::ranges::find_if( - monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + monitors, [&](const Json::Value& monitor) { return monitor["name"] == monitorName; }); if (monitor == std::end(monitors)) { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{ @@ -139,7 +139,7 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces"); if (workspaces.isArray()) { auto workspace = std::ranges::find_if( - workspaces, [&](Json::Value workspace) { return workspace["id"] == id; }); + workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; }); if (workspace == std::end(workspaces)) { spdlog::warn("No workspace with id {}", id); return Workspace{ @@ -190,7 +190,7 @@ void Window::queryActiveWorkspace() { const auto clients = m_ipc.getSocket1JsonReply("clients"); if (clients.isArray()) { auto activeWindow = std::ranges::find_if( - clients, [&](Json::Value window) { return window["address"] == workspace_.last_window; }); + clients, [&](const Json::Value& window) { return window["address"] == workspace_.last_window; }); if (activeWindow == std::end(clients)) { focused_ = false; @@ -200,19 +200,19 @@ void Window::queryActiveWorkspace() { windowData_ = WindowData::parse(*activeWindow); updateAppIconName(windowData_.class_name, windowData_.initial_class_name); std::vector workspaceWindows; - std::ranges::copy_if(clients, std::back_inserter(workspaceWindows), [&](Json::Value window) { + std::ranges::copy_if(clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) { return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); }); - swallowing_ = std::ranges::any_of(workspaceWindows, [&](Json::Value window) { + swallowing_ = std::ranges::any_of(workspaceWindows, [&](const Json::Value& window) { return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; }); std::vector visibleWindows; std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), - [&](Json::Value window) { return !window["hidden"].asBool(); }); + [&](const Json::Value& window) { return !window["hidden"].asBool(); }); solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(), - [&](Json::Value window) { return !window["floating"].asBool(); }); + [&](const Json::Value& window) { return !window["floating"].asBool(); }); allFloating_ = std::ranges::all_of( - visibleWindows, [&](Json::Value window) { return window["floating"].asBool(); }); + visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); }); fullscreen_ = windowData_.fullscreen; // Fullscreen windows look like they are solo diff --git a/src/modules/hyprland/windowcount.cpp b/src/modules/hyprland/windowcount.cpp index ab573cca..5f5db80c 100644 --- a/src/modules/hyprland/windowcount.cpp +++ b/src/modules/hyprland/windowcount.cpp @@ -79,7 +79,7 @@ auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspac const auto monitors = m_ipc.getSocket1JsonReply("monitors"); if (monitors.isArray()) { auto monitor = std::ranges::find_if( - monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + monitors, [&](const Json::Value& monitor) { return monitor["name"] == monitorName; }); if (monitor == std::end(monitors)) { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{ @@ -93,7 +93,7 @@ auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspac const auto workspaces = m_ipc.getSocket1JsonReply("workspaces"); if (workspaces.isArray()) { auto workspace = std::ranges::find_if( - workspaces, [&](Json::Value workspace) { return workspace["id"] == id; }); + workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; }); if (workspace == std::end(workspaces)) { spdlog::warn("No workspace with id {}", id); return Workspace{ diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 87933ac0..21e7ef9b 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -96,7 +96,7 @@ bool Workspace::handleClicked(GdkEventButton* bt) const { void Workspace::initializeWindowMap(const Json::Value& clients_data) { m_windowMap.clear(); - for (auto client : clients_data) { + for (const auto& client : clients_data) { if (client["workspace"]["id"].asInt() == id()) { insertWindow({client}); } diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index dd891673..cbd14f90 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -197,7 +197,7 @@ void Workspaces::initializeWorkspaces() { auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); auto const clientsJson = m_ipc.getSocket1JsonReply("clients"); - for (Json::Value workspaceJson : workspacesJson) { + for (const auto& workspaceJson : workspacesJson) { std::string workspaceName = workspaceJson["name"].asString(); if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) && (!workspaceName.starts_with("special") || showSpecial()) && @@ -401,7 +401,7 @@ void Workspaces::onWorkspaceCreated(std::string const& payload, Json::Value cons auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules"); auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); - for (Json::Value workspaceJson : workspacesJson) { + for (auto workspaceJson : workspacesJson) { const auto currentId = workspaceJson["id"].asInt(); if (currentId == *workspaceId) { std::string workspaceName = workspaceJson["name"].asString(); @@ -1004,7 +1004,7 @@ void Workspaces::setUrgentWorkspace(std::string const& windowaddress) { const Json::Value clientsJson = m_ipc.getSocket1JsonReply("clients"); int workspaceId = -1; - for (Json::Value clientJson : clientsJson) { + for (const auto& clientJson : clientsJson) { if (clientJson["address"].asString().ends_with(windowaddress)) { workspaceId = clientJson["workspace"]["id"].asInt(); break; diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 1bdd7df6..93f73690 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -109,6 +109,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) player_ = config_["player"].asString(); } if (config_["ignored-players"].isArray()) { + ignored_players_.reserve(config_["ignored-players"].size()); for (const auto& item : config_["ignored-players"]) { if (item.isString()) { ignored_players_.push_back(item.asString()); diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp index 3359ef4c..14ca555f 100644 --- a/src/modules/niri/language.cpp +++ b/src/modules/niri/language.cpp @@ -32,6 +32,7 @@ void Language::updateFromIPC() { auto ipcLock = gIPC->lockData(); layouts_.clear(); + layouts_.reserve(gIPC->keyboardLayoutNames().size()); for (const auto& fullName : gIPC->keyboardLayoutNames()) layouts_.push_back(getLayout(fullName)); current_idx_ = gIPC->keyboardLayoutCurrent(); From c5449bd361bed8be9875c1322fd0d693ba975cdd Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 2 Mar 2026 23:12:54 -0600 Subject: [PATCH 051/167] fix(network): log new address only when state changes Signed-off-by: Austin Horstman --- src/modules/network.cpp | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index e0866011..a25024e8 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -634,18 +634,31 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) { case IFA_LOCAL: char ipaddr[INET6_ADDRSTRLEN]; if (!is_del_event) { + bool addr_changed = false; + std::string changed_ipaddr; + int changed_cidr = 0; if ((net->addr_pref_ == ip_addr_pref::IPV4 || net->addr_pref_ == ip_addr_pref::IPV4_6) && net->cidr_ == 0 && ifa->ifa_family == AF_INET) { - net->ipaddr_ = - inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); - net->cidr_ = ifa->ifa_prefixlen; + if (inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)) != + nullptr) { + net->ipaddr_ = ipaddr; + net->cidr_ = ifa->ifa_prefixlen; + addr_changed = true; + changed_ipaddr = net->ipaddr_; + changed_cidr = net->cidr_; + } } else if ((net->addr_pref_ == ip_addr_pref::IPV6 || net->addr_pref_ == ip_addr_pref::IPV4_6) && net->cidr6_ == 0 && ifa->ifa_family == AF_INET6) { - net->ipaddr6_ = - inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)); - net->cidr6_ = ifa->ifa_prefixlen; + if (inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)) != + nullptr) { + net->ipaddr6_ = ipaddr; + net->cidr6_ = ifa->ifa_prefixlen; + addr_changed = true; + changed_ipaddr = net->ipaddr6_; + changed_cidr = net->cidr6_; + } } switch (ifa->ifa_family) { @@ -665,7 +678,10 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) { net->netmask6_ = inet_ntop(ifa->ifa_family, &netmask6, ipaddr, sizeof(ipaddr)); } } - spdlog::debug("network: {}, new addr {}/{}", net->ifname_, net->ipaddr_, net->cidr_); + if (addr_changed) { + spdlog::debug("network: {}, new addr {}/{}", net->ifname_, changed_ipaddr, + changed_cidr); + } } else { net->ipaddr_.clear(); net->ipaddr6_.clear(); From a97e8dad7cc739abf27a1562e9270000ac4124f1 Mon Sep 17 00:00:00 2001 From: Alison Date: Mon, 2 Mar 2026 23:40:15 -0800 Subject: [PATCH 052/167] Revert "Fix tooltip sync issue by removing conditional checks" This reverts commit 2b29c9a5d6e353ce6d6686be2d51a6240c0778bf. --- src/modules/custom.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index e2b705da..ff897fef 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -188,9 +188,13 @@ auto waybar::modules::Custom::update() -> void { fmt::arg("percentage", percentage_)); label_.set_tooltip_markup(tooltip); } else if (text_ == tooltip_) { - label_.set_tooltip_markup(str); + if (label_.get_tooltip_markup() != str) { + label_.set_tooltip_markup(str); + } } else { - label_.set_tooltip_markup(tooltip_); + if (label_.get_tooltip_markup() != tooltip_) { + label_.set_tooltip_markup(tooltip_); + } } } auto style = label_.get_style_context(); From 3eb2c7e8f43b25387e68b9e320321edf274840ee Mon Sep 17 00:00:00 2001 From: Alison Date: Mon, 2 Mar 2026 23:49:22 -0800 Subject: [PATCH 053/167] sync tooltip updates without resetting hover state --- include/modules/custom.hpp | 1 + src/modules/custom.cpp | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index 6c17c6e4..a345a33b 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -35,6 +35,7 @@ class Custom : public ALabel { std::string id_; std::string alt_; std::string tooltip_; + std::string last_tooltip_markup_; const bool tooltip_format_enabled_; std::vector class_; int percentage_; diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index ff897fef..93956614 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -1,6 +1,7 @@ #include "modules/custom.hpp" #include +#include #include "util/scope_guard.hpp" @@ -180,21 +181,22 @@ auto waybar::modules::Custom::update() -> void { } else { label_.set_markup(str); if (tooltipEnabled()) { + std::string tooltip_markup; if (tooltip_format_enabled_) { auto tooltip = config_["tooltip-format"].asString(); - tooltip = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_), - fmt::arg("tooltip", tooltip_), fmt::arg("alt", alt_), - fmt::arg("icon", getIcon(percentage_, alt_)), - fmt::arg("percentage", percentage_)); - label_.set_tooltip_markup(tooltip); + tooltip_markup = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_), + fmt::arg("tooltip", tooltip_), fmt::arg("alt", alt_), + fmt::arg("icon", getIcon(percentage_, alt_)), + fmt::arg("percentage", percentage_)); } else if (text_ == tooltip_) { - if (label_.get_tooltip_markup() != str) { - label_.set_tooltip_markup(str); - } + tooltip_markup = str; } else { - if (label_.get_tooltip_markup() != tooltip_) { - label_.set_tooltip_markup(tooltip_); - } + tooltip_markup = tooltip_; + } + + if (last_tooltip_markup_ != tooltip_markup) { + label_.set_tooltip_markup(tooltip_markup); + last_tooltip_markup_ = std::move(tooltip_markup); } } auto style = label_.get_style_context(); From a81621863790ce42cff243b215a01c229cef9142 Mon Sep 17 00:00:00 2001 From: Neale Swinnerton Date: Tue, 3 Mar 2026 16:02:04 +0000 Subject: [PATCH 054/167] fix(mpris): disconnect GLib signals before destroying objects Waybar SEGVs in Glib::DispatchNotifier::pipe_io_handler when the MPRIS module is enabled. The crash is intermittent because it requires a race between signal emission and object destruction: a playerctl GLib signal callback (e.g. onPlayerPlay) calls dp.emit(), which writes a pointer to the Dispatcher into an internal pipe. If the Mpris object is destroyed before the GLib main loop reads that pipe entry, pipe_io_handler dereferences a dangling pointer. This typically occurs when a media player appears or vanishes on D-Bus (browser closing, player quitting) or during waybar shutdown/config reload. The root cause is that ~Mpris() calls g_object_unref() on the manager and player GObjects without first disconnecting the signal handlers that hold raw `this` pointers. If playerctl holds additional references to these GObjects, they survive the unref and can still fire signals targeting the already-destroyed Mpris instance. Adopt the same cleanup pattern used by the Wireplumber module: call g_signal_handlers_disconnect_by_data() to sever all signal connections referencing `this` before releasing the GObjects with g_clear_object(). This guarantees no callbacks can enqueue stale Dispatcher notifications after teardown begins. Additionally: - Clean up old player in onPlayerNameAppeared before replacing it, fixing a GObject leak and accumulation of dangling signal connections - Remove duplicate onPlayerStop signal registration (copy-paste bug) --- src/modules/mpris/mpris.cpp | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 1bdd7df6..5771a5ba 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -161,8 +161,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) if (player) { g_object_connect(player, "signal::play", G_CALLBACK(onPlayerPlay), this, "signal::pause", G_CALLBACK(onPlayerPause), this, "signal::stop", G_CALLBACK(onPlayerStop), - this, "signal::stop", G_CALLBACK(onPlayerStop), this, "signal::metadata", - G_CALLBACK(onPlayerMetadata), this, NULL); + this, "signal::metadata", G_CALLBACK(onPlayerMetadata), this, NULL); } // allow setting an interval count that triggers periodic refreshes @@ -178,9 +177,17 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } Mpris::~Mpris() { - if (last_active_player_ && last_active_player_ != player) g_object_unref(last_active_player_); - if (manager != nullptr) g_object_unref(manager); - if (player != nullptr) g_object_unref(player); + if (manager != nullptr) { + g_signal_handlers_disconnect_by_data(manager, this); + } + if (player != nullptr) { + g_signal_handlers_disconnect_by_data(player, this); + } + if (last_active_player_ != nullptr && last_active_player_ != player) { + g_object_unref(last_active_player_); + } + g_clear_object(&manager); + g_clear_object(&player); } auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) -> std::string { @@ -411,11 +418,14 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye return; } + if (mpris->player != nullptr) { + g_signal_handlers_disconnect_by_data(mpris->player, mpris); + g_clear_object(&mpris->player); + } mpris->player = playerctl_player_new_from_name(player_name, nullptr); g_object_connect(mpris->player, "signal::play", G_CALLBACK(onPlayerPlay), mpris, "signal::pause", G_CALLBACK(onPlayerPause), mpris, "signal::stop", G_CALLBACK(onPlayerStop), - mpris, "signal::stop", G_CALLBACK(onPlayerStop), mpris, "signal::metadata", - G_CALLBACK(onPlayerMetadata), mpris, NULL); + mpris, "signal::metadata", G_CALLBACK(onPlayerMetadata), mpris, NULL); mpris->dp.emit(); } From f48fce57dc6f40d8b3992041a715776fe6072dd1 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Fri, 6 Mar 2026 18:38:21 -0600 Subject: [PATCH 055/167] fix(menu): keep popup menus alive after builder teardown The popup menu was retrieved from GtkBuilder and stored in menu_, but the builder was unref'd immediately after construction. That left the later popup path operating on a builder-owned GtkMenu whose lifetime was no longer guaranteed, which matches the GTK_IS_WIDGET and GTK_IS_MENU assertions from the regression report. Take an owned reference to the built menu and release it in AModule teardown so popup menus stay valid without extending the lifetime of the whole builder. Signed-off-by: Austin Horstman --- src/ALabel.cpp | 2 ++ src/AModule.cpp | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index fe87e098..795f87f1 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -100,6 +100,8 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st g_object_unref(builder); throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); } + // Keep the menu alive after dropping the transient GtkBuilder. + g_object_ref(menu_); submenus_ = std::map(); menuActionsMap_ = std::map(); diff --git a/src/AModule.cpp b/src/AModule.cpp index 5f3a187a..a5ba69d3 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -88,6 +88,10 @@ AModule::~AModule() { killpg(pid, SIGTERM); } } + if (menu_ != nullptr) { + g_object_unref(menu_); + menu_ = nullptr; + } } auto AModule::update() -> void { From 790101f824513a6b48396fd956dd3a9dfae5ad4b Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Fri, 6 Mar 2026 18:49:02 -0600 Subject: [PATCH 056/167] chore: format Signed-off-by: Austin Horstman --- include/modules/sway/ipc/client.hpp | 2 +- src/modules/custom.cpp | 1 + src/modules/hyprland/window.cpp | 17 ++++++++++------- src/modules/niri/backend.cpp | 2 +- test/utils/command.cpp | 5 +++-- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/include/modules/sway/ipc/client.hpp b/include/modules/sway/ipc/client.hpp index eb0f32f9..f6eb7c40 100644 --- a/include/modules/sway/ipc/client.hpp +++ b/include/modules/sway/ipc/client.hpp @@ -13,8 +13,8 @@ #include "ipc.hpp" #include "util/SafeSignal.hpp" -#include "util/sleeper_thread.hpp" #include "util/scoped_fd.hpp" +#include "util/sleeper_thread.hpp" namespace waybar::modules::sway { diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 93956614..28def8c9 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -1,6 +1,7 @@ #include "modules/custom.hpp" #include + #include #include "util/scope_guard.hpp" diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 97d49eaf..a67e81ac 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -189,8 +189,9 @@ void Window::queryActiveWorkspace() { if (workspace_.windows > 0) { const auto clients = m_ipc.getSocket1JsonReply("clients"); if (clients.isArray()) { - auto activeWindow = std::ranges::find_if( - clients, [&](const Json::Value& window) { return window["address"] == workspace_.last_window; }); + auto activeWindow = std::ranges::find_if(clients, [&](const Json::Value& window) { + return window["address"] == workspace_.last_window; + }); if (activeWindow == std::end(clients)) { focused_ = false; @@ -200,17 +201,19 @@ void Window::queryActiveWorkspace() { windowData_ = WindowData::parse(*activeWindow); updateAppIconName(windowData_.class_name, windowData_.initial_class_name); std::vector workspaceWindows; - std::ranges::copy_if(clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) { - return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); - }); + std::ranges::copy_if( + clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) { + return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); + }); swallowing_ = std::ranges::any_of(workspaceWindows, [&](const Json::Value& window) { return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; }); std::vector visibleWindows; std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), [&](const Json::Value& window) { return !window["hidden"].asBool(); }); - solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(), - [&](const Json::Value& window) { return !window["floating"].asBool(); }); + solo_ = 1 == std::count_if( + visibleWindows.begin(), visibleWindows.end(), + [&](const Json::Value& window) { return !window["floating"].asBool(); }); allFloating_ = std::ranges::all_of( visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); }); fullscreen_ = windowData_.fullscreen; diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index c23aaa47..de3f8a9a 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -13,11 +13,11 @@ #include #include -#include "util/scoped_fd.hpp" #include "giomm/datainputstream.h" #include "giomm/dataoutputstream.h" #include "giomm/unixinputstream.h" #include "giomm/unixoutputstream.h" +#include "util/scoped_fd.hpp" namespace waybar::modules::niri { diff --git a/test/utils/command.cpp b/test/utils/command.cpp index 2ccb3383..053a2b77 100644 --- a/test/utils/command.cpp +++ b/test/utils/command.cpp @@ -4,11 +4,12 @@ #include #endif +#include +#include + #include #include #include -#include -#include std::mutex reap_mtx; std::list reap; From 0a35b86e2088360d3c27e46a4e61e97ef428eb51 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Fri, 6 Mar 2026 18:33:12 -0600 Subject: [PATCH 057/167] fix(hyprland/ipc): honor the requested instance signature The Hyprland IPC helper cached the socket folder with the first instance signature already appended, so later calls ignored their instanceSig argument and always reused the first path. That made the helper violate its own API even though most real Waybar sessions only talk to a single Hyprland instance. Cache only the base socket directory and append the requested signature per lookup. This fixes correctness for tests, nested or debug multi-instance setups, and future code that needs to resolve a different signature, without claiming support for one Waybar process managing multiple Hyprland sessions. Signed-off-by: Austin Horstman --- src/modules/hyprland/backend.cpp | 70 +++++++++++++++++++------------- test/hyprland/backend.cpp | 18 ++++++++ 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 0f02b919..d0371202 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -25,27 +25,23 @@ std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { static std::mutex folderMutex; std::unique_lock lock(folderMutex); - // socket path, specified by EventManager of Hyprland - if (!socketFolder_.empty()) { - return socketFolder_; + if (socketFolder_.empty()) { + const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR"); + std::filesystem::path xdgRuntimeDir; + // Only set path if env variable is set + if (xdgRuntimeDirEnv != nullptr) { + xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv); + } + + if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) { + socketFolder_ = xdgRuntimeDir / "hypr"; + } else { + spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr"); + socketFolder_ = std::filesystem::path("/tmp") / "hypr"; + } } - const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR"); - std::filesystem::path xdgRuntimeDir; - // Only set path if env variable is set - if (xdgRuntimeDirEnv != nullptr) { - xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv); - } - - if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) { - socketFolder_ = xdgRuntimeDir / "hypr"; - } else { - spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr"); - socketFolder_ = std::filesystem::path("/tmp") / "hypr"; - } - - socketFolder_ = socketFolder_ / instanceSig; - return socketFolder_; + return socketFolder_ / instanceSig; } IPC::IPC() { @@ -91,7 +87,7 @@ void IPC::socketListener() { spdlog::info("Hyprland IPC starting"); - struct sockaddr_un addr; + struct sockaddr_un addr = {}; const int socketfd = socket(AF_UNIX, SOCK_STREAM, 0); if (socketfd == -1) { @@ -102,10 +98,13 @@ void IPC::socketListener() { addr.sun_family = AF_UNIX; auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock"; + if (socketPath.native().size() >= sizeof(addr.sun_path)) { + spdlog::error("Hyprland IPC: Socket path is too long: {}", socketPath.string()); + close(socketfd); + return; + } strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); - addr.sun_path[sizeof(addr.sun_path) - 1] = 0; - int l = sizeof(struct sockaddr_un); if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) { @@ -233,8 +232,10 @@ std::string IPC::getSocket1Reply(const std::string& rq) { std::string socketPath = IPC::getSocketFolder(instanceSig) / ".socket.sock"; // Use snprintf to copy the socketPath string into serverAddress.sun_path - if (snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()) < - 0) { + const auto socketPathLength = + snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()); + if (socketPathLength < 0 || + socketPathLength >= static_cast(sizeof(serverAddress.sun_path))) { throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)"); } @@ -243,15 +244,28 @@ std::string IPC::getSocket1Reply(const std::string& rq) { throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)"); } - auto sizeWritten = write(serverSocket, rq.c_str(), rq.length()); + std::size_t totalWritten = 0; + while (totalWritten < rq.length()) { + const auto sizeWritten = + write(serverSocket, rq.c_str() + totalWritten, rq.length() - totalWritten); - if (sizeWritten < 0) { - spdlog::error("Hyprland IPC: Couldn't write (4)"); - return ""; + if (sizeWritten < 0) { + if (errno == EINTR) { + continue; + } + spdlog::error("Hyprland IPC: Couldn't write (4)"); + return ""; + } + if (sizeWritten == 0) { + spdlog::error("Hyprland IPC: Socket write made no progress"); + return ""; + } + totalWritten += static_cast(sizeWritten); } std::array buffer = {0}; std::string response; + ssize_t sizeWritten = 0; do { sizeWritten = read(serverSocket, buffer.data(), 8192); diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp index cc7295ec..ccc2da65 100644 --- a/test/hyprland/backend.cpp +++ b/test/hyprland/backend.cpp @@ -86,6 +86,24 @@ TEST_CASE("XDGRuntimeDirExistsNoHyprDir", "[getSocketFolder]") { fs::remove_all(tempDir, ec); } +TEST_CASE("Socket folder is resolved per instance signature", "[getSocketFolder]") { + const fs::path tempDir = fs::temp_directory_path() / "hypr_test/run/user/1000"; + std::error_code ec; + fs::remove_all(tempDir, ec); + fs::create_directories(tempDir / "hypr"); + setenv("XDG_RUNTIME_DIR", tempDir.c_str(), 1); + IPCTestHelper::resetSocketFolder(); + + const auto firstPath = hyprland::IPC::getSocketFolder("instance_a"); + const auto secondPath = hyprland::IPC::getSocketFolder("instance_b"); + + REQUIRE(firstPath == tempDir / "hypr" / "instance_a"); + REQUIRE(secondPath == tempDir / "hypr" / "instance_b"); + REQUIRE(firstPath != secondPath); + + fs::remove_all(tempDir, ec); +} + TEST_CASE("getSocket1Reply throws on no socket", "[getSocket1Reply]") { unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); IPCTestHelper::resetSocketFolder(); From b1a87f943c4da2cc6ab2fef700f64497db7fc205 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Fri, 6 Mar 2026 18:33:14 -0600 Subject: [PATCH 058/167] fix(hyprland/window): avoid stale state during IPC refresh The window module re-entered the same shared_mutex while refreshing IPC state: update() took the lock and then called queryActiveWorkspace(), which tried to lock it again. That is undefined behavior for std::shared_mutex and could manifest as a deadlock. Remove the recursive lock path and reset the derived window state before each IPC refresh. That keeps solo/floating/swallowing/fullscreen classes from sticking around when the client lookup fails or a workspace becomes empty. Signed-off-by: Austin Horstman --- include/modules/hyprland/window.hpp | 22 +++--- src/modules/hyprland/window.cpp | 100 ++++++++++++++-------------- 2 files changed, 60 insertions(+), 62 deletions(-) diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index 2be64594..9725d33a 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -20,8 +20,8 @@ class Window : public waybar::AAppIconLabel, public EventHandler { private: struct Workspace { - int id; - int windows; + int id = 0; + int windows = 0; std::string last_window; std::string last_window_title; @@ -29,14 +29,14 @@ class Window : public waybar::AAppIconLabel, public EventHandler { }; struct WindowData { - bool floating; + bool floating = false; int monitor = -1; std::string class_name; std::string initial_class_name; std::string title; std::string initial_title; - bool fullscreen; - bool grouped; + bool fullscreen = false; + bool grouped = false; static auto parse(const Json::Value&) -> WindowData; }; @@ -47,7 +47,7 @@ class Window : public waybar::AAppIconLabel, public EventHandler { void queryActiveWorkspace(); void setClass(const std::string&, bool enable); - bool separateOutputs_; + bool separateOutputs_ = false; std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; @@ -55,11 +55,11 @@ class Window : public waybar::AAppIconLabel, public EventHandler { Workspace workspace_; std::string soloClass_; std::string lastSoloClass_; - bool solo_; - bool allFloating_; - bool swallowing_; - bool fullscreen_; - bool focused_; + bool solo_ = false; + bool allFloating_ = false; + bool swallowing_ = false; + bool fullscreen_ = false; + bool focused_ = false; IPC& m_ipc; }; diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index a67e81ac..d3fe6edf 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -32,7 +32,6 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) windowIpcUniqueLock.unlock(); - queryActiveWorkspace(); update(); dp.emit(); } @@ -177,66 +176,65 @@ auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData { } void Window::queryActiveWorkspace() { - std::shared_lock windowIpcShareLock(windowIpcSmtx); - if (separateOutputs_) { workspace_ = getActiveWorkspace(this->bar_.output->name); } else { workspace_ = getActiveWorkspace(); } + focused_ = false; + windowData_ = WindowData{}; + allFloating_ = false; + swallowing_ = false; + fullscreen_ = false; + solo_ = false; + soloClass_.clear(); + + if (workspace_.windows <= 0) { + return; + } + + const auto clients = m_ipc.getSocket1JsonReply("clients"); + if (!clients.isArray()) { + return; + } + + auto activeWindow = std::ranges::find_if(clients, [&](const Json::Value& window) { + return window["address"] == workspace_.last_window; + }); + + if (activeWindow == std::end(clients)) { + return; + } + focused_ = true; - if (workspace_.windows > 0) { - const auto clients = m_ipc.getSocket1JsonReply("clients"); - if (clients.isArray()) { - auto activeWindow = std::ranges::find_if(clients, [&](const Json::Value& window) { - return window["address"] == workspace_.last_window; + windowData_ = WindowData::parse(*activeWindow); + updateAppIconName(windowData_.class_name, windowData_.initial_class_name); + std::vector workspaceWindows; + std::ranges::copy_if( + clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) { + return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); }); + swallowing_ = std::ranges::any_of(workspaceWindows, [&](const Json::Value& window) { + return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; + }); + std::vector visibleWindows; + std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), + [&](const Json::Value& window) { return !window["hidden"].asBool(); }); + solo_ = 1 == std::count_if( + visibleWindows.begin(), visibleWindows.end(), + [&](const Json::Value& window) { return !window["floating"].asBool(); }); + allFloating_ = std::ranges::all_of( + visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); }); + fullscreen_ = windowData_.fullscreen; - if (activeWindow == std::end(clients)) { - focused_ = false; - return; - } + // Fullscreen windows look like they are solo + if (fullscreen_) { + solo_ = true; + } - windowData_ = WindowData::parse(*activeWindow); - updateAppIconName(windowData_.class_name, windowData_.initial_class_name); - std::vector workspaceWindows; - std::ranges::copy_if( - clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) { - return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool(); - }); - swallowing_ = std::ranges::any_of(workspaceWindows, [&](const Json::Value& window) { - return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0"; - }); - std::vector visibleWindows; - std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), - [&](const Json::Value& window) { return !window["hidden"].asBool(); }); - solo_ = 1 == std::count_if( - visibleWindows.begin(), visibleWindows.end(), - [&](const Json::Value& window) { return !window["floating"].asBool(); }); - allFloating_ = std::ranges::all_of( - visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); }); - fullscreen_ = windowData_.fullscreen; - - // Fullscreen windows look like they are solo - if (fullscreen_) { - solo_ = true; - } - - if (solo_) { - soloClass_ = windowData_.class_name; - } else { - soloClass_ = ""; - } - } - } else { - focused_ = false; - windowData_ = WindowData{}; - allFloating_ = false; - swallowing_ = false; - fullscreen_ = false; - solo_ = false; - soloClass_ = ""; + if (solo_) { + soloClass_ = windowData_.class_name; } } From dd47a2b826eecf31a66ed1c6a2caf769507cdca5 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Fri, 6 Mar 2026 18:33:17 -0600 Subject: [PATCH 059/167] fix(hyprland/workspaces): stabilize reload and event handling Hyprland workspace reloads could stack duplicate scroll-event connections, causing a single wheel gesture to switch multiple workspaces after repeated config reloads. The persistent-workspaces monitor-array form also created the monitor name instead of the configured workspace name. Disconnect and replace the scroll handler on reinit, fix the persistent workspace name selection, normalize urgent-window address matching, and reject malformed workspace payloads before they corrupt the local state machine. Signed-off-by: Austin Horstman --- include/modules/hyprland/workspaces.hpp | 2 + src/modules/hyprland/workspaces.cpp | 59 +++++++++++++++++-------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 0cec3c1d..03548ccb 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -208,6 +209,7 @@ class Workspaces : public AModule, public EventHandler { std::mutex m_mutex; const Bar& m_bar; Gtk::Box m_box; + sigc::connection m_scrollEventConnection_; IPC& m_ipc; }; diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index cbd14f90..f794249b 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -34,6 +34,9 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& } Workspaces::~Workspaces() { + if (m_scrollEventConnection_.connected()) { + m_scrollEventConnection_.disconnect(); + } m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(m_mutex); @@ -44,10 +47,14 @@ void Workspaces::init() { initializeWorkspaces(); + if (m_scrollEventConnection_.connected()) { + m_scrollEventConnection_.disconnect(); + } if (barScroll()) { auto& window = const_cast(m_bar).window; window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); - window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); + m_scrollEventConnection_ = + window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); } dp.emit(); @@ -271,7 +278,7 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const& clientsJs // key is the workspace and value is array of monitors to create on for (const Json::Value& monitor : value) { if (monitor.isString() && monitor.asString() == currentMonitor) { - persistentWorkspacesToCreate.emplace_back(currentMonitor); + persistentWorkspacesToCreate.emplace_back(key); break; } } @@ -332,8 +339,13 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c void Workspaces::onEvent(const std::string& ev) { std::lock_guard lock(m_mutex); - std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>')); - std::string payload = ev.substr(eventName.size() + 2); + const auto separator = ev.find(">>"); + if (separator == std::string::npos) { + spdlog::warn("Malformed Hyprland workspace event: {}", ev); + return; + } + std::string eventName = ev.substr(0, separator); + std::string payload = ev.substr(separator + 2); if (eventName == "workspacev2") { onWorkspaceActivated(payload); @@ -496,19 +508,21 @@ void Workspaces::onMonitorFocused(std::string const& payload) { void Workspaces::onWindowOpened(std::string const& payload) { spdlog::trace("Window opened: {}", payload); updateWindowCount(); - size_t lastCommaIdx = 0; - size_t nextCommaIdx = payload.find(','); - std::string windowAddress = payload.substr(lastCommaIdx, nextCommaIdx - lastCommaIdx); + const auto firstComma = payload.find(','); + const auto secondComma = + firstComma == std::string::npos ? std::string::npos : payload.find(',', firstComma + 1); + const auto thirdComma = + secondComma == std::string::npos ? std::string::npos : payload.find(',', secondComma + 1); + if (firstComma == std::string::npos || secondComma == std::string::npos || + thirdComma == std::string::npos) { + spdlog::warn("Malformed Hyprland openwindow payload: {}", payload); + return; + } - lastCommaIdx = nextCommaIdx; - nextCommaIdx = payload.find(',', nextCommaIdx + 1); - std::string workspaceName = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1); - - lastCommaIdx = nextCommaIdx; - nextCommaIdx = payload.find(',', nextCommaIdx + 1); - std::string windowClass = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1); - - std::string windowTitle = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx); + std::string windowAddress = payload.substr(0, firstComma); + std::string workspaceName = payload.substr(firstComma + 1, secondComma - firstComma - 1); + std::string windowClass = payload.substr(secondComma + 1, thirdComma - secondComma - 1); + std::string windowTitle = payload.substr(thirdComma + 1); bool isActive = m_currentActiveWindowAddress == windowAddress; m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle, isActive); @@ -1002,10 +1016,12 @@ void Workspaces::sortWorkspaces() { void Workspaces::setUrgentWorkspace(std::string const& windowaddress) { const Json::Value clientsJson = m_ipc.getSocket1JsonReply("clients"); + const std::string normalizedAddress = + windowaddress.starts_with("0x") ? windowaddress : fmt::format("0x{}", windowaddress); int workspaceId = -1; for (const auto& clientJson : clientsJson) { - if (clientJson["address"].asString().ends_with(windowaddress)) { + if (clientJson["address"].asString() == normalizedAddress) { workspaceId = clientJson["workspace"]["id"].asInt(); break; } @@ -1134,7 +1150,11 @@ std::string Workspaces::makePayload(Args const&... args) { } std::pair Workspaces::splitDoublePayload(std::string const& payload) { - const std::string part1 = payload.substr(0, payload.find(',')); + const auto separator = payload.find(','); + if (separator == std::string::npos) { + throw std::invalid_argument("Expected a two-part Hyprland payload"); + } + const std::string part1 = payload.substr(0, separator); const std::string part2 = payload.substr(part1.size() + 1); return {part1, part2}; } @@ -1143,6 +1163,9 @@ std::tuple Workspaces::splitTriplePayload std::string const& payload) { const size_t firstComma = payload.find(','); const size_t secondComma = payload.find(',', firstComma + 1); + if (firstComma == std::string::npos || secondComma == std::string::npos) { + throw std::invalid_argument("Expected a three-part Hyprland payload"); + } const std::string part1 = payload.substr(0, firstComma); const std::string part2 = payload.substr(firstComma + 1, secondComma - (firstComma + 1)); From 6317022304a49c0033c52f0c6cdfe038e88dd314 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Fri, 6 Mar 2026 18:33:19 -0600 Subject: [PATCH 060/167] fix(hyprland): guard malformed module events The language and submap modules assumed their Hyprland payload delimiters were always present. When that assumption is violated, the old code could perform invalid iterator math or throw while slicing the event string. Validate the expected separators up front and bail out with a warning when the event is malformed so the modules degrade safely instead of crashing the update path. Signed-off-by: Austin Horstman --- src/modules/hyprland/language.cpp | 26 +++++++++++++++++++++----- src/modules/hyprland/submap.cpp | 7 ++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 25f6789d..6e0fe23d 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -63,19 +63,35 @@ auto Language::update() -> void { void Language::onEvent(const std::string& ev) { std::lock_guard lg(mutex_); - std::string kbName(begin(ev) + ev.find_last_of('>') + 1, begin(ev) + ev.find_first_of(',')); + const auto payloadStart = ev.find(">>"); + if (payloadStart == std::string::npos) { + spdlog::warn("hyprland language received malformed event: {}", ev); + return; + } + const auto payload = ev.substr(payloadStart + 2); + const auto kbSeparator = payload.find(','); + if (kbSeparator == std::string::npos) { + spdlog::warn("hyprland language received malformed event payload: {}", ev); + return; + } + std::string kbName = payload.substr(0, kbSeparator); // Last comma before variants parenthesis, eg: // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., // with dead keys) std::string beforeParenthesis; - auto parenthesisPos = ev.find_last_of('('); + auto parenthesisPos = payload.find_last_of('('); if (parenthesisPos == std::string::npos) { - beforeParenthesis = ev; + beforeParenthesis = payload; } else { - beforeParenthesis = std::string(begin(ev), begin(ev) + parenthesisPos); + beforeParenthesis = payload.substr(0, parenthesisPos); } - auto layoutName = ev.substr(beforeParenthesis.find_last_of(',') + 1); + const auto layoutSeparator = beforeParenthesis.find_last_of(','); + if (layoutSeparator == std::string::npos) { + spdlog::warn("hyprland language received malformed layout payload: {}", ev); + return; + } + auto layoutName = payload.substr(layoutSeparator + 1); if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) return; // ignore diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp index ff18e7f3..36b9bf79 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -75,7 +75,12 @@ void Submap::onEvent(const std::string& ev) { return; } - auto submapName = ev.substr(ev.find_first_of('>') + 2); + const auto separator = ev.find(">>"); + if (separator == std::string::npos) { + spdlog::warn("hyprland submap received malformed event: {}", ev); + return; + } + auto submapName = ev.substr(separator + 2); submap_ = submapName; From 2a748f1a56ef9c49d3022657f7265ae4d3f65b46 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 01:08:39 -0600 Subject: [PATCH 061/167] fix(sni): delay tray item insertion until proxies are ready Only add tray widgets after the SNI proxy has finished initializing and the item has a valid id/category pair. This also removes invalid items through the host teardown path, refreshes the tray when item status changes, and avoids calling DBus methods through a null proxy during early clicks or scroll events. Signed-off-by: Austin Horstman --- include/modules/sni/host.hpp | 7 ++++- include/modules/sni/item.hpp | 14 +++++++++- include/modules/sni/tray.hpp | 1 + src/modules/sni/host.cpp | 54 +++++++++++++++++++++++++++++++----- src/modules/sni/item.cpp | 48 +++++++++++++++++++++++++++----- src/modules/sni/tray.cpp | 5 +++- 6 files changed, 112 insertions(+), 17 deletions(-) diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index 7248ad2f..d76ec74a 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -16,7 +16,7 @@ class Host { public: Host(const std::size_t id, const Json::Value&, const Bar&, const std::function&)>&, - const std::function&)>&); + const std::function&)>&, const std::function&); ~Host(); private: @@ -28,6 +28,10 @@ class Host { static void registerHost(GObject*, GAsyncResult*, gpointer); static void itemRegistered(SnWatcher*, const gchar*, gpointer); static void itemUnregistered(SnWatcher*, const gchar*, gpointer); + void itemReady(Item&); + void itemInvalidated(Item&); + void removeItem(std::vector>::iterator); + void clearItems(); std::tuple getBusNameAndObjectPath(const std::string); void addRegisteredItem(const std::string& service); @@ -43,6 +47,7 @@ class Host { const Bar& bar_; const std::function&)> on_add_; const std::function&)> on_remove_; + const std::function on_update_; }; } // namespace waybar::modules::SNI diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 503ab637..43200fdb 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -25,9 +26,13 @@ struct ToolTip { class Item : public sigc::trackable { public: - Item(const std::string&, const std::string&, const Json::Value&, const Bar&); + Item(const std::string&, const std::string&, const Json::Value&, const Bar&, + const std::function&, const std::function&, + const std::function&); ~Item(); + bool isReady() const; + std::string bus_name; std::string object_path; @@ -62,6 +67,8 @@ class Item : public sigc::trackable { void proxyReady(Glib::RefPtr& result); void setProperty(const Glib::ustring& name, Glib::VariantBase& value); void setStatus(const Glib::ustring& value); + void setReady(); + void invalidate(); void setCustomIcon(const std::string& id); void getUpdatedProperties(); void processUpdatedProperties(Glib::RefPtr& result); @@ -86,8 +93,13 @@ class Item : public sigc::trackable { gdouble distance_scrolled_y_ = 0; // visibility of items with Status == Passive bool show_passive_ = false; + bool ready_ = false; + Glib::ustring status_ = "active"; const Bar& bar_; + const std::function on_ready_; + const std::function on_invalidate_; + const std::function on_updated_; Glib::RefPtr proxy_; Glib::RefPtr cancellable_; diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 5f12d7f2..3d90b3fd 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -19,6 +19,7 @@ class Tray : public AModule { private: void onAdd(std::unique_ptr& item); void onRemove(std::unique_ptr& item); + void queueUpdate(); static inline std::size_t nb_hosts_ = 0; bool show_passive_ = false; diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 6bd1154a..18eac643 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -8,7 +8,8 @@ namespace waybar::modules::SNI { Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, const std::function&)>& on_add, - const std::function&)>& on_remove) + const std::function&)>& on_remove, + const std::function& on_update) : bus_name_("org.kde.StatusNotifierHost-" + std::to_string(getpid()) + "-" + std::to_string(id)), object_path_("/StatusNotifierHost/" + std::to_string(id)), @@ -17,7 +18,8 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, config_(config), bar_(bar), on_add_(on_add), - on_remove_(on_remove) {} + on_remove_(on_remove), + on_update_(on_update) {} Host::~Host() { if (bus_name_id_ > 0) { @@ -54,7 +56,7 @@ void Host::nameVanished(const Glib::RefPtr& conn, const G g_cancellable_cancel(cancellable_); g_clear_object(&cancellable_); g_clear_object(&watcher_); - items_.clear(); + clearItems(); } void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { @@ -117,13 +119,50 @@ void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer d auto [bus_name, object_path] = host->getBusNameAndObjectPath(service); for (auto it = host->items_.begin(); it != host->items_.end(); ++it) { if ((*it)->bus_name == bus_name && (*it)->object_path == object_path) { - host->on_remove_(*it); - host->items_.erase(it); + host->removeItem(it); break; } } } +void Host::itemReady(Item& item) { + auto it = std::find_if(items_.begin(), items_.end(), + [&item](const auto& candidate) { return candidate.get() == &item; }); + if (it != items_.end() && (*it)->isReady()) { + on_add_(*it); + } +} + +void Host::itemInvalidated(Item& item) { + auto it = std::find_if(items_.begin(), items_.end(), + [&item](const auto& candidate) { return candidate.get() == &item; }); + if (it != items_.end()) { + removeItem(it); + } +} + +void Host::removeItem(std::vector>::iterator it) { + if ((*it)->isReady()) { + on_remove_(*it); + } + items_.erase(it); +} + +void Host::clearItems() { + bool removed_ready_item = false; + for (auto& item : items_) { + if (item->isReady()) { + on_remove_(item); + removed_ready_item = true; + } + } + bool had_items = !items_.empty(); + items_.clear(); + if (had_items && !removed_ready_item) { + on_update_(); + } +} + std::tuple Host::getBusNameAndObjectPath(const std::string service) { auto it = service.find('/'); if (it != std::string::npos) { @@ -139,8 +178,9 @@ void Host::addRegisteredItem(const std::string& service) { return bus_name == item->bus_name && object_path == item->object_path; }); if (it == items_.end()) { - items_.emplace_back(new Item(bus_name, object_path, config_, bar_)); - on_add_(items_.back()); + items_.emplace_back(new Item( + bus_name, object_path, config_, bar_, [this](Item& item) { itemReady(item); }, + [this](Item& item) { itemInvalidated(item); }, on_update_)); } } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 1428bd8e..9820cc62 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -37,13 +37,18 @@ namespace waybar::modules::SNI { static const Glib::ustring SNI_INTERFACE_NAME = sn_item_interface_info()->name; static const unsigned UPDATE_DEBOUNCE_TIME = 10; -Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar) +Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar, + const std::function& on_ready, + const std::function& on_invalidate, const std::function& on_updated) : bus_name(bn), object_path(op), icon_size(16), effective_icon_size(0), icon_theme(Gtk::IconTheme::create()), - bar_(bar) { + bar_(bar), + on_ready_(on_ready), + on_invalidate_(on_invalidate), + on_updated_(on_updated) { if (config["icon-size"].isUInt()) { icon_size = config["icon-size"].asUInt(); } @@ -85,6 +90,8 @@ Item::~Item() { } } +bool Item::isReady() const { return ready_; } + bool Item::handleMouseEnter(GdkEventCrossing* const& e) { event_box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); return false; @@ -112,14 +119,18 @@ void Item::proxyReady(Glib::RefPtr& result) { if (this->id.empty() || this->category.empty()) { spdlog::error("Invalid Status Notifier Item: {}, {}", bus_name, object_path); + invalidate(); return; } this->updateImage(); + setReady(); } catch (const Glib::Error& err) { spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what()); + invalidate(); } catch (const std::exception& err) { spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what()); + invalidate(); } } @@ -217,18 +228,35 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } void Item::setStatus(const Glib::ustring& value) { - Glib::ustring lower = value.lowercase(); - event_box.set_visible(show_passive_ || lower.compare("passive") != 0); + status_ = value.lowercase(); + event_box.set_visible(show_passive_ || status_.compare("passive") != 0); auto style = event_box.get_style_context(); for (const auto& class_name : style->list_classes()) { style->remove_class(class_name); } - if (lower.compare("needsattention") == 0) { + auto css_class = status_; + if (css_class.compare("needsattention") == 0) { // convert status to dash-case for CSS - lower = "needs-attention"; + css_class = "needs-attention"; } - style->add_class(lower); + style->add_class(css_class); + on_updated_(); +} + +void Item::setReady() { + if (ready_) { + return; + } + ready_ = true; + on_ready_(*this); +} + +void Item::invalidate() { + if (ready_) { + ready_ = false; + } + on_invalidate_(*this); } void Item::setCustomIcon(const std::string& id) { @@ -464,6 +492,9 @@ void Item::makeMenu() { } bool Item::handleClick(GdkEventButton* const& ev) { + if (!proxy_) { + return false; + } auto parameters = Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(ev->x_root + bar_.x_global), Glib::Variant::create(ev->y_root + bar_.y_global)}); @@ -491,6 +522,9 @@ bool Item::handleClick(GdkEventButton* const& ev) { } bool Item::handleScroll(GdkEventScroll* const& ev) { + if (!proxy_) { + return false; + } int dx = 0, dy = 0; switch (ev->direction) { case GDK_SCROLL_UP: diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 34a3c05f..114aba78 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -13,7 +13,8 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) box_(bar.orientation, 0), watcher_(SNI::Watcher::getInstance()), host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1), - std::bind(&Tray::onRemove, this, std::placeholders::_1)) { + std::bind(&Tray::onRemove, this, std::placeholders::_1), + std::bind(&Tray::queueUpdate, this)) { box_.set_name("tray"); event_box_.add(box_); if (!id.empty()) { @@ -33,6 +34,8 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) dp.emit(); } +void Tray::queueUpdate() { dp.emit(); } + void Tray::onAdd(std::unique_ptr& item) { if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) { box_.pack_end(item->event_box); From 78f6cde232147ee98986edd3cacfe9c6570bd7ec Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 01:08:43 -0600 Subject: [PATCH 062/167] fix(sni): correct watcher host teardown signaling Return the host registration method correctly on duplicate host registration and emit HostUnregistered instead of HostRegistered when the last host vanishes. Also free the corresponding name watch once the tracked host/item disappears so the watcher does not leak stale watch records. Signed-off-by: Austin Horstman --- src/modules/sni/watcher.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/sni/watcher.cpp b/src/modules/sni/watcher.cpp index 1534d924..66083c99 100644 --- a/src/modules/sni/watcher.cpp +++ b/src/modules/sni/watcher.cpp @@ -69,7 +69,7 @@ gboolean Watcher::handleRegisterHost(Watcher* obj, GDBusMethodInvocation* invoca if (watch != nullptr) { g_warning("Status Notifier Host with bus name '%s' and object path '%s' is already registered", bus_name, object_path); - sn_watcher_complete_register_item(obj->watcher_, invocation); + sn_watcher_complete_register_host(obj->watcher_, invocation); return TRUE; } watch = gfWatchNew(GF_WATCH_TYPE_HOST, service, bus_name, object_path, obj); @@ -158,7 +158,7 @@ void Watcher::nameVanished(GDBusConnection* connection, const char* name, gpoint watch->watcher->hosts_ = g_slist_remove(watch->watcher->hosts_, watch); if (watch->watcher->hosts_ == nullptr) { sn_watcher_set_is_host_registered(watch->watcher->watcher_, FALSE); - sn_watcher_emit_host_registered(watch->watcher->watcher_); + sn_watcher_emit_host_unregistered(watch->watcher->watcher_); } } else if (watch->type == GF_WATCH_TYPE_ITEM) { watch->watcher->items_ = g_slist_remove(watch->watcher->items_, watch); @@ -167,6 +167,7 @@ void Watcher::nameVanished(GDBusConnection* connection, const char* name, gpoint sn_watcher_emit_item_unregistered(watch->watcher->watcher_, tmp); g_free(tmp); } + gfWatchFree(watch); } void Watcher::updateRegisteredItems(SnWatcher* obj) { From f6d92fd708d0fd87b008e3ed8005bc6b65fb3e05 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 01:08:53 -0600 Subject: [PATCH 063/167] fix(sni): render attention and overlay tray icon assets Load attention and overlay pixmaps from item properties, watch the corresponding update signals, and prefer attention artwork while an item is in NeedsAttention state. When an item only exports an attention movie asset, fall back to loading that asset as a static pixbuf so the tray still shows the alert state. Signed-off-by: Austin Horstman --- include/modules/sni/item.hpp | 9 ++- src/modules/sni/item.cpp | 110 +++++++++++++++++++++++++++-------- 2 files changed, 93 insertions(+), 26 deletions(-) diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 43200fdb..74d54f4c 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -48,7 +48,9 @@ class Item : public sigc::trackable { Glib::RefPtr icon_pixmap; Glib::RefPtr icon_theme; std::string overlay_icon_name; + Glib::RefPtr overlay_icon_pixmap; std::string attention_icon_name; + Glib::RefPtr attention_icon_pixmap; std::string attention_movie_name; std::string icon_theme_path; std::string menu; @@ -76,8 +78,13 @@ class Item : public sigc::trackable { const Glib::VariantContainerBase& arguments); void updateImage(); - Glib::RefPtr extractPixBuf(GVariant* variant); + static Glib::RefPtr extractPixBuf(GVariant* variant); Glib::RefPtr getIconPixbuf(); + Glib::RefPtr getAttentionIconPixbuf(); + Glib::RefPtr getOverlayIconPixbuf(); + Glib::RefPtr loadIconFromNameOrFile(const std::string& name, bool log_failure); + static Glib::RefPtr overlayPixbufs(const Glib::RefPtr&, + const Glib::RefPtr&); Glib::RefPtr getIconByName(const std::string& name, int size); double getScaledIconSize(); static void onMenuDestroyed(Item* self, GObject* old_menu_pointer); diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 9820cc62..2f368083 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -195,11 +196,11 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } else if (name == "OverlayIconName") { overlay_icon_name = get_variant(value); } else if (name == "OverlayIconPixmap") { - // TODO: overlay_icon_pixmap + overlay_icon_pixmap = extractPixBuf(value.gobj()); } else if (name == "AttentionIconName") { attention_icon_name = get_variant(value); } else if (name == "AttentionIconPixmap") { - // TODO: attention_icon_pixmap + attention_icon_pixmap = extractPixBuf(value.gobj()); } else if (name == "AttentionMovieName") { attention_movie_name = get_variant(value); } else if (name == "ToolTip") { @@ -315,8 +316,8 @@ void Item::processUpdatedProperties(Glib::RefPtr& _result) { static const std::map> signal2props = { {"NewTitle", {"Title"}}, {"NewIcon", {"IconName", "IconPixmap"}}, - // {"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}}, - // {"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}}, + {"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}}, + {"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}}, {"NewIconThemePath", {"IconThemePath"}}, {"NewToolTip", {"ToolTip"}}, {"NewStatus", {"Status"}}, @@ -406,36 +407,24 @@ void Item::updateImage() { pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); } + pixbuf = overlayPixbufs(pixbuf, getOverlayIconPixbuf()); + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image.get_scale_factor(), image.get_window()); image.set(surface); } Glib::RefPtr Item::getIconPixbuf() { - if (!icon_name.empty()) { - try { - std::ifstream temp(icon_name); - if (temp.is_open()) { - return Gdk::Pixbuf::create_from_file(icon_name); - } - } catch (Glib::Error& e) { - // Ignore because we want to also try different methods of getting an icon. - // - // But a warning is logged, as the file apparently exists, but there was - // a failure in creating a pixbuf out of it. - - spdlog::warn("Item '{}': {}", id, static_cast(e.what())); - } - - try { - // Will throw if it can not find an icon. - return getIconByName(icon_name, getScaledIconSize()); - } catch (Glib::Error& e) { - spdlog::trace("Item '{}': {}", id, static_cast(e.what())); + if (status_ == "needsattention") { + if (auto attention_pixbuf = getAttentionIconPixbuf()) { + return attention_pixbuf; } } - // Return the pixmap only if an icon for the given name could not be found. + if (auto pixbuf = loadIconFromNameOrFile(icon_name, true)) { + return pixbuf; + } + if (icon_pixmap) { return icon_pixmap; } @@ -450,6 +439,77 @@ Glib::RefPtr Item::getIconPixbuf() { return getIconByName("image-missing", getScaledIconSize()); } +Glib::RefPtr Item::getAttentionIconPixbuf() { + if (auto pixbuf = loadIconFromNameOrFile(attention_icon_name, false)) { + return pixbuf; + } + if (auto pixbuf = loadIconFromNameOrFile(attention_movie_name, false)) { + return pixbuf; + } + return attention_icon_pixmap; +} + +Glib::RefPtr Item::getOverlayIconPixbuf() { + if (auto pixbuf = loadIconFromNameOrFile(overlay_icon_name, false)) { + return pixbuf; + } + return overlay_icon_pixmap; +} + +Glib::RefPtr Item::loadIconFromNameOrFile(const std::string& name, bool log_failure) { + if (name.empty()) { + return {}; + } + + try { + std::ifstream temp(name); + if (temp.is_open()) { + return Gdk::Pixbuf::create_from_file(name); + } + } catch (const Glib::Error& e) { + if (log_failure) { + spdlog::warn("Item '{}': {}", id, static_cast(e.what())); + } + } + + try { + return getIconByName(name, getScaledIconSize()); + } catch (const Glib::Error& e) { + if (log_failure) { + spdlog::trace("Item '{}': {}", id, static_cast(e.what())); + } + } + + return {}; +} + +Glib::RefPtr Item::overlayPixbufs(const Glib::RefPtr& base, + const Glib::RefPtr& overlay) { + if (!base || !overlay) { + return base; + } + + auto composed = base->copy(); + if (!composed) { + return base; + } + + int overlay_target_size = + std::max(1, std::min(composed->get_width(), composed->get_height()) / 2); + auto scaled_overlay = overlay->scale_simple(overlay_target_size, overlay_target_size, + Gdk::InterpType::INTERP_BILINEAR); + if (!scaled_overlay) { + return composed; + } + + int dest_x = std::max(0, composed->get_width() - scaled_overlay->get_width()); + int dest_y = std::max(0, composed->get_height() - scaled_overlay->get_height()); + scaled_overlay->composite(composed, dest_x, dest_y, scaled_overlay->get_width(), + scaled_overlay->get_height(), dest_x, dest_y, 1.0, 1.0, + Gdk::InterpType::INTERP_BILINEAR, 255); + return composed; +} + Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { if (!icon_theme_path.empty()) { auto icon_info = icon_theme->lookup_icon(name.c_str(), request_size, From 8e2e437ec665af43a7d16d80b53934ea3fefe733 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 01:16:25 -0600 Subject: [PATCH 064/167] fix(sni): silence duplicate item registration warnings Some tray items re-register the same bus name and object path during normal operation. Treat that path as an idempotent registration instead of logging a warning, while still completing the DBus method successfully. Signed-off-by: Austin Horstman --- src/modules/sni/watcher.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/sni/watcher.cpp b/src/modules/sni/watcher.cpp index 66083c99..969806cf 100644 --- a/src/modules/sni/watcher.cpp +++ b/src/modules/sni/watcher.cpp @@ -98,8 +98,8 @@ gboolean Watcher::handleRegisterItem(Watcher* obj, GDBusMethodInvocation* invoca } auto watch = gfWatchFind(obj->items_, bus_name, object_path); if (watch != nullptr) { - g_warning("Status Notifier Item with bus name '%s' and object path '%s' is already registered", - bus_name, object_path); + spdlog::debug("Ignoring duplicate Status Notifier Item registration for '{}' at '{}'", bus_name, + object_path); sn_watcher_complete_register_item(obj->watcher_, invocation); return TRUE; } From 558c2753d79bcbaa87ef4c3c2cf4f63578bd6484 Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Mon, 16 Mar 2026 21:04:59 +0530 Subject: [PATCH 065/167] Add simple calendar tooltip example for clock module --- man/waybar-clock.5.scd | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index b4b5d5b7..5c5509f6 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -259,7 +259,19 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe "tooltip-format": "{tz_list}" } ``` +5. Simple calendar tooltip +``` +"clock": { +"format": "{:%H:%M}", +"tooltip-format": "{calendar}", +"calendar": { +"format": { +"today": "{}" +} +} +} +``` # STYLE - *#clock* From 3cfb62266033f8266f09d861e88845c55ac2777c Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Mon, 16 Mar 2026 23:45:28 +0530 Subject: [PATCH 066/167] Fix menu-actions GTK callback pointer handling --- src/ALabel.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 795f87f1..e6f46d94 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -116,8 +116,9 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st } submenus_[key] = GTK_MENU_ITEM(item); menuActionsMap_[key] = it->asString(); - g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), - (gpointer)menuActionsMap_[key].c_str()); + g_signal_connect(submenus_[key], "activate", + G_CALLBACK(handleGtkMenuEvent), + (gpointer)menuActionsMap_[key].c_str()); } g_object_unref(builder); } catch (std::runtime_error& e) { From acf6f117eabb92b0e3cba35d7499635ccd36c8a9 Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Tue, 17 Mar 2026 01:13:14 +0530 Subject: [PATCH 067/167] Update ALabel.cpp --- src/ALabel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index e6f46d94..7f34bca3 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -118,7 +118,7 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st menuActionsMap_[key] = it->asString(); g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), - (gpointer)menuActionsMap_[key].c_str()); + (gpointer)g_strdup(menuActionsMap_[key].c_str())); } g_object_unref(builder); } catch (std::runtime_error& e) { From 196589cf321ef4fdf848d18f1e249199bc7821e9 Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Tue, 17 Mar 2026 01:17:08 +0530 Subject: [PATCH 068/167] Update ALabel.cpp --- src/ALabel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 7f34bca3..b23926d8 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -118,7 +118,7 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st menuActionsMap_[key] = it->asString(); g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), - (gpointer)g_strdup(menuActionsMap_[key].c_str())); + (gpointer)g_strdup(menuActionsMap_[key].c_str())); } g_object_unref(builder); } catch (std::runtime_error& e) { From d046c19b855853dc08ef1bb7abdb6c16f9a420b4 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Tue, 17 Mar 2026 21:33:21 +0000 Subject: [PATCH 069/167] systemd-failed-units: small tweaks - Remove unneeded destructor impl - Rename member variables for consistench with other files - manpage wording fixes - updateData(): small logic tweak --- include/modules/systemd_failed_units.hpp | 16 ++--- man/waybar-systemd-failed-units.5.scd | 16 ++--- src/modules/systemd_failed_units.cpp | 91 +++++++++++++----------- 3 files changed, 64 insertions(+), 59 deletions(-) diff --git a/include/modules/systemd_failed_units.hpp b/include/modules/systemd_failed_units.hpp index ffb25082..fdc6eb8e 100644 --- a/include/modules/systemd_failed_units.hpp +++ b/include/modules/systemd_failed_units.hpp @@ -11,18 +11,18 @@ namespace waybar::modules { class SystemdFailedUnits : public ALabel { public: SystemdFailedUnits(const std::string&, const Json::Value&); - virtual ~SystemdFailedUnits(); + virtual ~SystemdFailedUnits() = default; auto update() -> void override; private: - bool hide_on_ok; - std::string format_ok; + bool hide_on_ok_; + std::string format_ok_; - bool update_pending; - std::string system_state, user_state, overall_state; - uint32_t nr_failed_system, nr_failed_user, nr_failed; - std::string last_status; - Glib::RefPtr system_proxy, user_proxy; + bool update_pending_; + std::string system_state_, user_state_, overall_state_; + uint32_t nr_failed_system_, nr_failed_user_, nr_failed_; + std::string last_status_; + Glib::RefPtr system_props_proxy_, user_props_proxy_; void notify_cb(const Glib::ustring& sender_name, const Glib::ustring& signal_name, const Glib::VariantContainerBase& arguments); diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd index 8d7c980a..f99283f2 100644 --- a/man/waybar-systemd-failed-units.5.scd +++ b/man/waybar-systemd-failed-units.5.scd @@ -19,7 +19,7 @@ Addressed by *systemd-failed-units* *format-ok*: ++ typeof: string ++ - This format is used when there is no failing units. + This format is used when there are no failing units. *user*: ++ typeof: bool ++ @@ -34,15 +34,15 @@ Addressed by *systemd-failed-units* *hide-on-ok*: ++ typeof: bool ++ default: *true* ++ - Option to hide this module when there is no failing units. + Option to hide this module when there are no failed units. *menu*: ++ typeof: string ++ - Action that popups the menu. + Action that pops up the menu. *menu-file*: ++ typeof: string ++ - Location of the menu descriptor file. There need to be an element of type + Location of the menu descriptor file. There needs to be an element of type GtkMenu with id *menu* *menu-actions*: ++ @@ -52,7 +52,7 @@ Addressed by *systemd-failed-units* *expand*: ++ typeof: bool ++ default: false ++ - Enables this module to consume all left over space dynamically. + Enables this module to consume all leftover space dynamically. # FORMAT REPLACEMENTS @@ -62,11 +62,11 @@ Addressed by *systemd-failed-units* *{nr_failed}*: Number of total failed units. -*{systemd_state}:* State of the systemd system session +*{system_state}:* State of the systemd system session. -*{user_state}:* State of the systemd user session +*{user_state}:* State of the systemd user session. -*{overall_state}:* Overall state of the systemd and user session. ("Ok" or "Degraded") +*{overall_state}:* Overall state of the systemd and user session. ("ok" or "degraded") # EXAMPLES diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 68e61fe9..44e66393 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -1,10 +1,12 @@ #include "modules/systemd_failed_units.hpp" +#include #include #include #include #include +#include static const unsigned UPDATE_DEBOUNCE_TIME_MS = 1000; @@ -12,39 +14,41 @@ namespace waybar::modules { SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& config) : ALabel(config, "systemd-failed-units", id, "{nr_failed} failed", 1), - hide_on_ok(true), - update_pending(false), - nr_failed_system(0), - nr_failed_user(0), - nr_failed(0), - last_status() { + hide_on_ok_(true), + update_pending_(false), + nr_failed_system_(0), + nr_failed_user_(0), + nr_failed_(0), + last_status_() { if (config["hide-on-ok"].isBool()) { - hide_on_ok = config["hide-on-ok"].asBool(); + hide_on_ok_ = config["hide-on-ok"].asBool(); } if (config["format-ok"].isString()) { - format_ok = config["format-ok"].asString(); + format_ok_ = config["format-ok"].asString(); } else { - format_ok = format_; + format_ok_ = format_; } /* Default to enable both "system" and "user". */ if (!config["system"].isBool() || config["system"].asBool()) { - system_proxy = Gio::DBus::Proxy::create_for_bus_sync( + system_props_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1", "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties"); - if (!system_proxy) { + if (!system_props_proxy_) { throw std::runtime_error("Unable to connect to systemwide systemd DBus!"); } - system_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + system_props_proxy_->signal_signal().connect( + sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); } if (!config["user"].isBool() || config["user"].asBool()) { - user_proxy = Gio::DBus::Proxy::create_for_bus_sync( + user_props_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1", "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties"); - if (!user_proxy) { + if (!user_props_proxy_) { throw std::runtime_error("Unable to connect to user systemd DBus!"); } - user_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + user_props_proxy_->signal_signal().connect( + sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); } updateData(); @@ -52,16 +56,11 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& dp.emit(); } -SystemdFailedUnits::~SystemdFailedUnits() { - if (system_proxy) system_proxy.reset(); - if (user_proxy) user_proxy.reset(); -} - auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, const Glib::ustring& signal_name, const Glib::VariantContainerBase& arguments) -> void { - if (signal_name == "PropertiesChanged" && !update_pending) { - update_pending = true; + if (signal_name == "PropertiesChanged" && !update_pending_) { + update_pending_ = true; /* The fail count may fluctuate due to restarting. */ Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &SystemdFailedUnits::updateData), UPDATE_DEBOUNCE_TIME_MS); @@ -88,12 +87,12 @@ void SystemdFailedUnits::RequestSystemState() { return "unknown"; }; - system_state = load("systemwide", system_proxy); - user_state = load("user", user_proxy); - if (system_state == "running" && user_state == "running") - overall_state = "ok"; + system_state_ = load("systemwide", system_props_proxy_); + user_state_ = load("user", user_props_proxy_); + if (system_state_ == "running" && user_state_ == "running") + overall_state_ = "ok"; else - overall_state = "degraded"; + overall_state_ = "degraded"; } void SystemdFailedUnits::RequestFailedUnits() { @@ -116,25 +115,31 @@ void SystemdFailedUnits::RequestFailedUnits() { return 0; }; - nr_failed_system = load("systemwide", system_proxy); - nr_failed_user = load("user", user_proxy); - nr_failed = nr_failed_system + nr_failed_user; + nr_failed_system_ = load("systemwide", system_props_proxy_); + nr_failed_user_ = load("user", user_props_proxy_); + nr_failed_ = nr_failed_system_ + nr_failed_user_; } void SystemdFailedUnits::updateData() { - update_pending = false; + update_pending_ = false; RequestSystemState(); - if (overall_state == "degraded") RequestFailedUnits(); + if (overall_state_ == "degraded") { + RequestFailedUnits(); + } else { + nr_failed_system_ = 0; + nr_failed_user_ = 0; + nr_failed_ = 0; + } dp.emit(); } auto SystemdFailedUnits::update() -> void { - if (last_status == overall_state) return; + if (last_status_ == overall_state_) return; // Hide if needed. - if (overall_state == "ok" && hide_on_ok) { + if (overall_state_ == "ok" && hide_on_ok_) { event_box_.set_visible(false); return; } @@ -142,20 +147,20 @@ auto SystemdFailedUnits::update() -> void { event_box_.set_visible(true); // Set state class. - if (!last_status.empty() && label_.get_style_context()->has_class(last_status)) { - label_.get_style_context()->remove_class(last_status); + if (!last_status_.empty() && label_.get_style_context()->has_class(last_status_)) { + label_.get_style_context()->remove_class(last_status_); } - if (!label_.get_style_context()->has_class(overall_state)) { - label_.get_style_context()->add_class(overall_state); + if (!label_.get_style_context()->has_class(overall_state_)) { + label_.get_style_context()->add_class(overall_state_); } - last_status = overall_state; + last_status_ = overall_state_; label_.set_markup(fmt::format( - fmt::runtime(nr_failed == 0 ? format_ok : format_), fmt::arg("nr_failed", nr_failed), - fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user), - fmt::arg("system_state", system_state), fmt::arg("user_state", user_state), - fmt::arg("overall_state", overall_state))); + fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_), + fmt::arg("nr_failed_system", nr_failed_system_), + fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), + fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_))); ALabel::update(); } From 3b1262061da2276c208c7241205d6326deda8c58 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Tue, 17 Mar 2026 21:33:41 +0000 Subject: [PATCH 070/167] systemd-failed-units: tooltip w/ failed units list --- include/modules/systemd_failed_units.hpp | 20 +++ man/waybar-systemd-failed-units.5.scd | 29 +++++ src/modules/systemd_failed_units.cpp | 148 ++++++++++++++++++++++- 3 files changed, 192 insertions(+), 5 deletions(-) diff --git a/include/modules/systemd_failed_units.hpp b/include/modules/systemd_failed_units.hpp index fdc6eb8e..3eda799a 100644 --- a/include/modules/systemd_failed_units.hpp +++ b/include/modules/systemd_failed_units.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "ALabel.hpp" @@ -15,19 +16,38 @@ class SystemdFailedUnits : public ALabel { auto update() -> void override; private: + struct FailedUnit { + std::string name; + std::string description; + std::string load_state; + std::string active_state; + std::string sub_state; + std::string scope; + }; + bool hide_on_ok_; std::string format_ok_; + std::string tooltip_format_; + std::string tooltip_format_ok_; + std::string tooltip_unit_format_; bool update_pending_; std::string system_state_, user_state_, overall_state_; uint32_t nr_failed_system_, nr_failed_user_, nr_failed_; std::string last_status_; Glib::RefPtr system_props_proxy_, user_props_proxy_; + Glib::RefPtr system_manager_proxy_, user_manager_proxy_; + std::vector failed_units_; void notify_cb(const Glib::ustring& sender_name, const Glib::ustring& signal_name, const Glib::VariantContainerBase& arguments); void RequestFailedUnits(); + void RequestFailedUnitsList(); void RequestSystemState(); + std::vector LoadFailedUnitsList(const char* kind, + Glib::RefPtr& proxy, + const std::string& scope); + std::string BuildTooltipFailedList() const; void updateData(); }; diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd index f99283f2..aaeee2d1 100644 --- a/man/waybar-systemd-failed-units.5.scd +++ b/man/waybar-systemd-failed-units.5.scd @@ -36,6 +36,21 @@ Addressed by *systemd-failed-units* default: *true* ++ Option to hide this module when there are no failed units. +*tooltip-format*: ++ + typeof: string ++ + default: *System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n{failed_units_list}* ++ + Tooltip format shown when there are failed units. + +*tooltip-format-ok*: ++ + typeof: string ++ + default: *System: {system_state}\nUser: {user_state}* ++ + Tooltip format used when there are no failed units. + +*tooltip-unit-format*: ++ + typeof: string ++ + default: *{name}: {description}* ++ + Format used to render each failed unit inside the tooltip. Each item is prefixed with a bullet. + *menu*: ++ typeof: string ++ Action that pops up the menu. @@ -68,6 +83,18 @@ Addressed by *systemd-failed-units* *{overall_state}:* Overall state of the systemd and user session. ("ok" or "degraded") +*{failed_units_list}:* Bulleted list of failed units using *tooltip-unit-format*. Empty when +there are no failed units. + +The *tooltip-unit-format* string supports the following replacements: + +*{name}*: Unit name ++ +*{description}*: Unit description ++ +*{load_state}*: Unit load state ++ +*{active_state}*: Unit active state ++ +*{sub_state}*: Unit sub state ++ +*{scope}*: Either *system* or *user* depending on where the unit originated + # EXAMPLES ``` @@ -77,6 +104,8 @@ Addressed by *systemd-failed-units* "format-ok": "✓", "system": true, "user": false, + "tooltip-format": "{nr_failed} failed units:\n{failed_units_list}", + "tooltip-unit-format": "{scope}: {name} ({active_state})", } ``` diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 44e66393..600d0fb1 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -2,11 +2,14 @@ #include #include +#include #include #include #include +#include #include +#include static const unsigned UPDATE_DEBOUNCE_TIME_MS = 1000; @@ -15,6 +18,10 @@ namespace waybar::modules { SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& config) : ALabel(config, "systemd-failed-units", id, "{nr_failed} failed", 1), hide_on_ok_(true), + tooltip_format_("System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n" + "{failed_units_list}"), + tooltip_format_ok_("System: {system_state}\nUser: {user_state}"), + tooltip_unit_format_("{name}: {description}"), update_pending_(false), nr_failed_system_(0), nr_failed_user_(0), @@ -29,6 +36,16 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& format_ok_ = format_; } + if (config["tooltip-format"].isString()) { + tooltip_format_ = config["tooltip-format"].asString(); + } + if (config["tooltip-format-ok"].isString()) { + tooltip_format_ok_ = config["tooltip-format-ok"].asString(); + } + if (config["tooltip-unit-format"].isString()) { + tooltip_unit_format_ = config["tooltip-unit-format"].asString(); + } + /* Default to enable both "system" and "user". */ if (!config["system"].isBool() || config["system"].asBool()) { system_props_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( @@ -39,6 +56,14 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& } system_props_proxy_->signal_signal().connect( sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + try { + system_manager_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager"); + } catch (const Glib::Error& e) { + spdlog::warn("Unable to connect to systemwide systemd Manager interface: {}", + e.what().c_str()); + } } if (!config["user"].isBool() || config["user"].asBool()) { user_props_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( @@ -49,6 +74,13 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& } user_props_proxy_->signal_signal().connect( sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); + try { + user_manager_proxy_ = Gio::DBus::Proxy::create_for_bus_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager"); + } catch (const Glib::Error& e) { + spdlog::warn("Unable to connect to user systemd Manager interface: {}", e.what().c_str()); + } } updateData(); @@ -120,27 +152,117 @@ void SystemdFailedUnits::RequestFailedUnits() { nr_failed_ = nr_failed_system_ + nr_failed_user_; } +void SystemdFailedUnits::RequestFailedUnitsList() { + failed_units_.clear(); + if (!tooltipEnabled() || nr_failed_ == 0) { + return; + } + + if (system_manager_proxy_) { + auto units = LoadFailedUnitsList("systemwide", system_manager_proxy_, "system"); + failed_units_.insert(failed_units_.end(), units.begin(), units.end()); + } + if (user_manager_proxy_) { + auto units = LoadFailedUnitsList("user", user_manager_proxy_, "user"); + failed_units_.insert(failed_units_.end(), units.begin(), units.end()); + } +} + +auto SystemdFailedUnits::LoadFailedUnitsList(const char* kind, + Glib::RefPtr& proxy, + const std::string& scope) + -> std::vector { + // org.freedesktop.systemd1.Manager.ListUnits returns + // (name, description, load_state, active_state, sub_state, followed, unit_path, job_id, + // job_type, job_path). + using UnitRow = std::tuple; + using ListUnitsReply = Glib::Variant>>; + + std::vector units; + if (!proxy) { + return units; + } + + try { + auto data = proxy->call_sync("ListUnits"); + if (!data) return units; + if (!data.is_of_type(ListUnitsReply::variant_type())) { + spdlog::warn("Unexpected DBus signature for ListUnits: {}", data.get_type_string()); + return units; + } + + auto [rows] = Glib::VariantBase::cast_dynamic(data).get(); + for (const auto& row : rows) { + const auto& name = std::get<0>(row); + const auto& description = std::get<1>(row); + const auto& load_state = std::get<2>(row); + const auto& active_state = std::get<3>(row); + const auto& sub_state = std::get<4>(row); + if (active_state == "failed" || sub_state == "failed") { + units.push_back({name, description, load_state, active_state, sub_state, scope}); + } + } + } catch (const Glib::Error& e) { + spdlog::error("Failed to list {} units: {}", kind, e.what().c_str()); + } + + return units; +} + +std::string SystemdFailedUnits::BuildTooltipFailedList() const { + if (failed_units_.empty()) { + return ""; + } + + std::string list; + list.reserve(failed_units_.size() * 16); + bool first = true; + for (const auto& unit : failed_units_) { + try { + auto line = fmt::format( + fmt::runtime(tooltip_unit_format_), + fmt::arg("name", Glib::Markup::escape_text(unit.name).raw()), + fmt::arg("description", Glib::Markup::escape_text(unit.description).raw()), + fmt::arg("load_state", unit.load_state), + fmt::arg("active_state", unit.active_state), + fmt::arg("sub_state", unit.sub_state), + fmt::arg("scope", unit.scope)); + if (!first) { + list += "\n"; + } + first = false; + list += "- "; + list += line; + } catch (const std::exception& e) { + spdlog::warn("Failed to format tooltip for unit {}: {}", unit.name, e.what()); + } + } + + return list; +} + void SystemdFailedUnits::updateData() { update_pending_ = false; RequestSystemState(); if (overall_state_ == "degraded") { RequestFailedUnits(); + RequestFailedUnitsList(); } else { - nr_failed_system_ = 0; - nr_failed_user_ = 0; - nr_failed_ = 0; + nr_failed_system_ = nr_failed_user_ = nr_failed_ = 0; + failed_units_.clear(); } dp.emit(); } auto SystemdFailedUnits::update() -> void { - if (last_status_ == overall_state_) return; - // Hide if needed. if (overall_state_ == "ok" && hide_on_ok_) { event_box_.set_visible(false); + last_status_ = overall_state_; return; } @@ -161,6 +283,22 @@ auto SystemdFailedUnits::update() -> void { fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_))); + if (tooltipEnabled()) { + std::string failed_list = BuildTooltipFailedList(); + auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_; + if (!tooltip_template.empty()) { + label_.set_tooltip_markup( + fmt::format(fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_), + fmt::arg("nr_failed_system", nr_failed_system_), + fmt::arg("nr_failed_user", nr_failed_user_), + fmt::arg("system_state", system_state_), + fmt::arg("user_state", user_state_), + fmt::arg("overall_state", overall_state_), + fmt::arg("failed_units_list", failed_list))); + } else { + label_.set_tooltip_text(""); + } + } ALabel::update(); } From e7c077ab9a5cd959bd412d18df84462cbd97e3b1 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Tue, 17 Mar 2026 21:39:55 +0000 Subject: [PATCH 071/167] clang-format --- src/modules/systemd_failed_units.cpp | 34 ++++++++++++---------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 600d0fb1..8d7e866f 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -18,8 +18,9 @@ namespace waybar::modules { SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& config) : ALabel(config, "systemd-failed-units", id, "{nr_failed} failed", 1), hide_on_ok_(true), - tooltip_format_("System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n" - "{failed_units_list}"), + tooltip_format_( + "System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n" + "{failed_units_list}"), tooltip_format_ok_("System: {system_state}\nUser: {user_state}"), tooltip_unit_format_("{name}: {description}"), update_pending_(false), @@ -170,8 +171,7 @@ void SystemdFailedUnits::RequestFailedUnitsList() { auto SystemdFailedUnits::LoadFailedUnitsList(const char* kind, Glib::RefPtr& proxy, - const std::string& scope) - -> std::vector { + const std::string& scope) -> std::vector { // org.freedesktop.systemd1.Manager.ListUnits returns // (name, description, load_state, active_state, sub_state, followed, unit_path, job_id, // job_type, job_path). @@ -225,10 +225,8 @@ std::string SystemdFailedUnits::BuildTooltipFailedList() const { fmt::runtime(tooltip_unit_format_), fmt::arg("name", Glib::Markup::escape_text(unit.name).raw()), fmt::arg("description", Glib::Markup::escape_text(unit.description).raw()), - fmt::arg("load_state", unit.load_state), - fmt::arg("active_state", unit.active_state), - fmt::arg("sub_state", unit.sub_state), - fmt::arg("scope", unit.scope)); + fmt::arg("load_state", unit.load_state), fmt::arg("active_state", unit.active_state), + fmt::arg("sub_state", unit.sub_state), fmt::arg("scope", unit.scope)); if (!first) { list += "\n"; } @@ -280,21 +278,19 @@ auto SystemdFailedUnits::update() -> void { label_.set_markup(fmt::format( fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_), - fmt::arg("nr_failed_system", nr_failed_system_), - fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), - fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_))); + fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_), + fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_), + fmt::arg("overall_state", overall_state_))); if (tooltipEnabled()) { std::string failed_list = BuildTooltipFailedList(); auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_; if (!tooltip_template.empty()) { - label_.set_tooltip_markup( - fmt::format(fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_), - fmt::arg("nr_failed_system", nr_failed_system_), - fmt::arg("nr_failed_user", nr_failed_user_), - fmt::arg("system_state", system_state_), - fmt::arg("user_state", user_state_), - fmt::arg("overall_state", overall_state_), - fmt::arg("failed_units_list", failed_list))); + label_.set_tooltip_markup(fmt::format( + fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_), + fmt::arg("nr_failed_system", nr_failed_system_), + fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), + fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_), + fmt::arg("failed_units_list", failed_list))); } else { label_.set_tooltip_text(""); } From c62e987dbd4f8e402f625cdd61cfc2c9dd3c376f Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:22:34 +0530 Subject: [PATCH 072/167] Update waybar.5.scd.in --- man/waybar.5.scd.in | 1 + 1 file changed, 1 insertion(+) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 624798ed..7223daec 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -168,6 +168,7 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( # MODULE FORMAT You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). +Note: Tooltip appearance in Waybar is controlled globally via CSS (e.g., `tooltip { ... }`). It is currently not possible to apply tooltip styles per module. e.g. From 215c952137f614e71b1485dfde2c3968477bef3d Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Thu, 19 Mar 2026 00:28:50 -0500 Subject: [PATCH 073/167] chore: remove heaptrack dump Accidentally committed... Signed-off-by: Austin Horstman --- heaptrack.waybar.2711357.zst | Bin 12780 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 heaptrack.waybar.2711357.zst diff --git a/heaptrack.waybar.2711357.zst b/heaptrack.waybar.2711357.zst deleted file mode 100644 index 88921cba1ce1ab95825bb77f62a6f1a72e958a87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12780 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 From 86234a8946787798ba39530ea3c74b017efe87d2 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Thu, 19 Mar 2026 00:29:01 -0500 Subject: [PATCH 074/167] feat: gitignore heaptrack dumps Signed-off-by: Austin Horstman --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f42bf85e..f21700d6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ result-* .ccls-cache _codeql_detected_source_root +heaptrack* From 95f9922ccc6e931d161b6d846f83b747f63b01af Mon Sep 17 00:00:00 2001 From: Viktar Lukashonak Date: Thu, 19 Mar 2026 11:03:08 +0300 Subject: [PATCH 075/167] cava bump --- nix/default.nix | 6 +++--- subprojects/libcava.wrap | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nix/default.nix b/nix/default.nix index 43b6e097..1850a414 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -6,13 +6,13 @@ }: let libcava = rec { - version = "0.10.7-beta"; + version = "0.10.7"; src = pkgs.fetchFromGitHub { owner = "LukashonakV"; repo = "cava"; # NOTE: Needs to match the cava.wrap - tag = "v${version}"; - hash = "sha256-IX1B375gTwVDRjpRfwKGuzTAZOV2pgDWzUd4bW2cTDU="; + tag = "${version}"; + hash = "sha256-zkyj1vBzHtoypX4Bxdh1Vmwh967DKKxN751v79hzmgQ="; }; }; in diff --git a/subprojects/libcava.wrap b/subprojects/libcava.wrap index 466e2262..d66d657b 100644 --- a/subprojects/libcava.wrap +++ b/subprojects/libcava.wrap @@ -4,9 +4,9 @@ #depth = 1 [wrap-file] -directory = cava-0.10.7-beta -source_url = https://github.com/LukashonakV/cava/archive/v0.10.7-beta.tar.gz +directory = cava-0.10.7 +source_url = https://github.com/LukashonakV/cava/archive/0.10.7.tar.gz source_filename = cava-0.10.7.tar.gz -source_hash = 8915d7214f2046554c158fe6f2ae518881dfb573e421ea848727be11a5dfa8c4 +source_hash = 50cc6413e9c96c503657f814744a2baf429a24ff9fed31a8343e0ed285269eff [provide] libcava = cava_dep From 50c1431348ca544e47b6397e9891cd3bc7b33c7d Mon Sep 17 00:00:00 2001 From: Jakob Sjudin Date: Thu, 19 Mar 2026 15:36:32 +0100 Subject: [PATCH 076/167] Add start-expanded option to group --- src/group.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/group.cpp b/src/group.cpp index 51caf0e2..acd7bd42 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -64,11 +64,19 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& : true); click_to_reveal = drawer_config["click-to-reveal"].asBool(); + const bool start_expanded = + (drawer_config["start-expanded"].isBool() ? drawer_config["start-expanded"].asBool() + : false); + auto transition_type = getPreferredTransitionType(vertical); revealer.set_transition_type(transition_type); revealer.set_transition_duration(transition_duration); - revealer.set_reveal_child(false); + revealer.set_reveal_child(start_expanded); + + if (start_expanded) { + box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + } revealer.get_style_context()->add_class("drawer"); From 83e1949dd8435d5f8991de366a8327a2a6e58786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Baha=20Y=C4=B1ld=C4=B1r=C4=B1m?= Date: Thu, 19 Mar 2026 19:32:21 +0300 Subject: [PATCH 077/167] fix(hyprland/window): Fix segfault caused by use-after-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The window module registers itself with the Hyprland IPC singleton at the start of its constructor, before calling update(). If update() throws an exception (e.g. from an invalid format string), the object is destroyed without the destructor running, leaving a dangling pointer in the IPC callback list. When the IPC thread receives an event, it attempts to call onEvent() on this invalid memory, causing a crash. Moving the update() call before IPC registration ensures that any initialization errors occur before the pointer is shared. If the configuration is invalid, the module fails to construct and is gracefully disabled by the factory without leaving a "landmine" in the background IPC thread. Fixes: #4923 Signed-off-by: Emir Baha Yıldırım --- src/modules/hyprland/window.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index d3fe6edf..2e3e0bb9 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -19,20 +19,19 @@ std::shared_mutex windowIpcSmtx; Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), m_ipc(IPC::inst()) { - std::unique_lock windowIpcUniqueLock(windowIpcSmtx); - separateOutputs_ = config["separate-outputs"].asBool(); + update(); + // register for hyprland ipc + std::unique_lock windowIpcUniqueLock(windowIpcSmtx); m_ipc.registerForIPC("activewindow", this); m_ipc.registerForIPC("closewindow", this); m_ipc.registerForIPC("movewindow", this); m_ipc.registerForIPC("changefloatingmode", this); m_ipc.registerForIPC("fullscreen", this); - windowIpcUniqueLock.unlock(); - update(); dp.emit(); } From a05c7bc28ffaf889826102cb8e9ac75e9577b4ce Mon Sep 17 00:00:00 2001 From: ignormies Date: Thu, 19 Mar 2026 17:39:57 -0700 Subject: [PATCH 078/167] chore: Stop using deprecated/removed Nerdfonts codepoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These Nerdfonts codepoints were removed in [3.0.0](https://www.nerdfonts.com/releases#v3.0.0): > **Breaking 2: Material Design Icons Codepoints** > > The old Material Design Icon codepoints are finally dropped. Due to an historic mistake we placed them in between some asiatic glyphs, breaking that script. Since v2.3.0 the (updated and expanded) Material Design Icons have new codepoints in the 5 digit region. > > - Dropped codepoints `F500`… and class names `nf-mdi-*` > - New codepoints `F0001`… and class names `nf-md-*` > - The whole discussions are here: https://github.com/ryanoasis/nerd-fonts/issues/365 > - A translation table is available here: https://github.com/ryanoasis/nerd-fonts/issues/1059#issuecomment-1404891287 > - There are tools out there that probably can update your configuration. For the majority of the removed symbols, it was as easy as finding the replacement with the exact same name. For example, `f76b` (`nf-mdi-format_header_2`) became `f026c` (`nf-md-format_header_2`). There was one symbol that was completely removed (it was removed from Material Design): `f5fc` (`nf-mdi-camcorder_box`). I have substituted it with `f03d` (`nf-fa-video_camera`) which is not Material Design, but the closest icon I could find. Here's some example output from [nerdfix](https://github.com/loichyan/nerdfix) ``` ╭─(ignormies)(~/g/fork-waybar)  removed-nerdfonts-codepoints ╰──▪ nix-shell -p nerdfix --run "nerdfix check resources/config.jsonc" INFO Check input from 'resources/config.jsonc' ☞ Found obsolete icon U+F76B ╭─[resources/config.jsonc:131:27] 130 │ "format": "{temperatureC}°C {icon}", 131 │ "format-icons": ["", "", ""] · ┬ · ╰── Icon 'mdi-format_header_2' is marked as obsolete 132 │ }, ╰──── help: You could replace it with: 1. 󰉬 U+F026C md-format_header_2 2. 󰉫 U+F026B md-format_header_1 3. 󰉭 U+F026D md-format_header_3 4. 󰉮 U+F026E md-format_header_4 ☞ Found obsolete icon U+F769 ╭─[resources/config.jsonc:131:41] 130 │ "format": "{temperatureC}°C {icon}", 131 │ "format-icons": ["", "", ""] · ┬ · ╰── Icon 'mdi-format_float_right' is marked as obsolete 132 │ }, ╰──── help: You could replace it with: 1. 󰉪 U+F026A md-format_float_right 2. 󰉨 U+F0268 md-format_float_left 3. 󰉩 U+F0269 md-format_float_none 4. 󰉣 U+F0263 md-format_align_right ☞ Found obsolete icon U+F5E7 ╭─[resources/config.jsonc:146:41] 145 │ "format-full": "{capacity}% {icon}", 146 │ "format-charging": "{capacity}% ", · ┬ · ╰── Icon 'mdi-cached' is marked as obsolete 147 │ "format-plugged": "{capacity}% ", ╰──── help: You could replace it with: 1. 󰃨 U+F00E8 md-cached ☞ Found obsolete icon U+F796 ╭─[resources/config.jsonc:170:45] 169 │ "format-wifi": "{essid} ({signalStrength}%) ", 170 │ "format-ethernet": "{ipaddr}/{cidr} ", · ┬ · ╰── Icon 'mdi-gamepad_variant' is marked as obsolete 171 │ "tooltip-format": "{ifname} via {gwaddr} ", ╰──── help: You could replace it with: 1. 󰊗 U+F0297 md-gamepad_variant 2. 󰺷 U+F0EB7 md-gamepad_variant_outline 3. 󰑢 U+F0462 md-road_variant 4. 󰉜 U+F025C md-food_variant ☞ Found obsolete icon U+F796 ╭─[resources/config.jsonc:171:50] 170 │ "format-ethernet": "{ipaddr}/{cidr} ", 171 │ "tooltip-format": "{ifname} via {gwaddr} ", · ┬ · ╰── Icon 'mdi-gamepad_variant' is marked as obsolete 172 │ "format-linked": "{ifname} (No IP) ", ╰──── help: You could replace it with: 1. 󰊗 U+F0297 md-gamepad_variant 2. 󰺷 U+F0EB7 md-gamepad_variant_outline 3. 󰑢 U+F0462 md-road_variant 4. 󰉜 U+F025C md-food_variant ☞ Found obsolete icon U+F796 ╭─[resources/config.jsonc:172:44] 171 │ "tooltip-format": "{ifname} via {gwaddr} ", 172 │ "format-linked": "{ifname} (No IP) ", · ┬ · ╰── Icon 'mdi-gamepad_variant' is marked as obsolete 173 │ "format-disconnected": "Disconnected ⚠", ╰──── help: You could replace it with: 1. 󰊗 U+F0297 md-gamepad_variant 2. 󰺷 U+F0EB7 md-gamepad_variant_outline 3. 󰑢 U+F0462 md-road_variant 4. 󰉜 U+F025C md-food_variant ☞ Found obsolete icon U+F6A9 ╭─[resources/config.jsonc:180:36] 179 │ "format-bluetooth": "{volume}% {icon} {format_source}", 180 │ "format-bluetooth-muted": " {icon} {format_source}", · ┬ · ╰── Icon 'mdi-cup' is marked as obsolete 181 │ "format-muted": " {format_source}", ╰──── help: You could replace it with: 1. 󰆪 U+F01AA md-cup ☞ Found obsolete icon U+F6A9 ╭─[resources/config.jsonc:181:26] 180 │ "format-bluetooth-muted": " {icon} {format_source}", 181 │ "format-muted": " {format_source}", · ┬ · ╰── Icon 'mdi-cup' is marked as obsolete 182 │ "format-source": "{volume}% ", ╰──── help: You could replace it with: 1. 󰆪 U+F01AA md-cup ☞ Found obsolete icon U+F590 ╭─[resources/config.jsonc:186:28] 185 │ "headphone": "", 186 │ "hands-free": "", · ┬ · ╰── Icon 'mdi-battery_unknown' is marked as obsolete 187 │ "headset": "", ╰──── help: You could replace it with: 1. 󰂑 U+F0091 md-battery_unknown 2. 󰥊 U+F094A md-battery_unknown_bluetooth 3. 󱟞 U+F17DE md-battery_arrow_down 4. 󰝐 U+F0750 md-microsoft_xbox_controller_battery_unknown ☞ Found obsolete icon U+F590 ╭─[resources/config.jsonc:187:25] 186 │ "hands-free": "", 187 │ "headset": "", · ┬ · ╰── Icon 'mdi-battery_unknown' is marked as obsolete 188 │ "phone": "", ╰──── help: You could replace it with: 1. 󰂑 U+F0091 md-battery_unknown 2. 󰥊 U+F094A md-battery_unknown_bluetooth 3. 󱟞 U+F17DE md-battery_arrow_down 4. 󰝐 U+F0750 md-microsoft_xbox_controller_battery_unknown ``` --- include/modules/gamemode.hpp | 2 +- man/waybar-clock.5.scd | 4 ++-- man/waybar-gamemode.5.scd | 4 ++-- man/waybar-pulseaudio.5.scd | 4 ++-- man/waybar-river-mode.5.scd | 2 +- man/waybar-sway-mode.5.scd | 2 +- man/waybar-wireplumber.5.scd | 2 +- resources/config.jsonc | 18 +++++++++--------- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/include/modules/gamemode.hpp b/include/modules/gamemode.hpp index f63861a3..89f66272 100644 --- a/include/modules/gamemode.hpp +++ b/include/modules/gamemode.hpp @@ -26,7 +26,7 @@ class Gamemode : public AModule { const std::string DEFAULT_FORMAT = "{glyph}"; const std::string DEFAULT_FORMAT_ALT = "{glyph} {count}"; const std::string DEFAULT_TOOLTIP_FORMAT = "Games running: {count}"; - const std::string DEFAULT_GLYPH = ""; + const std::string DEFAULT_GLYPH = "󰊴"; void appear(const Glib::RefPtr& connection, const Glib::ustring& name, const Glib::ustring& name_owner); diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 5c5509f6..1b13dcb7 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -204,7 +204,7 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe ``` "clock": { "format": "{:%H:%M}  ", - "format-alt": "{:%A, %B %d, %Y (%R)}  ", + "format-alt": "{:%A, %B %d, %Y (%R)} 󰃰 ", "tooltip-format": "{calendar}", "calendar": { "mode" : "year", @@ -299,7 +299,7 @@ Example of working config ``` "clock": { "format": "{:%H:%M}  ", - "format-alt": "{:%A, %B %d, %Y (%R)}  ", + "format-alt": "{:%A, %B %d, %Y (%R)} 󰃰 ", "tooltip-format": "\n{calendar}", "calendar": { "mode" : "year", diff --git a/man/waybar-gamemode.5.scd b/man/waybar-gamemode.5.scd index a6ca9af0..bb82e0aa 100644 --- a/man/waybar-gamemode.5.scd +++ b/man/waybar-gamemode.5.scd @@ -43,7 +43,7 @@ Feral Gamemode optimizations. *glyph*: ++ typeof: string ++ - default:  ++ + default: 󰊴 ++ The string icon to display. Only visible if *use-icon* is set to false. *icon-name*: ++ @@ -82,7 +82,7 @@ Feral Gamemode optimizations. "gamemode": { "format": "{glyph}", "format-alt": "{glyph} {count}", - "glyph": "", + "glyph": "󰊴", "hide-not-running": true, "use-icon": true, "icon-name": "input-gaming-symbolic", diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index f555fd4d..a52d5b3e 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -178,8 +178,8 @@ to be selected when the corresponding audio device is muted. This applies to *de "alsa_output.pci-0000_00_1f.3.analog-stereo": "", "alsa_output.pci-0000_00_1f.3.analog-stereo-muted": "", "headphone": "", - "hands-free": "", - "headset": "", + "hands-free": "󰂑", + "headset": "󰂑", "phone": "", "phone-muted": "", "portable": "", diff --git a/man/waybar-river-mode.5.scd b/man/waybar-river-mode.5.scd index 5837411d..ac209f2c 100644 --- a/man/waybar-river-mode.5.scd +++ b/man/waybar-river-mode.5.scd @@ -87,7 +87,7 @@ Addressed by *river/mode* ``` "river/mode": { - "format": " {}" + "format": " {}" } ``` diff --git a/man/waybar-sway-mode.5.scd b/man/waybar-sway-mode.5.scd index 52827376..2c5ba7e6 100644 --- a/man/waybar-sway-mode.5.scd +++ b/man/waybar-sway-mode.5.scd @@ -92,7 +92,7 @@ Addressed by *sway/mode* ``` "sway/mode": { - "format": " {}", + "format": " {}", "max-length": 50 } ``` diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index ae78f184..8d97d7af 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -128,7 +128,7 @@ The *wireplumber* module displays the current volume reported by WirePlumber. ``` "wireplumber#sink": { "format": "{volume}% {icon}", - "format-muted": "", + "format-muted": "󰅶", "format-icons": ["", "", ""], "on-click": "helvum", "on-click-right": "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle", diff --git a/resources/config.jsonc b/resources/config.jsonc index 67d5ff5b..9ba8b97a 100644 --- a/resources/config.jsonc +++ b/resources/config.jsonc @@ -128,7 +128,7 @@ "critical-threshold": 80, // "format-critical": "{temperatureC}°C {icon}", "format": "{temperatureC}°C {icon}", - "format-icons": ["", "", ""] + "format-icons": ["󰉬", "", "󰉪"] }, "backlight": { // "device": "acpi_video1", @@ -143,7 +143,7 @@ }, "format": "{capacity}% {icon}", "format-full": "{capacity}% {icon}", - "format-charging": "{capacity}% ", + "format-charging": "{capacity}% 󰃨", "format-plugged": "{capacity}% ", "format-alt": "{time} {icon}", // "format-good": "", // An empty format will hide the module @@ -167,9 +167,9 @@ "network": { // "interface": "wlp2*", // (Optional) To force the use of this interface "format-wifi": "{essid} ({signalStrength}%) ", - "format-ethernet": "{ipaddr}/{cidr} ", - "tooltip-format": "{ifname} via {gwaddr} ", - "format-linked": "{ifname} (No IP) ", + "format-ethernet": "{ipaddr}/{cidr} 󰊗", + "tooltip-format": "{ifname} via {gwaddr} 󰊗", + "format-linked": "{ifname} (No IP) 󰊗", "format-disconnected": "Disconnected ⚠", "format-alt": "{ifname}: {ipaddr}/{cidr}" }, @@ -177,14 +177,14 @@ // "scroll-step": 1, // %, can be a float "format": "{volume}% {icon} {format_source}", "format-bluetooth": "{volume}% {icon} {format_source}", - "format-bluetooth-muted": " {icon} {format_source}", - "format-muted": " {format_source}", + "format-bluetooth-muted": "󰅶 {icon} {format_source}", + "format-muted": "󰅶 {format_source}", "format-source": "{volume}% ", "format-source-muted": "", "format-icons": { "headphone": "", - "hands-free": "", - "headset": "", + "hands-free": "󰂑", + "headset": "󰂑", "phone": "", "portable": "", "car": "", From 6afe10864279dd09a66c13d222f61e9d1bcaa260 Mon Sep 17 00:00:00 2001 From: Jakob Sjudin Date: Fri, 20 Mar 2026 08:15:23 +0100 Subject: [PATCH 079/167] Update manpages --- man/waybar.5.scd.in | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 624798ed..8b923eef 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -363,6 +363,11 @@ A group may hide all but one element, showing them only on mouse hover. In order default: false ++ Whether left click should reveal the content rather than mouse over. Note that grouped modules may still process their own on-click events. +*start-expanded*: ++ + typeof: bool ++ + default: false ++ + Defines whether the drawer should initialize in an expanded state. + *transition-left-to-right*: ++ typeof: bool ++ default: true ++ From c0c1a4223ad06dc7690e15825aab56a409e37498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gr=C3=BCndel?= Date: Sat, 21 Mar 2026 13:17:06 +0100 Subject: [PATCH 080/167] Fix network bandwidth rate calculation for early updates --- include/modules/network.hpp | 1 + src/modules/network.cpp | 43 +++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 4abe26bd..3bc43b23 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -70,6 +70,7 @@ class Network : public ALabel { unsigned long long bandwidth_down_total_{0}; unsigned long long bandwidth_up_total_{0}; + std::chrono::steady_clock::time_point bandwidth_last_sample_time_; std::string state_; std::string essid_; diff --git a/src/modules/network.cpp b/src/modules/network.cpp index a4f2bcc2..ede4b988 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -105,6 +105,7 @@ waybar::modules::Network::Network(const std::string& id, const Json::Value& conf bandwidth_down_total_ = 0; bandwidth_up_total_ = 0; } + bandwidth_last_sample_time_ = std::chrono::steady_clock::now(); if (!config_["interface"].isString()) { // "interface" isn't configured, then try to guess the external @@ -277,6 +278,12 @@ const std::string waybar::modules::Network::getNetworkState() const { auto waybar::modules::Network::update() -> void { std::lock_guard lock(mutex_); std::string tooltip_format; + auto now = std::chrono::steady_clock::now(); + auto elapsed_seconds = std::chrono::duration(now - bandwidth_last_sample_time_).count(); + if (elapsed_seconds <= 0.0) { + elapsed_seconds = std::chrono::duration(interval_).count(); + } + bandwidth_last_sample_time_ = now; auto bandwidth = readBandwidthUsage(); auto bandwidth_down = 0ull; @@ -336,22 +343,22 @@ auto waybar::modules::Network::update() -> void { fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", - pow_format(bandwidth_down * 8ull / (interval_.count() / 1000.0), "b/s")), + pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), fmt::arg("bandwidthUpBits", - pow_format(bandwidth_up * 8ull / (interval_.count() / 1000.0), "b/s")), + pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), fmt::arg( "bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / (interval_.count() / 1000.0), "b/s")), + pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), fmt::arg("bandwidthDownOctets", - pow_format(bandwidth_down / (interval_.count() / 1000.0), "o/s")), - fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / (interval_.count() / 1000.0), "o/s")), + pow_format(bandwidth_down / elapsed_seconds, "o/s")), + fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")), fmt::arg("bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "o/s")), + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), fmt::arg("bandwidthDownBytes", - pow_format(bandwidth_down / (interval_.count() / 1000.0), "B/s")), - fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / (interval_.count() / 1000.0), "B/s")), + pow_format(bandwidth_down / elapsed_seconds, "B/s")), + fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "B/s"))); + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (text.compare(label_.get_label()) != 0) { label_.set_markup(text); if (text.empty()) { @@ -374,26 +381,26 @@ auto waybar::modules::Network::update() -> void { fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), fmt::arg("bandwidthDownBits", - pow_format(bandwidth_down * 8ull / (interval_.count() / 1000.0), "b/s")), + pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), fmt::arg("bandwidthUpBits", - pow_format(bandwidth_up * 8ull / (interval_.count() / 1000.0), "b/s")), + pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), fmt::arg("bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / (interval_.count() / 1000.0), + pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), fmt::arg("bandwidthDownOctets", - pow_format(bandwidth_down / (interval_.count() / 1000.0), "o/s")), + pow_format(bandwidth_down / elapsed_seconds, "o/s")), fmt::arg("bandwidthUpOctets", - pow_format(bandwidth_up / (interval_.count() / 1000.0), "o/s")), + pow_format(bandwidth_up / elapsed_seconds, "o/s")), fmt::arg( "bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "o/s")), + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), fmt::arg("bandwidthDownBytes", - pow_format(bandwidth_down / (interval_.count() / 1000.0), "B/s")), + pow_format(bandwidth_down / elapsed_seconds, "B/s")), fmt::arg("bandwidthUpBytes", - pow_format(bandwidth_up / (interval_.count() / 1000.0), "B/s")), + pow_format(bandwidth_up / elapsed_seconds, "B/s")), fmt::arg( "bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / (interval_.count() / 1000.0), "B/s"))); + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (label_.get_tooltip_text() != tooltip_text) { label_.set_tooltip_markup(tooltip_text); } From dc31db6d0c79e30fe5c5f219d8c53f1d4bfdb74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gr=C3=BCndel?= Date: Sat, 21 Mar 2026 18:38:33 +0100 Subject: [PATCH 081/167] fix: linting error --- src/modules/network.cpp | 56 ++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index ede4b988..31092133 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -342,23 +342,18 @@ auto waybar::modules::Network::update() -> void { fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), - fmt::arg("bandwidthDownBits", - pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), - fmt::arg("bandwidthUpBits", - pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), - fmt::arg( - "bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), - fmt::arg("bandwidthDownOctets", - pow_format(bandwidth_down / elapsed_seconds, "o/s")), - fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")), + fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthTotalBits", + pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")), + fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")), fmt::arg("bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), - fmt::arg("bandwidthDownBytes", - pow_format(bandwidth_down / elapsed_seconds, "B/s")), - fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), + fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), + fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (text.compare(label_.get_label()) != 0) { label_.set_markup(text); if (text.empty()) { @@ -380,27 +375,18 @@ auto waybar::modules::Network::update() -> void { fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), fmt::arg("icon", getIcon(signal_strength_, state_)), - fmt::arg("bandwidthDownBits", - pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), - fmt::arg("bandwidthUpBits", - pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), fmt::arg("bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, - "b/s")), - fmt::arg("bandwidthDownOctets", - pow_format(bandwidth_down / elapsed_seconds, "o/s")), - fmt::arg("bandwidthUpOctets", - pow_format(bandwidth_up / elapsed_seconds, "o/s")), - fmt::arg( - "bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), - fmt::arg("bandwidthDownBytes", - pow_format(bandwidth_down / elapsed_seconds, "B/s")), - fmt::arg("bandwidthUpBytes", - pow_format(bandwidth_up / elapsed_seconds, "B/s")), - fmt::arg( - "bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); + pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), + fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")), + fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")), + fmt::arg("bandwidthTotalOctets", + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), + fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), + fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), + fmt::arg("bandwidthTotalBytes", + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (label_.get_tooltip_text() != tooltip_text) { label_.set_tooltip_markup(tooltip_text); } From d2aad467a7b888e3b0e65be6d44a258433822407 Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:12:36 +0530 Subject: [PATCH 082/167] Update waybar.5.scd.in --- man/waybar.5.scd.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 7223daec..80628285 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -168,7 +168,7 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( # MODULE FORMAT You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). -Note: Tooltip appearance in Waybar is controlled globally via CSS (e.g., `tooltip { ... }`). It is currently not possible to apply tooltip styles per module. +Tooltip appearance is generally controlled globally via CSS and cannot be truly scoped per module. Some modules (such as the clock) provide limited workarounds for customization. e.g. From 60c57b7195dabc750dfe1faca01ad936961b120b Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Sun, 22 Mar 2026 11:40:44 +0530 Subject: [PATCH 083/167] clarify logical condition in wlr taskbar module --- src/modules/wlr/taskbar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 2aef0ae3..bde90910 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -198,7 +198,7 @@ void Task::handle_title(const char* title) { title_ = title; hide_if_ignored(); - if (!with_icon_ && !with_name_ || app_info_) { + if ((!with_icon_ && !with_name_) || app_info_) { return; } From 1aa32def3bff7a1af47f20440f443d53a60a9c46 Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Sun, 22 Mar 2026 15:41:50 +0530 Subject: [PATCH 084/167] fix: prevent resource leak when rebinding Wayland globals --- src/client.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 103b093f..1da3ffcb 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -19,11 +19,26 @@ waybar::Client* waybar::Client::inst() { void waybar::Client::handleGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { auto* client = static_cast(data); + if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) { - client->xdg_output_manager = static_cast(wl_registry_bind( - registry, name, &zxdg_output_manager_v1_interface, ZXDG_OUTPUT_V1_NAME_SINCE_VERSION)); + + if (client->xdg_output_manager != nullptr) { + zxdg_output_manager_v1_destroy(client->xdg_output_manager); + client->xdg_output_manager = nullptr; + } + + client->xdg_output_manager = static_cast( + wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, + ZXDG_OUTPUT_V1_NAME_SINCE_VERSION)); + } else if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) { + + if (client->idle_inhibit_manager != nullptr) { + zwp_idle_inhibit_manager_v1_destroy(client->idle_inhibit_manager); + client->idle_inhibit_manager = nullptr; + } + client->idle_inhibit_manager = static_cast( wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1)); } From b64265bdf71eba390b0dbebcea674bfa5434c231 Mon Sep 17 00:00:00 2001 From: Greg Darke Date: Mon, 23 Mar 2026 14:33:51 +1100 Subject: [PATCH 085/167] Network: Fix default interface selection. When an interface is not specified for the network module, we parse the routing table to look for default routes. We have defined a default route to: - have a gateway specified, and - have no destination specified, or have an all-zero destination. Previous versions of Waybar had the second condition inverted, causing it to incorrectly pick interfaces are used to route a subnet/single host. For example, with the following routing table, we should pick `eth0` to show information about, not `wg0`. ``` ip -4 route default via 192.168.252.1 dev eth0 proto dhcp src 192.168.252.200 metric 100 192.168.252.0/24 dev eth0 proto kernel scope link src 192.168.252.200 metric 100 192.168.2.254 via 192.168.1.1 dev wg0 proto static metric 50 192.168.1.0/24 dev wg0 proto static scope link metric 50 192.168.1.0/24 dev wg0 proto kernel scope link src 192.168.1.254 metric 50 ``` --- src/modules/network.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index a4f2bcc2..6c31324a 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -744,16 +744,20 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) { /* The destination address. * Should be either missing, or maybe all 0s. Accept both. */ - const uint32_t nr_zeroes = (family == AF_INET) ? 4 : 16; - unsigned char c = 0; - size_t dstlen = RTA_PAYLOAD(attr); - if (dstlen != nr_zeroes) { - break; + auto* dest = (const unsigned char*)RTA_DATA(attr); + size_t dest_size = RTA_PAYLOAD(attr); + for (size_t i = 0; i < dest_size; ++i) { + if (dest[i] != 0) { + has_destination = true; + break; + } } - for (uint32_t i = 0; i < dstlen; i += 1) { - c |= *((unsigned char*)RTA_DATA(attr) + i); + + if (rtm->rtm_dst_len != 0) { + // We have found a destination like 0.0.0.0/24, this is not a + // default gateway route. + has_destination = true; } - has_destination = (c == 0); break; } case RTA_OIF: From 0332da0cef5080f31ca6a901a9ca3ea8e11da59f Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Wed, 25 Mar 2026 15:18:00 +0530 Subject: [PATCH 086/167] bar: warn on invalid height type in configuration --- src/bar.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bar.cpp b/src/bar.cpp index 6a78707e..fe66b806 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -172,6 +172,10 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) right_.set_spacing(spacing); } + if (config.isMember("height") && !config["height"].isUInt()) { + spdlog::warn("Invalid type for 'height', expected unsigned integer"); + } + height_ = config["height"].isUInt() ? config["height"].asUInt() : 0; width_ = config["width"].isUInt() ? config["width"].asUInt() : 0; From e55bff662e18cc853d6df5c820d05e3823f00ef1 Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Wed, 25 Mar 2026 21:45:14 +0530 Subject: [PATCH 087/167] Remove unnecessary g_strdup in GTK callback --- src/ALabel.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index b23926d8..795f87f1 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -116,9 +116,8 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st } submenus_[key] = GTK_MENU_ITEM(item); menuActionsMap_[key] = it->asString(); - g_signal_connect(submenus_[key], "activate", - G_CALLBACK(handleGtkMenuEvent), - (gpointer)g_strdup(menuActionsMap_[key].c_str())); + g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), + (gpointer)menuActionsMap_[key].c_str()); } g_object_unref(builder); } catch (std::runtime_error& e) { From a49990c2667a3d6ba7c1fa4c4607b47d4ce0491a Mon Sep 17 00:00:00 2001 From: Jolan Oubrier Date: Fri, 27 Mar 2026 18:31:05 +0100 Subject: [PATCH 088/167] fix: issue #4955 wrong behaviour of regex for sway/window 'rewrite' --- src/util/rewrite_string.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/rewrite_string.cpp b/src/util/rewrite_string.cpp index 3f6ae4ca..8c377d3f 100644 --- a/src/util/rewrite_string.cpp +++ b/src/util/rewrite_string.cpp @@ -18,7 +18,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) { // malformated regexes will cause an exception. // in this case, log error and try the next rule. const std::regex rule{it.key().asString(), std::regex_constants::icase}; - if (std::regex_match(value, rule)) { + if (std::regex_match(res, rule)) { res = std::regex_replace(res, rule, it->asString()); } } catch (const std::regex_error& e) { From 3b512d1a2cfcfcb454616e3e3988fab34b1ac3c4 Mon Sep 17 00:00:00 2001 From: Keepo Date: Sat, 28 Mar 2026 21:10:15 -0400 Subject: [PATCH 089/167] feat(client): add support for 8-bit hex color codes in CSS This allows users to use #RRGGBBAA format in their style.css. The client now detects 8-bit hex codes, transforms them into GTK-compatible rgba() syntax, and loads the modified data into the CSS provider. - Added utility to detect 8-bit hex patterns. - Added transformation logic to convert hex alpha to decimal. - Intercepted CSS loading in Client::setupCss to handle the conversion. --- include/util/hex_checker.hpp | 17 ++++++++++++ meson.build | 4 ++- src/client.cpp | 22 +++++++++------- src/util/has_8bit_hex.cpp | 15 +++++++++++ src/util/transform_8bit_to_rgba.cpp | 41 +++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 include/util/hex_checker.hpp create mode 100644 src/util/has_8bit_hex.cpp create mode 100644 src/util/transform_8bit_to_rgba.cpp diff --git a/include/util/hex_checker.hpp b/include/util/hex_checker.hpp new file mode 100644 index 00000000..5e742436 --- /dev/null +++ b/include/util/hex_checker.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +/** + * Reads a CSS file, searches for 8-bit hex codes (#RRGGBBAA), + * and returns true if found, or false if not found. + */ + +bool has_8bit_hex(std::string file_path); + +/** + * Reads a CSS file, searches for 8-bit hex codes (#RRGGBBAA), + * and transforms them into GTK-compatible rgba() syntax. + */ + +std::string transform_8bit_to_hex(std::string file_path); diff --git a/meson.build b/meson.build index db9407eb..b9df3f80 100644 --- a/meson.build +++ b/meson.build @@ -185,7 +185,9 @@ src_files = files( 'src/util/gtk_icon.cpp', 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', - 'src/util/css_reload_helper.cpp' + 'src/util/css_reload_helper.cpp', + 'src/util/has_8bit_hex.cpp', + 'src/util/transform_8bit_to_rgba.cpp' ) man_files = files( diff --git a/src/client.cpp b/src/client.cpp index 1da3ffcb..178552a4 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -10,6 +10,7 @@ #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" #include "util/format.hpp" +#include "util/hex_checker.hpp" waybar::Client* waybar::Client::inst() { static auto* c = new Client(); @@ -22,21 +23,18 @@ void waybar::Client::handleGlobal(void* data, struct wl_registry* registry, uint if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) { - if (client->xdg_output_manager != nullptr) { zxdg_output_manager_v1_destroy(client->xdg_output_manager); client->xdg_output_manager = nullptr; } - client->xdg_output_manager = static_cast( - wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, - ZXDG_OUTPUT_V1_NAME_SINCE_VERSION)); + client->xdg_output_manager = static_cast(wl_registry_bind( + registry, name, &zxdg_output_manager_v1_interface, ZXDG_OUTPUT_V1_NAME_SINCE_VERSION)); } else if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) { - if (client->idle_inhibit_manager != nullptr) { zwp_idle_inhibit_manager_v1_destroy(client->idle_inhibit_manager); - client->idle_inhibit_manager = nullptr; + client->idle_inhibit_manager = nullptr; } client->idle_inhibit_manager = static_cast( @@ -209,11 +207,15 @@ auto waybar::Client::setupCss(const std::string& css_file) -> void { } css_provider_ = Gtk::CssProvider::create(); - if (!css_provider_->load_from_path(css_file)) { - css_provider_.reset(); - throw std::runtime_error("Can't open style file"); + if (has_8bit_hex(css_file)) { + std::string modified_css = transform_8bit_to_hex(css_file); + css_provider_->load_from_data(modified_css); + } else { + if (!css_provider_->load_from_path(css_file)) { + css_provider_.reset(); + throw std::runtime_error("Can't open style file"); + } } - Gtk::StyleContext::add_provider_for_screen(screen, css_provider_, GTK_STYLE_PROVIDER_PRIORITY_USER); } diff --git a/src/util/has_8bit_hex.cpp b/src/util/has_8bit_hex.cpp new file mode 100644 index 00000000..db594980 --- /dev/null +++ b/src/util/has_8bit_hex.cpp @@ -0,0 +1,15 @@ +#include +#include +#include + +namespace fs = std::filesystem; + +bool has_8bit_hex(std::string file_path) { + std::ifstream f(file_path, std::ios::in | std::ios::binary); + const auto size = fs::file_size(file_path); + std::string result(size, '\0'); + f.read(result.data(), size); + std::regex pattern( + R"((?:\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})))"); + return std::regex_search(result, pattern); +} diff --git a/src/util/transform_8bit_to_rgba.cpp b/src/util/transform_8bit_to_rgba.cpp new file mode 100644 index 00000000..4e0cd3a8 --- /dev/null +++ b/src/util/transform_8bit_to_rgba.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include +namespace fs = std::filesystem; +std::string transform_8bit_to_hex(std::string file_path) { + std::ifstream f(file_path, std::ios::in | std::ios::binary); + const auto size = fs::file_size(file_path); + std::string result(size, '\0'); + f.read(result.data(), size); + + std::regex pattern( + R"((?:\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})))"); + std::string final_output; + + auto it = std::sregex_iterator(result.begin(), result.end(), pattern); + auto eof = std::sregex_iterator(); + std::smatch match = *it; + + while (it != eof) { + + final_output += match.prefix().str(); + + int r = stoi(match[1].str(), nullptr, 16); + int g = stoi(match[2].str(), nullptr, 16); + int b = stoi(match[3].str(), nullptr, 16); + double a = (stoi(match[4].str(), nullptr, 16) / 255.0); + + std::stringstream ss; + ss << "rgba(" << r << "," << g << "," << b << "," << std::fixed + << std::setprecision(2) << a << ")"; + final_output += ss.str(); + + ++it; + } + + final_output += match.suffix().str(); + + return final_output; +} From 937ef176ffc5af51e243e744adc3d965ec8dd006 Mon Sep 17 00:00:00 2001 From: Keepo Date: Sat, 28 Mar 2026 22:45:42 -0400 Subject: [PATCH 090/167] fix: memory issues & duplicate file loads. --- src/client.cpp | 4 ++-- src/util/transform_8bit_to_rgba.cpp | 33 +++++++++++++++++++++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 178552a4..d1489186 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -207,8 +207,8 @@ auto waybar::Client::setupCss(const std::string& css_file) -> void { } css_provider_ = Gtk::CssProvider::create(); - if (has_8bit_hex(css_file)) { - std::string modified_css = transform_8bit_to_hex(css_file); + auto [modified_css, was_transformed] = transform_8bit_to_hex(css_file); + if (was_transformed) { css_provider_->load_from_data(modified_css); } else { if (!css_provider_->load_from_path(css_file)) { diff --git a/src/util/transform_8bit_to_rgba.cpp b/src/util/transform_8bit_to_rgba.cpp index 4e0cd3a8..ca95784d 100644 --- a/src/util/transform_8bit_to_rgba.cpp +++ b/src/util/transform_8bit_to_rgba.cpp @@ -4,21 +4,40 @@ #include #include namespace fs = std::filesystem; -std::string transform_8bit_to_hex(std::string file_path) { + +struct TransformResult { + std::string css; + bool was_transformed; +}; + +TransformResult transform_8bit_to_hex(const std::string& file_path) { std::ifstream f(file_path, std::ios::in | std::ios::binary); const auto size = fs::file_size(file_path); std::string result(size, '\0'); + if (!f.is_open() || !f.good()) { + throw std::runtime_error("Cannot open file: " + file_path); + } + + if (size == 0) { + return {.css = result, .was_transformed = false}; + } + f.read(result.data(), size); - std::regex pattern( - R"((?:\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})))"); + static std::regex pattern( + R"((\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})))"); std::string final_output; auto it = std::sregex_iterator(result.begin(), result.end(), pattern); auto eof = std::sregex_iterator(); - std::smatch match = *it; + if (it == eof) { + return {.css = result, .was_transformed = false}; + } + + std::smatch match; while (it != eof) { + match = *it; final_output += match.prefix().str(); @@ -28,8 +47,8 @@ std::string transform_8bit_to_hex(std::string file_path) { double a = (stoi(match[4].str(), nullptr, 16) / 255.0); std::stringstream ss; - ss << "rgba(" << r << "," << g << "," << b << "," << std::fixed - << std::setprecision(2) << a << ")"; + ss << "rgba(" << r << "," << g << "," << b << "," << std::fixed << std::setprecision(2) << a + << ")"; final_output += ss.str(); ++it; @@ -37,5 +56,5 @@ std::string transform_8bit_to_hex(std::string file_path) { final_output += match.suffix().str(); - return final_output; + return {.css = final_output, .was_transformed = true}; } From 3533265675084b07ff5fb51f9b1ec8570080e954 Mon Sep 17 00:00:00 2001 From: Keepo Date: Sat, 28 Mar 2026 23:02:16 -0400 Subject: [PATCH 091/167] fix: removed unnecessary function, update header file for signature change. --- include/util/hex_checker.hpp | 9 +-------- src/util/has_8bit_hex.cpp | 15 --------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 src/util/has_8bit_hex.cpp diff --git a/include/util/hex_checker.hpp b/include/util/hex_checker.hpp index 5e742436..e896031b 100644 --- a/include/util/hex_checker.hpp +++ b/include/util/hex_checker.hpp @@ -2,16 +2,9 @@ #include -/** - * Reads a CSS file, searches for 8-bit hex codes (#RRGGBBAA), - * and returns true if found, or false if not found. - */ - -bool has_8bit_hex(std::string file_path); - /** * Reads a CSS file, searches for 8-bit hex codes (#RRGGBBAA), * and transforms them into GTK-compatible rgba() syntax. */ -std::string transform_8bit_to_hex(std::string file_path); +std::string transform_8bit_to_hex(const std::string& file_path); diff --git a/src/util/has_8bit_hex.cpp b/src/util/has_8bit_hex.cpp deleted file mode 100644 index db594980..00000000 --- a/src/util/has_8bit_hex.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include -#include -#include - -namespace fs = std::filesystem; - -bool has_8bit_hex(std::string file_path) { - std::ifstream f(file_path, std::ios::in | std::ios::binary); - const auto size = fs::file_size(file_path); - std::string result(size, '\0'); - f.read(result.data(), size); - std::regex pattern( - R"((?:\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})))"); - return std::regex_search(result, pattern); -} From 72d6a51fb7e2fe04aa36515b8db8107c03d3372e Mon Sep 17 00:00:00 2001 From: Keepo Date: Sat, 28 Mar 2026 23:26:34 -0400 Subject: [PATCH 092/167] fix: fix compile/run bugs from trying to be safer. rip --- include/util/hex_checker.hpp | 2 +- meson.build | 1 - src/util/transform_8bit_to_rgba.cpp | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/include/util/hex_checker.hpp b/include/util/hex_checker.hpp index e896031b..9855c2de 100644 --- a/include/util/hex_checker.hpp +++ b/include/util/hex_checker.hpp @@ -7,4 +7,4 @@ * and transforms them into GTK-compatible rgba() syntax. */ -std::string transform_8bit_to_hex(const std::string& file_path); +std::pair transform_8bit_to_hex(const std::string& file_path); diff --git a/meson.build b/meson.build index b9df3f80..0c494eb2 100644 --- a/meson.build +++ b/meson.build @@ -186,7 +186,6 @@ src_files = files( 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', 'src/util/css_reload_helper.cpp', - 'src/util/has_8bit_hex.cpp', 'src/util/transform_8bit_to_rgba.cpp' ) diff --git a/src/util/transform_8bit_to_rgba.cpp b/src/util/transform_8bit_to_rgba.cpp index ca95784d..dc87a88e 100644 --- a/src/util/transform_8bit_to_rgba.cpp +++ b/src/util/transform_8bit_to_rgba.cpp @@ -25,7 +25,7 @@ TransformResult transform_8bit_to_hex(const std::string& file_path) { f.read(result.data(), size); static std::regex pattern( - R"((\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})))"); + R"(\#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2}))"); std::string final_output; auto it = std::sregex_iterator(result.begin(), result.end(), pattern); From a9aab4e3569a0c771a873e2a4b1c8695f70430e9 Mon Sep 17 00:00:00 2001 From: Keepo Date: Sat, 28 Mar 2026 23:44:41 -0400 Subject: [PATCH 093/167] fix: type consistency --- include/util/hex_checker.hpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/include/util/hex_checker.hpp b/include/util/hex_checker.hpp index 9855c2de..27a84d1f 100644 --- a/include/util/hex_checker.hpp +++ b/include/util/hex_checker.hpp @@ -2,9 +2,16 @@ #include +/** + * Result of transforming 8-bit hex codes to rgba(). + */ +struct TransformResult { + std::string css; + bool was_transformed; +}; + /** * Reads a CSS file, searches for 8-bit hex codes (#RRGGBBAA), * and transforms them into GTK-compatible rgba() syntax. */ - -std::pair transform_8bit_to_hex(const std::string& file_path); +TransformResult transform_8bit_to_hex(const std::string& file_path); From 3f69bacff018371507c9a8f8dbd78aa558a02cd1 Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Sun, 29 Mar 2026 22:12:16 +0530 Subject: [PATCH 094/167] fix: prevent resource leak when rebinding dwl globals --- src/modules/dwl/tags.cpp | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp index 399c7d4b..243aaa92 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -70,17 +70,33 @@ static const zdwl_ipc_output_v2_listener output_status_listener_impl{ static void handle_global(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { - if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) { - static_cast(data)->status_manager_ = static_cast( - (zdwl_ipc_manager_v2*)wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1)); - } - if (std::strcmp(interface, wl_seat_interface.name) == 0) { - version = std::min(version, 1); - static_cast(data)->seat_ = - static_cast(wl_registry_bind(registry, name, &wl_seat_interface, version)); - } + +if (std::strcmp(interface, zdwl_ipc_manager_v2_interface.name) == 0) { + auto* self = static_cast(data); + + if (self->status_manager_) { + zdwl_ipc_manager_v2_destroy(self->status_manager_); + self->status_manager_ = nullptr; } + self->status_manager_ = static_cast( + wl_registry_bind(registry, name, &zdwl_ipc_manager_v2_interface, 1)); +} + +if (std::strcmp(interface, wl_seat_interface.name) == 0) { + auto* self = static_cast(data); + + if (self->seat_) { + wl_seat_destroy(self->seat_); + self->seat_ = nullptr; + } + + version = std::min(version, 1); + + self->seat_ = static_cast( + wl_registry_bind(registry, name, &wl_seat_interface, version)); +} +} static void handle_global_remove(void* data, struct wl_registry* registry, uint32_t name) { /* Ignore event */ } From 753294dbf4da6a7276e072c314fc7c078a8404c4 Mon Sep 17 00:00:00 2001 From: Adrian Lopez Date: Mon, 30 Mar 2026 19:38:24 +0200 Subject: [PATCH 095/167] fix(network): prevent near-zero bandwidth on rapid event-driven updates When netlink events (link/addr/route changes) fire between timer intervals, dp.emit() triggers update() which consumes the byte delta and resets bandwidth_down_total_. A subsequent timer update sees near-zero delta, displaying very small bandwidth. Cache the last computed bandwidth values and skip recalculation when update() is called within half the interval. Event-driven updates reuse the cached values instead. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- include/modules/network.hpp | 2 ++ src/modules/network.cpp | 40 ++++++++++++++++++++++++++----------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 3bc43b23..66fc6d04 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -70,6 +70,8 @@ class Network : public ALabel { unsigned long long bandwidth_down_total_{0}; unsigned long long bandwidth_up_total_{0}; + unsigned long long bandwidth_down_prev_{0}; + unsigned long long bandwidth_up_prev_{0}; std::chrono::steady_clock::time_point bandwidth_last_sample_time_; std::string state_; diff --git a/src/modules/network.cpp b/src/modules/network.cpp index a39a5ed3..572b5fcc 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -280,23 +280,39 @@ auto waybar::modules::Network::update() -> void { std::string tooltip_format; auto now = std::chrono::steady_clock::now(); auto elapsed_seconds = std::chrono::duration(now - bandwidth_last_sample_time_).count(); - if (elapsed_seconds <= 0.0) { - elapsed_seconds = std::chrono::duration(interval_).count(); - } - bandwidth_last_sample_time_ = now; - auto bandwidth = readBandwidthUsage(); auto bandwidth_down = 0ull; auto bandwidth_up = 0ull; - if (bandwidth.has_value()) { - auto down_octets = (*bandwidth).first; - auto up_octets = (*bandwidth).second; - bandwidth_down = down_octets - bandwidth_down_total_; - bandwidth_down_total_ = down_octets; + // Only recalculate bandwidth when enough time has elapsed since the last + // sample. Event-driven dp.emit() calls (link/addr/route changes) can + // trigger update() between timer intervals, which would consume the byte + // delta prematurely and show near-zero bandwidth. + auto min_elapsed = std::chrono::duration(interval_).count() * 0.5; + if (elapsed_seconds >= min_elapsed) { + if (elapsed_seconds <= 0.0) { + elapsed_seconds = std::chrono::duration(interval_).count(); + } + bandwidth_last_sample_time_ = now; - bandwidth_up = up_octets - bandwidth_up_total_; - bandwidth_up_total_ = up_octets; + auto bandwidth = readBandwidthUsage(); + if (bandwidth.has_value()) { + auto down_octets = (*bandwidth).first; + auto up_octets = (*bandwidth).second; + + bandwidth_down = down_octets - bandwidth_down_total_; + bandwidth_down_total_ = down_octets; + + bandwidth_up = up_octets - bandwidth_up_total_; + bandwidth_up_total_ = up_octets; + + bandwidth_down_prev_ = bandwidth_down; + bandwidth_up_prev_ = bandwidth_up; + } + } else { + bandwidth_down = bandwidth_down_prev_; + bandwidth_up = bandwidth_up_prev_; + elapsed_seconds = std::chrono::duration(interval_).count(); } if (!alt_) { From 8b1e5740634093b89582d9708981aeb37b8f09ab Mon Sep 17 00:00:00 2001 From: cebem1nt Date: Tue, 31 Mar 2026 17:49:30 -0300 Subject: [PATCH 096/167] niri/workspaces: feature - add "hide-empty" config option (#4965) --- src/modules/niri/workspaces.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 3e8a432e..97d15215 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -114,6 +114,11 @@ void Workspaces::doUpdate() { button.show(); else button.hide(); + } else if (config_["hide-empty"].asBool()) { + if (ws["active_window_id"].isNull() && !ws["is_focused"].asBool()) + button.hide(); + else + button.show(); } else { button.show(); } From 16886117b3645db1cc32055a9732b4b9cf1562d9 Mon Sep 17 00:00:00 2001 From: Anubhab Ghosh Date: Thu, 2 Apr 2026 19:54:19 +0530 Subject: [PATCH 097/167] Network: fix: delete correct address type Only delete the corresponding address type (IPv4 or IPv6) when an event about a specific type (AF_INET or AF_INET6) is received This fixes situations where only one type of the address is deleted (and possibly added again) but Waybar still thinks the interface is in "linked" (no IP) state. --- src/modules/network.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index a39a5ed3..6dbb8bf0 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -677,12 +677,15 @@ int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) { changed_cidr); } } else { - net->ipaddr_.clear(); - net->ipaddr6_.clear(); - net->cidr_ = 0; - net->cidr6_ = 0; - net->netmask_.clear(); - net->netmask6_.clear(); + if (ifa->ifa_family == AF_INET) { + net->ipaddr_.clear(); + net->cidr_ = 0; + net->netmask_.clear(); + } else if (ifa->ifa_family == AF_INET6) { + net->ipaddr6_.clear(); + net->cidr6_ = 0; + net->netmask6_.clear(); + } spdlog::debug("network: {} addr deleted {}/{}", net->ifname_, inet_ntop(ifa->ifa_family, RTA_DATA(ifa_rta), ipaddr, sizeof(ipaddr)), ifa->ifa_prefixlen); From 36518e4eca9ccfc09403b80466621fd58a1fabfb Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Fri, 3 Apr 2026 19:47:17 +0530 Subject: [PATCH 098/167] fix(bar): ensure exception safety when creating group modules using std::unique_ptr --- src/bar.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/bar.cpp b/src/bar.cpp index 6a78707e..6e81332c 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -545,9 +545,11 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, if (group_config["modules"].isNull()) { spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); } - auto* group_module = new waybar::Group(id_name, class_name, group_config, vertical); - getModules(factory, ref, group_module); - module = group_module; + auto group_module = std::make_unique( + id_name, class_name, group_config, vertical); + + getModules(factory, ref, group_module.get()); + module = group_module.release(); } else { module = factory.makeModule(ref, pos); } From ae11954398e5d9692805d46f8d52b6ffbf7e3597 Mon Sep 17 00:00:00 2001 From: cebem1nt Date: Sat, 4 Apr 2026 22:11:19 -0300 Subject: [PATCH 099/167] fix: ensure passing group_config as reference --- src/bar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bar.cpp b/src/bar.cpp index 6a78707e..f42f31a3 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -541,7 +541,7 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, auto vertical = (group != nullptr ? group->getBox().get_orientation() : box_.get_orientation()) == Gtk::ORIENTATION_VERTICAL; - auto group_config = config[ref]; + const Json::Value& group_config = config[ref]; if (group_config["modules"].isNull()) { spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); } From fc11789a4f6d437fd329cf3dada6ffb4b9aaa9e0 Mon Sep 17 00:00:00 2001 From: Duke B Date: Sun, 5 Apr 2026 19:01:36 -0400 Subject: [PATCH 100/167] add unit config option to memory module --- include/modules/memory.hpp | 4 +++ man/waybar-memory.5.scd | 18 ++++++---- src/modules/memory/common.cpp | 66 ++++++++++++++++++++++++----------- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/include/modules/memory.hpp b/include/modules/memory.hpp index 3b6342b3..c73ece23 100644 --- a/include/modules/memory.hpp +++ b/include/modules/memory.hpp @@ -19,9 +19,13 @@ class Memory : public ALabel { private: void parseMeminfo(); + static float calc_divisor(const std::string& divisor); + std::unordered_map meminfo_; util::SleeperThread thread_; + + std::string unit_; }; } // namespace waybar::modules diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index 567c2c72..99f57299 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -102,23 +102,29 @@ Addressed by *memory* default: false ++ Enables this module to consume all left over space dynamically. +*unit*: ++ + typeof: string ++ + default: GiB ++ + Used to specify unit for total, swapTotal, used, swapUsed, avail, swapAvail, + and swapState. Accepts B, kB, kiB, MB, MiB, GB, GiB, TB, and TiB. + # FORMAT REPLACEMENTS *{percentage}*: Percentage of memory in use. *{swapPercentage}*: Percentage of swap in use. -*{total}*: Amount of total memory available in GiB. +*{total}*: Amount of total memory available. Defaults to GiB. -*{swapTotal}*: Amount of total swap available in GiB. +*{swapTotal}*: Amount of total swap available. Defaults to GiB. -*{used}*: Amount of used memory in GiB. +*{used}*: Amount of used memory. Defaults to GiB. -*{swapUsed}*: Amount of used swap in GiB. +*{swapUsed}*: Amount of used swap. Defaults to GiB. -*{avail}*: Amount of available memory in GiB. +*{avail}*: Amount of available memory. Defaults to GiB. -*{swapAvail}*: Amount of available swap in GiB. +*{swapAvail}*: Amount of available swap. Defaults to GiB. *{swapState}*: Signals if swap is activated or not diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 3c486a33..731f0b08 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -6,6 +6,9 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config dp.emit(); thread_.sleep_for(interval_); }; + if (config["unit"].isString()) { + unit_ = config["unit"].asString(); + } } auto waybar::modules::Memory::update() -> void { @@ -13,15 +16,15 @@ auto waybar::modules::Memory::update() -> void { unsigned long memtotal = meminfo_["MemTotal"]; unsigned long swaptotal = 0; - if (meminfo_.count("SwapTotal")) { + if (meminfo_.contains("SwapTotal")) { swaptotal = meminfo_["SwapTotal"]; } unsigned long memfree; unsigned long swapfree = 0; - if (meminfo_.count("SwapFree")) { + if (meminfo_.contains("SwapFree")) { swapfree = meminfo_["SwapFree"]; } - if (meminfo_.count("MemAvailable")) { + if (meminfo_.contains("MemAvailable")) { // New kernels (3.4+) have an accurate available memory field. memfree = meminfo_["MemAvailable"] + meminfo_["zfs_size"]; } else { @@ -31,18 +34,19 @@ auto waybar::modules::Memory::update() -> void { } if (memtotal > 0 && memfree >= 0) { - float total_ram_gigabytes = - 0.01 * round(memtotal / 10485.76); // 100*10485.76 = 2^20 = 1024^2 = GiB/KiB - float total_swap_gigabytes = 0.01 * round(swaptotal / 10485.76); int used_ram_percentage = 100 * (memtotal - memfree) / memtotal; int used_swap_percentage = 0; - if (swaptotal) { + if ((bool) swaptotal) { used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; } - float used_ram_gigabytes = 0.01 * round((memtotal - memfree) / 10485.76); - float used_swap_gigabytes = 0.01 * round((swaptotal - swapfree) / 10485.76); - float available_ram_gigabytes = 0.01 * round(memfree / 10485.76); - float available_swap_gigabytes = 0.01 * round(swapfree / 10485.76); + + float divisor = calc_divisor(unit_); + float total_ram = memtotal / divisor; + float total_swap = swaptotal / divisor; + float used_ram = (memtotal - memfree) / divisor; + float used_swap = (swaptotal - swapfree) / divisor; + float available_ram = memfree / divisor; + float available_swap = swapfree / divisor; auto format = format_; auto state = getState(used_ram_percentage); @@ -58,12 +62,12 @@ auto waybar::modules::Memory::update() -> void { label_.set_markup(fmt::format( fmt::runtime(format), used_ram_percentage, fmt::arg("icon", getIcon(used_ram_percentage, icons)), - fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), + fmt::arg("total", total_ram), fmt::arg("swapTotal", total_swap), fmt::arg("percentage", used_ram_percentage), fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), - fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), - fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), - fmt::arg("swapAvail", available_swap_gigabytes))); + fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram), + fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), + fmt::arg("swapAvail", available_swap))); } if (tooltipEnabled()) { @@ -71,14 +75,14 @@ auto waybar::modules::Memory::update() -> void { auto tooltip_format = config_["tooltip-format"].asString(); label_.set_tooltip_markup(fmt::format( fmt::runtime(tooltip_format), used_ram_percentage, - fmt::arg("total", total_ram_gigabytes), fmt::arg("swapTotal", total_swap_gigabytes), + fmt::arg("total", total_ram), fmt::arg("swapTotal", total_swap), fmt::arg("percentage", used_ram_percentage), fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), - fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram_gigabytes), - fmt::arg("swapUsed", used_swap_gigabytes), fmt::arg("avail", available_ram_gigabytes), - fmt::arg("swapAvail", available_swap_gigabytes))); + fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram), + fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), + fmt::arg("swapAvail", available_swap))); } else { - label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram_gigabytes, 1)); + label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram, 1)); } } } else { @@ -87,3 +91,25 @@ auto waybar::modules::Memory::update() -> void { // Call parent update ALabel::update(); } + +float waybar::modules::Memory::calc_divisor(const std::string& divisor) { + if (divisor == "kB") { + return 1.0; + } else if (divisor == "kiB") { + return 1.024; + } else if (divisor == "MB") { + return 1.000 * 1000.0; + } else if (divisor == "MiB") { + return 1.024 * 1024.0; + } else if (divisor == "GB") { + return 1.000 * 1000.0 * 1000.0; + } else if (divisor == "GiB") { + return 1.024 * 1024.0 * 1024.0; + } else if (divisor == "TB") { + return 1.000 * 1000.0 * 1000.0 * 1000.0; + } else if (divisor == "TiB") { + return 1.024 * 1024.0 * 1024.0 * 1024.0; + } else { // default to GiB if it is anything that we don't recongnise + return 1.024 * 1024.0 * 1024.0; + } +} From 906a589715dc69876da77ce5e20c15ff6b985abf Mon Sep 17 00:00:00 2001 From: yubo <3597387401@qq.com> Date: Thu, 9 Apr 2026 21:18:15 +0800 Subject: [PATCH 101/167] fix the io failure for hotplug-in device --- src/modules/battery.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index d39c4920..7984c24d 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -126,7 +126,14 @@ void waybar::modules::Battery::refreshBatteries() { // Ignore non-system power supplies unless explicitly requested if (!bat_defined && fs::exists(node.path() / "scope")) { std::string scope; - std::ifstream(node.path() / "scope") >> scope; + try{ + //for hotplug-in device, access it is always unstable because you may remove the device anytime + //so just allow failure happen and do nothing + std::ifstream(node.path()/"scope")>>scope; + }catch(const std::ifstream::failure& e){ + scope.clear(); + continue; + } if (g_ascii_strcasecmp(scope.data(), "device") == 0) { continue; } From a8a46d1f1e1834e652bdeff9640e98b5ab36eb25 Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Thu, 9 Apr 2026 19:40:05 +0530 Subject: [PATCH 102/167] Refactor: simplify complex conditional expressions in Hyprland workspace module --- src/modules/hyprland/workspace.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 21e7ef9b..da383aa5 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -115,7 +115,11 @@ void Workspace::setActiveWindow(WindowAddress const& addr) { } auto activeWindowPos = m_workspaceManager.activeWindowPosition(); - if (activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE) { + const bool has_active_window = + activeIdx.has_value() && + activeWindowPos != Workspaces::ActiveWindowPosition::NONE; + + if (has_active_window) { auto window = std::move(m_windowMap[*activeIdx]); m_windowMap.erase(m_windowMap.begin() + *activeIdx); if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) { @@ -130,7 +134,10 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) { if (!create_window_payload.isEmpty(m_workspaceManager)) { auto repr = create_window_payload.repr(m_workspaceManager); - if (!repr.empty() || m_workspaceManager.enableTaskbar()) { + const bool should_display = + !repr.empty() || m_workspaceManager.enableTaskbar(); + + if (should_display) { auto addr = create_window_payload.getAddress(); auto it = std::ranges::find_if( m_windowMap, [&addr](const auto& window) { return window.address == addr; }); @@ -142,7 +149,7 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) { } } } -}; +} bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) { if (create_window_payload.getWorkspaceName() == name()) { @@ -270,7 +277,9 @@ void Workspace::update(const std::string& workspace_icon) { bool Workspace::isEmpty() const { auto ignore_list = m_workspaceManager.getIgnoredWindows(); - if (ignore_list.empty()) { + const bool no_ignore_rules = ignore_list.empty(); + + if (no_ignore_rules) { return m_windows == 0; } // If there are windows but they are all ignored, consider the workspace empty @@ -349,7 +358,9 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { } auto formatAfter = m_workspaceManager.formatAfter(); - if (!formatAfter.empty()) { + const bool has_format_after = !formatAfter.empty(); + + if (has_format_after) { m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()), fmt::arg("name", name()), fmt::arg("icon", workspace_icon))); From efd245b552ee279c5eb88cff3bc64c486fd88a60 Mon Sep 17 00:00:00 2001 From: Mervius Date: Fri, 10 Apr 2026 11:42:23 -0500 Subject: [PATCH 103/167] Fix timeout with multiple monitors --- include/client.hpp | 3 +++ src/client.cpp | 31 ++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/include/client.hpp b/include/client.hpp index c3df3f21..8598e356 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -44,6 +44,7 @@ class Client { const char* interface, uint32_t version); static void handleGlobalRemove(void* data, struct wl_registry* registry, uint32_t name); static void handleOutputDone(void*, struct zxdg_output_v1*); + void createBarsBatch(); static void handleOutputName(void*, struct zxdg_output_v1*, const char*); static void handleOutputDescription(void*, struct zxdg_output_v1*, const char*); void handleMonitorAdded(Glib::RefPtr monitor); @@ -58,6 +59,8 @@ class Client { std::string m_cssFile; sigc::connection monitor_added_connection_; sigc::connection monitor_removed_connection_; + std::vector pending_outputs_; + bool bars_scheduled_ = false; }; } // namespace waybar diff --git a/src/client.cpp b/src/client.cpp index d1489186..ff354e18 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -91,11 +91,14 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o output.xdg_output.reset(); spdlog::debug("Output detection done: {} ({})", output.name, output.identifier); - auto configs = client->getOutputConfigs(output); - if (!configs.empty()) { - for (const auto& config : configs) { - client->bars.emplace_back(std::make_unique(&output, config)); - } + client->pending_outputs_.push_back(&output); + + if (!client->bars_scheduled_) { + client->bars_scheduled_ = true; + + Glib::signal_idle().connect_once([client]() { + client->createBarsBatch(); + }, Glib::PRIORITY_HIGH_IDLE); } } } catch (const std::exception& e) { @@ -103,6 +106,24 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } } +void waybar::Client::createBarsBatch() { + for (auto* output : pending_outputs_) { + try { + auto configs = getOutputConfigs(*output); + if (!configs.empty()) { + for (const auto& config : configs) { + bars.emplace_back(std::make_unique(output, config)); + } + } + } catch (const std::exception& e) { + spdlog::warn("Error creating bar: {}", e.what()); + } + } + + pending_outputs_.clear(); + bars_scheduled_ = false; +} + void waybar::Client::handleOutputName(void* data, struct zxdg_output_v1* /*xdg_output*/, const char* name) { auto* client = waybar::Client::inst(); From ac62754b289569b71da33f6982c63eb927796182 Mon Sep 17 00:00:00 2001 From: yubo <3597387401@qq.com> Date: Thu, 9 Apr 2026 21:18:15 +0800 Subject: [PATCH 104/167] fix the lint problem --- src/modules/battery.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 7984c24d..30be3857 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -126,11 +126,11 @@ void waybar::modules::Battery::refreshBatteries() { // Ignore non-system power supplies unless explicitly requested if (!bat_defined && fs::exists(node.path() / "scope")) { std::string scope; - try{ - //for hotplug-in device, access it is always unstable because you may remove the device anytime - //so just allow failure happen and do nothing - std::ifstream(node.path()/"scope")>>scope; - }catch(const std::ifstream::failure& e){ + try { + // for hotplug-in device, access it is always unstable because you may remove the + // device anytime so just allow failure happen and do nothing + std::ifstream(node.path() / "scope") >> scope; + } catch (const std::ifstream::failure& e) { scope.clear(); continue; } From 8b5b1eebbfa40325b77629b03a62f82d4a048158 Mon Sep 17 00:00:00 2001 From: Mervius Date: Wed, 15 Apr 2026 10:35:26 -0500 Subject: [PATCH 105/167] Check if output still is in outputs before doing anything with it --- src/client.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client.cpp b/src/client.cpp index ff354e18..99a74123 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -95,7 +95,7 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o if (!client->bars_scheduled_) { client->bars_scheduled_ = true; - + Glib::signal_idle().connect_once([client]() { client->createBarsBatch(); }, Glib::PRIORITY_HIGH_IDLE); @@ -107,6 +107,8 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } void waybar::Client::createBarsBatch() { + pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), + [output](const auto& o) { return &o == output; }); }); for (auto* output : pending_outputs_) { try { auto configs = getOutputConfigs(*output); From 09b6df37caa2d05bfc783e5729120ecc81f74576 Mon Sep 17 00:00:00 2001 From: Mervius Date: Wed, 15 Apr 2026 10:39:21 -0500 Subject: [PATCH 106/167] Check if output still is in outputs before doing anything with it --- include/client.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/client.hpp b/include/client.hpp index 8598e356..10a634fc 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -59,7 +59,7 @@ class Client { std::string m_cssFile; sigc::connection monitor_added_connection_; sigc::connection monitor_removed_connection_; - std::vector pending_outputs_; + std::list pending_outputs_; bool bars_scheduled_ = false; }; From d9b5d24284f2e91f53f496ad15d31843bb879497 Mon Sep 17 00:00:00 2001 From: Mervius Date: Wed, 15 Apr 2026 10:55:49 -0500 Subject: [PATCH 107/167] Check if output still is in outputs before doing anything with it --- src/client.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 99a74123..4d8bcb65 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -107,8 +107,7 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } void waybar::Client::createBarsBatch() { - pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), - [output](const auto& o) { return &o == output; }); }); + pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [output](const auto& o) { return &o == output; }); }); for (auto* output : pending_outputs_) { try { auto configs = getOutputConfigs(*output); From e2d608e7fdc4e69aff491a6561ef43212fd4a5fa Mon Sep 17 00:00:00 2001 From: Mervius Date: Wed, 15 Apr 2026 10:59:03 -0500 Subject: [PATCH 108/167] Check if output still is in outputs before doing anything with it --- src/client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.cpp b/src/client.cpp index 4d8bcb65..94fcfe41 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -107,7 +107,7 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } void waybar::Client::createBarsBatch() { - pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [output](const auto& o) { return &o == output; }); }); + pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [&output](const auto& o) { return &o == output; }); }); for (auto* output : pending_outputs_) { try { auto configs = getOutputConfigs(*output); From 7e9c46e4d15563ba6b4dc6e470b7a4b445c26c63 Mon Sep 17 00:00:00 2001 From: B2krobbery <150381094+B2krobbery@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:34:39 +0530 Subject: [PATCH 109/167] fix(sni): use std::make_unique for Item allocation --- src/modules/sni/host.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 18eac643..567fbf9f 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -178,9 +178,11 @@ void Host::addRegisteredItem(const std::string& service) { return bus_name == item->bus_name && object_path == item->object_path; }); if (it == items_.end()) { - items_.emplace_back(new Item( - bus_name, object_path, config_, bar_, [this](Item& item) { itemReady(item); }, - [this](Item& item) { itemInvalidated(item); }, on_update_)); + items_.emplace_back(std::make_unique( + bus_name, object_path, config_, bar_, + [this](Item& item) { itemReady(item); }, + [this](Item& item) { itemInvalidated(item); }, + on_update_)); } } From 0241d0eac6febbfbcffd498535cad680263c0391 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:27:39 +0200 Subject: [PATCH 110/167] Remove redefinition or SIGRTMIN/SIGRTMAX for OpenBSD --- include/util/SafeSignal.hpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp index afb0893e..b1e3a91a 100644 --- a/include/util/SafeSignal.hpp +++ b/include/util/SafeSignal.hpp @@ -12,11 +12,6 @@ #include #include -#ifdef __OpenBSD__ -#define SIGRTMIN SIGUSR1 - 1 -#define SIGRTMAX SIGUSR1 + 1 -#endif - namespace waybar { /** From bd39795f806048a2d480c0aa5522fe3cde4047b1 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:31:59 +0200 Subject: [PATCH 111/167] Fix build for systems without SIGRTMIN/SIGRTMAX --- src/main.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 9d7d0ba2..0866a9b7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,9 +55,11 @@ static void catchSignals(waybar::SafeSignal& signal_handler) { std::signal(SIGINT, writeSignalToPipe); std::signal(SIGCHLD, writeSignalToPipe); +#ifdef SIGRTMIN for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { std::signal(sig, writeSignalToPipe); } +#endif while (true) { int signum; @@ -119,13 +121,15 @@ void handleUserSignal(int signal, bool& reload) { // If this signal should restart or close the bar, this function will write // `true` or `false`, respectively, into `reload`. static void handleSignalMainThread(int signum, bool& reload) { +#ifdef SIGRTMIN if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) { for (auto& bar : waybar::Client::inst()->bars) { bar->handleSignal(signum); } return; } - +#endif + switch (signum) { case SIGUSR1: handleUserSignal(SIGUSR1, reload); From 729dbbb82e81b5feb095c1312ccd04a5e3795289 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:33:27 +0200 Subject: [PATCH 112/167] Fix build for systems without SIGRTMIN/SIGRTMAX --- src/modules/image.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 98bf3c46..48729cc4 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -46,9 +46,11 @@ void waybar::modules::Image::delayWorker() { } void waybar::modules::Image::refresh(int sig) { +#ifdef SIGRTMIN if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); } +#endif } auto waybar::modules::Image::update() -> void { From 29979e9227e06db41bde47e89a088ecfc169c756 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:35:31 +0200 Subject: [PATCH 113/167] Fix build for systems without SIGRTMIN/SIGRTMAX --- src/modules/custom.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 28def8c9..99d072de 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -138,9 +138,11 @@ void waybar::modules::Custom::waitingWorker() { } void waybar::modules::Custom::refresh(int sig) { +#ifdef SIGRTMIN if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); } +#endif } void waybar::modules::Custom::handleEvent() { From 59d09c2c1281cb678d98e925c94b987d31280d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Schl=C3=B6mer?= Date: Mon, 27 Apr 2026 11:29:03 +0200 Subject: [PATCH 114/167] fix various toctou bugs in battery.cpp --- src/modules/battery.cpp | 92 +++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 30be3857..25436baf 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -117,24 +117,16 @@ void waybar::modules::Battery::refreshBatteries() { if (((bat_defined && dir_name == config_["bat"].asString()) || !bat_defined) && (fs::exists(node.path() / "capacity") || fs::exists(node.path() / "charge_now")) && fs::exists(node.path() / "uevent") && - (fs::exists(node.path() / "status") || bat_compatibility) && - fs::exists(node.path() / "type")) { + (fs::exists(node.path() / "status") || bat_compatibility)) { std::string type; - std::ifstream(node.path() / "type") >> type; - - if (!type.compare("Battery")) { + if (std::ifstream{node.path() / "type"} >> type && !type.compare("Battery")) { // Ignore non-system power supplies unless explicitly requested - if (!bat_defined && fs::exists(node.path() / "scope")) { + if (!bat_defined) { std::string scope; - try { - // for hotplug-in device, access it is always unstable because you may remove the - // device anytime so just allow failure happen and do nothing - std::ifstream(node.path() / "scope") >> scope; - } catch (const std::ifstream::failure& e) { - scope.clear(); - continue; - } - if (g_ascii_strcasecmp(scope.data(), "device") == 0) { + // for hotplug-in device, access it is always unstable because you may remove the + // device anytime so just allow failure happen and do nothing + if (std::ifstream{node.path() / "scope"} >> scope && + g_ascii_strcasecmp(scope.data(), "device") == 0) { continue; } } @@ -282,11 +274,11 @@ waybar::modules::Battery::getInfos() { auto bat = item.first; std::string _status; - /* Check for adapter status if battery is not available */ - if (!std::ifstream(bat / "status")) { - std::getline(std::ifstream(adapter_ / "status"), _status); - } else { - std::getline(std::ifstream(bat / "status"), _status); + { + std::ifstream f{bat / "status"}; + if (!std::getline(f, _status)) { + std::getline(std::ifstream(adapter_ / "status"), _status); + } } // Some battery will report current and charge in μA/μAh. @@ -295,64 +287,64 @@ waybar::modules::Battery::getInfos() { uint32_t current_now = 0; int32_t _current_now_int = 0; bool current_now_exists = false; - if (fs::exists(bat / "current_now")) { + if (std::ifstream current_now_f{bat / "current_now"}) { current_now_exists = true; - std::ifstream(bat / "current_now") >> _current_now_int; - } else if (fs::exists(bat / "current_avg")) { + current_now_f >> _current_now_int; + } else if (std::ifstream current_avg_f{bat / "current_avg"}) { current_now_exists = true; - std::ifstream(bat / "current_avg") >> _current_now_int; + current_avg_f >> _current_now_int; } // Documentation ABI allows a negative value when discharging, positive // value when charging. current_now = std::abs(_current_now_int); - if (fs::exists(bat / "time_to_empty_now")) { + if (std::ifstream f{bat / "time_to_empty_now"}) { time_to_empty_now_exists = true; - std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now; + f >> time_to_empty_now; } - if (fs::exists(bat / "time_to_full_now")) { + if (std::ifstream f{bat / "time_to_full_now"}) { time_to_full_now_exists = true; - std::ifstream(bat / "time_to_full_now") >> time_to_full_now; + f >> time_to_full_now; } uint32_t voltage_now = 0; bool voltage_now_exists = false; - if (fs::exists(bat / "voltage_now")) { + if (std::ifstream voltage_now_f{bat / "voltage_now"}) { voltage_now_exists = true; - std::ifstream(bat / "voltage_now") >> voltage_now; - } else if (fs::exists(bat / "voltage_avg")) { + voltage_now_f >> voltage_now; + } else if (std::ifstream voltage_avg_f{bat / "voltage_avg"}) { voltage_now_exists = true; - std::ifstream(bat / "voltage_avg") >> voltage_now; + voltage_avg_f >> voltage_now; } uint32_t charge_full = 0; bool charge_full_exists = false; - if (fs::exists(bat / "charge_full")) { + if (std::ifstream f{bat / "charge_full"}) { charge_full_exists = true; - std::ifstream(bat / "charge_full") >> charge_full; + f >> charge_full; } uint32_t charge_full_design = 0; bool charge_full_design_exists = false; - if (fs::exists(bat / "charge_full_design")) { + if (std::ifstream f{bat / "charge_full_design"}) { charge_full_design_exists = true; - std::ifstream(bat / "charge_full_design") >> charge_full_design; + f >> charge_full_design; } uint32_t charge_now = 0; bool charge_now_exists = false; - if (fs::exists(bat / "charge_now")) { + if (std::ifstream f{bat / "charge_now"}) { charge_now_exists = true; - std::ifstream(bat / "charge_now") >> charge_now; + f >> charge_now; } uint32_t power_now = 0; int32_t _power_now_int = 0; bool power_now_exists = false; - if (fs::exists(bat / "power_now")) { + if (std::ifstream f{bat / "power_now"}) { power_now_exists = true; - std::ifstream(bat / "power_now") >> _power_now_int; + f >> _power_now_int; } // Some drivers (example: Qualcomm) exposes use a negative value when // discharging, positive value when charging. @@ -360,28 +352,28 @@ waybar::modules::Battery::getInfos() { uint32_t energy_now = 0; bool energy_now_exists = false; - if (fs::exists(bat / "energy_now")) { + if (std::ifstream f{bat / "energy_now"}) { energy_now_exists = true; - std::ifstream(bat / "energy_now") >> energy_now; + f >> energy_now; } uint32_t energy_full = 0; bool energy_full_exists = false; - if (fs::exists(bat / "energy_full")) { + if (std::ifstream f{bat / "energy_full"}) { energy_full_exists = true; - std::ifstream(bat / "energy_full") >> energy_full; + f >> energy_full; } uint32_t energy_full_design = 0; bool energy_full_design_exists = false; - if (fs::exists(bat / "energy_full_design")) { + if (std::ifstream f{bat / "energy_full_design"}) { energy_full_design_exists = true; - std::ifstream(bat / "energy_full_design") >> energy_full_design; + f >> energy_full_design; } uint16_t cycleCount = 0; - if (fs::exists(bat / "cycle_count")) { - std::ifstream(bat / "cycle_count") >> cycleCount; + if (std::ifstream f{bat / "cycle_count"}) { + f >> cycleCount; } if (charge_full_design >= largestDesignCapacity) { largestDesignCapacity = charge_full_design; @@ -411,9 +403,9 @@ waybar::modules::Battery::getInfos() { } else if (energy_now_exists && energy_full_exists && energy_full != 0) { capacity_exists = true; capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; - } else if (fs::exists(bat / "capacity")) { + } else if (std::ifstream f{bat / "capacity"}) { capacity_exists = true; - std::ifstream(bat / "capacity") >> capacity; + f >> capacity; } if (!voltage_now_exists) { From 5af324f375edf8a6a033869d5386e711b4e49f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Schl=C3=B6mer?= Date: Mon, 27 Apr 2026 11:41:09 +0200 Subject: [PATCH 115/167] two more toctou bugs --- src/AAppIconLabel.cpp | 26 +++++++++++++------------- src/modules/cpu_frequency/linux.cpp | 18 ++++++++---------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index b72906c3..784f30c2 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -33,21 +33,21 @@ std::string toLowerCase(const std::string& input) { std::optional getFileBySuffix(const std::string& dir, const std::string& suffix, bool check_lower_case) { - if (!std::filesystem::exists(dir)) { - return {}; - } - for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { - if (entry.is_regular_file()) { - std::string filename = entry.path().filename().string(); - if (filename.size() < suffix.size()) { - continue; - } - if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) || - (check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(), - toLowerCase(suffix)) == 0)) { - return entry.path().string(); + try { + for (const auto& entry : std::filesystem::recursive_directory_iterator(dir)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + if (filename.size() < suffix.size()) { + continue; + } + if ((filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0) || + (check_lower_case && filename.compare(filename.size() - suffix.size(), suffix.size(), + toLowerCase(suffix)) == 0)) { + return entry.path().string(); + } } } + } catch (const std::filesystem::filesystem_error&) { } return {}; diff --git a/src/modules/cpu_frequency/linux.cpp b/src/modules/cpu_frequency/linux.cpp index 83f06aa5..7b927cdc 100644 --- a/src/modules/cpu_frequency/linux.cpp +++ b/src/modules/cpu_frequency/linux.cpp @@ -23,23 +23,21 @@ std::vector waybar::modules::CpuFrequency::parseCpuFrequencies() { if (frequencies.size() <= 0) { std::string cpufreq_dir = "/sys/devices/system/cpu/cpufreq"; - if (std::filesystem::exists(cpufreq_dir)) { + try { std::vector frequency_files = {"/cpuinfo_min_freq", "/cpuinfo_max_freq"}; for (auto& p : std::filesystem::directory_iterator(cpufreq_dir)) { for (const auto& freq_file : frequency_files) { std::string freq_file_path = p.path().string() + freq_file; - if (std::filesystem::exists(freq_file_path)) { - std::string freq_value; - std::ifstream freq(freq_file_path); - if (freq.is_open()) { - getline(freq, freq_value); - float frequency = std::strtol(freq_value.c_str(), nullptr, 10); - frequencies.push_back(frequency / 1000); - freq.close(); - } + std::string freq_value; + std::ifstream freq(freq_file_path); + if (freq.is_open()) { + getline(freq, freq_value); + float frequency = std::strtol(freq_value.c_str(), nullptr, 10); + frequencies.push_back(frequency / 1000); } } } + } catch (const std::filesystem::filesystem_error&) { } } From 255f17d13cf2d920f9459c3762d159b5dbcd9a91 Mon Sep 17 00:00:00 2001 From: Evans Date: Tue, 28 Apr 2026 03:24:55 +0000 Subject: [PATCH 116/167] battery: guard _exists = true assignment inside if condition --- src/modules/battery.cpp | 77 ++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 30be3857..537c62b0 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -296,63 +296,73 @@ waybar::modules::Battery::getInfos() { int32_t _current_now_int = 0; bool current_now_exists = false; if (fs::exists(bat / "current_now")) { - current_now_exists = true; - std::ifstream(bat / "current_now") >> _current_now_int; + if (std::ifstream(bat / "current_now") >> _current_now_int) { + current_now_exists = true; + } } else if (fs::exists(bat / "current_avg")) { - current_now_exists = true; - std::ifstream(bat / "current_avg") >> _current_now_int; + if (std::ifstream(bat / "current_avg") >> _current_now_int) { + current_now_exists = true; + } } // Documentation ABI allows a negative value when discharging, positive // value when charging. current_now = std::abs(_current_now_int); if (fs::exists(bat / "time_to_empty_now")) { - time_to_empty_now_exists = true; - std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now; + if (std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now) { + time_to_empty_now_exists = true; + } } if (fs::exists(bat / "time_to_full_now")) { - time_to_full_now_exists = true; - std::ifstream(bat / "time_to_full_now") >> time_to_full_now; + if (std::ifstream(bat / "time_to_full_now") >> time_to_full_now) { + time_to_full_now_exists = true; + } } uint32_t voltage_now = 0; bool voltage_now_exists = false; if (fs::exists(bat / "voltage_now")) { - voltage_now_exists = true; - std::ifstream(bat / "voltage_now") >> voltage_now; + if (std::ifstream(bat / "voltage_now") >> voltage_now) { + voltage_now_exists = true; + } } else if (fs::exists(bat / "voltage_avg")) { - voltage_now_exists = true; - std::ifstream(bat / "voltage_avg") >> voltage_now; + if (std::ifstream(bat / "voltage_avg") >> voltage_now) { + voltage_now_exists = true; + } } uint32_t charge_full = 0; bool charge_full_exists = false; if (fs::exists(bat / "charge_full")) { - charge_full_exists = true; - std::ifstream(bat / "charge_full") >> charge_full; + if (std::ifstream(bat / "charge_full") >> charge_full) { + charge_full_exists = true; + } } uint32_t charge_full_design = 0; bool charge_full_design_exists = false; if (fs::exists(bat / "charge_full_design")) { - charge_full_design_exists = true; - std::ifstream(bat / "charge_full_design") >> charge_full_design; + if (std::ifstream(bat / "charge_full_design") >> charge_full_design) { + charge_full_design_exists = true; + } } uint32_t charge_now = 0; bool charge_now_exists = false; if (fs::exists(bat / "charge_now")) { - charge_now_exists = true; - std::ifstream(bat / "charge_now") >> charge_now; + if (std::ifstream(bat / "charge_now") >> charge_now) { + charge_now_exists = true; + } } uint32_t power_now = 0; int32_t _power_now_int = 0; bool power_now_exists = false; if (fs::exists(bat / "power_now")) { - power_now_exists = true; - std::ifstream(bat / "power_now") >> _power_now_int; + if (std::ifstream(bat / "power_now") >> _power_now_int) { + power_now_exists = true; + } } // Some drivers (example: Qualcomm) exposes use a negative value when // discharging, positive value when charging. @@ -361,22 +371,25 @@ waybar::modules::Battery::getInfos() { uint32_t energy_now = 0; bool energy_now_exists = false; if (fs::exists(bat / "energy_now")) { - energy_now_exists = true; - std::ifstream(bat / "energy_now") >> energy_now; + if (std::ifstream(bat / "energy_now") >> energy_now) { + energy_now_exists = true; + } } uint32_t energy_full = 0; bool energy_full_exists = false; if (fs::exists(bat / "energy_full")) { - energy_full_exists = true; - std::ifstream(bat / "energy_full") >> energy_full; + if (std::ifstream(bat / "energy_full") >> energy_full) { + energy_full_exists = true; + } } uint32_t energy_full_design = 0; bool energy_full_design_exists = false; if (fs::exists(bat / "energy_full_design")) { - energy_full_design_exists = true; - std::ifstream(bat / "energy_full_design") >> energy_full_design; + if (std::ifstream(bat / "energy_full_design") >> energy_full_design) { + energy_full_design_exists = true; + } } uint16_t cycleCount = 0; @@ -412,8 +425,9 @@ waybar::modules::Battery::getInfos() { capacity_exists = true; capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; } else if (fs::exists(bat / "capacity")) { - capacity_exists = true; - std::ifstream(bat / "capacity") >> capacity; + if (std::ifstream(bat / "capacity") >> capacity) { + capacity_exists = true; + } } if (!voltage_now_exists) { @@ -593,11 +607,12 @@ waybar::modules::Battery::getInfos() { float calculated_capacity{0.0f}; if (total_capacity_exists) { - if (total_capacity > 0.0f) + if (total_capacity > 0.0f) { calculated_capacity = (float)total_capacity / batteries_.size(); - else if (total_energy_full_exists && total_energy_exists) { - if (total_energy_full > 0.0f) + } else if (total_energy_full_exists && total_energy_exists) { + if (total_energy_full > 0.0f) { calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full); + } } } From a66d6b1596a6c32697776f5d1b4a7ed09da70d52 Mon Sep 17 00:00:00 2001 From: Neltharion Date: Wed, 29 Apr 2026 03:40:06 +0300 Subject: [PATCH 117/167] Do not reconnect if connection was refused Otherwise, it will try to reconnect recursively --- src/util/audio_backend.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 4087e096..fb368ca5 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -99,15 +99,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { nullptr, nullptr); break; case PA_CONTEXT_FAILED: - // When pulseaudio server restarts, the connection is "failed". Try to reconnect. - // pa_threaded_mainloop_lock is already acquired in callback threads. - // So there is no need to lock it again. - if (backend->context_ != nullptr) { - pa_context_disconnect(backend->context_); - pa_context_unref(backend->context_); - backend->context_ = nullptr; + if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) { + // When pulseaudio server restarts, the connection is "failed". Try to reconnect. + // pa_threaded_mainloop_lock is already acquired in callback threads. + // So there is no need to lock it again. + if (backend->context_ != nullptr) { + pa_context_disconnect(backend->context_); + pa_context_unref(backend->context_); + backend->context_ = nullptr; + } + backend->connectContext(); } - backend->connectContext(); break; case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: From e17c0d9f0a73acc370df60ec8c532b1ed2385c73 Mon Sep 17 00:00:00 2001 From: Higor Prado Date: Wed, 29 Apr 2026 15:53:09 -0300 Subject: [PATCH 118/167] fix(hyprland/workspaces): adapt dispatch commands for Lua IPC protocol Hyprland 0.54 replaced the text-based dispatch socket protocol with a Lua-based one. Commands like "dispatch workspace 1" are now interpreted as invalid Lua (return hl.dispatch(workspace 1)), breaking workspace clicks and scroll navigation. Add IPC::dispatch() that probes the running Hyprland on first call and routes commands through the new hl.dsp Lua API when the Lua protocol is detected, falling back to the old text format otherwise. --- include/modules/hyprland/backend.hpp | 14 ++++++ src/modules/hyprland/backend.cpp | 67 ++++++++++++++++++++++++++++ src/modules/hyprland/workspace.cpp | 12 ++--- src/modules/hyprland/workspaces.cpp | 8 ++-- 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index a6ebd191..4e16299b 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,10 @@ class IPC { Json::Value getSocket1JsonReply(const std::string& rq); static std::filesystem::path getSocketFolder(const char* instanceSig); + /// Dispatch a Hyprland command. Automatically uses the correct protocol + /// (legacy text or Lua-based) depending on the running Hyprland version. + static std::string dispatch(const std::string& dispatcher, const std::string& arg); + protected: static std::filesystem::path socketFolder_; @@ -42,6 +47,15 @@ class IPC { void socketListener(); void parseIPC(const std::string&); + /// Detect whether the running Hyprland uses the Lua-based IPC protocol. + /// Returns true for Hyprland >= 0.54 (Lua config), false for older versions. + static bool isLuaProtocol(); + + /// Build a Lua-format dispatch command string. + static std::string buildLuaDispatch(const std::string& dispatcher, const std::string& arg); + + static std::optional s_luaProtocolDetected_; // cached detection result + std::thread ipcThread_; std::mutex callbackMutex_; std::mutex socketMutex_; diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index d0371202..08cf97c1 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include "util/scoped_fd.hpp" @@ -20,6 +21,7 @@ namespace waybar::modules::hyprland { std::filesystem::path IPC::socketFolder_; +std::optional IPC::s_luaProtocolDetected_; std::filesystem::path IPC::getSocketFolder(const char* instanceSig) { static std::mutex folderMutex; @@ -290,4 +292,69 @@ Json::Value IPC::getSocket1JsonReply(const std::string& rq) { return parser_.parse(reply); } +bool IPC::isLuaProtocol() { + if (s_luaProtocolDetected_.has_value()) { + return *s_luaProtocolDetected_; + } + + // Probe: send a harmless old-style dispatch and check the error. + // In Lua-based Hyprland (>= 0.54) the error contains "hl.dispatch". + // In older versions it returns "ok" or a different error. + auto reply = getSocket1Reply("dispatch workspace __waybar_probe__"); + bool luaProto = reply.find("hl.dispatch") != std::string::npos; + + if (luaProto) { + spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol (Hyprland >= 0.54)"); + } else { + spdlog::info("Hyprland IPC: detected legacy dispatch protocol"); + } + + s_luaProtocolDetected_ = luaProto; + return luaProto; +} + +std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::string& arg) { + // Map old-style dispatchers to the new Lua hl.dsp API. + // + // Old format: dispatch workspace 1 + // New format: /dispatch hl.dsp.focus({ workspace = "1" }) + // + // Old format: dispatch focusworkspaceoncurrentmonitor 2 + // New format: /dispatch hl.dsp.focus({ workspace = "2", monitor = "current" }) + // + // Old format: dispatch togglespecialworkspace name + // New format: /dispatch hl.dsp.workspace.toggle_special("name") + + if (dispatcher == "workspace") { + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; + } + if (dispatcher == "focusworkspaceoncurrentmonitor") { + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", monitor = \"current\" })"; + } + if (dispatcher == "togglespecialworkspace") { + if (arg.empty()) { + return "/dispatch hl.dsp.workspace.toggle_special()"; + } + return "/dispatch hl.dsp.workspace.toggle_special(\"" + arg + "\")"; + } + + // Fallback for any other dispatcher: try the old format wrapped in dispatch(). + // This may not work for all dispatchers, but it's a reasonable default. + spdlog::warn("Hyprland IPC: unknown dispatcher '{}' in Lua mode, attempting generic format", + dispatcher); + return "/dispatch hl.dsp." + dispatcher + "(\"" + arg + "\")"; +} + +std::string IPC::dispatch(const std::string& dispatcher, const std::string& arg) { + if (isLuaProtocol()) { + return getSocket1Reply(buildLuaDispatch(dispatcher, arg)); + } + // Legacy format: "dispatch " + std::string cmd = "dispatch " + dispatcher; + if (!arg.empty()) { + cmd += " " + arg; + } + return getSocket1Reply(cmd); +} + } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 21e7ef9b..753893f2 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -71,20 +71,20 @@ bool Workspace::handleClicked(GdkEventButton* bt) const { try { if (id() > 0) { // normal if (m_workspaceManager.moveToMonitor()) { - m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id())); + IPC::dispatch("focusworkspaceoncurrentmonitor", std::to_string(id())); } else { - m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id())); + IPC::dispatch("workspace", std::to_string(id())); } } else if (!isSpecial()) { // named (this includes persistent) if (m_workspaceManager.moveToMonitor()) { - m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name()); + IPC::dispatch("focusworkspaceoncurrentmonitor", "name:" + name()); } else { - m_ipc.getSocket1Reply("dispatch workspace name:" + name()); + IPC::dispatch("workspace", "name:" + name()); } } else if (id() != -99) { // named special - m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name()); + IPC::dispatch("togglespecialworkspace", name()); } else { // special - m_ipc.getSocket1Reply("dispatch togglespecialworkspace"); + IPC::dispatch("togglespecialworkspace", ""); } return true; } catch (const std::exception& e) { diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index f794249b..2496117f 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -1195,15 +1195,15 @@ bool Workspaces::handleScroll(GdkEventScroll* e) { if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) { if (allOutputs()) { - m_ipc.getSocket1Reply("dispatch workspace e+1"); + IPC::dispatch("workspace", "e+1"); } else { - m_ipc.getSocket1Reply("dispatch workspace m+1"); + IPC::dispatch("workspace", "m+1"); } } else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) { if (allOutputs()) { - m_ipc.getSocket1Reply("dispatch workspace e-1"); + IPC::dispatch("workspace", "e-1"); } else { - m_ipc.getSocket1Reply("dispatch workspace m-1"); + IPC::dispatch("workspace", "m-1"); } } From 97917db59369b66ef412a87f90cfdbda3ad55225 Mon Sep 17 00:00:00 2001 From: Higor Prado Date: Sat, 2 May 2026 20:25:43 -0300 Subject: [PATCH 119/167] test(hyprland): expose dispatch internals for unit tests Move buildLuaDispatch and isLuaProtocol from private to protected/public so IPCTestHelper can access them. Add 7 tests covering all buildLuaDispatch branches, dispatch error path, and isLuaProtocol cache behavior. --- include/modules/hyprland/backend.hpp | 14 ++--- test/hyprland/backend.cpp | 79 ++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/include/modules/hyprland/backend.hpp b/include/modules/hyprland/backend.hpp index 4e16299b..7c6369da 100644 --- a/include/modules/hyprland/backend.hpp +++ b/include/modules/hyprland/backend.hpp @@ -40,22 +40,22 @@ class IPC { /// (legacy text or Lua-based) depending on the running Hyprland version. static std::string dispatch(const std::string& dispatcher, const std::string& arg); + /// Build a Lua-format dispatch command string. + static std::string buildLuaDispatch(const std::string& dispatcher, const std::string& arg); + protected: static std::filesystem::path socketFolder_; - private: - void socketListener(); - void parseIPC(const std::string&); - /// Detect whether the running Hyprland uses the Lua-based IPC protocol. /// Returns true for Hyprland >= 0.54 (Lua config), false for older versions. static bool isLuaProtocol(); - /// Build a Lua-format dispatch command string. - static std::string buildLuaDispatch(const std::string& dispatcher, const std::string& arg); - static std::optional s_luaProtocolDetected_; // cached detection result + private: + void socketListener(); + void parseIPC(const std::string&); + std::thread ipcThread_; std::mutex callbackMutex_; std::mutex socketMutex_; diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp index ccc2da65..62d23ae4 100644 --- a/test/hyprland/backend.cpp +++ b/test/hyprland/backend.cpp @@ -15,6 +15,10 @@ namespace { class IPCTestHelper : public hyprland::IPC { public: static void resetSocketFolder() { socketFolder_.clear(); } + static void resetLuaProtocolDetection() { s_luaProtocolDetected_.reset(); } + static void setLuaProtocolDetected(bool value) { s_luaProtocolDetected_ = value; } + using hyprland::IPC::buildLuaDispatch; + using hyprland::IPC::isLuaProtocol; }; std::size_t countOpenFds() { @@ -133,3 +137,78 @@ TEST_CASE("getSocket1Reply failure paths do not leak fds", "[getSocket1Reply][fd REQUIRE(after_connect_failures == baseline); } #endif + +// --- Tests for new Lua IPC dispatch functions --- + +TEST_CASE("buildLuaDispatch workspace", "[buildLuaDispatch]") { + SECTION("numeric workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "1"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"1\" })"); + } + SECTION("named workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "name:term"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"name:term\" })"); + } + SECTION("relative workspace") { + auto result = IPCTestHelper::buildLuaDispatch("workspace", "e+1"); + REQUIRE(result == "/dispatch hl.dsp.focus({ workspace = \"e+1\" })"); + } +} + +TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch]") { + auto result = + IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3"); + REQUIRE( + result == + "/dispatch hl.dsp.focus({ workspace = \"3\", monitor = \"current\" })"); +} + +TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") { + SECTION("with name") { + auto result = + IPCTestHelper::buildLuaDispatch("togglespecialworkspace", "scratchpad"); + REQUIRE(result == + "/dispatch hl.dsp.workspace.toggle_special(\"scratchpad\")"); + } + SECTION("empty arg") { + auto result = + IPCTestHelper::buildLuaDispatch("togglespecialworkspace", ""); + REQUIRE(result == "/dispatch hl.dsp.workspace.toggle_special()"); + } +} + +TEST_CASE("buildLuaDispatch unknown dispatcher fallback", "[buildLuaDispatch]") { + auto result = + IPCTestHelper::buildLuaDispatch("unknown_dispatcher", "some_arg"); + REQUIRE(result == + "/dispatch hl.dsp.unknown_dispatcher(\"some_arg\")"); +} + +TEST_CASE("dispatch throws when Hyprland is not running", "[dispatch]") { + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + IPCTestHelper::resetSocketFolder(); + IPCTestHelper::resetLuaProtocolDetection(); + + CHECK_THROWS(hyprland::IPC::dispatch("workspace", "1")); +} + +TEST_CASE("isLuaProtocol uses cached value and avoids socket call", + "[isLuaProtocol]") { + unsetenv("HYPRLAND_INSTANCE_SIGNATURE"); + IPCTestHelper::resetSocketFolder(); + + SECTION("cached false") { + IPCTestHelper::setLuaProtocolDetected(false); + // Should return false without throwing (no socket call needed) + REQUIRE(IPCTestHelper::isLuaProtocol() == false); + } + + SECTION("cached true") { + IPCTestHelper::setLuaProtocolDetected(true); + // Should return true without throwing (no socket call needed) + REQUIRE(IPCTestHelper::isLuaProtocol() == true); + } + + // Cleanup: reset detection so other tests aren't affected + IPCTestHelper::resetLuaProtocolDetection(); +} From 5c3ec1fd634731ccc61aca7c41211643af0295c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Rodon?= Date: Mon, 11 May 2026 22:47:24 +0200 Subject: [PATCH 120/167] fix(tray): hide module when no items are visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tray::update() already had logic to hide the module when all child items were passive, but it was only re-run on item add/remove — never on a status change. When the last visible item transitioned to Passive, the item hid itself but the now-empty module remained visible. Connect Tray to each item's Gtk::EventBox signal_show/signal_hide so update() runs on every visibility transition, and simplify update() to check child->get_visible() directly instead of inspecting the `passive` CSS class. The Tray-level `show-passive-items` read becomes redundant since Item already honours it when deciding its own visibility; remove it along with the now-unused Tray::show_passive_ member. Fixes: #3721 --- include/modules/sni/tray.hpp | 1 - src/modules/sni/tray.cpp | 17 ++++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 5f12d7f2..6cda35d7 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -21,7 +21,6 @@ class Tray : public AModule { void onRemove(std::unique_ptr& item); static inline std::size_t nb_hosts_ = 0; - bool show_passive_ = false; Gtk::Box box_; SNI::Watcher::singleton watcher_; SNI::Host host_; diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 34a3c05f..126ee03d 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -23,9 +23,6 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) if (config_["spacing"].isUInt()) { box_.set_spacing(config_["spacing"].asUInt()); } - if (config["show-passive-items"].isBool()) { - show_passive_ = config["show-passive-items"].asBool(); - } nb_hosts_ += 1; if (config_["icons"].isObject()) { IconManager::instance().setIconsConfig(config_["icons"]); @@ -39,6 +36,8 @@ void Tray::onAdd(std::unique_ptr& item) { } else { box_.pack_start(item->event_box); } + item->event_box.signal_show().connect([this] { dp.emit(); }); + item->event_box.signal_hide().connect([this] { dp.emit(); }); dp.emit(); } @@ -48,17 +47,9 @@ void Tray::onRemove(std::unique_ptr& item) { } auto Tray::update() -> void { - // Show tray only when items are available std::vector children = box_.get_children(); - if (show_passive_) { - event_box_.set_visible(!children.empty()); - } else { - event_box_.set_visible(!std::all_of(children.begin(), children.end(), [](Gtk::Widget* child) { - return child->get_style_context()->has_class("passive"); - })); - } - - // Call parent update + event_box_.set_visible(std::any_of(children.begin(), children.end(), + [](Gtk::Widget* child) { return child->get_visible(); })); AModule::update(); } From 0ec6d06472cb42a061d9503fc426b47bf6cf3301 Mon Sep 17 00:00:00 2001 From: Milad Alizadeh Date: Fri, 15 May 2026 17:29:21 +0100 Subject: [PATCH 121/167] Guard set_current_layout against unknown layout names (wtype, hot-plugged devices) so transient events stop blanking layout_. Refresh layouts_map_ on "added"/"xkb_keymap" input events and union layouts across all keyboards so new devices contribute their layouts. Release mutex_ before the refresh sendCmd to avoid self-deadlock from the synchronous signal_cmd emit. --- src/modules/sway/language.cpp | 57 ++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index 2e1103d3..28f1ef24 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -54,21 +54,28 @@ void Language::onCmd(const struct Ipc::ipc_response& res) { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); std::vector used_layouts; - // Display current layout of a device with a maximum count of layouts, expecting that all will - // be OK + // Union layout names across every keyboard input so hot-plugged devices contribute their + // layouts to the map. Track the device with the most layouts to seed the initially displayed + // layout, matching the previous behaviour at startup. Json::ArrayIndex max_id = 0, max = 0; for (Json::ArrayIndex i = 0; i < payload.size(); i++) { - auto size = payload[i][XKB_LAYOUT_NAMES_KEY].size(); - if (size > max) { - max = size; + if (payload[i]["type"].asString() != "keyboard") continue; + const auto& names = payload[i][XKB_LAYOUT_NAMES_KEY]; + if (names.size() > max) { + max = names.size(); max_id = i; } + for (const auto& layout : names) { + const auto name = layout.asString(); + if (std::find(used_layouts.begin(), used_layouts.end(), name) == used_layouts.end()) { + used_layouts.push_back(name); + } + } } - for (const auto& layout : payload[max_id][XKB_LAYOUT_NAMES_KEY]) { - used_layouts.push_back(layout.asString()); - } - + // Rebuild from scratch so init_layouts_map's duplicate-suffix pass doesn't compound across + // refreshes (e.g. "us" -> "us1" -> "us11"). + layouts_map_.clear(); init_layouts_map(used_layouts); set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); dp.emit(); @@ -82,13 +89,26 @@ void Language::onEvent(const struct Ipc::ipc_response& res) { return; } + bool refresh_inputs = false; try { - std::lock_guard lock(mutex_); - auto payload = parser_.parse(res.payload)["input"]; - if (payload["type"].asString() == "keyboard") { - set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + { + std::lock_guard lock(mutex_); + auto root = parser_.parse(res.payload); + auto change = root["change"].asString(); + auto payload = root["input"]; + if (payload["type"].asString() == "keyboard") { + // A device was added or its keymap changed - the layout set may have grown, so refresh + // layouts_map_ via IPC_GET_INPUTS once we've released mutex_. + refresh_inputs = (change == "added" || change == "xkb_keymap"); + set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + } + dp.emit(); + } + // sendCmd is synchronous: it blocks on the IPC reply and then emits signal_cmd on this same + // thread, which lands in onCmd and re-locks mutex_. Must call it with mutex_ released. + if (refresh_inputs) { + ipc_.sendCmd(IPC_GET_INPUTS); } - dp.emit(); } catch (const std::exception& e) { spdlog::error("Language: {}", e.what()); } @@ -125,8 +145,15 @@ auto Language::update() -> void { } auto Language::set_current_layout(const std::string& current_layout) -> void { + // Guard against unknown / empty layout names: transient virtual keyboards (e.g. wtype) and + // hot-plugged devices whose layouts haven't made it into the map yet would otherwise blank out + // layout_ via map::operator[]'s default-construct-on-miss. + auto it = layouts_map_.find(current_layout); + if (it == layouts_map_.end()) { + return; + } label_.get_style_context()->remove_class(layout_.short_name); - layout_ = layouts_map_[current_layout]; + layout_ = it->second; label_.get_style_context()->add_class(layout_.short_name); } From cdb792af41e78d01263c2152d8b5050c40705ea9 Mon Sep 17 00:00:00 2001 From: Tom Benham Date: Sat, 16 May 2026 18:03:09 +0200 Subject: [PATCH 122/167] fix(hyprland/workspaces): fix IPC call when using `"move-to-monitor": true` --- src/modules/hyprland/backend.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 08cf97c1..f38f8216 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -320,7 +320,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri // New format: /dispatch hl.dsp.focus({ workspace = "1" }) // // Old format: dispatch focusworkspaceoncurrentmonitor 2 - // New format: /dispatch hl.dsp.focus({ workspace = "2", monitor = "current" }) + // New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true }) // // Old format: dispatch togglespecialworkspace name // New format: /dispatch hl.dsp.workspace.toggle_special("name") @@ -329,7 +329,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; } if (dispatcher == "focusworkspaceoncurrentmonitor") { - return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", monitor = \"current\" })"; + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })"; } if (dispatcher == "togglespecialworkspace") { if (arg.empty()) { From 40c9d92e3de1ee287da1ac4901264d1571fe803d Mon Sep 17 00:00:00 2001 From: Tom Benham Date: Sat, 16 May 2026 21:29:23 +0200 Subject: [PATCH 123/167] test(hyprland): fix dispatch test --- test/hyprland/backend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp index 62d23ae4..f2e10dae 100644 --- a/test/hyprland/backend.cpp +++ b/test/hyprland/backend.cpp @@ -160,7 +160,7 @@ TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch] IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3"); REQUIRE( result == - "/dispatch hl.dsp.focus({ workspace = \"3\", monitor = \"current\" })"); + "/dispatch hl.dsp.focus({ workspace = \"3\", on_current_monitor = true })"); } TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") { From 7e2afaa5b3d78ae5c526e4d95708c9e9f52bb0e5 Mon Sep 17 00:00:00 2001 From: Desuwa Date: Sat, 16 May 2026 14:05:17 -0700 Subject: [PATCH 124/167] fix: mpd specify timer interval in seconds --- include/modules/mpd/state.inl.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp index 895970e6..a55a7948 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -1,13 +1,14 @@ #pragma once namespace detail { +using namespace std::literals::chrono_literals; inline bool Context::is_connected() const { return mpd_module_->connection_ != nullptr; } inline bool Context::is_playing() const { return mpd_module_->playing(); } 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(); } +constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_ / 1s; } 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_; } From ff76e96786cbdd0f586bc9c97e88a414635f2365 Mon Sep 17 00:00:00 2001 From: Adam Druzd Date: Sun, 17 May 2026 09:12:08 +0200 Subject: [PATCH 125/167] fix(hyprland): keep workspace hover over taskbar icons --- include/modules/hyprland/workspace.hpp | 11 +++ src/modules/hyprland/workspace.cpp | 102 +++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 1d80d331..5730dda9 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -45,6 +45,15 @@ class Workspace { bool isUrgent() const { return m_isUrgent; }; bool handleClicked(GdkEventButton* bt) const; + + bool handleEnter(GdkEventCrossing* event); + bool handleLeave(GdkEventCrossing* event); + + void startHoverCheck(); + void stopHoverCheck(); + bool syncHoverClass(); + bool pointerInsideButton(); + void setActive(bool value = true) { m_isActive = value; }; void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; @@ -80,6 +89,8 @@ class Workspace { bool m_isUrgent = false; bool m_isVisible = false; + sigc::connection m_hoverCheckConnection; + std::vector m_windowMap; Gtk::Button m_button; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 753893f2..67ef4506 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -30,6 +31,11 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma } m_button.add_events(Gdk::BUTTON_PRESS_MASK); + m_button.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); + + m_button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleEnter)); + m_button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleLeave)); + m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), false); @@ -66,6 +72,102 @@ std::optional Workspace::closeWindow(WindowAddress const& addr) { return std::nullopt; } +bool Workspace::pointerInsideButton() { + auto display = Gdk::Display::get_default(); + if (!display) { + return false; + } + + auto seat = display->get_default_seat(); + if (!seat) { + return false; + } + + auto pointer = seat->get_pointer(); + if (!pointer) { + return false; + } + + Glib::RefPtr screen; + int pointerRootX = 0; + int pointerRootY = 0; + + pointer->get_position(screen, pointerRootX, pointerRootY); + + Gtk::Widget* toplevel = m_button.get_toplevel(); + if (toplevel == nullptr || !toplevel->get_window()) { + return false; + } + + int buttonX = 0; + int buttonY = 0; + + if (!m_button.translate_coordinates(*toplevel, 0, 0, buttonX, buttonY)) { + return false; + } + + int windowRootX = 0; + int windowRootY = 0; + toplevel->get_window()->get_root_origin(windowRootX, windowRootY); + + const auto allocation = m_button.get_allocation(); + + const int buttonRootX = windowRootX + buttonX; + const int buttonRootY = windowRootY + buttonY; + const int buttonWidth = allocation.get_width(); + const int buttonHeight = allocation.get_height(); + + return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY && + pointerRootX < buttonRootX + buttonWidth && + pointerRootY < buttonRootY + buttonHeight; +} + +bool Workspace::syncHoverClass() { + auto styleContext = m_button.get_style_context(); + + if (pointerInsideButton()) { + styleContext->add_class("workspace-hover"); + return true; + } + + styleContext->remove_class("workspace-hover"); + stopHoverCheck(); + return false; +} + +void Workspace::startHoverCheck() { + if (m_hoverCheckConnection.connected()) { + return; + } + + m_hoverCheckConnection = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &Workspace::syncHoverClass), + 50); +} + +void Workspace::stopHoverCheck() { + if (m_hoverCheckConnection.connected()) { + m_hoverCheckConnection.disconnect(); + } +} + +bool Workspace::handleEnter(GdkEventCrossing* /*event*/) { + m_button.get_style_context()->add_class("workspace-hover"); + startHoverCheck(); + return false; +} + +bool Workspace::handleLeave(GdkEventCrossing* /*event*/) { + /* + * Do not remove immediately. + * Workspace taskbar children can fire misleading leave events while the + * pointer is still visually inside the workspace button. + * + * The polling check will remove the class once the pointer really leaves. + */ + startHoverCheck(); + return false; +} bool Workspace::handleClicked(GdkEventButton* bt) const { if (bt->type == GDK_BUTTON_PRESS) { try { From 0351ac8fb2d0f204f67f9b2ddbe7a1c8d54ba413 Mon Sep 17 00:00:00 2001 From: finitemonkey <56678540+finitemonkey@users.noreply.github.com> Date: Sat, 16 May 2026 20:40:01 +0100 Subject: [PATCH 126/167] Additional options when muted for the PulseAudio-Slider module: - Add "muted" class for styling the muted state. - Add "zero-on-mute" boolean option to control slider position when muted. - Add "unmute-on-volume-change" boolean option to control whether to automatically unmute when the volume changes. --- include/modules/pulseaudio_slider.hpp | 16 +++-- include/util/audio_backend.hpp | 11 +++- man/waybar-pulseaudio-slider.5.scd | 28 +++++++-- src/modules/pulseaudio_slider.cpp | 84 ++++++++++----------------- src/util/audio_backend.cpp | 24 ++++++++ 5 files changed, 98 insertions(+), 65 deletions(-) diff --git a/include/modules/pulseaudio_slider.hpp b/include/modules/pulseaudio_slider.hpp index 3ef44684..c9f82341 100644 --- a/include/modules/pulseaudio_slider.hpp +++ b/include/modules/pulseaudio_slider.hpp @@ -6,10 +6,6 @@ #include "util/audio_backend.hpp" namespace waybar::modules { -enum class PulseaudioSliderTarget { - Sink, - Source, -}; class PulseaudioSlider : public ASlider { public: @@ -21,7 +17,15 @@ class PulseaudioSlider : public ASlider { private: std::shared_ptr backend = nullptr; - PulseaudioSliderTarget target = PulseaudioSliderTarget::Sink; + util::PulseaudioTarget target = util::PulseaudioTarget::Sink; + + bool zero_on_mute = true; + bool unmute_on_volume_change = true; + // zero_on_mute and unmute_on_volume_change default to true + // in order to maintain previous behaviour when using a + // config in which these values are undefined + + bool previously_muted = false; }; -} // namespace waybar::modules \ No newline at end of file +} // namespace waybar::modules diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp index 3737ae26..f5904b43 100644 --- a/include/util/audio_backend.hpp +++ b/include/util/audio_backend.hpp @@ -14,6 +14,11 @@ namespace waybar::util { +enum class PulseaudioTarget { + Sink, + Source, +}; + class AudioBackend { private: static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); @@ -92,7 +97,11 @@ class AudioBackend { void toggleSourceMute(); void toggleSourceMute(bool); + uint16_t getVolume(PulseaudioTarget) const; + bool getMuted(PulseaudioTarget) const; + void unmute(PulseaudioTarget); + bool isBluetooth(); }; -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index 0271e7c5..3f56a80c 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -28,9 +28,20 @@ The volume can be controlled by dragging the slider across the bar or clicking o The orientation of the slider. Can be either `horizontal` or `vertical`. *expand*: ++ - typeof: bool ++ - default: false ++ - Enables this module to consume all left over space dynamically. + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +*zero-on-mute*: ++ + typeof: bool ++ + default: true ++ + `true` = The slider will be set to `min` when the source/sink is muted. ++ + `false` = The slider will continue to show the unmuted volume level when the source/sink is muted. + +*unmute-on-volume-change*: ++ + typeof: bool ++ + default: true ++ + Specifies whether to unmute a muted souce/sink when its volume is changed by the user moving the slider. # EXAMPLES @@ -41,7 +52,9 @@ The volume can be controlled by dragging the slider across the bar or clicking o "pulseaudio/slider": { "min": 0, "max": 100, - "orientation": "horizontal" + "orientation": "horizontal", + "zero-on-mute": false, + "unmute-on-volume-change": false } ``` @@ -52,6 +65,9 @@ The slider is a component with multiple CSS Nodes, of which the following are ex *#pulseaudio-slider*: ++ Controls the style of the box *around* the slider and bar. +*#pulseaudio-slider.muted*: ++ + Controls the style when the audio source/sink is muted. + *#pulseaudio-slider slider*: ++ Controls the style of the slider handle. @@ -85,4 +101,8 @@ The slider is a component with multiple CSS Nodes, of which the following are ex border-radius: 5px; background: green; } + +#pulseaudio-slider.muted highlight { + background-color: orange; +} ``` diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp index bf85584e..aeddeb24 100644 --- a/src/modules/pulseaudio_slider.cpp +++ b/src/modules/pulseaudio_slider.cpp @@ -10,73 +10,49 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con if (config_["target"].isString()) { std::string target = config_["target"].asString(); if (target == "sink") { - this->target = PulseaudioSliderTarget::Sink; + this->target = util::PulseaudioTarget::Sink; } else if (target == "source") { - this->target = PulseaudioSliderTarget::Source; + this->target = util::PulseaudioTarget::Source; } } + + if (config_["zero-on-mute"].isBool()) { + zero_on_mute = config_["zero-on-mute"].asBool(); + } + + if (config_["unmute-on-volume-change"].isBool()) { + unmute_on_volume_change = config_["unmute-on-volume-change"].asBool(); + } } void PulseaudioSlider::update() { - switch (target) { - case PulseaudioSliderTarget::Sink: - if (backend->getSinkMuted()) { - scale_.set_value(min_); - } else { - scale_.set_value(backend->getSinkVolume()); - } - break; + uint16_t display_value = backend->getVolume(target); + bool is_muted = backend->getMuted(target); - case PulseaudioSliderTarget::Source: - if (backend->getSourceMuted()) { - scale_.set_value(min_); - } else { - scale_.set_value(backend->getSourceVolume()); - } - break; + if (is_muted && !previously_muted) { + if (zero_on_mute) { + display_value = min_; + } + scale_.get_style_context()->add_class("muted"); + } else if (previously_muted && !is_muted) { + scale_.get_style_context()->remove_class("muted"); } + + scale_.set_value(display_value); + + previously_muted = is_muted; } void PulseaudioSlider::onValueChanged() { - bool is_mute = false; + uint16_t slider_value = scale_.get_value(); - switch (target) { - case PulseaudioSliderTarget::Sink: - if (backend->getSinkMuted()) { - is_mute = true; - } - break; - - case PulseaudioSliderTarget::Source: - if (backend->getSourceMuted()) { - is_mute = true; - } - break; - } - - uint16_t volume = scale_.get_value(); - - if (is_mute) { - // Avoid setting sink/source to volume 0 if the user muted if via another mean. - if (volume == 0) { - return; - } - - // If the sink/source is mute, but the user clicked the slider, unmute it! - else { - switch (target) { - case PulseaudioSliderTarget::Sink: - backend->toggleSinkMute(false); - break; - - case PulseaudioSliderTarget::Source: - backend->toggleSourceMute(false); - break; - } + // Avoid setting sink/source to volume 0 if the user muted it via other means. + if (!backend->getMuted(target) || slider_value != 0) { + if (unmute_on_volume_change) { + backend->unmute(target); } + backend->changeVolume(slider_value, min_, max_); } - - 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 4087e096..d617813d 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -259,6 +259,14 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo pa_context_get_source_info_list(context, sourceInfoCb, data); } +uint16_t AudioBackend::getVolume(PulseaudioTarget target) const { + if (target == PulseaudioTarget::Source) { + return source_volume_; + } else { + return volume_; + } +} + void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) { // Early return if context is not ready if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { @@ -368,6 +376,14 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma pa_threaded_mainloop_unlock(mainloop_); } +bool AudioBackend::getMuted(PulseaudioTarget target) const { + if (target == PulseaudioTarget::Source) { + return source_muted_; + } else { + return muted_; + } +} + void AudioBackend::toggleSinkMute() { if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = !muted_; @@ -404,6 +420,14 @@ void AudioBackend::toggleSourceMute(bool mute) { pa_threaded_mainloop_unlock(mainloop_); } +void AudioBackend::unmute(PulseaudioTarget target) { + if (target == PulseaudioTarget::Source) { + toggleSourceMute(false); + } else { + toggleSinkMute(false); + } +} + bool AudioBackend::isBluetooth() { return monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio monitor_.find("a2dp-sink") != std::string::npos || // PipeWire From 876eaaf70f9ec9d42992849e0cb44533637a7f8c Mon Sep 17 00:00:00 2001 From: Francois Lachese Date: Wed, 20 May 2026 15:57:38 +0200 Subject: [PATCH 127/167] fix(amodule): prevent crash when cursor timeout fires after destruction --- include/AModule.hpp | 1 + src/AModule.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index a338ffe3..7119b2f6 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -57,6 +57,7 @@ class AModule : public IModule { bool hasUserEvents_; gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; + sigc::connection cursor_timeout_conn_; std::map eventActionMap_; static const inline std::map, std::string> eventMap_{ {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, diff --git a/src/AModule.cpp b/src/AModule.cpp index a5ba69d3..ea9fd2b4 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -17,7 +17,8 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false}, distance_scrolled_y_(0.0), - distance_scrolled_x_(0.0) { + distance_scrolled_x_(0.0), + cursor_timeout_conn_() { // Configure module action Map const Json::Value actions{config_["actions"]}; @@ -83,6 +84,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } AModule::~AModule() { + if (cursor_timeout_conn_.connected()) { + cursor_timeout_conn_.disconnect(); + } for (const auto& pid : pid_children_) { if (pid != -1) { killpg(pid, SIGTERM); @@ -118,7 +122,7 @@ void AModule::setCursor(Gdk::CursorType const& c) { } else { // window may not be accessible yet, in this case, // schedule another call for setting the cursor in 1 sec - Glib::signal_timeout().connect_seconds( + cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds( [this, c]() { setCursor(c); return false; From fcf1fb798cf9674761a4d83a5c11ff15213e2006 Mon Sep 17 00:00:00 2001 From: Ezequiel Colunga Acha Date: Wed, 20 May 2026 15:06:41 -0300 Subject: [PATCH 128/167] fix(wlr/taskbar): let task titles shrink within available space The taskbar currently requests enough width for every task title, which can push right-side modules off-screen when many windows are open. Allow task buttons to share the taskbar allocation and ellipsize title labels instead of forcing the whole module to grow. This keeps neighboring modules visible while preserving the existing taskbar click actions and formatting behavior. Co-authored-by: Cursor --- src/modules/wlr/taskbar.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index bde90910..6ba41ce3 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -93,10 +93,20 @@ Task::Task(const waybar::Bar& bar, const Json::Value& config, Taskbar* tbar, zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); button.set_relief(Gtk::RELIEF_NONE); + button.set_hexpand(true); + content_.set_hexpand(true); + text_before_.set_ellipsize(Pango::ELLIPSIZE_END); + text_before_.set_single_line_mode(true); + text_before_.set_width_chars(1); + text_before_.set_xalign(0.0); + text_after_.set_ellipsize(Pango::ELLIPSIZE_END); + text_after_.set_single_line_mode(true); + text_after_.set_width_chars(1); + text_after_.set_xalign(0.0); - content_.add(text_before_); - content_.add(icon_); - content_.add(text_after_); + content_.pack_start(text_before_, true, true, 0); + content_.pack_start(icon_, false, false, 0); + content_.pack_start(text_after_, true, true, 0); content_.show(); button.add(content_); @@ -718,7 +728,7 @@ void Taskbar::handle_finished() { } void Taskbar::add_button(Gtk::Button& bt) { - box_.pack_start(bt, false, false); + box_.pack_start(bt, true, true); box_.get_style_context()->remove_class("empty"); } From eb3b86c4d83a8e2e54eeca77a36861b5ccc8d432 Mon Sep 17 00:00:00 2001 From: buzz Date: Thu, 21 May 2026 17:02:39 +0200 Subject: [PATCH 129/167] fix(hyprland/workspaces): support on-scroll-up and on-scroll-down config options The handleScroll method unconditionally dispatched workspace cycling commands and never checked for custom on-scroll-up/on-scroll-down config values. Register the scroll handler when custom scroll commands are configured (even without enable-bar-scroll), and delegate to AModule::handleScroll so user commands are executed, matching the pattern used by other modules. --- src/modules/hyprland/workspaces.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 2496117f..c9fffc97 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -50,7 +50,8 @@ void Workspaces::init() { if (m_scrollEventConnection_.connected()) { m_scrollEventConnection_.disconnect(); } - if (barScroll()) { + bool hasScrollConfig = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString(); + if (barScroll() || hasScrollConfig) { auto& window = const_cast(m_bar).window; window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); m_scrollEventConnection_ = @@ -1188,6 +1189,12 @@ bool Workspaces::handleScroll(GdkEventScroll* e) { if (gdk_event_get_pointer_emulated((GdkEvent*)e)) { return false; } + + // Check for custom scroll commands first; delegate to base class + if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { + return AModule::handleScroll(e); + } + auto dir = AModule::getScrollDir(e); if (dir == SCROLL_DIR::NONE) { return true; From ea36aab3babe79f646e87b8cd3a49d041dc32443 Mon Sep 17 00:00:00 2001 From: buzz Date: Thu, 21 May 2026 17:33:43 +0200 Subject: [PATCH 130/167] docs(hyprland/workspaces): add on-scroll-up and on-scroll-down to man page --- man/waybar-hyprland-workspaces.5.scd | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 5284ce99..c852f2ba 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -135,6 +135,14 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. 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. +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. This replaces the default behaviour of workspace cycling. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. This replaces the default behaviour of workspace cycling. + *ignore-workspaces*: ++ typeof: array ++ default: [] ++ From 85163c684746fe3f08180598bcb05768bbf1016d Mon Sep 17 00:00:00 2001 From: buzz Date: Thu, 21 May 2026 18:50:43 +0200 Subject: [PATCH 131/167] fix(hyprland/workspaces): replace EventBox with Button for taskbar windows --- src/modules/hyprland/workspace.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 753893f2..ca427dc9 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -301,15 +301,18 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { auto window_box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); window_box->set_tooltip_markup(window_repr.window_title); - window_box->get_style_context()->add_class("taskbar-window"); + + auto button = Gtk::manage(new Gtk::Button()); + button->set_relief(Gtk::RELIEF_NONE); + button->add(*window_box); + button->get_style_context()->add_class("taskbar-window"); if (window_repr.isActive) { - window_box->get_style_context()->add_class("active"); + button->get_style_context()->add_class("active"); } - auto event_box = Gtk::manage(new Gtk::EventBox()); - event_box->add(*window_box); if (m_workspaceManager.onClickWindow() != "") { - event_box->signal_button_press_event().connect( - sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address)); + button->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), + false); } auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), @@ -334,8 +337,8 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { window_box->pack_start(*window_label_after, true, true); } - m_content.pack_start(*event_box, true, false); - event_box->show_all(); + m_content.pack_start(*button, true, false); + button->show_all(); }; if (m_workspaceManager.taskbarReverseDirection()) { From f8cc7173cae8e2913ca03d33a638f83ddfb84edd Mon Sep 17 00:00:00 2001 From: Siriusuna Date: Fri, 22 May 2026 02:44:50 +0800 Subject: [PATCH 132/167] refactor: extract utf8 truncation logic to utility Move the UTF-8 visual-width measurement and truncation helper functions from the mpris module to a common utility (include/util/utf8_string.hpp and src/util/utf8_string.cpp). This decouples string truncation and width measurement from the mpris module, allowing other modules (like mpd) to reuse. The helper function `utf8_truncate` (formerly `truncate` in mpris) and `utf8_width` is exported under the `waybar::util` namespace, while the low-level `measure_and_truncate` (formerly `utf8_truncate`) is encapsulated in an anonymous namespace in src/util/utf8_string.cpp to avoid unnecessary API exposure. No functional changes were made to the mpris module's behavior. --- include/util/utf8_string.hpp | 7 ++++ meson.build | 3 +- src/modules/mpris/mpris.cpp | 62 +++++------------------------------- src/util/utf8_string.cpp | 62 ++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 55 deletions(-) create mode 100644 include/util/utf8_string.hpp create mode 100644 src/util/utf8_string.cpp diff --git a/include/util/utf8_string.hpp b/include/util/utf8_string.hpp new file mode 100644 index 00000000..07a04466 --- /dev/null +++ b/include/util/utf8_string.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace waybar::util { +size_t utf8_width(const std::string& str); +void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len); +} // namespace waybar::util diff --git a/meson.build b/meson.build index 0c494eb2..e528a6da 100644 --- a/meson.build +++ b/meson.build @@ -186,7 +186,8 @@ src_files = files( 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', 'src/util/css_reload_helper.cpp', - 'src/util/transform_8bit_to_rgba.cpp' + 'src/util/transform_8bit_to_rgba.cpp', + 'src/util/utf8_string.cpp' ) man_files = files( diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index e0a27c92..6ebf34f7 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -7,6 +7,7 @@ #include #include "util/scope_guard.hpp" +#include "util/utf8_string.hpp" extern "C" { #include @@ -202,69 +203,22 @@ auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) -> // Wide characters count as two, zero-width characters count as zero // Modifies str in-place (unless width = std::string::npos) // Returns the total width of the string pre-truncating -size_t utf8_truncate(std::string& str, size_t width = std::string::npos) { - if (str.length() == 0) return 0; - - const gchar* trunc_end = nullptr; - - size_t total_width = 0; - - for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) { - gunichar c = g_utf8_get_char_validated(data, end - data); - if (c == -1U || c == -2U) { - // invalid unicode, treat string as ascii - if (width != std::string::npos && str.length() > width) str.resize(width); - return str.length(); - } else if (g_unichar_iswide(c)) { - total_width += 2; - } else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen - total_width += 1; - } - - data = g_utf8_find_next_char(data, end); - if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c)) - trunc_end = data; - } - - if (trunc_end) str.resize(trunc_end - str.data()); - - return total_width; -} - -size_t utf8_width(const std::string& str) { return utf8_truncate(const_cast(str)); } - -void truncate(std::string& s, const std::string& ellipsis, size_t max_len) { - if (max_len == 0) { - s.resize(0); - return; - } - size_t len = utf8_truncate(s, max_len); - if (len > max_len) { - size_t ellipsis_len = utf8_width(ellipsis); - if (max_len >= ellipsis_len) { - if (ellipsis_len) utf8_truncate(s, max_len - ellipsis_len); - s += ellipsis; - } else { - s.resize(0); - } - } -} auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string { auto artist = info.artist.value_or(std::string()); - if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_); + if (truncated && artist_len_ >= 0) waybar::util::utf8_truncate(artist, ellipsis_, artist_len_); return artist; } auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string { auto album = info.album.value_or(std::string()); - if (truncated && album_len_ >= 0) truncate(album, ellipsis_, album_len_); + if (truncated && album_len_ >= 0) waybar::util::utf8_truncate(album, ellipsis_, album_len_); return album; } auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string { auto title = info.title.value_or(std::string()); - if (truncated && title_len_ >= 0) truncate(title, ellipsis_, title_len_); + if (truncated && title_len_ >= 0) waybar::util::utf8_truncate(title, ellipsis_, title_len_); return title; } @@ -292,9 +246,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> // keep position format same as length format auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6); - size_t artistLen = utf8_width(artist); - size_t albumLen = utf8_width(album); - size_t titleLen = utf8_width(title); + size_t artistLen = waybar::util::utf8_width(artist); + size_t albumLen = waybar::util::utf8_width(album); + size_t titleLen = waybar::util::utf8_width(title); size_t lengthLen = length.length(); size_t posLen = position.length(); @@ -313,7 +267,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> // Since the first element doesn't present a separator and we don't know a priori which one // it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the // separatorLen to all the other lengths. - size_t separatorLen = utf8_width(dynamic_separator_); + size_t separatorLen = waybar::util::utf8_width(dynamic_separator_); size_t dynamicLen = dynamic_len_ + separatorLen; if (showArtist) artistLen += separatorLen; if (showAlbum) albumLen += separatorLen; diff --git a/src/util/utf8_string.cpp b/src/util/utf8_string.cpp new file mode 100644 index 00000000..f96f2bd1 --- /dev/null +++ b/src/util/utf8_string.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include + +namespace waybar::util { + +namespace { +// Wide characters count as two, zero-width characters count as zero +// Modifies str in-place (unless width = std::string::npos) +// Returns the total width of the string pre-truncating +size_t measure_and_truncate(std::string& str, size_t width = std::string::npos) { + if (str.length() == 0) return 0; + + const gchar* trunc_end = nullptr; + + size_t total_width = 0; + + for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) { + gunichar c = g_utf8_get_char_validated(data, end - data); + if (c == -1U || c == -2U) { + // invalid unicode, treat string as ascii + if (width != std::string::npos && str.length() > width) str.resize(width); + return str.length(); + } else if (g_unichar_iswide(c)) { + total_width += 2; + } else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen + total_width += 1; + } + + data = g_utf8_find_next_char(data, end); + if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c)) + trunc_end = data; + } + + if (trunc_end) str.resize(trunc_end - str.data()); + + return total_width; +} +} // namespace + +size_t utf8_width(const std::string& str) { + return measure_and_truncate(const_cast(str)); +} + +void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len) { + if (max_len == 0) { + s.resize(0); + return; + } + size_t len = measure_and_truncate(s, max_len); + if (len > max_len) { + size_t ellipsis_len = utf8_width(ellipsis); + if (max_len >= ellipsis_len) { + if (ellipsis_len) measure_and_truncate(s, max_len - ellipsis_len); + s += ellipsis; + } else { + s.resize(0); + } + } +} +} // namespace waybar::util From 435c212be10125cf0bde4c823ad2f66c10d8c229 Mon Sep 17 00:00:00 2001 From: Siriusuna Date: Fri, 22 May 2026 02:59:09 +0800 Subject: [PATCH 133/167] fix(mpris): escape Pango markup in tooltip Fix an issue where the mpris module's tooltip failed to render when track metadata contained unescaped XML/HTML markup characters. This occurred because the mpris module formatted raw strings into the tooltip template before passing the final string directly to `set_tooltip_markup()`, triggering GTK/Pango parsing warnings and rendering failures. Resolve this by wrapping tooltip metadata fields in `Glib::Markup::escape_text` before formatting, ensuring Pango-compliant strings are always delivered to the GTK tooltip markup. --- src/modules/mpris/mpris.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 6ebf34f7..13ba836d 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -729,9 +729,12 @@ auto Mpris::update() -> void { auto tooltip_text = fmt::format( fmt::runtime(tooltipstr), fmt::arg("player", info.name), fmt::arg("status", info.status_string), - fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)), - fmt::arg("title", getTitleStr(info, tooltip_len_limits_)), - fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)), + fmt::arg("artist", + std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))), + fmt::arg("title", + std::string(Glib::Markup::escape_text(getTitleStr(info, tooltip_len_limits_)))), + fmt::arg("album", + std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))), fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition), fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)), fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), From 81fd9ee4ab39b90b496c34aabe9abf6bc253b0eb Mon Sep 17 00:00:00 2001 From: Siriusuna Date: Fri, 22 May 2026 03:04:35 +0800 Subject: [PATCH 134/167] fix(mpd): truncate before sanitization and improve tooltip UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix an issue in the MPD module where text failed to render when raw tags containing markup characters were truncated mid-sequence after sanitization, resulting in fragmented XML entities (such as `&` cut into `&am`). Resolve this by shifting the order of operations to truncate the raw strings *before* running `sanitize_string`. To keep `setLabel` decoupled and clean, we introduce helper methods (`getArtistStr`, `getAlbumArtistStr`, `getAlbumStr`, `getTitleStr`) inside the `MPD` class, matching the architectural style of MPRIS. Additionally: - Introduce support for the customizable `ellipsis` config option (defaulting to `…`). - Switch the truncation logic of the MPD module to be visual-width-aware using the newly extracted `waybar::util::utf8_truncate`. - Decouple the tooltip text from the main label's truncated text. The tooltip now displays un-truncated, complete metadata (properly escaped), providing a significantly better user experience. --- include/modules/mpd/mpd.hpp | 5 ++++ src/modules/mpd/mpd.cpp | 58 ++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 9d81638e..609f69ed 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -33,6 +33,7 @@ class MPD : public ALabel { detail::unique_status status_; mpd_state state_; detail::unique_song song_; + std::string ellipsis_; public: MPD(const std::string&, const Json::Value&); @@ -45,6 +46,10 @@ class MPD : public ALabel { void setLabel(); std::string getStateIcon() const; std::string getOptionIcon(const std::string& optionName, bool activated) const; + std::string getArtistStr(bool truncated) const; + std::string getAlbumArtistStr(bool truncated) const; + std::string getAlbumStr(bool truncated) const; + std::string getTitleStr(bool truncated) const; // GUI-side methods bool handlePlayPause(GdkEventButton* const&); diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..a3e7c23d 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -6,6 +6,7 @@ #include #include +#include using namespace waybar::util; #include "modules/mpd/state.hpp" @@ -24,7 +25,8 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000), connection_(nullptr, &mpd_connection_free), status_(nullptr, &mpd_status_free), - song_(nullptr, &mpd_song_free) { + song_(nullptr, &mpd_song_free), + ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "\u2026") { if (!config_["port"].isNull() && !config_["port"].isUInt()) { spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_); } @@ -117,7 +119,6 @@ void waybar::modules::MPD::setLabel() { label_.get_style_context()->remove_class("disconnected"); auto format = format_; - Glib::ustring artist, album_artist, album, title; std::string date, filename, uri; int song_pos = 0, queue_length = 0, volume = 0; std::chrono::seconds elapsedTime, totalTime; @@ -145,10 +146,6 @@ void waybar::modules::MPD::setLabel() { stateIcon = getStateIcon(); - artist = sanitize_string(getTag(MPD_TAG_ARTIST)); - album_artist = sanitize_string(getTag(MPD_TAG_ALBUM_ARTIST)); - album = sanitize_string(getTag(MPD_TAG_ALBUM)); - title = sanitize_string(getTag(MPD_TAG_TITLE)); date = sanitize_string(getTag(MPD_TAG_DATE)); filename = sanitize_string(getFilename()); uri = mpd_song_get_uri(song_.get()); @@ -170,17 +167,12 @@ void waybar::modules::MPD::setLabel() { std::string repeatIcon = getOptionIcon("repeat", repeatActivated); bool singleActivated = mpd_status_get_single(status_.get()); std::string singleIcon = getOptionIcon("single", singleActivated); - if (config_["artist-len"].isInt()) artist = artist.substr(0, config_["artist-len"].asInt()); - if (config_["album-artist-len"].isInt()) - album_artist = album_artist.substr(0, config_["album-artist-len"].asInt()); - if (config_["album-len"].isInt()) album = album.substr(0, config_["album-len"].asInt()); - if (config_["title-len"].isInt()) title = title.substr(0, config_["title-len"].asInt()); try { auto text = fmt::format( - fmt::runtime(format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::runtime(format), fmt::arg("artist", getArtistStr(true)), + fmt::arg("albumArtist", getAlbumArtistStr(true)), fmt::arg("album", getAlbumStr(true)), + fmt::arg("title", getTitleStr(true)), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), @@ -202,9 +194,9 @@ void waybar::modules::MPD::setLabel() { : "MPD (connected)"; try { auto tooltip_text = fmt::format( - fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::runtime(tooltip_format), fmt::arg("artist", getArtistStr(false)), + fmt::arg("albumArtist", getAlbumArtistStr(false)), fmt::arg("album", getAlbumStr(false)), + fmt::arg("title", getTitleStr(false)), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), @@ -257,6 +249,38 @@ std::string waybar::modules::MPD::getOptionIcon(const std::string& optionName, } } +std::string waybar::modules::MPD::getArtistStr(bool truncated) const { + std::string artist = getTag(MPD_TAG_ARTIST); + if (truncated && config_["artist-len"].isInt()) { + waybar::util::utf8_truncate(artist, ellipsis_, config_["artist-len"].asInt()); + } + return sanitize_string(artist); +} + +std::string waybar::modules::MPD::getAlbumArtistStr(bool truncated) const { + std::string album_artist = getTag(MPD_TAG_ALBUM_ARTIST); + if (truncated && config_["album-artist-len"].isInt()) { + waybar::util::utf8_truncate(album_artist, ellipsis_, config_["album-artist-len"].asInt()); + } + return sanitize_string(album_artist); +} + +std::string waybar::modules::MPD::getAlbumStr(bool truncated) const { + std::string album = getTag(MPD_TAG_ALBUM); + if (truncated && config_["album-len"].isInt()) { + waybar::util::utf8_truncate(album, ellipsis_, config_["album-len"].asInt()); + } + return sanitize_string(album); +} + +std::string waybar::modules::MPD::getTitleStr(bool truncated) const { + std::string title = getTag(MPD_TAG_TITLE); + if (truncated && config_["title-len"].isInt()) { + waybar::util::utf8_truncate(title, ellipsis_, config_["title-len"].asInt()); + } + return sanitize_string(title); +} + static bool isServerUnavailable(const std::error_code& ec) { if (ec.category() == std::system_category()) { switch (ec.value()) { From ec2651370e68e171e408ab2c932ca6631f7a50cf Mon Sep 17 00:00:00 2001 From: jacobs Date: Tue, 26 May 2026 11:09:45 +0100 Subject: [PATCH 135/167] (Fix) (sway workspace module) persistent workspaces always removed --- src/modules/sway/workspaces.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 6abe5383..b7d63b58 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -229,7 +229,10 @@ bool Workspaces::filterButtons() { auto ws = std::find_if(workspaces_.begin(), workspaces_.end(), [it](const auto& node) { return node["name"].asString() == it->first; }); if (ws == workspaces_.end() || - (!config_["all-outputs"].asBool() && (*ws)["output"].asString() != bar_.output->name)) { + ((*ws).isMember("target_output") ? (*ws)["target_output"].asString() != bar_.output->name && + (*ws)["target_output"].asString() != "" + : !config_["all-outputs"].asBool() && + (*ws)["output"].asString() != bar_.output->name)) { it = buttons_.erase(it); needReorder = true; } else { From 31a161eb06d82ff792c5339b1f9650f06d6b8b53 Mon Sep 17 00:00:00 2001 From: muratyalcin-ce Date: Wed, 27 May 2026 23:39:39 +0300 Subject: [PATCH 136/167] Fix (wayfire/backend): Fixed unhandled exception (out_of_range) after no valid outputs in unordered_map --- src/modules/wayfire/backend.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 42976d20..037a61e1 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -347,10 +347,13 @@ auto IPC::update_state_handler(const std::string& event, const Json::Value& data if (event == "output-wset-changed") { // data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output } - auto& output = state.outputs.at(data["output-data"]["name"].asString()); - auto wset_idx = data["new-wset-data"]["index"].asUInt(); - state.wsets.at(wset_idx).output = output; - output.wset_idx = wset_idx; + try { + auto& output = state.outputs.at(data["output-data"]["name"].asString()); + auto wset_idx = data["new-wset-data"]["index"].asUInt(); + state.wsets.at(wset_idx).output = output; + output.wset_idx = wset_idx; + } catch (const std::exception&) { + } return; } From 5ea824c24cfbf503fd13200db875fbc489d2027d Mon Sep 17 00:00:00 2001 From: muratyalcin-ce Date: Thu, 28 May 2026 23:26:40 +0300 Subject: [PATCH 137/167] Use unit in config for memory module tooltip --- include/modules/memory.hpp | 2 -- src/modules/memory/common.cpp | 44 ++++++++++++++++------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/include/modules/memory.hpp b/include/modules/memory.hpp index c73ece23..0e405187 100644 --- a/include/modules/memory.hpp +++ b/include/modules/memory.hpp @@ -19,8 +19,6 @@ class Memory : public ALabel { private: void parseMeminfo(); - static float calc_divisor(const std::string& divisor); - std::unordered_map meminfo_; util::SleeperThread thread_; diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 731f0b08..d41b4fc2 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -1,5 +1,18 @@ #include "modules/memory.hpp" +namespace { +const std::unordered_map kUnits = { + {"kB", 1.000}, + {"kiB", 1.024}, + {"MB", 1.000 * 1000.0}, + {"MiB", 1.024 * 1024.0}, + {"GB", 1.000 * 1000.0 * 1000.0}, + {"GiB", 1.024 * 1024.0 * 1024.0}, + {"TB", 1.000 * 1000.0 * 1000.0 * 1000.0}, + {"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0} +}; +} + waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) : ALabel(config, "memory", id, "{}%", 30) { thread_ = [this] { @@ -8,6 +21,11 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config }; if (config["unit"].isString()) { unit_ = config["unit"].asString(); + if (!kUnits.contains(unit_)) { + unit_ = "GiB"; + } + } else { + unit_ = "GiB"; } } @@ -40,7 +58,7 @@ auto waybar::modules::Memory::update() -> void { used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; } - float divisor = calc_divisor(unit_); + float divisor = kUnits.at(unit_); float total_ram = memtotal / divisor; float total_swap = swaptotal / divisor; float used_ram = (memtotal - memfree) / divisor; @@ -82,7 +100,7 @@ auto waybar::modules::Memory::update() -> void { fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), fmt::arg("swapAvail", available_swap))); } else { - label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram, 1)); + label_.set_tooltip_markup(fmt::format("{:.{}f}{} used", used_ram, 1, unit_)); } } } else { @@ -91,25 +109,3 @@ auto waybar::modules::Memory::update() -> void { // Call parent update ALabel::update(); } - -float waybar::modules::Memory::calc_divisor(const std::string& divisor) { - if (divisor == "kB") { - return 1.0; - } else if (divisor == "kiB") { - return 1.024; - } else if (divisor == "MB") { - return 1.000 * 1000.0; - } else if (divisor == "MiB") { - return 1.024 * 1024.0; - } else if (divisor == "GB") { - return 1.000 * 1000.0 * 1000.0; - } else if (divisor == "GiB") { - return 1.024 * 1024.0 * 1024.0; - } else if (divisor == "TB") { - return 1.000 * 1000.0 * 1000.0 * 1000.0; - } else if (divisor == "TiB") { - return 1.024 * 1024.0 * 1024.0 * 1024.0; - } else { // default to GiB if it is anything that we don't recongnise - return 1.024 * 1024.0 * 1024.0; - } -} From 0af3ca1a4ed399d53d853ebe7dd6c7f8c00ea321 Mon Sep 17 00:00:00 2001 From: Matthew Orlando Date: Sun, 31 May 2026 09:58:09 -0700 Subject: [PATCH 138/167] custom_modules: Fix mediaplayer.py handling untitled track * Fix crash calling method on None * Show artist if title is None --- resources/custom_modules/mediaplayer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 524d4d2a..4de1edf8 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -105,24 +105,26 @@ class PlayerManager: current_player = self.get_first_playing_player() if current_player is not None: self.on_metadata_changed(current_player, current_player.props.metadata) - else: + else: self.clear_output() def on_metadata_changed(self, player, metadata, _=None): logger.debug(f"Metadata changed for player {player.props.player_name}") player_name = player.props.player_name artist = player.get_artist() - artist = artist.replace("&", "&") + artist = artist and artist.replace("&", "&") title = player.get_title() - title = title.replace("&", "&") + title = title and title.replace("&", "&") track_info = "" if player_name == "spotify" and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]: track_info = "Advertisement" elif artist is not None and title is not None: track_info = f"{artist} - {title}" - else: + elif title is not None: track_info = title + elif artist is not None: + track_info = artist if track_info: if player.props.status == "Playing": From 3315631012befb1bd33f7e231272b54a86b79488 Mon Sep 17 00:00:00 2001 From: oqlatulesba3 Date: Tue, 2 Jun 2026 10:51:05 +0300 Subject: [PATCH 139/167] fix(sni): retry Host proxy creation on transient errors When Host::proxyReady fails to create the SnWatcher proxy (e.g. because the Watcher has not finished exporting /StatusNotifierWatcher yet), the cancellable is left set, causing nameAppeared to early-return on every subsequent event (see the // TODO marker). The Host is then stuck without a watcher, and the tray module reports 'No such object path /StatusNotifierWatcher' until the bar is fully restarted. Clear the cancellable on non-CANCELLED errors and schedule a single delayed retry of nameAppeared. The guard `watcher_ != nullptr` skips the retry if a parallel call already succeeded. Closes #3468 --- src/modules/sni/host.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 567fbf9f..091c45d5 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -6,6 +6,8 @@ namespace waybar::modules::SNI { +static const unsigned RETRY_DELAY_MS = 200; + Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, const std::function&)>& on_add, const std::function&)>& on_remove, @@ -72,11 +74,21 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { return; } auto host = static_cast(data); - host->watcher_ = watcher; if (error != nullptr) { spdlog::error("Host: {}", error->message); + g_clear_object(&host->cancellable_); + Glib::signal_timeout().connect_once( + [host]() { + if (host->watcher_ != nullptr) { + return; + } + auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); + host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + }, + RETRY_DELAY_MS); return; } + host->watcher_ = watcher; sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_, &Host::registerHost, data); } From 5cce616d3d72b402dabc64aabb872e0cfb25165c Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 14:41:31 -0700 Subject: [PATCH 140/167] perf(json): parse directly from string buffers --- include/util/json.hpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/include/util/json.hpp b/include/util/json.hpp index 5f756f35..5f45ae92 100644 --- a/include/util/json.hpp +++ b/include/util/json.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #if (FMT_VERSION >= 90000) @@ -26,14 +27,19 @@ class JsonParser { Json::Value root; // replace all occurrences of "\x" with "\u00", because JSON doesn't allow "\x" escape sequences - std::string modifiedJsonStr = replaceHexadecimalEscape(jsonStr); + std::string modifiedJsonStr; + const std::string* json = &jsonStr; + if (jsonStr.find("\\x") != std::string::npos) { + modifiedJsonStr = replaceHexadecimalEscape(jsonStr); + json = &modifiedJsonStr; + } - std::istringstream jsonStream(modifiedJsonStr); std::string errs; // Use local CharReaderBuilder for thread safety - the IPC singleton's // parser can be called concurrently from multiple module threads Json::CharReaderBuilder readerBuilder; - if (!Json::parseFromStream(readerBuilder, jsonStream, &root, &errs)) { + auto reader = std::unique_ptr(readerBuilder.newCharReader()); + if (!reader->parse(json->data(), json->data() + json->size(), &root, &errs)) { throw std::runtime_error("Error parsing JSON: " + errs); } return root; From 2190871a68730d52cd0e0ce7e2ed959e11c23ead Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 15:18:43 -0700 Subject: [PATCH 141/167] perf(label): skip redundant markup updates --- include/ALabel.hpp | 9 +++++++++ src/ALabel.cpp | 20 ++++++++++++++++++++ src/modules/mpd/mpd.cpp | 8 ++++---- src/modules/sway/window.cpp | 4 ++-- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/include/ALabel.hpp b/include/ALabel.hpp index b3852877..7c5150a0 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -4,6 +4,8 @@ #include #include +#include + #include "AModule.hpp" namespace waybar { @@ -25,12 +27,19 @@ class ALabel : public AModule { bool alt_ = false; std::string default_format_; + bool setLabelMarkup(const Glib::ustring& markup); + bool setTooltipMarkup(const Glib::ustring& markup); + bool handleToggle(GdkEventButton* const& e) override; virtual std::string getState(uint8_t value, bool lesser = false); std::map submenus_; std::map menuActionsMap_; static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data); + + private: + std::optional last_label_markup_; + std::optional last_tooltip_markup_; }; } // namespace waybar diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 795f87f1..f2a585fe 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -139,6 +139,26 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st auto ALabel::update() -> void { AModule::update(); } +bool ALabel::setLabelMarkup(const Glib::ustring& markup) { + if (last_label_markup_ == markup) { + return false; + } + + label_.set_markup(markup); + last_label_markup_ = markup; + return true; +} + +bool ALabel::setTooltipMarkup(const Glib::ustring& markup) { + if (last_tooltip_markup_ == markup) { + return false; + } + + label_.set_tooltip_markup(markup); + last_tooltip_markup_ = markup; + return true; +} + std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_t max) { auto format_icons = config_["format-icons"]; if (format_icons.isObject()) { diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..873b6251 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -98,7 +98,7 @@ void waybar::modules::MPD::setLabel() { ? config_["format-disconnected"].asString() : "disconnected"; if (format.empty()) { - label_.set_markup(format); + setLabelMarkup(format); label_.show(); } else { label_.hide(); @@ -110,7 +110,7 @@ void waybar::modules::MPD::setLabel() { ? config_["tooltip-format-disconnected"].asString() : "MPD (disconnected)"; // Nothing to format - label_.set_tooltip_markup(tooltip_format); + setTooltipMarkup(tooltip_format); } return; } @@ -190,7 +190,7 @@ void waybar::modules::MPD::setLabel() { label_.hide(); } else { label_.show(); - label_.set_markup(text); + setLabelMarkup(text); } } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error: {}", e.what()); @@ -210,7 +210,7 @@ void waybar::modules::MPD::setLabel() { fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon), fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri)); - label_.set_tooltip_markup(tooltip_text); + setTooltipMarkup(tooltip_text); } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error (tooltip): {}", e.what()); } diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index 2908b85c..2b62da14 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -94,12 +94,12 @@ auto Window::update() -> void { old_app_id_ = app_id_; } - label_.set_markup(waybar::util::rewriteString( + setLabelMarkup(waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_), fmt::arg("shell", shell_), fmt::arg("marks", marks_)), config_["rewrite"])); if (tooltipEnabled()) { - label_.set_tooltip_markup(window_); + setTooltipMarkup(window_); } updateAppIcon(); From 0cccf0d8b50dd7576f2295b24065616ef4fe4d0a Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 15:23:54 -0700 Subject: [PATCH 142/167] perf(mpd): avoid duplicate playing-state updates --- include/modules/mpd/mpd.hpp | 1 - include/modules/mpd/state.hpp | 1 - include/modules/mpd/state.inl.hpp | 1 - src/modules/mpd/mpd.cpp | 15 --------------- src/modules/mpd/state.cpp | 1 - 5 files changed, 19 deletions(-) diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 9d81638e..27c56153 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -54,7 +54,6 @@ class MPD : public ALabel { void tryConnect(); void checkErrors(mpd_connection* conn); void fetchState(); - void queryMPD(); inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 2c9071b4..0624c196 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -197,7 +197,6 @@ class Context { void tryConnect() const; void checkErrors(mpd_connection*) const; void do_update(); - void queryMPD() const; void fetchState() const; constexpr mpd_state state() const; void emit() const; diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp index 895970e6..06232b95 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -15,7 +15,6 @@ constexpr inline mpd_state Context::state() const { return mpd_module_->state_; inline void Context::do_update() { mpd_module_->setLabel(); } inline void Context::checkErrors(mpd_connection* conn) const { mpd_module_->checkErrors(conn); } -inline void Context::queryMPD() const { mpd_module_->queryMPD(); } inline void Context::fetchState() const { mpd_module_->fetchState(); } inline void Context::emit() const { mpd_module_->emit(); } diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..a84abf5c 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -51,21 +51,6 @@ auto waybar::modules::MPD::update() -> void { ALabel::update(); } -void waybar::modules::MPD::queryMPD() { - if (connection_ != nullptr) { - spdlog::trace("{}: fetching state information", module_name_); - try { - fetchState(); - spdlog::trace("{}: fetch complete", module_name_); - } catch (std::exception const& e) { - spdlog::error("{}: {}", module_name_, e.what()); - state_ = MPD_STATE_UNKNOWN; - } - - dp.emit(); - } -} - std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const { std::string result = config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A"; diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 3d7c8561..eb95665f 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -152,7 +152,6 @@ bool Playing::on_timer() { return false; } - ctx_->queryMPD(); ctx_->emit(); } catch (std::exception const& e) { spdlog::warn("mpd: Playing: error: {}", e.what()); From bef7df8f08b66c1f33910037e84fdfe170d2bbcf Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 16:08:50 -0700 Subject: [PATCH 143/167] fix(mpd): avoid false no-song warning when stopped --- src/modules/mpd/mpd.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..19a59239 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -124,8 +124,9 @@ void waybar::modules::MPD::setLabel() { std::string stateIcon = ""; bool no_song = song_.get() == nullptr; - if (stopped() || no_song) { - if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped."); + bool is_stopped = stopped(); + if (is_stopped || no_song) { + if (no_song && !is_stopped) spdlog::warn("mpd: no current song while state is not stopped"); format = config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped"; label_.get_style_context()->add_class("stopped"); From fb829c8a8e037fd8631af94dc2f888075612d1d0 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 9 Jun 2026 06:20:05 +0000 Subject: [PATCH 144/167] ## fix(niri/workspaces): crash (segfault) when setting workspace button markup ### Problem Waybar segfaults on startup when `niri/workspaces` is the only configured module (default options). Right after the first `WorkspacesChanged` event: Thread 1 "waybar" received signal SIGSEGV #0 gtk_label_set_markup () #1 waybar::modules::niri::Workspaces::doUpdate () at src/modules/niri/workspaces.cpp:106 ### Root cause [doUpdate()](cci:1://file:///home/lj/Downloads/Waybar/src/modules/niri/window.cpp:27:0-90:1) set the label markup via: static_cast(button.get_children()[0])->set_markup(name); The button's child *is* a valid `GtkLabel`, but gtkmm's `get_children()` returns it wrapped as a generic `Gtk::Widget` (confirmed: `dynamic_cast` to `Gtk::Label*` yields `nullptr`). The unchecked `static_cast` then performs an invalid downcast, producing a corrupt pointer whose `gobj()` is garbage (`0x1`), so `gtk_label_set_markup()` dereferences it and crashes. This is reliably triggered when no other module has instantiated a `Gtk::Label` yet (so the `Gtk::Label` wrapper isn't registered), which is exactly the case for a `niri/workspaces`-only bar. The same idiom exists in `sway/workspaces` and `wayfire/workspaces`; it's masked there because typical configs include other label-using modules. ### Testing - Reproduced the crash on 0.15.0 / current `master` with a minimal `"modules-left": ["niri/workspaces"]` config under niri. - After the fix: no crash; three workspace buttons render with correct names/labels (`niri-workspace-1/2/3`). --- src/modules/niri/workspaces.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 97d15215..a817ab9c 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -103,7 +103,9 @@ void Workspaces::doUpdate() { fmt::arg("output", ws["output"].asString())); } if (!config_["disable-markup"].asBool()) { - static_cast(button.get_children()[0])->set_markup(name); + auto* child = gtk_bin_get_child(GTK_BIN(button.gobj())); + if (child != nullptr && GTK_IS_LABEL(child)) + gtk_label_set_markup(GTK_LABEL(child), name.c_str()); } else { button.set_label(name); } From 25061962744c196560cc9853243c2ce986b80d60 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 9 Jun 2026 06:49:41 +0000 Subject: [PATCH 145/167] Update privacy.hpp --- include/modules/privacy/privacy.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index 68d2bf19..729fcc6a 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -28,6 +28,7 @@ class Privacy : public AModule { // Config Gtk::Box box_; + std::vector modules_; uint iconSpacing = 4; uint iconSize = 20; uint transition_duration = 250; From 2d9fee144e7427fb3abfd48921931ade5d08149d Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 9 Jun 2026 06:51:14 +0000 Subject: [PATCH 146/167] Update privacy.cpp --- src/modules/privacy/privacy.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 904c8fd9..4e613971 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -71,6 +71,7 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat auto* item = Gtk::make_managed(module, nodeType, nodePtr, orientation, pos, iconSize, transition_duration); box_.add(*item); + modules_.push_back(item); } } @@ -148,9 +149,7 @@ auto Privacy::update() -> void { bool useAudioOut = false; mutex_.lock(); - for (Gtk::Widget* widget : box_.get_children()) { - auto* module = dynamic_cast(widget); - if (module == nullptr) continue; + for (PrivacyItem* module : modules_) { switch (module->privacy_type) { case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: setScreenshare = true; From 61178f7b0e99e30617cc0a7b51aaecbea5e9da9a Mon Sep 17 00:00:00 2001 From: InetAddress <78892259+InetAddress@users.noreply.github.com> Date: Fri, 12 Jun 2026 04:38:10 +0000 Subject: [PATCH 147/167] Change error handling to warning Fixes a bug where removing a hot-plugged device mid-loop causes a filesystem error that turns into a fatal runtime error that crashes waybar entirely, changed to warning instead of runtime error. --- src/modules/battery.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 25436baf..67d6bb57 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -153,7 +153,7 @@ void waybar::modules::Battery::refreshBatteries() { } } } catch (fs::filesystem_error& e) { - throw std::runtime_error(e.what()); + spdlog::warn("Battery directory tracking failed: {}", e.what()); } if (warnFirstTime_ && batteries_.empty()) { if (config_["bat"].isString()) { From 4ef828f7123a6067a619c409f330e2c21660179c Mon Sep 17 00:00:00 2001 From: martuscellifaria Date: Fri, 12 Jun 2026 09:14:04 +0200 Subject: [PATCH 148/167] Fixing CPU usage spike on Waybar --- src/modules/custom.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 28def8c9..c6104e40 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -61,6 +61,10 @@ void waybar::modules::Custom::delayWorker() { } dp.emit(); } + if (!config_["exec"].isString() && !config_["exec-if"].isString()) { + thread_.stop(); + return; + } thread_.sleep_for(interval_); }; } From eb3669decc8efb1a95d4a29b6846f7e93ed5abeb Mon Sep 17 00:00:00 2001 From: martuscellifaria Date: Fri, 12 Jun 2026 09:48:44 +0200 Subject: [PATCH 149/167] Rearranging code in a non-redundant way. --- src/modules/custom.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index c6104e40..7fa5cf01 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -39,12 +39,16 @@ waybar::modules::Custom::~Custom() { } void waybar::modules::Custom::delayWorker() { + if (!config_["exec"].isString() && !config_["exec-if"].isString()) { + dp.emit(); + return; + } + thread_ = [this] { for (int i : this->pid_children_) { int status; waitpid(i, &status, 0); } - this->pid_children_.clear(); bool can_update = true; @@ -61,10 +65,6 @@ void waybar::modules::Custom::delayWorker() { } dp.emit(); } - if (!config_["exec"].isString() && !config_["exec-if"].isString()) { - thread_.stop(); - return; - } thread_.sleep_for(interval_); }; } From 0bc664a99eb27dab7bc351b7e490d82868e3470d Mon Sep 17 00:00:00 2001 From: martuscellifaria Date: Fri, 12 Jun 2026 09:52:03 +0200 Subject: [PATCH 150/167] Readded the whitespace from the original code --- src/modules/custom.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 7fa5cf01..1d9b320c 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -49,6 +49,7 @@ void waybar::modules::Custom::delayWorker() { int status; waitpid(i, &status, 0); } + this->pid_children_.clear(); bool can_update = true; From 942411d7a102951fd3b38407823c066481521120 Mon Sep 17 00:00:00 2001 From: "Robert M. Thomson" Date: Mon, 15 Jun 2026 09:53:44 +0200 Subject: [PATCH 151/167] Force layer-shell commit after configure Assisted-by: pi:gpt-5.5 --- include/bar.hpp | 1 + src/bar.cpp | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/include/bar.hpp b/include/bar.hpp index 54b332bf..292085ae 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -99,6 +99,7 @@ class Bar : public sigc::trackable { void setMode(const bar_mode&); void setPassThrough(bool passthrough); void setPosition(Gtk::PositionType position); + void forceLayerCommit(); void onConfigure(GdkEventConfigure* ev); void configureGlobalOffset(int width, int height); void onOutputGeometryChanged(); diff --git a/src/bar.cpp b/src/bar.cpp index 7ff73c4f..65017c8b 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -309,6 +309,19 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) setupWidgets(); window.show_all(); + /* + * If gtk-layer-shell's synchronous wait for the initial configure timed out, show_all() can + * return with a configured but not-yet-presented surface. Kick GTK/layer-shell once control has + * returned to the main loop, when any late initial configure has been dispatched and widgets have + * had a chance to allocate/draw. + */ + Glib::signal_idle().connect(sigc::track_obj([this] { + window.queue_resize(); + window.queue_draw(); + forceLayerCommit(); + return false; + }, *this)); + if (spdlog::should_log(spdlog::level::debug)) { // Unfortunately, this function isn't in the C++ bindings, so we have to call the C version. char* gtk_tree = gtk_style_context_to_string( @@ -374,7 +387,14 @@ void waybar::Bar::setMode(const struct bar_mode& mode) { * gtk-layer-shell schedules a commit on the next frame event in GTK, but this could fail in * certain scenarios, such as fully occluded bar. */ - gtk_layer_try_force_commit(gtk_window); + forceLayerCommit(); +} + +void waybar::Bar::forceLayerCommit() { + auto* gtk_window = window.gobj(); + if (gtk_window != nullptr && gtk_widget_get_realized(GTK_WIDGET(gtk_window))) { + gtk_layer_try_force_commit(gtk_window); + } wl_display_flush(Client::inst()->wl_display); } @@ -652,6 +672,17 @@ void waybar::Bar::onConfigure(GdkEventConfigure* ev) { configureGlobalOffset(ev->width, ev->height); spdlog::info(BAR_SIZE_MSG, ev->width, ev->height, output->name); + + /* + * gtk-layer-shell waits for the compositor's initial configure while realizing the window. On a + * busy compositor (common during session startup) that wait can time out even though the initial + * configure arrives shortly afterwards. In that case GTK may not schedule the frame commit that + * presents the first layer-surface buffer, leaving an otherwise configured bar invisible. Force a + * commit after every configure so late initial configures, and later compositor-driven resizes, + * always result in a submitted surface state. + */ + window.queue_draw(); + forceLayerCommit(); } void waybar::Bar::configureGlobalOffset(int width, int height) { From e37fcee35d3124a98e90c220fa6cb5c0862fd296 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Thu, 18 Jun 2026 14:28:39 +0000 Subject: [PATCH 152/167] fix(sni): ignore name/pixmap updates for custom icons --- include/modules/sni/item.hpp | 1 + src/modules/sni/item.cpp | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 74d54f4c..df87bfb7 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -46,6 +46,7 @@ class Item : public sigc::trackable { std::string title; std::string icon_name; Glib::RefPtr icon_pixmap; + bool has_custom_icon_ = false; Glib::RefPtr icon_theme; std::string overlay_icon_name; Glib::RefPtr overlay_icon_pixmap; diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 2f368083..8e2e2570 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -190,9 +190,17 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } else if (name == "Status") { setStatus(get_variant(value)); } else if (name == "IconName") { - icon_name = get_variant(value); + if (has_custom_icon_) { + spdlog::trace("Item '{}': ignoring IconName update, custom icon is set", id); + } else { + icon_name = get_variant(value); + } } else if (name == "IconPixmap") { - icon_pixmap = this->extractPixBuf(value.gobj()); + if (has_custom_icon_) { + spdlog::trace("Item '{}': ignoring IconPixmap update, custom icon is set", id); + } else { + icon_pixmap = this->extractPixBuf(value.gobj()); + } } else if (name == "OverlayIconName") { overlay_icon_name = get_variant(value); } else if (name == "OverlayIconPixmap") { @@ -270,11 +278,13 @@ void Item::setCustomIcon(const std::string& id) { Glib::RefPtr custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon); icon_name = ""; // icon_name has priority over pixmap icon_pixmap = custom_pixbuf; + has_custom_icon_ = true; } catch (const Glib::Error& e) { spdlog::error("Failed to load custom icon {}: {}", custom_icon, e.what()); } } else { // if file doesn't exist it's most likely an icon_name icon_name = custom_icon; + has_custom_icon_ = true; } } } From 42f2832d212aacc8e663212ea095ba78c86d03fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20NICOLAS?= Date: Sat, 20 Jun 2026 17:42:23 +0200 Subject: [PATCH 153/167] docs(man): document light and dark stylesheets --- man/waybar-styles.5.scd.in | 26 ++++++++++++++++++++------ man/waybar.5.scd.in | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in index b11e15bd..130e5db3 100644 --- a/man/waybar-styles.5.scd.in +++ b/man/waybar-styles.5.scd.in @@ -8,13 +8,27 @@ waybar-styles - using stylesheets for waybar Waybar uses Cascading Style Sheets (CSS) to configure its appearance. -It uses the first file found in this search order: +When the system appearance is light, Waybar first looks for *style-light.css*. +When the system appearance is dark, Waybar first looks for *style-dark.css*. +If no appearance-specific stylesheet is found, it falls back to *style.css*. -- *$XDG_CONFIG_HOME/waybar/style.css* -- *~/.config/waybar/style.css* -- *~/waybar/style.css* -- */etc/xdg/waybar/style.css* -- *@sysconfdir@/xdg/waybar/style.css* +Waybar uses the first file found in this search order for each stylesheet name: + +- *$XDG_CONFIG_HOME/waybar/* +- *~/.config/waybar/* +- *~/waybar/* +- */etc/xdg/waybar/* +- *@sysconfdir@/xdg/waybar/* + +For example, a light theme is loaded from the first available file among: + +- *$XDG_CONFIG_HOME/waybar/style-light.css* +- *~/.config/waybar/style-light.css* +- *~/waybar/style-light.css* +- */etc/xdg/waybar/style-light.css* +- *@sysconfdir@/xdg/waybar/style-light.css* + +If no file is found there, Waybar repeats the same search using *style.css*. # EXAMPLE diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 8b923eef..4dba6694 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -19,7 +19,7 @@ Valid locations for this file are: A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config.jsonc Also, a minimal example configuration can be found at the bottom of this man page. -The visual display elements for waybar use a CSS stylesheet, see *waybar-styles(5)* for details. +The visual display elements for waybar use a CSS stylesheet. Waybar supports *style.css* and the appearance-specific *style-light.css* and *style-dark.css* files; see *waybar-styles(5)* for details. # BAR CONFIGURATION From 22ce5887835b87653ca6cdccdbc317ec69d7a6f0 Mon Sep 17 00:00:00 2001 From: Muhammed Sahin Date: Sat, 27 Jun 2026 17:18:52 +0200 Subject: [PATCH 154/167] Fallback formatting when format-paused is missing uses ALabel With the minimal change the paused state, if format-paused state is not set, will show the Alabel if the Alabel was active. --- src/modules/mpd/mpd.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..80cb6559 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -137,8 +137,9 @@ void waybar::modules::MPD::setLabel() { label_.get_style_context()->add_class("playing"); label_.get_style_context()->remove_class("paused"); } else if (paused()) { - format = config_["format-paused"].isString() ? config_["format-paused"].asString() - : config_["format"].asString(); + if (config_["format-paused"].isString()) { + format = config_["format-paused"].asString(); + } label_.get_style_context()->add_class("paused"); label_.get_style_context()->remove_class("playing"); } From 4d1a561cdcbacb9e408a534b49b567821923331a Mon Sep 17 00:00:00 2001 From: Ibrahim Issa Date: Fri, 3 Jul 2026 18:44:03 +0200 Subject: [PATCH 155/167] feat(modules): pause background threads when display is asleep Currently, modules polling hardware nodes continuously wake up kernel drivers even when the monitor is asleep (e.g., DPMS off), flooding dmesg. - Adds 'disable-on-sleep' config flag - Hooks GTK window map/unmap signals to track DPMS state - Propagates suspend/resume calls to AModule worker threads --- include/AModule.hpp | 6 ++++++ include/bar.hpp | 2 ++ include/modules/temperature.hpp | 2 ++ include/util/sleeper_thread.hpp | 24 ++++++++++++++++++++++++ src/AModule.cpp | 3 +++ src/bar.cpp | 33 +++++++++++++++++++++++++++++++-- src/modules/temperature.cpp | 4 ++++ 7 files changed, 72 insertions(+), 2 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index a338ffe3..e4e4a0ca 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -25,6 +25,10 @@ class AModule : public IModule { bool expandEnabled() const; + virtual void suspend() {}; + virtual void resume() {}; + bool shouldSuspend() const { return disable_on_sleep_; } + protected: // Don't need to make an object directly // Derived classes are able to use it @@ -48,6 +52,8 @@ class AModule : public IModule { virtual bool handleMouseLeave(GdkEventCrossing* const& ev); virtual bool handleScroll(GdkEventScroll*); virtual bool handleRelease(GdkEventButton* const& ev); + + bool disable_on_sleep_{false}; GObject* menu_ = nullptr; private: diff --git a/include/bar.hpp b/include/bar.hpp index 54b332bf..8f23d6e8 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -75,6 +75,8 @@ class Bar : public sigc::trackable { util::KillSignalAction getOnSigusr1Action(); util::KillSignalAction getOnSigusr2Action(); + void toggleSuspend(bool suspend); + struct waybar_output* output; Json::Value config; struct wl_surface* surface; diff --git a/include/modules/temperature.hpp b/include/modules/temperature.hpp index 918281be..d65ef84d 100644 --- a/include/modules/temperature.hpp +++ b/include/modules/temperature.hpp @@ -14,6 +14,8 @@ class Temperature : public ALabel { Temperature(const std::string&, const Json::Value&); virtual ~Temperature() = default; auto update() -> void override; + void suspend() override; + void resume() override; private: float getTemperature(); diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 1f3a372d..9d20f252 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -79,6 +79,12 @@ class SleeperThread { auto sleep_for(std::chrono::system_clock::duration dur) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; + + condvar_.wait(lk, [this] { + return !is_paused_ || signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); + constexpr auto max_time_point = std::chrono::steady_clock::time_point::max(); auto wait_end = max_time_point; auto now = std::chrono::steady_clock::now(); @@ -95,6 +101,12 @@ class SleeperThread { time_point) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; + + condvar_.wait(lk, [this] { + return !is_paused_ || signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); + return condvar_.wait_until(lk, time_point, [this] { return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); }); @@ -122,6 +134,17 @@ class SleeperThread { } } + void pause() { + std::lock_guard lock(mutex_); + is_paused_ = true; + } + + void resume() { + std::lock_guard lock(mutex_); + is_paused_ = false; + condvar_.notify_all(); + } + ~SleeperThread() { connection_.disconnect(); stop(); @@ -137,6 +160,7 @@ class SleeperThread { std::atomic do_run_ = true; std::atomic signal_ = false; sigc::connection connection_; + bool is_paused_{false}; }; } // namespace waybar::util diff --git a/src/AModule.cpp b/src/AModule.cpp index a5ba69d3..d36688f8 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -21,6 +21,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: // Configure module action Map const Json::Value actions{config_["actions"]}; + disable_on_sleep_ = + config_["disable-on-sleep"].isBool() ? config_["disable-on-sleep"].asBool() : false; + for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) { if (it.key().isString() && it->isString()) if (!eventActionMap_.contains(it.key().asString())) { diff --git a/src/bar.cpp b/src/bar.cpp index 7ff73c4f..44d02c5d 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -263,6 +263,16 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); + window.signal_unmap().connect([this]() { + spdlog::debug("Output {} unmapped (DPMS off), suspending modules", output->name); + toggleSuspend(true); + }); + + window.signal_map().connect([this]() { + spdlog::debug("Output {} mapped (DPMS on), resuming modules", output->name); + toggleSuspend(false); + }); + #if HAVE_SWAY if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) { bar_id = Client::inst()->bar_id; @@ -545,8 +555,8 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, if (group_config["modules"].isNull()) { spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); } - auto group_module = std::make_unique( - id_name, class_name, group_config, vertical); + auto group_module = + std::make_unique(id_name, class_name, group_config, vertical); getModules(factory, ref, group_module.get()); module = group_module.release(); @@ -696,3 +706,22 @@ void waybar::Bar::configureGlobalOffset(int width, int height) { void waybar::Bar::onOutputGeometryChanged() { configureGlobalOffset(window.get_width(), window.get_height()); } + +void waybar::Bar::toggleSuspend(bool suspend) { + auto process_modules = [suspend](Gtk::Box& module_box) { + for (auto* widget : module_box.get_children()) { + auto* module = dynamic_cast(widget); + if (module && module->shouldSuspend()) { + if (suspend) { + module->suspend(); + } else { + module->resume(); + } + } + } + }; + + process_modules(left_); + process_modules(center_); + process_modules(right_); +} diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index bb5ff02c..dcfb0b2f 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -155,3 +155,7 @@ bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) { return config_["critical-threshold"].isInt() && temperature_c >= config_["critical-threshold"].asInt(); } + +void waybar::modules::Temperature::suspend() { thread_.pause(); } + +void waybar::modules::Temperature::resume() { thread_.resume(); } From 6793b97eed90bbe3d97cf4f0ac31ea722b84a394 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:15:35 +0200 Subject: [PATCH 156/167] fix(hyprland/language): handle commas in keyboard-name config The active-layout event payload is `KEYBOARDNAME,LAYOUT`, and kbName was taken as everything before the first comma. Keyboard names reported by Hyprland can themselves contain commas (e.g. some MSI keyboards), so a `keyboard-name` configured with a comma never matched the truncated value and the module stopped updating. Match the configured keyboard-name as a full prefix (followed by the ',' separator) instead. Layout parsing already handles commas via the last-comma-before-parenthesis heuristic, so only the keyboard-name filter needed fixing. Addresses the keyboard-name case originally reported by @tirsek in #3406. --- src/modules/hyprland/language.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 6e0fe23d..890dc86b 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -74,8 +74,6 @@ void Language::onEvent(const std::string& ev) { spdlog::warn("hyprland language received malformed event payload: {}", ev); return; } - std::string kbName = payload.substr(0, kbSeparator); - // Last comma before variants parenthesis, eg: // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., // with dead keys) @@ -93,8 +91,15 @@ void Language::onEvent(const std::string& ev) { } auto layoutName = payload.substr(layoutSeparator + 1); - if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) - return; // ignore + if (config_.isMember("keyboard-name")) { + const auto keyboardName = config_["keyboard-name"].asString(); + // The keyboard name itself can contain commas, so match it as a full prefix + // (followed by the ',' separator) rather than comparing against the substring + // before the first comma, which would truncate such names and drop the event. + if (payload.size() <= keyboardName.size() || payload[keyboardName.size()] != ',' || + payload.compare(0, keyboardName.size(), keyboardName) != 0) + return; // ignore + } layoutName = waybar::util::sanitize_string(layoutName); From dc017d8fd3e2550fb397627bea9981c6ec707ab7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:32:55 +0200 Subject: [PATCH 157/167] fix(hyprland/workspaces): disconnect hover-check timeout on Workspace destruction The hover-check Glib::signal_timeout re-arms every 50ms and is not owned by the Workspace. Since Workspaces are destroyed at runtime, a workspace removed while its check is armed would let the timeout fire on freed memory (use-after- free, also touching the destroyed m_button). Add a destructor that calls stopHoverCheck() to disconnect the source. --- include/modules/hyprland/workspace.hpp | 1 + src/modules/hyprland/workspace.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 5730dda9..a4cccee6 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -30,6 +30,7 @@ class Workspace { public: explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, const Json::Value& clients_data = Json::Value::nullRef); + ~Workspace(); std::string& selectIcon(std::map& icons_map); Gtk::Button& button() { return m_button; }; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index e31132fe..b8868236 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -51,6 +51,12 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma initializeWindowMap(clients_data); } +Workspace::~Workspace() { + // Disconnect the hover-check timeout so it can't fire on this destroyed + // instance (Workspaces are removed at runtime while a check may be armed). + stopHoverCheck(); +} + void addOrRemoveClass(const Glib::RefPtr& context, bool condition, const std::string& class_name) { if (condition) { From 806f6333182c4015e61e20fca7a0bc38e682f629 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:38:46 +0200 Subject: [PATCH 158/167] hyprland/workspaces: refresh locally instead of reloading Hyprland config Replace the getSocket1JsonReply("reload") call in onSpecialWorkspaceActivated with the module's normal local refresh path: the state update to m_activeSpecialWorkspaceName is picked up by the update() render (triggered by the dp.emit() already issued at the end of onEvent), which re-evaluates active state, icons and CSS classes. This avoids reloading the entire Hyprland config on every special workspace activation while keeping the special-active icon and CSS improvements. --- src/modules/hyprland/workspaces.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index a95fd9a5..3fa92920 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -396,7 +396,6 @@ void Workspaces::onWorkspaceActivated(std::string const& payload) { void Workspaces::onSpecialWorkspaceActivated(std::string const& payload) { std::string name(begin(payload), begin(payload) + payload.find_first_of(',')); m_activeSpecialWorkspaceName = (!name.starts_with("special:") ? name : name.substr(8)); - m_ipc.getSocket1JsonReply("reload"); } void Workspaces::onWorkspaceDestroyed(std::string const& payload) { From 5181f84648c20742e2a73146387b887c3ad1a3eb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:39:36 +0200 Subject: [PATCH 159/167] fix(sni): prevent use-after-free and infinite retry in Host proxy retry Store the retry timeout connection in a Host member and disconnect it in ~Host, so a Host destroyed within the retry window no longer invokes the timeout callback on freed memory. Cap the retries (MAX_RETRIES) so a watcher that never reappears no longer spins forever every 200ms; reset the counter on success and when the watcher name vanishes. --- include/modules/sni/host.hpp | 2 ++ src/modules/sni/host.cpp | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index d76ec74a..f18cf414 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -43,6 +43,8 @@ class Host { std::size_t watcher_id_; GCancellable* cancellable_ = nullptr; SnWatcher* watcher_ = nullptr; + sigc::connection retry_connection_; + unsigned retry_count_ = 0; const Json::Value& config_; const Bar& bar_; const std::function&)> on_add_; diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 091c45d5..2c0c42c7 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -7,6 +7,7 @@ namespace waybar::modules::SNI { static const unsigned RETRY_DELAY_MS = 200; +static const unsigned MAX_RETRIES = 10; Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, const std::function&)>& on_add, @@ -24,6 +25,7 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, on_update_(on_update) {} Host::~Host() { + retry_connection_.disconnect(); if (bus_name_id_ > 0) { Gio::DBus::unown_name(bus_name_id_); bus_name_id_ = 0; @@ -55,6 +57,8 @@ void Host::nameAppeared(const Glib::RefPtr& conn, const G } void Host::nameVanished(const Glib::RefPtr& conn, const Glib::ustring name) { + retry_connection_.disconnect(); + retry_count_ = 0; g_cancellable_cancel(cancellable_); g_clear_object(&cancellable_); g_clear_object(&watcher_); @@ -77,17 +81,26 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { if (error != nullptr) { spdlog::error("Host: {}", error->message); g_clear_object(&host->cancellable_); - Glib::signal_timeout().connect_once( + if (host->retry_count_ >= MAX_RETRIES) { + spdlog::warn("Host: giving up on watcher proxy creation after {} retries", host->retry_count_); + return; + } + host->retry_count_ += 1; + // Store the timeout connection so it is disconnected in ~Host, avoiding a + // use-after-free if the Host is destroyed before the retry fires. + host->retry_connection_ = Glib::signal_timeout().connect( [host]() { if (host->watcher_ != nullptr) { - return; + return false; } auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + return false; }, RETRY_DELAY_MS); return; } + host->retry_count_ = 0; host->watcher_ = watcher; sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_, &Host::registerHost, data); From 488a75fadaa110c4a4fa16ab73fd3795d38829b3 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:39:48 +0200 Subject: [PATCH 160/167] fix(wlr/taskbar): gate title shrink/expand behind 'expand' option Only stretch buttons and ellipsize titles when the new boolean 'expand' option is enabled and the bar is horizontal. Restores the previous content-based sizing as the default and avoids truncating every title to an ellipsis on vertical bars. --- man/waybar-wlr-taskbar.5.scd | 5 ++++ src/modules/wlr/taskbar.cpp | 52 ++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index af1ba97f..2aa26f3f 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -57,6 +57,11 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, group tasks by their app_id. Cannot be used with 'active-first'. +*expand*: ++ + typeof: bool ++ + default: false ++ + If set to true, task buttons stretch to fill the available space in the taskbar and long titles are ellipsized to fit. Only takes effect on a horizontal bar; on a vertical bar the buttons keep their content-based size. If set to false, buttons are sized to their content. + *on-click*: ++ typeof: string ++ The action which should be triggered when clicking on the application button with the left mouse button. diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 6ba41ce3..f382e1fc 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -93,20 +93,36 @@ Task::Task(const waybar::Bar& bar, const Json::Value& config, Taskbar* tbar, zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); button.set_relief(Gtk::RELIEF_NONE); - button.set_hexpand(true); - content_.set_hexpand(true); - text_before_.set_ellipsize(Pango::ELLIPSIZE_END); - text_before_.set_single_line_mode(true); - text_before_.set_width_chars(1); - text_before_.set_xalign(0.0); - text_after_.set_ellipsize(Pango::ELLIPSIZE_END); - text_after_.set_single_line_mode(true); - text_after_.set_width_chars(1); - text_after_.set_xalign(0.0); - content_.pack_start(text_before_, true, true, 0); - content_.pack_start(icon_, false, false, 0); - content_.pack_start(text_after_, true, true, 0); + /* When "expand" is enabled the buttons stretch to fill the taskbar and the + * titles ellipsize to fit within the available space. This only makes sense + * on a horizontal bar; on a vertical bar the box grows along the vertical + * axis, so ellipsizing/forcing width_chars(1) would truncate every title to + * "…". Keep the historical behavior (buttons sized to their content) as the + * default and when the bar is vertical. */ + bool expand = config_["expand"].isBool() && config_["expand"].asBool(); + bool horizontal = bar.orientation == Gtk::ORIENTATION_HORIZONTAL; + + if (expand && horizontal) { + button.set_hexpand(true); + content_.set_hexpand(true); + text_before_.set_ellipsize(Pango::ELLIPSIZE_END); + text_before_.set_single_line_mode(true); + text_before_.set_width_chars(1); + text_before_.set_xalign(0.0); + text_after_.set_ellipsize(Pango::ELLIPSIZE_END); + text_after_.set_single_line_mode(true); + text_after_.set_width_chars(1); + text_after_.set_xalign(0.0); + + content_.pack_start(text_before_, true, true, 0); + content_.pack_start(icon_, false, false, 0); + content_.pack_start(text_after_, true, true, 0); + } else { + content_.add(text_before_); + content_.add(icon_); + content_.add(text_after_); + } content_.show(); button.add(content_); @@ -728,7 +744,15 @@ void Taskbar::handle_finished() { } void Taskbar::add_button(Gtk::Button& bt) { - box_.pack_start(bt, true, true); + /* Only let the buttons expand to fill the taskbar when "expand" is enabled + * and the bar is horizontal (see the Task constructor for details). */ + bool expand = config_["expand"].isBool() && config_["expand"].asBool(); + bool horizontal = bar_.orientation == Gtk::ORIENTATION_HORIZONTAL; + if (expand && horizontal) { + box_.pack_start(bt, true, true); + } else { + box_.pack_start(bt, false, false); + } box_.get_style_context()->remove_class("empty"); } From 31a3d7e1bb9c047ca3449490cb5d695abbd98e0b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:40:41 +0200 Subject: [PATCH 161/167] fix(battery): gate Full-as-Plugged behind full-at-plugged option, drop debug output Reporting a full battery on AC as 'Plugged' is now opt-in via the 'full-at-plugged' config option (default false), so existing format-full / tooltip-format-full setups keep working. Also documents the option. --- man/waybar-battery.5.scd | 5 +++++ src/modules/battery.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 42b9e046..0c71518c 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -27,6 +27,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: false ++ Option to use the battery design capacity instead of its current maximal capacity. +*full-at-plugged*: ++ + typeof: bool ++ + default: false ++ + When enabled, a battery that is *Full* while the adapter is online is reported with the *Plugged* status instead of *Full* (so you can style/format it separately). Disabled by default to preserve the existing *Full* behaviour. + *interval*: ++ typeof: integer ++ default: 60 ++ diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 53305068..2581ef9a 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -706,7 +706,7 @@ auto waybar::modules::Battery::update() -> void { if (!adapter_.empty()) { std::ifstream(adapter_ / "online") >> adapter_online; } - if (status == "Full" && adapter_online) { + if (config_["full-at-plugged"].asBool() && status == "Full" && adapter_online) { status = "Plugged"; } auto status_pretty = status; From 9ac3f9b95198408f8bb084f7fc900b29d4be900e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:41:10 +0200 Subject: [PATCH 162/167] clock: make calendar padding portable across FormatStream char types std::setfill is templated on the fill char type, which must match the Glib::ustring FormatStream's char type. That type is wchar_t with libstdc++/glibmm but char with libc++, so no fixed literal (' ' or L' ') compiles everywhere. The stream's default fill is already a space, so drop std::setfill entirely; std::setw/std::left/std::right are char-type-agnostic and keep the exact padding behavior. --- src/modules/clock.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 36581e51..97777667 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -402,8 +402,13 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea data = g_utf8_find_next_char(data, end); } } + // Note: the stream's default fill character is already a space (L' ' on + // libstdc++'s wide FormatStream, ' ' on libc++'s narrow one), so no + // std::setfill is needed. Passing std::setfill(' ')/std::setfill(L' ') + // here is not portable because the fill char type must match the + // FormatStream's char type, which differs between standard libraries. os << Glib::ustring::format( - (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(' '), + (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), calendarLine); From 8f7ed880c413d85fc51c13e0252d8e18fdaf827c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:49:25 +0200 Subject: [PATCH 163/167] bar: fix indentation of height type warning --- src/bar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bar.cpp b/src/bar.cpp index 626124da..fe44b3ed 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -173,7 +173,7 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) } if (config.isMember("height") && !config["height"].isUInt()) { - spdlog::warn("Invalid type for 'height', expected unsigned integer"); + spdlog::warn("Invalid type for 'height', expected unsigned integer"); } height_ = config["height"].isUInt() ? config["height"].asUInt() : 0; From cbc70d10f81f85ab51f5c18d04fecbce340c7f53 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:49:54 +0200 Subject: [PATCH 164/167] pulseaudio_slider: keep zero-on-mute applied on every update while muted The zero-on-mute display_value = min_ assignment was nested inside the !previously_muted transition branch, so subsequent updates while still muted re-displayed the real volume. Apply it on every update as long as the target is muted, keeping the CSS class add/remove on transitions only. --- src/modules/pulseaudio_slider.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp index aeddeb24..d0e54459 100644 --- a/src/modules/pulseaudio_slider.cpp +++ b/src/modules/pulseaudio_slider.cpp @@ -29,12 +29,14 @@ void PulseaudioSlider::update() { uint16_t display_value = backend->getVolume(target); bool is_muted = backend->getMuted(target); - if (is_muted && !previously_muted) { + if (is_muted) { if (zero_on_mute) { display_value = min_; } - scale_.get_style_context()->add_class("muted"); - } else if (previously_muted && !is_muted) { + if (!previously_muted) { + scale_.get_style_context()->add_class("muted"); + } + } else if (previously_muted) { scale_.get_style_context()->remove_class("muted"); } From 26c834a59b65f56b0e3faa864c57a95c75346d58 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:51:07 +0200 Subject: [PATCH 165/167] cpu: respect tooltip-format- without overwriting it The tooltip selection block redundantly reassigned tooltip_format to config_["tooltip-format"], overwriting the state-specific format (and emptying it when no global tooltip-format was set). A lone tooltip-format- therefore produced an empty tooltip. Consolidate the tooltip handling into a single block that prefers tooltip-format-, falls back to the global tooltip-format, then to the computed default tooltip, reusing the existing format arg store. --- src/modules/cpu.cpp | 47 ++++++++------------------------------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 254c6945..5c461851 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -28,15 +28,11 @@ auto waybar::modules::Cpu::update() -> void { auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); auto format = format_; - std::string tooltip_format; auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; auto state = getState(total_usage); if (!state.empty() && config_["format-" + state].isString()) { format = config_["format-" + state].asString(); } - if (config_["tooltip-format-" + state].isString()) { - tooltip_format = config_["tooltip-format-" + state].asString(); - } if (format.empty()) { event_box_.hide(); @@ -62,47 +58,20 @@ auto waybar::modules::Cpu::update() -> void { label_.set_markup(fmt::vformat(format, store)); if (tooltipEnabled()) { - if (config_["tooltip-format"].isString()) { - tooltip = config_["tooltip-format"].asString(); - label_.set_tooltip_markup(fmt::vformat(tooltip, store)); + std::string tooltip_format; + if (!state.empty() && config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } else if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + label_.set_tooltip_markup(fmt::vformat(tooltip_format, store)); } else { label_.set_tooltip_markup(tooltip); } } } - if (tooltipEnabled()) { - if (tooltip_format.empty() && config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); - } - if (!tooltip_format.empty()) { - tooltip_format = config_["tooltip-format"].asString(); - - auto icons = std::vector{state}; - fmt::dynamic_format_arg_store store; - store.push_back(fmt::arg("load", load1)); - store.push_back(fmt::arg("usage", total_usage)); - store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); - store.push_back(fmt::arg("max_frequency", max_frequency)); - store.push_back(fmt::arg("min_frequency", min_frequency)); - store.push_back(fmt::arg("avg_frequency", avg_frequency)); - for (size_t i = 1; i < cpu_usage.size(); ++i) { - auto core_i = i - 1; - auto core_format = fmt::format("usage{}", core_i); - store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i])); - auto icon_format = fmt::format("icon{}", core_i); - store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); - } - auto tooltip_text = fmt::vformat(tooltip_format, store); - - if (label_.get_tooltip_text() != tooltip_text) { - label_.set_tooltip_markup(tooltip_text); - } - } else { - label_.set_tooltip_text(tooltip); - } - } - // Call parent update ALabel::update(); } From 25e57bc62585793593660420a8a17c82fcb0a64f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:51:20 +0200 Subject: [PATCH 166/167] fix(hyprland/window): use 2-space indentation for fallback block (clang-format) --- src/modules/hyprland/window.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 423257db..029f9722 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -1,5 +1,6 @@ #include "modules/hyprland/window.hpp" +#include #include #include #include @@ -13,8 +14,6 @@ #include "util/rewrite_string.hpp" #include "util/sanitize_str.hpp" -#include - namespace waybar::modules::hyprland { std::shared_mutex windowIpcSmtx; @@ -56,11 +55,11 @@ auto Window::update() -> void { if (!format_.empty()) { label_.show(); - // If the focused window name is empty and fallback is configured, use fallback text - std::string displayTitle = windowName; - if (displayTitle.empty() && config_["fallback"].isString()) { - displayTitle = config_["fallback"].asString(); - } + // If the focused window name is empty and fallback is configured, use fallback text + std::string displayTitle = windowName; + if (displayTitle.empty() && config_["fallback"].isString()) { + displayTitle = config_["fallback"].asString(); + } label_text = waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle), From 4bf82b88a06a8ee10f4468616d177087b904eae6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:01:20 +0200 Subject: [PATCH 167/167] style(pulseaudio): clang-format audio_backend helpers --- include/modules/pulseaudio_slider.hpp | 1 - src/util/audio_backend.cpp | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/include/modules/pulseaudio_slider.hpp b/include/modules/pulseaudio_slider.hpp index c9f82341..597b3cc8 100644 --- a/include/modules/pulseaudio_slider.hpp +++ b/include/modules/pulseaudio_slider.hpp @@ -6,7 +6,6 @@ #include "util/audio_backend.hpp" namespace waybar::modules { - class PulseaudioSlider : public ASlider { public: PulseaudioSlider(const std::string&, const Json::Value&); diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 601a9cc0..96a5bdb4 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -263,9 +263,9 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo uint16_t AudioBackend::getVolume(PulseaudioTarget target) const { if (target == PulseaudioTarget::Source) { - return source_volume_; + return source_volume_; } else { - return volume_; + return volume_; } } @@ -380,9 +380,9 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma bool AudioBackend::getMuted(PulseaudioTarget target) const { if (target == PulseaudioTarget::Source) { - return source_muted_; + return source_muted_; } else { - return muted_; + return muted_; } } @@ -424,9 +424,9 @@ void AudioBackend::toggleSourceMute(bool mute) { void AudioBackend::unmute(PulseaudioTarget target) { if (target == PulseaudioTarget::Source) { - toggleSourceMute(false); + toggleSourceMute(false); } else { - toggleSinkMute(false); + toggleSinkMute(false); } }