From 0cbb280de635aa3ddcdbb416d8ebd55a1fb82b15 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 24 Feb 2024 15:00:39 +0100 Subject: [PATCH 001/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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 4a7da0cab2747981bdab15acb0fc3af07c48a6e3 Mon Sep 17 00:00:00 2001 From: Steffen Kothe Date: Fri, 15 Aug 2025 10:55:21 +0000 Subject: [PATCH 007/137] 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 008/137] 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 0a5da13fbf5fa5e0c30de4d6d42d23c574b4450a Mon Sep 17 00:00:00 2001 From: Mathis Raguin Date: Wed, 31 Dec 2025 14:44:49 +0100 Subject: [PATCH 009/137] 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 010/137] 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 49083131d18fc0d11e315b7c046fa1cb167149a1 Mon Sep 17 00:00:00 2001 From: Ansari Date: Sat, 3 Jan 2026 11:16:03 +0530 Subject: [PATCH 011/137] 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 012/137] 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 40200afb6890f4488b9c850b6d3a98526828d212 Mon Sep 17 00:00:00 2001 From: cebem1nt Date: Fri, 6 Feb 2026 14:43:12 -0300 Subject: [PATCH 013/137] fix: owerwrite handleScroll to remove any scrolling handling from group module --- include/group.hpp | 1 + src/group.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/include/group.hpp b/include/group.hpp index 41cb5ee0..d5cbb289 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -30,6 +30,7 @@ class Group : public AModule { bool handleMouseEnter(GdkEventCrossing* const& ev) override; bool handleMouseLeave(GdkEventCrossing* const& ev) override; bool handleToggle(GdkEventButton* const& ev) override; + bool handleScroll(GdkEventScroll* e) override; void show_group(); void hide_group(); }; diff --git a/src/group.cpp b/src/group.cpp index 294743d7..51caf0e2 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -124,6 +124,11 @@ auto Group::update() -> void { // noop } +bool Group::handleScroll(GdkEventScroll* e) { + // no scroll. + return true; +} + Gtk::Box& Group::getBox() { return is_drawer ? (is_first_widget ? box : revealer_box) : box; } void Group::addWidget(Gtk::Widget& widget) { From f96fb53eebba39130cd7ec20b06c77aaff066472 Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Tue, 10 Feb 2026 00:30:03 +0530 Subject: [PATCH 014/137] feat: add tooltip in format replacments --- src/modules/custom.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 0a8d9cf6..e2b705da 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -182,9 +182,10 @@ auto waybar::modules::Custom::update() -> void { if (tooltipEnabled()) { if (tooltip_format_enabled_) { auto tooltip = config_["tooltip-format"].asString(); - tooltip = fmt::format( - fmt::runtime(tooltip), fmt::arg("text", text_), fmt::arg("alt", alt_), - fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); + 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); } else if (text_ == tooltip_) { label_.set_tooltip_markup(str); From a69b7a553676720d2782431dc100e93ab8c0b3b2 Mon Sep 17 00:00:00 2001 From: BlueManCZ Date: Wed, 11 Feb 2026 10:53:48 +0100 Subject: [PATCH 015/137] Handle fallback player for ignored MPRIS players --- src/modules/mpris/mpris.cpp | 50 ++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 2b345fc5..94939311 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -488,7 +488,13 @@ auto Mpris::getPlayerInfo() -> std::optional { char* player_status = nullptr; auto player_playback_status = PLAYERCTL_PLAYBACK_STATUS_STOPPED; - g_object_get(player, "status", &player_status, "playback-status", &player_playback_status, NULL); + + // When using playerctld and the most active player is ignored, we create a + // direct connection to the first non-ignored player for correct metadata. + PlayerctlPlayer* fallback_player = nullptr; + waybar::util::ScopeGuard fallback_deleter([&fallback_player]() { + if (fallback_player) g_object_unref(fallback_player); + }); std::string player_name = player_; if (player_name == "playerctld") { @@ -498,19 +504,33 @@ auto Mpris::getPlayerInfo() -> std::optional { } // > get the list of players [..] in order of activity // https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249 - players = g_list_first(players); - if (players) - player_name = static_cast(players->data)->name; - else - return std::nullopt; // no players found, hide the widget - } - - if (std::any_of(ignored_players_.begin(), ignored_players_.end(), - [&](const std::string& pn) { return player_name == pn; })) { + bool found = false; + for (auto* p = g_list_first(players); p != nullptr; p = p->next) { + auto* pn = static_cast(p->data); + std::string name = pn->name; + if (std::none_of(ignored_players_.begin(), ignored_players_.end(), + [&](const std::string& ignored) { return name == ignored; })) { + player_name = name; + if (p != g_list_first(players)) { + fallback_player = playerctl_player_new_from_name(pn, &error); + if (error || !fallback_player) return std::nullopt; + } + found = true; + break; + } + spdlog::warn("mpris[{}]: ignoring player update", name); + } + if (!found) return std::nullopt; + } else if (std::any_of(ignored_players_.begin(), ignored_players_.end(), + [&](const std::string& pn) { return player_name == pn; })) { spdlog::warn("mpris[{}]: ignoring player update", player_name); return std::nullopt; } + auto* source_player = fallback_player ? fallback_player : player; + g_object_get(source_player, "status", &player_status, "playback-status", &player_playback_status, + NULL); + // make status lowercase player_status[0] = std::tolower(player_status[0]); @@ -524,28 +544,28 @@ auto Mpris::getPlayerInfo() -> std::optional { .length = std::nullopt, }; - if (auto* artist_ = playerctl_player_get_artist(player, &error)) { + if (auto* artist_ = playerctl_player_get_artist(source_player, &error)) { spdlog::debug("mpris[{}]: artist = {}", info.name, artist_); info.artist = artist_; g_free(artist_); } if (error) goto errorexit; - if (auto* album_ = playerctl_player_get_album(player, &error)) { + if (auto* album_ = playerctl_player_get_album(source_player, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); info.album = album_; g_free(album_); } if (error) goto errorexit; - if (auto* title_ = playerctl_player_get_title(player, &error)) { + if (auto* title_ = playerctl_player_get_title(source_player, &error)) { spdlog::debug("mpris[{}]: title = {}", info.name, title_); info.title = title_; g_free(title_); } if (error) goto errorexit; - if (auto* length_ = playerctl_player_print_metadata_prop(player, "mpris:length", &error)) { + if (auto* length_ = playerctl_player_print_metadata_prop(source_player, "mpris:length", &error)) { spdlog::debug("mpris[{}]: mpris:length = {}", info.name, length_); auto len = std::chrono::microseconds(std::strtol(length_, nullptr, 10)); auto len_h = std::chrono::duration_cast(len); @@ -557,7 +577,7 @@ auto Mpris::getPlayerInfo() -> std::optional { if (error) goto errorexit; { - auto position_ = playerctl_player_get_position(player, &error); + auto position_ = playerctl_player_get_position(source_player, &error); if (error) { // it's fine to have an error here because not all players report a position g_error_free(error); From 0a50e82d0da0108d6b9a792451b09d29cfbde29a Mon Sep 17 00:00:00 2001 From: BlueManCZ Date: Wed, 11 Feb 2026 11:47:05 +0100 Subject: [PATCH 016/137] Prioritize currently playing player --- src/modules/mpris/mpris.cpp | 40 +++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 94939311..58c4f138 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -458,10 +458,7 @@ auto Mpris::onPlayerStop(PlayerctlPlayer* player, gpointer data) -> void { if (!mpris) return; spdlog::debug("mpris: player-stop callback"); - - // hide widget - mpris->event_box_.set_visible(false); - // update widget + // update widget (update() handles visibility) mpris->dp.emit(); } @@ -504,23 +501,36 @@ auto Mpris::getPlayerInfo() -> std::optional { } // > get the list of players [..] in order of activity // https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249 - bool found = false; + PlayerctlPlayerName* best = nullptr; + PlayerctlPlayerName* first_valid = nullptr; for (auto* p = g_list_first(players); p != nullptr; p = p->next) { auto* pn = static_cast(p->data); std::string name = pn->name; - if (std::none_of(ignored_players_.begin(), ignored_players_.end(), - [&](const std::string& ignored) { return name == ignored; })) { - player_name = name; - if (p != g_list_first(players)) { - fallback_player = playerctl_player_new_from_name(pn, &error); - if (error || !fallback_player) return std::nullopt; - } - found = true; + if (std::any_of(ignored_players_.begin(), ignored_players_.end(), + [&](const std::string& ignored) { return name == ignored; })) { + spdlog::warn("mpris[{}]: ignoring player update", name); + continue; + } + if (!first_valid) first_valid = pn; + // Check if this player is currently playing + auto* tmp = playerctl_player_new_from_name(pn, &error); + if (error || !tmp) continue; + PlayerctlPlaybackStatus status; + g_object_get(tmp, "playback-status", &status, NULL); + if (status == PLAYERCTL_PLAYBACK_STATUS_PLAYING) { + best = pn; + g_object_unref(tmp); break; } - spdlog::warn("mpris[{}]: ignoring player update", name); + g_object_unref(tmp); + } + if (!best) best = first_valid; + if (!best) return std::nullopt; + player_name = best->name; + if (best != static_cast(g_list_first(players)->data)) { + fallback_player = playerctl_player_new_from_name(best, &error); + if (error || !fallback_player) return std::nullopt; } - if (!found) return std::nullopt; } else if (std::any_of(ignored_players_.begin(), ignored_players_.end(), [&](const std::string& pn) { return player_name == pn; })) { spdlog::warn("mpris[{}]: ignoring player update", player_name); From a871d90161fadab34b494d0ee5d018eb90496b8d Mon Sep 17 00:00:00 2001 From: BlueManCZ Date: Wed, 11 Feb 2026 12:09:20 +0100 Subject: [PATCH 017/137] Fix button action handling to consistently use the active player --- include/modules/mpris/mpris.hpp | 1 + src/modules/mpris/mpris.cpp | 68 +++++++++++++++++---------------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/include/modules/mpris/mpris.hpp b/include/modules/mpris/mpris.hpp index ad4dac1e..a33db4d2 100644 --- a/include/modules/mpris/mpris.hpp +++ b/include/modules/mpris/mpris.hpp @@ -78,6 +78,7 @@ class Mpris : public ALabel { PlayerctlPlayerManager* manager; PlayerctlPlayer* player; + PlayerctlPlayer* last_active_player_ = nullptr; std::string lastStatus; std::string lastPlayer; diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 58c4f138..f8a341a8 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -178,6 +178,7 @@ 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); } @@ -486,12 +487,11 @@ auto Mpris::getPlayerInfo() -> std::optional { char* player_status = nullptr; auto player_playback_status = PLAYERCTL_PLAYBACK_STATUS_STOPPED; - // When using playerctld and the most active player is ignored, we create a - // direct connection to the first non-ignored player for correct metadata. - PlayerctlPlayer* fallback_player = nullptr; - waybar::util::ScopeGuard fallback_deleter([&fallback_player]() { - if (fallback_player) g_object_unref(fallback_player); - }); + // Clean up previous fallback player + if (last_active_player_ && last_active_player_ != player) { + g_object_unref(last_active_player_); + last_active_player_ = nullptr; + } std::string player_name = player_; if (player_name == "playerctld") { @@ -501,8 +501,8 @@ auto Mpris::getPlayerInfo() -> std::optional { } // > get the list of players [..] in order of activity // https://github.com/altdesktop/playerctl/blob/b19a71cb9dba635df68d271bd2b3f6a99336a223/playerctl/playerctl-common.c#L248-L249 - PlayerctlPlayerName* best = nullptr; - PlayerctlPlayerName* first_valid = nullptr; + PlayerctlPlayer* first_valid_player = nullptr; + std::string first_valid_name; for (auto* p = g_list_first(players); p != nullptr; p = p->next) { auto* pn = static_cast(p->data); std::string name = pn->name; @@ -511,35 +511,37 @@ auto Mpris::getPlayerInfo() -> std::optional { spdlog::warn("mpris[{}]: ignoring player update", name); continue; } - if (!first_valid) first_valid = pn; - // Check if this player is currently playing auto* tmp = playerctl_player_new_from_name(pn, &error); if (error || !tmp) continue; + if (!first_valid_player) { + first_valid_player = tmp; + first_valid_name = name; + } PlayerctlPlaybackStatus status; g_object_get(tmp, "playback-status", &status, NULL); if (status == PLAYERCTL_PLAYBACK_STATUS_PLAYING) { - best = pn; - g_object_unref(tmp); + if (tmp != first_valid_player) g_object_unref(first_valid_player); + last_active_player_ = tmp; + player_name = name; break; } - g_object_unref(tmp); + if (tmp != first_valid_player) g_object_unref(tmp); } - if (!best) best = first_valid; - if (!best) return std::nullopt; - player_name = best->name; - if (best != static_cast(g_list_first(players)->data)) { - fallback_player = playerctl_player_new_from_name(best, &error); - if (error || !fallback_player) return std::nullopt; + if (!last_active_player_) { + if (!first_valid_player) return std::nullopt; + last_active_player_ = first_valid_player; + player_name = first_valid_name; } } else if (std::any_of(ignored_players_.begin(), ignored_players_.end(), [&](const std::string& pn) { return player_name == pn; })) { spdlog::warn("mpris[{}]: ignoring player update", player_name); return std::nullopt; + } else { + last_active_player_ = player; } - auto* source_player = fallback_player ? fallback_player : player; - g_object_get(source_player, "status", &player_status, "playback-status", &player_playback_status, - NULL); + g_object_get(last_active_player_, "status", &player_status, "playback-status", + &player_playback_status, NULL); // make status lowercase player_status[0] = std::tolower(player_status[0]); @@ -554,28 +556,29 @@ auto Mpris::getPlayerInfo() -> std::optional { .length = std::nullopt, }; - if (auto* artist_ = playerctl_player_get_artist(source_player, &error)) { + if (auto* artist_ = playerctl_player_get_artist(last_active_player_, &error)) { spdlog::debug("mpris[{}]: artist = {}", info.name, artist_); info.artist = artist_; g_free(artist_); } if (error) goto errorexit; - if (auto* album_ = playerctl_player_get_album(source_player, &error)) { + if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); info.album = album_; g_free(album_); } if (error) goto errorexit; - if (auto* title_ = playerctl_player_get_title(source_player, &error)) { + if (auto* title_ = playerctl_player_get_title(last_active_player_, &error)) { spdlog::debug("mpris[{}]: title = {}", info.name, title_); info.title = title_; g_free(title_); } if (error) goto errorexit; - if (auto* length_ = playerctl_player_print_metadata_prop(source_player, "mpris:length", &error)) { + if (auto* length_ = + playerctl_player_print_metadata_prop(last_active_player_, "mpris:length", &error)) { spdlog::debug("mpris[{}]: mpris:length = {}", info.name, length_); auto len = std::chrono::microseconds(std::strtol(length_, nullptr, 10)); auto len_h = std::chrono::duration_cast(len); @@ -587,7 +590,7 @@ auto Mpris::getPlayerInfo() -> std::optional { if (error) goto errorexit; { - auto position_ = playerctl_player_get_position(source_player, &error); + auto position_ = playerctl_player_get_position(last_active_player_, &error); if (error) { // it's fine to have an error here because not all players report a position g_error_free(error); @@ -639,12 +642,13 @@ bool Mpris::handleToggle(GdkEventButton* const& e) { }); // Command pattern: encapsulate each button's action + auto* target = last_active_player_ ? last_active_player_ : player; const ButtonAction actions[] = { - {1, "on-click", [&]() { playerctl_player_play_pause(player, &error); }}, - {2, "on-click-middle", [&]() { playerctl_player_previous(player, &error); }}, - {3, "on-click-right", [&]() { playerctl_player_next(player, &error); }}, - {8, "on-click-backward", [&]() { playerctl_player_previous(player, &error); }}, - {9, "on-click-forward", [&]() { playerctl_player_next(player, &error); }}, + {1, "on-click", [&]() { playerctl_player_play_pause(target, &error); }}, + {2, "on-click-middle", [&]() { playerctl_player_previous(target, &error); }}, + {3, "on-click-right", [&]() { playerctl_player_next(target, &error); }}, + {8, "on-click-backward", [&]() { playerctl_player_previous(target, &error); }}, + {9, "on-click-forward", [&]() { playerctl_player_next(target, &error); }}, }; for (const auto& action : actions) { From 3bcadfdf5a57b3598194c9c795e7b1e9d8dce3f6 Mon Sep 17 00:00:00 2001 From: Alexey Vladimirov <> Date: Thu, 12 Feb 2026 01:14:16 +0300 Subject: [PATCH 018/137] battery: power switch events --- man/waybar-battery.5.scd | 3 +++ src/modules/battery.cpp | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 6d98fd4e..c5450c8e 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -182,6 +182,7 @@ Every entry in the *events* object consists of a ** (typeof: *string - *on--* - *on--* +- *on-* Where: @@ -204,6 +205,8 @@ Where: "on-discharging-warning": "notify-send -u normal 'Low Battery'", "on-discharging-critical": "notify-send -u critical 'Very Low Battery'", "on-charging-100": "notify-send -u normal 'Battery Full!'" + "on-discharging": "notify-send -u normal 'Power Switch' Discharging" + "on-charging": "notify-send -u normal 'Power Switch' Charging'" }, "format": "{capacity}% {icon}", "format-icons": ["", "", "", "", ""], diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 8bf4a431..58a7f50e 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -790,16 +790,19 @@ void waybar::modules::Battery::processEvents(std::string& state, std::string& st if (!events.isObject() || events.empty()) { return; } - std::string event_name = fmt::format("on-{}-{}", status == "discharging" ? status : "charging", - state.empty() ? std::to_string(capacity) : state); + auto exec = [](Json::Value const& event) { + if (!event.isString()) return; + if (auto command = event.asString(); !command.empty()) { + util::command::exec(command, ""); + } + }; + std::string status_name = status == "discharging" ? "on-discharging" : "on-charging"; + std::string event_name = status_name + '-' + (state.empty() ? std::to_string(capacity) : state); if (last_event_ != event_name) { spdlog::debug("battery: triggering event {}", event_name); - if (events[event_name].isString()) { - std::string exec = events[event_name].asString(); - // Execute the command if it is not empty - if (!exec.empty()) { - util::command::exec(exec, ""); - } + exec(events[event_name]); + if (!last_event_.empty() && last_event_[3] != event_name[3]) { + exec(events[status_name]); } last_event_ = event_name; } From 7110c455e2fa5c9763681a72685b7ffc6f5d5c2b Mon Sep 17 00:00:00 2001 From: Alexey Vladimirov <> Date: Thu, 12 Feb 2026 02:12:00 +0300 Subject: [PATCH 019/137] fix of example config typos --- man/waybar-battery.5.scd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index c5450c8e..42b9e046 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -204,8 +204,8 @@ Where: "events": { "on-discharging-warning": "notify-send -u normal 'Low Battery'", "on-discharging-critical": "notify-send -u critical 'Very Low Battery'", - "on-charging-100": "notify-send -u normal 'Battery Full!'" - "on-discharging": "notify-send -u normal 'Power Switch' Discharging" + "on-charging-100": "notify-send -u normal 'Battery Full!'", + "on-discharging": "notify-send -u normal 'Power Switch' Discharging", "on-charging": "notify-send -u normal 'Power Switch' Charging'" }, "format": "{capacity}% {icon}", From a194b576be36423c64618148a6f789dd4362e9c1 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 08:52:00 -0600 Subject: [PATCH 020/137] feat(niri): niri depends on socket Don't attempt to use niri modules when socket connection fails. Prevents rendering modules when running another compositor. In same concept as previous Hyprland change https://github.com/Alexays/Waybar/commit/4295faa7c4b23b7f6e86669d1fe8c93562e10241 Signed-off-by: Austin Horstman --- src/modules/niri/backend.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 1ee1bf3f..5284e0be 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -24,8 +24,7 @@ int IPC::connectToSocket() { const char* socket_path = getenv("NIRI_SOCKET"); if (socket_path == nullptr) { - spdlog::warn("Niri is not running, niri IPC will not be available."); - return -1; + throw std::runtime_error("Niri IPC: NIRI_SOCKET was not set! (Is Niri running?)"); } struct sockaddr_un addr; @@ -54,15 +53,9 @@ int IPC::connectToSocket() { void IPC::startIPC() { // will start IPC and relay events to parseIPC - std::thread([&]() { - int socketfd; - try { - socketfd = connectToSocket(); - } catch (std::exception& e) { - spdlog::error("Niri IPC: failed to start, reason: {}", e.what()); - return; - } - if (socketfd == -1) return; + int socketfd = connectToSocket(); + + std::thread([this, socketfd]() { spdlog::info("Niri IPC starting"); @@ -242,7 +235,6 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) { Json::Value IPC::send(const Json::Value& request) { int socketfd = connectToSocket(); - if (socketfd == -1) throw std::runtime_error("Niri is not running"); auto unix_istream = Gio::UnixInputStream::create(socketfd, true); auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false); From ae60ca62331edeba08d75ef68d0c6c5fe53c9c79 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 08:53:04 -0600 Subject: [PATCH 021/137] refactor(niri): declared constructor Move constructor from hpp to cpp to align with other modules Signed-off-by: Austin Horstman --- include/modules/niri/backend.hpp | 2 +- src/modules/niri/backend.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/modules/niri/backend.hpp b/include/modules/niri/backend.hpp index 42b9ff7f..07be039a 100644 --- a/include/modules/niri/backend.hpp +++ b/include/modules/niri/backend.hpp @@ -17,7 +17,7 @@ class EventHandler { class IPC { public: - IPC() { startIPC(); } + IPC(); void registerForIPC(const std::string& ev, EventHandler* ev_handler); void unregisterForIPC(EventHandler* handler); diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 5284e0be..ce96c51c 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -20,6 +20,8 @@ namespace waybar::modules::niri { +IPC::IPC() { startIPC(); } + int IPC::connectToSocket() { const char* socket_path = getenv("NIRI_SOCKET"); From f806ec03ed8412f79889de45879ace24016e3313 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:51:45 +0000 Subject: [PATCH 022/137] Initial plan From 65fadcf94b0df851eb43200b7efb04aead3e87d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:52:00 +0000 Subject: [PATCH 023/137] Initial plan From 37d65415920b1ca3132de5b1fd01c143b150a830 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:53:14 +0000 Subject: [PATCH 024/137] Initial plan From 0c46818e95fb694394f5c06e3965c18d377cacd6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:54:23 +0000 Subject: [PATCH 025/137] Fix crash when bluetooth DBus manager returns null on timeout Co-authored-by: Alexays <13947260+Alexays@users.noreply.github.com> --- src/modules/bluetooth.cpp | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index f491f19b..c59af3b5 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -111,13 +111,16 @@ waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& findConnectedDevices(cur_controller_->path, connected_devices_); } - g_signal_connect(manager_.get(), "object-added", G_CALLBACK(onObjectAdded), this); - g_signal_connect(manager_.get(), "object-removed", G_CALLBACK(onObjectRemoved), this); - g_signal_connect(manager_.get(), "interface-proxy-properties-changed", - G_CALLBACK(onInterfaceProxyPropertiesChanged), this); - g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), this); - g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), - this); + if (manager_) { + g_signal_connect(manager_.get(), "object-added", G_CALLBACK(onObjectAdded), this); + g_signal_connect(manager_.get(), "object-removed", G_CALLBACK(onObjectRemoved), this); + g_signal_connect(manager_.get(), "interface-proxy-properties-changed", + G_CALLBACK(onInterfaceProxyPropertiesChanged), this); + g_signal_connect(manager_.get(), "interface-added", G_CALLBACK(onInterfaceAddedOrRemoved), + this); + g_signal_connect(manager_.get(), "interface-removed", G_CALLBACK(onInterfaceAddedOrRemoved), + this); + } #ifdef WANT_RFKILL rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); @@ -446,6 +449,10 @@ auto waybar::modules::Bluetooth::getControllerProperties(GDBusObject* object, auto waybar::modules::Bluetooth::findCurController() -> std::optional { std::optional controller_info; + if (!manager_) { + return controller_info; + } + GList* objects = g_dbus_object_manager_get_objects(manager_.get()); for (GList* l = objects; l != NULL; l = l->next) { GDBusObject* object = G_DBUS_OBJECT(l->data); @@ -465,6 +472,9 @@ auto waybar::modules::Bluetooth::findCurController() -> std::optional& connected_devices) -> void { + if (!manager_) { + return; + } GList* objects = g_dbus_object_manager_get_objects(manager_.get()); for (GList* l = objects; l != NULL; l = l->next) { GDBusObject* object = G_DBUS_OBJECT(l->data); From 49d4049ea3f347f207abafb03eea0bcd5f698838 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:55:05 +0000 Subject: [PATCH 026/137] Fix SIGABRT on USB unplug race condition in battery module Co-authored-by: Alexays <13947260+Alexays@users.noreply.github.com> --- src/modules/battery.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 58a7f50e..bdf29ffb 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -139,7 +139,8 @@ void waybar::modules::Battery::refreshBatteries() { auto event_path = (node.path() / "uevent"); auto wd = inotify_add_watch(battery_watch_fd_, event_path.c_str(), IN_ACCESS); if (wd < 0) { - throw std::runtime_error("Could not watch events for " + node.path().string()); + spdlog::warn("Could not watch events for {} (device may have been removed)", node.path().string()); + continue; } batteries_[node.path()] = wd; } From e18939210bff31c44f7d6e7d56de2b7f9c3b4a68 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Feb 2026 00:00:57 +0100 Subject: [PATCH 027/137] fix: lint --- include/modules/sway/ipc/client.hpp | 2 +- src/modules/battery.cpp | 3 ++- src/modules/hyprland/workspaces.cpp | 6 +++--- src/modules/niri/backend.cpp | 1 - 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/modules/sway/ipc/client.hpp b/include/modules/sway/ipc/client.hpp index c5178c2b..281df7ab 100644 --- a/include/modules/sway/ipc/client.hpp +++ b/include/modules/sway/ipc/client.hpp @@ -12,8 +12,8 @@ #include #include "ipc.hpp" -#include "util/sleeper_thread.hpp" #include "util/SafeSignal.hpp" +#include "util/sleeper_thread.hpp" namespace waybar::modules::sway { diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index bdf29ffb..d39c4920 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -139,7 +139,8 @@ void waybar::modules::Battery::refreshBatteries() { auto event_path = (node.path() / "uevent"); auto wd = inotify_add_watch(battery_watch_fd_, event_path.c_str(), IN_ACCESS); if (wd < 0) { - spdlog::warn("Could not watch events for {} (device may have been removed)", node.path().string()); + spdlog::warn("Could not watch events for {} (device may have been removed)", + node.path().string()); continue; } batteries_[node.path()] = wd; diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8765d78b..88b01223 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -45,7 +45,7 @@ void Workspaces::init() { initializeWorkspaces(); if (barScroll()) { - auto &window = const_cast(m_bar).window; + 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)); } @@ -1159,9 +1159,9 @@ std::optional Workspaces::parseWorkspaceId(std::string const& workspaceIdSt } } -bool Workspaces::handleScroll(GdkEventScroll *e) { +bool Workspaces::handleScroll(GdkEventScroll* e) { // Ignore emulated scroll events on window - if (gdk_event_get_pointer_emulated((GdkEvent *)e)) { + if (gdk_event_get_pointer_emulated((GdkEvent*)e)) { return false; } auto dir = AModule::getScrollDir(e); diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index ce96c51c..68bb1724 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -58,7 +58,6 @@ void IPC::startIPC() { int socketfd = connectToSocket(); std::thread([this, socketfd]() { - spdlog::info("Niri IPC starting"); auto unix_istream = Gio::UnixInputStream::create(socketfd, true); From 82f076c6c200d0d7e29b0bc4cfd0a8955f3d1ead Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:04:36 +0000 Subject: [PATCH 028/137] mpd: fix FD leak by resetting connection on MPD_ERROR_SYSTEM errors Co-authored-by: Alexays <13947260+Alexays@users.noreply.github.com> --- src/modules/mpd/mpd.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index f0cc5757..1e28a58a 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -323,6 +323,7 @@ void waybar::modules::MPD::checkErrors(mpd_connection* conn) { case MPD_ERROR_SYSTEM: if (auto ec = mpd_connection_get_system_error(conn); ec != 0) { mpd_connection_clear_error(conn); + connection_.reset(); throw std::system_error(ec, std::system_category()); } G_GNUC_FALLTHROUGH; From ef3d55980ed3d38989a6576af4d0c7b83adf00e6 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Feb 2026 00:49:03 +0100 Subject: [PATCH 029/137] fix: some crashes --- include/AModule.hpp | 2 +- include/modules/wayfire/backend.hpp | 4 ++-- include/util/command.hpp | 1 + src/AAppIconLabel.cpp | 18 +++++++++------ src/ALabel.cpp | 16 ++++++++++--- src/AModule.cpp | 4 ++-- src/main.cpp | 14 +++++++----- src/modules/cava/cava_backend.cpp | 2 +- src/modules/cffi.cpp | 2 +- src/modules/cpu.cpp | 10 +++++---- src/modules/cpu_usage/common.cpp | 14 +++++++----- src/modules/disk.cpp | 2 +- src/modules/dwl/tags.cpp | 1 + src/modules/ext/workspace_manager.cpp | 4 +++- src/modules/gamemode.cpp | 6 ++--- src/modules/jack.cpp | 5 ++++- src/modules/mpris/mpris.cpp | 8 +++++-- src/modules/network.cpp | 1 + src/modules/power_profiles_daemon.cpp | 4 ++++ src/modules/river/layout.cpp | 1 + src/modules/river/mode.cpp | 1 + src/modules/river/tags.cpp | 3 +++ src/modules/river/window.cpp | 1 + src/modules/sndio.cpp | 4 +++- src/modules/sni/host.cpp | 4 ++-- src/modules/sni/item.cpp | 3 ++- src/modules/sni/watcher.cpp | 2 +- src/modules/systemd_failed_units.cpp | 8 +++---- src/modules/upower.cpp | 10 +++++---- src/modules/wayfire/backend.cpp | 31 +++++++++++++++++++------- src/modules/wayfire/window.cpp | 8 +++++-- src/modules/wayfire/workspaces.cpp | 16 +++++++++---- src/modules/wireplumber.cpp | 15 +++++++------ src/modules/wlr/taskbar.cpp | 4 +++- src/util/audio_backend.cpp | 13 +++++++---- src/util/backlight_backend.cpp | 20 ++++++++++++----- src/util/css_reload_helper.cpp | 6 ++++- src/util/icon_loader.cpp | 9 +++++--- src/util/pipewire/pipewire_backend.cpp | 11 ++++++++- src/util/portal.cpp | 31 +++++++++++++++----------- 40 files changed, 216 insertions(+), 103 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index 6b29045c..a338ffe3 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -48,7 +48,7 @@ class AModule : public IModule { virtual bool handleMouseLeave(GdkEventCrossing* const& ev); virtual bool handleScroll(GdkEventScroll*); virtual bool handleRelease(GdkEventButton* const& ev); - GObject* menu_; + GObject* menu_ = nullptr; private: bool handleUserEvent(GdkEventButton* const& ev); diff --git a/include/modules/wayfire/backend.hpp b/include/modules/wayfire/backend.hpp index 9d55c820..d3173269 100644 --- a/include/modules/wayfire/backend.hpp +++ b/include/modules/wayfire/backend.hpp @@ -89,7 +89,7 @@ struct Sock { } }; -class IPC { +class IPC : public std::enable_shared_from_this { static std::weak_ptr instance; Json::CharReaderBuilder reader_builder; Json::StreamWriterBuilder writer_builder; @@ -98,7 +98,7 @@ class IPC { State state; std::mutex state_mutex; - IPC() { start(); } + IPC() = default; static auto connect() -> Sock; auto receive(Sock& sock) -> Json::Value; diff --git a/include/util/command.hpp b/include/util/command.hpp index 89d4372a..b1adcd7c 100644 --- a/include/util/command.hpp +++ b/include/util/command.hpp @@ -59,6 +59,7 @@ inline int close(FILE* fp, pid_t pid) { spdlog::debug("Cmd continued"); } else if (ret == -1) { spdlog::debug("waitpid failed: {}", strerror(errno)); + break; } else { break; } diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index 3d62d69d..55de78f5 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -156,14 +156,18 @@ void AAppIconLabel::updateAppIcon() { if (app_icon_name_.empty()) { image_.set_visible(false); } else if (app_icon_name_.front() == '/') { - auto pixbuf = Gdk::Pixbuf::create_from_file(app_icon_name_); - int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); - pixbuf = Gdk::Pixbuf::create_from_file(app_icon_name_, scaled_icon_size, scaled_icon_size); + try { + int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); + auto pixbuf = Gdk::Pixbuf::create_from_file(app_icon_name_, scaled_icon_size, scaled_icon_size); - auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), - image_.get_window()); - image_.set(surface); - image_.set_visible(true); + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), + image_.get_window()); + image_.set(surface); + image_.set_visible(true); + } catch (const Glib::Exception& e) { + spdlog::warn("Failed to load app icon {}: {}", app_icon_name_, std::string(e.what())); + image_.set_visible(false); + } } else { image_.set_from_icon_name(app_icon_name_, Gtk::ICON_SIZE_INVALID); image_.set_visible(true); diff --git a/src/ALabel.cpp b/src/ALabel.cpp index d251d896..0d92c372 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -91,11 +91,13 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st // Make the GtkBuilder and check for errors in his parsing if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) { + g_object_unref(builder); throw std::runtime_error("Error found in the file " + menuFile); } menu_ = gtk_builder_get_object(builder, "menu"); if (menu_ == nullptr) { + g_object_unref(builder); throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); } submenus_ = std::map(); @@ -105,11 +107,17 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st for (Json::Value::const_iterator it = config_["menu-actions"].begin(); it != config_["menu-actions"].end(); ++it) { std::string key = it.key().asString(); - submenus_[key] = GTK_MENU_ITEM(gtk_builder_get_object(builder, key.c_str())); + auto* item = gtk_builder_get_object(builder, key.c_str()); + if (item == nullptr) { + spdlog::warn("Menu item '{}' not found in builder file", key); + continue; + } + 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_object_unref(builder); } catch (std::runtime_error& e) { spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what()); } @@ -141,7 +149,8 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ if (format_icons.isArray()) { auto size = format_icons.size(); if (size != 0U) { - auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); + auto divisor = std::max(1U, (max == 0 ? 100U : static_cast(max)) / size); + auto idx = std::clamp(percentage / divisor, 0U, size - 1); format_icons = format_icons[idx]; } } @@ -167,7 +176,8 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& if (format_icons.isArray()) { auto size = format_icons.size(); if (size != 0U) { - auto idx = std::clamp(percentage / ((max == 0 ? 100 : max) / size), 0U, size - 1); + auto divisor = std::max(1U, (max == 0 ? 100U : static_cast(max)) / size); + auto idx = std::clamp(percentage / divisor, 0U, size - 1); format_icons = format_icons[idx]; } } diff --git a/src/AModule.cpp b/src/AModule.cpp index c6fdff3e..5f3a187a 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -166,9 +166,9 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { } // Check that a menu has been configured - if (config_["menu"].isString()) { + if (rec != eventMap_.cend() && config_["menu"].isString()) { // Check if the event is the one specified for the "menu" option - if (rec->second == config_["menu"].asString()) { + if (rec->second == config_["menu"].asString() && menu_ != nullptr) { // Popup the menu gtk_widget_show_all(GTK_WIDGET(menu_)); gtk_menu_popup_at_pointer(GTK_MENU(menu_), reinterpret_cast(e)); diff --git a/src/main.cpp b/src/main.cpp index 021f8e1a..166dd2bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,7 +33,10 @@ static void writeSignalToPipe(int signum) { // to `signal_handler`. static void catchSignals(waybar::SafeSignal& signal_handler) { int fd[2]; - pipe(fd); + if (pipe(fd) != 0) { + spdlog::error("Failed to create signal pipe: {}", strerror(errno)); + return; + } int signal_pipe_read_fd = fd[0]; signal_pipe_write_fd = fd[1]; @@ -137,15 +140,16 @@ static void handleSignalMainThread(int signum, bool& reload) { break; case SIGCHLD: spdlog::debug("Received SIGCHLD in signalThread"); - if (!reap.empty()) { - reap_mtx.lock(); - for (auto it = reap.begin(); it != reap.end(); ++it) { + { + std::lock_guard lock(reap_mtx); + for (auto it = reap.begin(); it != reap.end(); ) { if (waitpid(*it, nullptr, WNOHANG) == *it) { spdlog::debug("Reaped child with PID: {}", *it); it = reap.erase(it); + } else { + ++it; } } - reap_mtx.unlock(); } break; default: diff --git a/src/modules/cava/cava_backend.cpp b/src/modules/cava/cava_backend.cpp index c576f0cf..6eb540c9 100644 --- a/src/modules/cava/cava_backend.cpp +++ b/src/modules/cava/cava_backend.cpp @@ -218,7 +218,7 @@ void waybar::modules::cava::CavaBackend::loadConfig() { prm_.input = ::cava::input_method_by_name(config_["method"].asString().c_str()); if (config_["source"].isString()) { if (prm_.audio_source) free(prm_.audio_source); - prm_.audio_source = config_["source"].asString().data(); + prm_.audio_source = strdup(config_["source"].asString().c_str()); } if (config_["sample_rate"].isNumeric()) prm_.samplerate = config_["sample_rate"].asLargestInt(); if (config_["sample_bits"].isInt()) prm_.samplebits = config_["sample_bits"].asInt(); diff --git a/src/modules/cffi.cpp b/src/modules/cffi.cpp index 5c095f46..930c4d47 100644 --- a/src/modules/cffi.cpp +++ b/src/modules/cffi.cpp @@ -35,7 +35,7 @@ CFFI::CFFI(const std::string& name, const std::string& id, const Json::Value& co throw std::runtime_error{std::string{"Missing wbcffi_init function: "} + dlerror()}; } hooks_.deinit = reinterpret_cast(dlsym(handle, "wbcffi_deinit")); - if (!hooks_.init) { + if (!hooks_.deinit) { throw std::runtime_error{std::string{"Missing wbcffi_deinit function: "} + dlerror()}; } // Optional functions diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index ab75b911..8cfda2c1 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -46,12 +46,14 @@ auto waybar::modules::Cpu::update() -> void { 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)); + std::vector arg_names; + arg_names.reserve(cpu_usage.size() * 2); 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))); + arg_names.push_back(fmt::format("usage{}", core_i)); + store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i])); + arg_names.push_back(fmt::format("icon{}", core_i)); + store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons))); } label_.set_markup(fmt::vformat(format, store)); diff --git a/src/modules/cpu_usage/common.cpp b/src/modules/cpu_usage/common.cpp index a2bb4596..82126dac 100644 --- a/src/modules/cpu_usage/common.cpp +++ b/src/modules/cpu_usage/common.cpp @@ -36,12 +36,14 @@ auto waybar::modules::CpuUsage::update() -> void { fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("usage", total_usage)); store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); + std::vector arg_names; + arg_names.reserve(cpu_usage.size() * 2); 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))); + arg_names.push_back(fmt::format("usage{}", core_i)); + store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i])); + arg_names.push_back(fmt::format("icon{}", core_i)); + store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons))); } label_.set_markup(fmt::vformat(format, store)); @@ -78,7 +80,7 @@ std::tuple, std::string> waybar::modules::CpuUsage::getCpu auto [prev_idle, prev_total] = prev_times[0]; const float delta_idle = curr_idle - prev_idle; const float delta_total = curr_total - prev_total; - uint16_t tmp = 100 * (1 - delta_idle / delta_total); + uint16_t tmp = (delta_total > 0) ? static_cast(100 * (1 - delta_idle / delta_total)) : 0; tooltip = fmt::format("Total: {}%\nCores: (pending)", tmp); usage.push_back(tmp); } else { @@ -100,7 +102,7 @@ std::tuple, std::string> waybar::modules::CpuUsage::getCpu } const float delta_idle = curr_idle - prev_idle; const float delta_total = curr_total - prev_total; - uint16_t tmp = 100 * (1 - delta_idle / delta_total); + uint16_t tmp = (delta_total > 0) ? static_cast(100 * (1 - delta_idle / delta_total)) : 0; if (i == 0) { tooltip = fmt::format("Total: {}%", tmp); } else { diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index 39703547..fd7ef817 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -41,7 +41,7 @@ auto waybar::modules::Disk::update() -> void { fs_used - File system used space */ - if (err != 0) { + if (err != 0 || stats.f_blocks == 0) { event_box_.hide(); return; } diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp index fb065650..399c7d4b 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -179,6 +179,7 @@ bool Tags::handle_button_press(GdkEventButton* event_button, uint32_t tag) { } void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint32_t focused) { + if (tag >= buttons_.size()) return; // First clear all occupied state auto& button = buttons_[tag]; if (clients) { diff --git a/src/modules/ext/workspace_manager.cpp b/src/modules/ext/workspace_manager.cpp index 637177d3..b4471c14 100644 --- a/src/modules/ext/workspace_manager.cpp +++ b/src/modules/ext/workspace_manager.cpp @@ -143,7 +143,9 @@ void WorkspaceManager::handle_finished() { ext_manager_ = nullptr; } -void WorkspaceManager::commit() const { ext_workspace_manager_v1_commit(ext_manager_); } +void WorkspaceManager::commit() const { + if (ext_manager_) ext_workspace_manager_v1_commit(ext_manager_); +} void WorkspaceManager::update() { spdlog::debug("[ext/workspaces]: Updating state"); diff --git a/src/modules/gamemode.cpp b/src/modules/gamemode.cpp index 8c33fcf3..1c846434 100644 --- a/src/modules/gamemode.cpp +++ b/src/modules/gamemode.cpp @@ -128,9 +128,9 @@ void Gamemode::getData() { Glib::VariantContainerBase data = gamemode_proxy->call_sync("Get", parameters); if (data && data.is_of_type(Glib::VariantType("(v)"))) { Glib::VariantBase variant; - g_variant_get(data.gobj_copy(), "(v)", &variant); + g_variant_get(data.gobj(), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_INT32)) { - g_variant_get(variant.gobj_copy(), "i", &gameCount); + g_variant_get(variant.gobj(), "i", &gameCount); return; } } @@ -158,7 +158,7 @@ void Gamemode::prepareForSleep_cb(const Glib::RefPtr& con const Glib::VariantContainerBase& parameters) { if (parameters.is_of_type(Glib::VariantType("(b)"))) { gboolean sleeping; - g_variant_get(parameters.gobj_copy(), "(b)", &sleeping); + g_variant_get(parameters.gobj(), "(b)", &sleeping); if (!sleeping) { getData(); dp.emit(); diff --git a/src/modules/jack.cpp b/src/modules/jack.cpp index 8e5a75a8..578fb4e0 100644 --- a/src/modules/jack.cpp +++ b/src/modules/jack.cpp @@ -53,7 +53,7 @@ std::string JACK::JACKState() { auto JACK::update() -> void { std::string format; std::string state = JACKState(); - float latency = 1000 * (float)bufsize_ / (float)samplerate_; + float latency = samplerate_ > 0 ? 1000.0f * (float)bufsize_ / (float)samplerate_ : 0.0f; if (label_.get_style_context()->has_class("xrun")) { label_.get_style_context()->remove_class("xrun"); @@ -91,16 +91,19 @@ auto JACK::update() -> void { } int JACK::bufSize(jack_nframes_t size) { + std::lock_guard lock(mutex_); bufsize_ = size; return 0; } int JACK::sampleRate(jack_nframes_t rate) { + std::lock_guard lock(mutex_); samplerate_ = rate; return 0; } int JACK::xrun() { + std::lock_guard lock(mutex_); xruns_ += 1; state_ = "xrun"; return 0; diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 1be21436..1bdd7df6 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -117,7 +117,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) } GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error) { g_error_free(error); } @@ -478,7 +478,7 @@ auto Mpris::getPlayerInfo() -> std::optional { } GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error) { g_error_free(error); } @@ -543,6 +543,10 @@ auto Mpris::getPlayerInfo() -> std::optional { g_object_get(last_active_player_, "status", &player_status, "playback-status", &player_playback_status, NULL); + if (!player_status) { + spdlog::error("mpris: failed to get player status"); + return std::nullopt; + } // make status lowercase player_status[0] = std::tolower(player_status[0]); diff --git a/src/modules/network.cpp b/src/modules/network.cpp index c33e750d..0dbdcc57 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -46,6 +46,7 @@ waybar::modules::Network::readBandwidthUsage() { std::string ifacename; iss >> ifacename; // ifacename contains "eth0:" + if (ifacename.empty()) continue; ifacename.pop_back(); // remove trailing ':' if (ifacename != ifname_) { continue; diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index 60569521..566787f3 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -176,6 +176,10 @@ auto PowerProfilesDaemon::update() -> void { bool PowerProfilesDaemon::handleToggle(GdkEventButton* const& e) { if (e->type == GdkEventType::GDK_BUTTON_PRESS && connected_) { + if (availableProfiles_.empty()) return true; + if (activeProfile_ == availableProfiles_.end()) { + activeProfile_ = availableProfiles_.begin(); + } if (e->button == 1) /* left click */ { activeProfile_++; if (activeProfile_ == availableProfiles_.end()) { diff --git a/src/modules/river/layout.cpp b/src/modules/river/layout.cpp index 31358f80..3f1745fe 100644 --- a/src/modules/river/layout.cpp +++ b/src/modules/river/layout.cpp @@ -118,6 +118,7 @@ Layout::Layout(const std::string& id, const waybar::Bar& bar, const Json::Value& if (!seat_) { spdlog::error("wl_seat not advertised"); + return; } label_.hide(); diff --git a/src/modules/river/mode.cpp b/src/modules/river/mode.cpp index 10f5089e..13545030 100644 --- a/src/modules/river/mode.cpp +++ b/src/modules/river/mode.cpp @@ -77,6 +77,7 @@ Mode::Mode(const std::string& id, const waybar::Bar& bar, const Json::Value& con if (!seat_) { spdlog::error("wl_seat not advertised"); + return; } label_.hide(); diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index fc4d98f9..f06565f6 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -101,10 +101,12 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con if (!control_) { spdlog::error("river_control_v1 not advertised"); + return; } if (!seat_) { spdlog::error("wl_seat not advertised"); + return; } box_.set_name("tags"); @@ -168,6 +170,7 @@ Tags::~Tags() { } void Tags::handle_show() { + if (!status_manager_) return; struct wl_output* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output); zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this); diff --git a/src/modules/river/window.cpp b/src/modules/river/window.cpp index 1f41ab4c..2daed002 100644 --- a/src/modules/river/window.cpp +++ b/src/modules/river/window.cpp @@ -78,6 +78,7 @@ Window::Window(const std::string& id, const waybar::Bar& bar, const Json::Value& if (!seat_) { spdlog::error("wl_seat not advertised"); + return; } label_.hide(); // hide the label until populated diff --git a/src/modules/sndio.cpp b/src/modules/sndio.cpp index 9779cd36..46bffdc2 100644 --- a/src/modules/sndio.cpp +++ b/src/modules/sndio.cpp @@ -102,7 +102,9 @@ Sndio::~Sndio() { sioctl_close(hdl_); } auto Sndio::update() -> void { auto format = format_; - unsigned int vol = 100. * static_cast(volume_) / static_cast(maxval_); + unsigned int vol = (maxval_ > 0) + ? static_cast(100. * static_cast(volume_) / static_cast(maxval_)) + : 0; if (volume_ == 0) { label_.get_style_context()->add_class("muted"); diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 54faa16c..75501207 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -59,7 +59,7 @@ void Host::nameVanished(const Glib::RefPtr& conn, const G void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error != nullptr) { g_error_free(error); } @@ -81,7 +81,7 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error != nullptr) { g_error_free(error); } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index ef2543b5..d33765d2 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -365,13 +365,14 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { void Item::updateImage() { auto pixbuf = getIconPixbuf(); + if (!pixbuf) return; auto scaled_icon_size = getScaledIconSize(); // If the loaded icon is not square, assume that the icon height should match the // requested icon size, but the width is allowed to be different. As such, if the // height of the image does not match the requested icon size, resize the icon such that // the aspect ratio is maintained, but the height matches the requested icon size. - if (pixbuf->get_height() != scaled_icon_size) { + if (pixbuf->get_height() > 0 && pixbuf->get_height() != scaled_icon_size) { int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); } diff --git a/src/modules/sni/watcher.cpp b/src/modules/sni/watcher.cpp index 324bd9f5..1534d924 100644 --- a/src/modules/sni/watcher.cpp +++ b/src/modules/sni/watcher.cpp @@ -31,7 +31,7 @@ Watcher::~Watcher() { void Watcher::busAcquired(const Glib::RefPtr& conn, Glib::ustring name) { GError* error = nullptr; - waybar::util::ScopeGuard error_deleter([error]() { + waybar::util::ScopeGuard error_deleter([&error]() { if (error) { g_error_free(error); } diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 90f33be7..d33e9c3c 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -77,9 +77,9 @@ void SystemdFailedUnits::RequestSystemState() { Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); if (data && data.is_of_type(Glib::VariantType("(v)"))) { Glib::VariantBase variant; - g_variant_get(data.gobj_copy(), "(v)", &variant); + g_variant_get(data.gobj(), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_STRING)) { - return g_variant_get_string(variant.gobj_copy(), NULL); + return g_variant_get_string(variant.gobj(), NULL); } } } catch (Glib::Error& e) { @@ -105,9 +105,9 @@ void SystemdFailedUnits::RequestFailedUnits() { Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); if (data && data.is_of_type(Glib::VariantType("(v)"))) { Glib::VariantBase variant; - g_variant_get(data.gobj_copy(), "(v)", &variant); + g_variant_get(data.gobj(), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_UINT32)) { - return g_variant_get_uint32(variant.gobj_copy()); + return g_variant_get_uint32(variant.gobj()); } } } catch (Glib::Error& e) { diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp index 7530890c..3006a34a 100644 --- a/src/modules/upower.cpp +++ b/src/modules/upower.cpp @@ -59,10 +59,12 @@ UPower::UPower(const std::string& id, const Json::Value& config) sigc::mem_fun(*this, &UPower::getConn_cb)); // Make UPower client - GError** gErr = NULL; - upClient_ = up_client_new_full(NULL, gErr); - if (upClient_ == NULL) - spdlog::error("Upower. UPower client connection error. {}", (*gErr)->message); + GError* gErr = NULL; + upClient_ = up_client_new_full(NULL, &gErr); + if (upClient_ == NULL) { + spdlog::error("Upower. UPower client connection error. {}", gErr ? gErr->message : "unknown error"); + if (gErr) g_error_free(gErr); + } // Subscribe UPower events g_signal_connect(upClient_, "device-added", G_CALLBACK(deviceAdded_cb), this); diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 5a9c0c1a..545aaa89 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -32,7 +33,13 @@ auto pack_and_write(Sock& sock, std::string&& buf) -> void { auto read_exact(Sock& sock, size_t n) -> std::string { auto buf = std::string(n, 0); - for (size_t i = 0; i < n;) i += read(sock.fd, &buf[i], n - i); + for (size_t i = 0; i < n;) { + auto r = read(sock.fd, &buf[i], n - i); + if (r <= 0) { + throw std::runtime_error("Wayfire IPC: read failed"); + } + i += static_cast(r); + } return buf; } @@ -53,6 +60,9 @@ auto State::Wset::locate_ws(const Json::Value& geo) -> Workspace& { } auto State::Wset::locate_ws(const Json::Value& geo) const -> const Workspace& { + if (!output.has_value()) { + throw std::runtime_error("Wayfire IPC: wset has no output assigned"); + } const auto& out = output.value().get(); auto [qx, rx] = std::div(geo["x"].asInt(), out.w); auto [qy, ry] = std::div(geo["y"].asInt(), out.h); @@ -88,7 +98,10 @@ auto State::update_view(const Json::Value& view) -> void { auto IPC::get_instance() -> std::shared_ptr { auto p = instance.lock(); - if (!p) instance = p = std::shared_ptr(new IPC); + if (!p) { + instance = p = std::shared_ptr(new IPC); + p->start(); + } return p; } @@ -116,7 +129,9 @@ auto IPC::connect() -> Sock { } auto IPC::receive(Sock& sock) -> Json::Value { - auto len = *reinterpret_cast(read_exact(sock, 4).data()); + auto len_buf = read_exact(sock, 4); + uint32_t len; + std::memcpy(&len, len_buf.data(), sizeof(len)); if constexpr (std::endian::native != std::endian::little) len = byteswap(len); auto buf = read_exact(sock, len); @@ -153,15 +168,15 @@ auto IPC::start() -> void { send("window-rules/get-focused-view", {}); send("window-rules/get-focused-output", {}); - std::thread([&] { + std::thread([self = shared_from_this()] { auto sock = connect(); { Json::Value json; json["method"] = "window-rules/events/watch"; - pack_and_write(sock, Json::writeString(writer_builder, json)); - if (receive(sock)["result"] != "ok") { + pack_and_write(sock, Json::writeString(self->writer_builder, json)); + if (self->receive(sock)["result"] != "ok") { spdlog::error( "Wayfire IPC: method \"window-rules/events/watch\"" " have failed"); @@ -169,10 +184,10 @@ auto IPC::start() -> void { } } - while (auto json = receive(sock)) { + while (auto json = self->receive(sock)) { auto ev = json["event"].asString(); spdlog::debug("Wayfire IPC: received event \"{}\"", ev); - root_event_handler(ev, json); + self->root_event_handler(ev, json); } }).detach(); } diff --git a/src/modules/wayfire/window.cpp b/src/modules/wayfire/window.cpp index fbcde6ec..8634f090 100644 --- a/src/modules/wayfire/window.cpp +++ b/src/modules/wayfire/window.cpp @@ -34,8 +34,12 @@ auto Window::update() -> void { auto Window::update_icon_label() -> void { auto _ = ipc->lock_state(); - const auto& output = ipc->get_outputs().at(bar_.output->name); - const auto& wset = ipc->get_wsets().at(output.wset_idx); + auto out_it = ipc->get_outputs().find(bar_.output->name); + if (out_it == ipc->get_outputs().end()) return; + const auto& output = out_it->second; + auto wset_it = ipc->get_wsets().find(output.wset_idx); + if (wset_it == ipc->get_wsets().end()) return; + const auto& wset = wset_it->second; const auto& views = ipc->get_views(); auto ctx = bar_.window.get_style_context(); diff --git a/src/modules/wayfire/workspaces.cpp b/src/modules/wayfire/workspaces.cpp index 4c4cd6c1..724a19f8 100644 --- a/src/modules/wayfire/workspaces.cpp +++ b/src/modules/wayfire/workspaces.cpp @@ -70,8 +70,12 @@ auto Workspaces::handleScroll(GdkEventScroll* e) -> bool { Json::Value data; { auto _ = ipc->lock_state(); - const auto& output = ipc->get_outputs().at(bar_.output->name); - const auto& wset = ipc->get_wsets().at(output.wset_idx); + auto out_it = ipc->get_outputs().find(bar_.output->name); + if (out_it == ipc->get_outputs().end()) return true; + const auto& output = out_it->second; + auto wset_it = ipc->get_wsets().find(output.wset_idx); + if (wset_it == ipc->get_wsets().end()) return true; + const auto& wset = wset_it->second; auto n = wset.ws_w * wset.ws_h; auto i = (wset.ws_idx() + delta + n) % n; data["x"] = Json::Value((uint64_t)i % wset.ws_w); @@ -92,8 +96,12 @@ auto Workspaces::update_box() -> void { auto _ = ipc->lock_state(); const auto& output_name = bar_.output->name; - const auto& output = ipc->get_outputs().at(output_name); - const auto& wset = ipc->get_wsets().at(output.wset_idx); + auto out_it = ipc->get_outputs().find(output_name); + if (out_it == ipc->get_outputs().end()) return; + const auto& output = out_it->second; + auto wset_it = ipc->get_wsets().find(output.wset_idx); + if (wset_it == ipc->get_wsets().end()) return; + const auto& wset = wset_it->second; auto output_focused = ipc->get_focused_output_name() == output_name; auto ws_w = wset.ws_w; diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 2480a0e7..e9cb7206 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -81,7 +81,7 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* if (proxy == nullptr) { auto err = fmt::format("Object '{}' not found\n", id); spdlog::error("[{}]: {}", self->name_, err); - throw std::runtime_error(err); + return; } g_autoptr(WpProperties) properties = @@ -153,7 +153,7 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se if (variant == nullptr) { auto err = fmt::format("Node {} does not support volume\n", id); spdlog::error("[{}]: {}", self->name_, err); - throw std::runtime_error(err); + return; } g_variant_lookup(variant, "volume", "d", &self->volume_); @@ -287,14 +287,14 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir if (self->def_nodes_api_ == nullptr) { spdlog::error("[{}]: default nodes api is not loaded.", self->name_); - throw std::runtime_error("Default nodes API is not loaded\n"); + return; } self->mixer_api_ = wp_plugin_find(self->wp_core_, "mixer-api"); if (self->mixer_api_ == nullptr) { spdlog::error("[{}]: mixer api is not loaded.", self->name_); - throw std::runtime_error("Mixer api is not loaded\n"); + return; } // Get default sink @@ -336,7 +336,7 @@ void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* if (wp_object_activate_finish(p, res, &error) == 0) { spdlog::error("[{}]: error activating plugin: {}", self->name_, error->message); - throw std::runtime_error(error->message); + return; } if (--self->pending_plugins_ == 0) { @@ -373,7 +373,7 @@ void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncRe if (success == FALSE) { spdlog::error("[{}]: default nodes API load failed", self->name_); - throw std::runtime_error(error->message); + return; } spdlog::debug("[{}]: loaded default nodes api", self->name_); g_ptr_array_add(self->apis_, wp_plugin_find(self->wp_core_, "default-nodes-api")); @@ -392,7 +392,7 @@ void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* r if (success == FALSE) { spdlog::error("[{}]: mixer API load failed", self->name_); - throw std::runtime_error(error->message); + return; } spdlog::debug("[{}]: loaded mixer API", self->name_); @@ -524,6 +524,7 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { } } if (newVol != volume_) { + if (mixer_api_ == nullptr) return true; GVariant* variant = g_variant_new_double(newVol); gboolean ret; g_signal_emit_by_name(mixer_api_, "set-volume", node_id_, variant, &ret); diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index c6985b94..2aef0ae3 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -437,7 +437,9 @@ void Task::handle_drag_data_get(const Glib::RefPtr& context, void Task::handle_drag_data_received(const Glib::RefPtr& context, int x, int y, Gtk::SelectionData selection_data, guint info, guint time) { spdlog::debug("drag_data_received"); - gpointer handle = *(gpointer*)selection_data.get_data(); + auto* raw = selection_data.get_data(); + if (!raw || selection_data.get_length() < static_cast(sizeof(gpointer))) return; + gpointer handle = *(gpointer*)raw; auto dragged_button = (Gtk::Button*)handle; if (dragged_button == &this->button) return; diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 342d40b2..f61ee945 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -237,9 +237,10 @@ void AudioBackend::sourceInfoCb(pa_context* /*context*/, const pa_source_info* i */ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, void* data) { auto* backend = static_cast(data); - backend->current_sink_name_ = i->default_sink_name; - backend->default_sink_name = i->default_sink_name; - backend->default_source_name_ = i->default_source_name; + if (i == nullptr) return; + backend->current_sink_name_ = i->default_sink_name ? i->default_sink_name : ""; + backend->default_sink_name = i->default_sink_name ? i->default_sink_name : ""; + backend->default_source_name_ = i->default_source_name ? i->default_source_name : ""; pa_context_get_sink_info_list(context, sinkInfoCb, data); pa_context_get_source_info_list(context, sourceInfoCb, data); @@ -355,6 +356,7 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma } void AudioBackend::toggleSinkMute() { + if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = !muted_; pa_threaded_mainloop_lock(mainloop_); pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, @@ -363,6 +365,7 @@ void AudioBackend::toggleSinkMute() { } void AudioBackend::toggleSinkMute(bool mute) { + if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = mute; pa_threaded_mainloop_lock(mainloop_); pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, @@ -371,7 +374,8 @@ void AudioBackend::toggleSinkMute(bool mute) { } void AudioBackend::toggleSourceMute() { - source_muted_ = !muted_; + if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; + source_muted_ = !source_muted_; pa_threaded_mainloop_lock(mainloop_); pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), nullptr, nullptr); @@ -379,6 +383,7 @@ void AudioBackend::toggleSourceMute() { } void AudioBackend::toggleSourceMute(bool mute) { + if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; source_muted_ = mute; pa_threaded_mainloop_lock(mainloop_); pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp index 48473dd2..c7a2b046 100644 --- a/src/util/backlight_backend.cpp +++ b/src/util/backlight_backend.cpp @@ -79,18 +79,20 @@ static void upsert_device(std::vector& devices, udev_device* de }); if (found != devices.end()) { if (actual != nullptr) { - found->set_actual(std::stoi(actual)); + try { found->set_actual(std::stoi(actual)); } catch (const std::exception&) {} } if (max != nullptr) { - found->set_max(std::stoi(max)); + try { found->set_max(std::stoi(max)); } catch (const std::exception&) {} } if (power != nullptr) { - found->set_powered(std::stoi(power) == 0); + try { found->set_powered(std::stoi(power) == 0); } catch (const std::exception&) {} } } else { - const int actual_int = actual == nullptr ? 0 : std::stoi(actual); - const int max_int = max == nullptr ? 0 : std::stoi(max); - const bool power_bool = power == nullptr ? true : std::stoi(power) == 0; + int actual_int = 0, max_int = 0; + bool power_bool = true; + try { if (actual != nullptr) actual_int = std::stoi(actual); } catch (const std::exception&) {} + try { if (max != nullptr) max_int = std::stoi(max); } catch (const std::exception&) {} + try { if (power != nullptr) power_bool = std::stoi(power) == 0; } catch (const std::exception&) {} devices.emplace_back(name, actual_int, max_int, power_bool); } } @@ -261,6 +263,11 @@ void BacklightBackend::set_brightness(const std::string& preferred_device, Chang void BacklightBackend::set_brightness_internal(const std::string& device_name, int brightness, int max_brightness) { + if (!login_proxy_) { + spdlog::error("Login proxy not available, cannot set brightness"); + return; + } + brightness = std::clamp(brightness, 0, max_brightness); auto call_args = Glib::VariantContainerBase( @@ -273,6 +280,7 @@ int BacklightBackend::get_scaled_brightness(const std::string& preferred_device) GET_BEST_DEVICE(best, (*this), preferred_device); if (best != nullptr) { + if (best->get_max() == 0) return 0; return static_cast(std::round(best->get_actual() * 100.0F / best->get_max())); } diff --git a/src/util/css_reload_helper.cpp b/src/util/css_reload_helper.cpp index 06381d60..be8c0a68 100644 --- a/src/util/css_reload_helper.cpp +++ b/src/util/css_reload_helper.cpp @@ -115,11 +115,15 @@ std::vector waybar::CssReloadHelper::parseImports(const std::string auto maxIterations = 100U; do { previousSize = imports.size(); + std::vector to_parse; for (const auto& [file, parsed] : imports) { if (!parsed) { - parseImports(file, imports); + to_parse.push_back(file); } } + for (const auto& file : to_parse) { + parseImports(file, imports); + } } while (imports.size() > previousSize && maxIterations-- > 0); diff --git a/src/util/icon_loader.cpp b/src/util/icon_loader.cpp index fe534f69..1d0bd067 100644 --- a/src/util/icon_loader.cpp +++ b/src/util/icon_loader.cpp @@ -5,8 +5,11 @@ std::vector IconLoader::search_prefix() { std::vector prefixes = {""}; - std::string home_dir = std::getenv("HOME"); - prefixes.push_back(home_dir + "/.local/share/"); + const char* home_env = std::getenv("HOME"); + std::string home_dir = home_env ? home_env : ""; + if (!home_dir.empty()) { + prefixes.push_back(home_dir + "/.local/share/"); + } auto xdg_data_dirs = std::getenv("XDG_DATA_DIRS"); if (!xdg_data_dirs) { @@ -139,7 +142,7 @@ bool IconLoader::image_load_icon(Gtk::Image& image, const Glib::RefPtrget_width() != scaled_icon_size) { + if (pixbuf->get_width() != scaled_icon_size && pixbuf->get_height() > 0) { int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); } diff --git a/src/util/pipewire/pipewire_backend.cpp b/src/util/pipewire/pipewire_backend.cpp index a88a3fbe..82ec0c0f 100644 --- a/src/util/pipewire/pipewire_backend.cpp +++ b/src/util/pipewire/pipewire_backend.cpp @@ -54,11 +54,17 @@ PipewireBackend::PipewireBackend(PrivateConstructorTag tag) context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0); if (context_ == nullptr) { pw_thread_loop_unlock(mainloop_); + pw_thread_loop_destroy(mainloop_); + mainloop_ = nullptr; throw std::runtime_error("pa_context_new() failed."); } core_ = pw_context_connect(context_, nullptr, 0); if (core_ == nullptr) { pw_thread_loop_unlock(mainloop_); + pw_context_destroy(context_); + context_ = nullptr; + pw_thread_loop_destroy(mainloop_); + mainloop_ = nullptr; throw std::runtime_error("pw_context_connect() failed"); } registry_ = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0); @@ -136,7 +142,10 @@ void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permission pw_proxy_add_object_listener(proxy, &pNodeInfo->object_listener, &NODE_EVENTS, pNodeInfo); - privacy_nodes.insert_or_assign(id, pNodeInfo); + { + std::lock_guard lock(mutex_); + privacy_nodes.insert_or_assign(id, pNodeInfo); + } } void PipewireBackend::handleRegistryEventGlobalRemove(uint32_t id) { diff --git a/src/util/portal.cpp b/src/util/portal.cpp index 6df2a6b6..0aa78cb9 100644 --- a/src/util/portal.cpp +++ b/src/util/portal.cpp @@ -58,21 +58,26 @@ void waybar::Portal::refreshAppearance() { // xdg-desktop-portal 1.17 will fix this issue with a new `ReadOne` method, // but this version is not yet released. // TODO(xdg-desktop-portal v1.17): switch to ReadOne - auto container = Glib::VariantBase::cast_dynamic(response); - Glib::VariantBase modev; - container.get_child(modev, 0); - auto mode = - Glib::VariantBase::cast_dynamic>>>(modev) - .get() - .get() - .get(); - auto newMode = Appearance(mode); - if (newMode == currentMode) { + try { + auto container = Glib::VariantBase::cast_dynamic(response); + Glib::VariantBase modev; + container.get_child(modev, 0); + auto mode = + Glib::VariantBase::cast_dynamic>>>(modev) + .get() + .get() + .get(); + auto newMode = Appearance(mode); + if (newMode == currentMode) { + return; + } + spdlog::info("Discovered appearance '{}'", newMode); + currentMode = newMode; + m_signal_appearance_changed.emit(currentMode); + } catch (const std::bad_cast& e) { + spdlog::error("Unexpected appearance variant format: {}", e.what()); return; } - spdlog::info("Discovered appearance '{}'", newMode); - currentMode = newMode; - m_signal_appearance_changed.emit(currentMode); } waybar::Appearance waybar::Portal::getAppearance() { return currentMode; }; From 802bf184fbf54b0e49dcb65edca0a2d410aba03e Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Feb 2026 00:49:23 +0100 Subject: [PATCH 030/137] fix: lint --- src/AAppIconLabel.cpp | 3 ++- src/main.cpp | 2 +- src/modules/cpu_usage/common.cpp | 6 ++++-- src/modules/network.cpp | 2 +- src/modules/sndio.cpp | 6 +++--- src/modules/upower.cpp | 3 ++- src/util/backlight_backend.cpp | 30 ++++++++++++++++++++++++------ src/util/portal.cpp | 3 ++- 8 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index 55de78f5..b72906c3 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -158,7 +158,8 @@ void AAppIconLabel::updateAppIcon() { } else if (app_icon_name_.front() == '/') { try { int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); - auto pixbuf = Gdk::Pixbuf::create_from_file(app_icon_name_, scaled_icon_size, scaled_icon_size); + auto pixbuf = + Gdk::Pixbuf::create_from_file(app_icon_name_, scaled_icon_size, scaled_icon_size); auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), image_.get_window()); diff --git a/src/main.cpp b/src/main.cpp index 166dd2bb..9d7d0ba2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -142,7 +142,7 @@ static void handleSignalMainThread(int signum, bool& reload) { spdlog::debug("Received SIGCHLD in signalThread"); { std::lock_guard lock(reap_mtx); - for (auto it = reap.begin(); it != reap.end(); ) { + for (auto it = reap.begin(); it != reap.end();) { if (waitpid(*it, nullptr, WNOHANG) == *it) { spdlog::debug("Reaped child with PID: {}", *it); it = reap.erase(it); diff --git a/src/modules/cpu_usage/common.cpp b/src/modules/cpu_usage/common.cpp index 82126dac..1571cb74 100644 --- a/src/modules/cpu_usage/common.cpp +++ b/src/modules/cpu_usage/common.cpp @@ -80,7 +80,8 @@ std::tuple, std::string> waybar::modules::CpuUsage::getCpu auto [prev_idle, prev_total] = prev_times[0]; const float delta_idle = curr_idle - prev_idle; const float delta_total = curr_total - prev_total; - uint16_t tmp = (delta_total > 0) ? static_cast(100 * (1 - delta_idle / delta_total)) : 0; + uint16_t tmp = + (delta_total > 0) ? static_cast(100 * (1 - delta_idle / delta_total)) : 0; tooltip = fmt::format("Total: {}%\nCores: (pending)", tmp); usage.push_back(tmp); } else { @@ -102,7 +103,8 @@ std::tuple, std::string> waybar::modules::CpuUsage::getCpu } const float delta_idle = curr_idle - prev_idle; const float delta_total = curr_total - prev_total; - uint16_t tmp = (delta_total > 0) ? static_cast(100 * (1 - delta_idle / delta_total)) : 0; + uint16_t tmp = + (delta_total > 0) ? static_cast(100 * (1 - delta_idle / delta_total)) : 0; if (i == 0) { tooltip = fmt::format("Total: {}%", tmp); } else { diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 0dbdcc57..34dcc03c 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -45,7 +45,7 @@ waybar::modules::Network::readBandwidthUsage() { std::istringstream iss(line); std::string ifacename; - iss >> ifacename; // ifacename contains "eth0:" + iss >> ifacename; // ifacename contains "eth0:" if (ifacename.empty()) continue; ifacename.pop_back(); // remove trailing ':' if (ifacename != ifname_) { diff --git a/src/modules/sndio.cpp b/src/modules/sndio.cpp index 46bffdc2..d878c4a4 100644 --- a/src/modules/sndio.cpp +++ b/src/modules/sndio.cpp @@ -102,9 +102,9 @@ Sndio::~Sndio() { sioctl_close(hdl_); } auto Sndio::update() -> void { auto format = format_; - unsigned int vol = (maxval_ > 0) - ? static_cast(100. * static_cast(volume_) / static_cast(maxval_)) - : 0; + unsigned int vol = (maxval_ > 0) ? static_cast(100. * static_cast(volume_) / + static_cast(maxval_)) + : 0; if (volume_ == 0) { label_.get_style_context()->add_class("muted"); diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp index 3006a34a..8202b718 100644 --- a/src/modules/upower.cpp +++ b/src/modules/upower.cpp @@ -62,7 +62,8 @@ UPower::UPower(const std::string& id, const Json::Value& config) GError* gErr = NULL; upClient_ = up_client_new_full(NULL, &gErr); if (upClient_ == NULL) { - spdlog::error("Upower. UPower client connection error. {}", gErr ? gErr->message : "unknown error"); + spdlog::error("Upower. UPower client connection error. {}", + gErr ? gErr->message : "unknown error"); if (gErr) g_error_free(gErr); } diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp index c7a2b046..61eb9b43 100644 --- a/src/util/backlight_backend.cpp +++ b/src/util/backlight_backend.cpp @@ -79,20 +79,38 @@ static void upsert_device(std::vector& devices, udev_device* de }); if (found != devices.end()) { if (actual != nullptr) { - try { found->set_actual(std::stoi(actual)); } catch (const std::exception&) {} + try { + found->set_actual(std::stoi(actual)); + } catch (const std::exception&) { + } } if (max != nullptr) { - try { found->set_max(std::stoi(max)); } catch (const std::exception&) {} + try { + found->set_max(std::stoi(max)); + } catch (const std::exception&) { + } } if (power != nullptr) { - try { found->set_powered(std::stoi(power) == 0); } catch (const std::exception&) {} + try { + found->set_powered(std::stoi(power) == 0); + } catch (const std::exception&) { + } } } else { int actual_int = 0, max_int = 0; bool power_bool = true; - try { if (actual != nullptr) actual_int = std::stoi(actual); } catch (const std::exception&) {} - try { if (max != nullptr) max_int = std::stoi(max); } catch (const std::exception&) {} - try { if (power != nullptr) power_bool = std::stoi(power) == 0; } catch (const std::exception&) {} + try { + if (actual != nullptr) actual_int = std::stoi(actual); + } catch (const std::exception&) { + } + try { + if (max != nullptr) max_int = std::stoi(max); + } catch (const std::exception&) { + } + try { + if (power != nullptr) power_bool = std::stoi(power) == 0; + } catch (const std::exception&) { + } devices.emplace_back(name, actual_int, max_int, power_bool); } } diff --git a/src/util/portal.cpp b/src/util/portal.cpp index 0aa78cb9..e2be97c7 100644 --- a/src/util/portal.cpp +++ b/src/util/portal.cpp @@ -63,7 +63,8 @@ void waybar::Portal::refreshAppearance() { Glib::VariantBase modev; container.get_child(modev, 0); auto mode = - Glib::VariantBase::cast_dynamic>>>(modev) + Glib::VariantBase::cast_dynamic>>>( + modev) .get() .get() .get(); From 7744320ab2c793d092620c2ec859327280b676a6 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 24 Feb 2026 00:55:27 +0100 Subject: [PATCH 031/137] fix: build --- src/modules/gamemode.cpp | 6 +++--- src/modules/systemd_failed_units.cpp | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/gamemode.cpp b/src/modules/gamemode.cpp index 1c846434..691a2844 100644 --- a/src/modules/gamemode.cpp +++ b/src/modules/gamemode.cpp @@ -128,9 +128,9 @@ void Gamemode::getData() { Glib::VariantContainerBase data = gamemode_proxy->call_sync("Get", parameters); if (data && data.is_of_type(Glib::VariantType("(v)"))) { Glib::VariantBase variant; - g_variant_get(data.gobj(), "(v)", &variant); + g_variant_get(const_cast(data.gobj()), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_INT32)) { - g_variant_get(variant.gobj(), "i", &gameCount); + g_variant_get(const_cast(variant.gobj()), "i", &gameCount); return; } } @@ -158,7 +158,7 @@ void Gamemode::prepareForSleep_cb(const Glib::RefPtr& con const Glib::VariantContainerBase& parameters) { if (parameters.is_of_type(Glib::VariantType("(b)"))) { gboolean sleeping; - g_variant_get(parameters.gobj(), "(b)", &sleeping); + g_variant_get(const_cast(parameters.gobj()), "(b)", &sleeping); if (!sleeping) { getData(); dp.emit(); diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index d33e9c3c..68e61fe9 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -77,9 +77,9 @@ void SystemdFailedUnits::RequestSystemState() { Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); if (data && data.is_of_type(Glib::VariantType("(v)"))) { Glib::VariantBase variant; - g_variant_get(data.gobj(), "(v)", &variant); + g_variant_get(const_cast(data.gobj()), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_STRING)) { - return g_variant_get_string(variant.gobj(), NULL); + return g_variant_get_string(const_cast(variant.gobj()), NULL); } } } catch (Glib::Error& e) { @@ -105,9 +105,9 @@ void SystemdFailedUnits::RequestFailedUnits() { Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); if (data && data.is_of_type(Glib::VariantType("(v)"))) { Glib::VariantBase variant; - g_variant_get(data.gobj(), "(v)", &variant); + g_variant_get(const_cast(data.gobj()), "(v)", &variant); if (variant && variant.is_of_type(Glib::VARIANT_TYPE_UINT32)) { - return g_variant_get_uint32(variant.gobj()); + return g_variant_get_uint32(const_cast(variant.gobj())); } } } catch (Glib::Error& e) { 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 032/137] 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 033/137] 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 034/137] 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 035/137] 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 036/137] 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 037/137] 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 038/137] 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 039/137] 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 040/137] 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 041/137] 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 042/137] 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 043/137] 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 044/137] 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 045/137] 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 046/137] 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 047/137] 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 048/137] 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 049/137] 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 050/137] 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 051/137] 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 052/137] 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 053/137] 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 054/137] 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 055/137] 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 056/137] 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 057/137] 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 058/137] 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 059/137] 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 060/137] 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 061/137] 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 062/137] 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 063/137] 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 064/137] 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 065/137] 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 066/137] 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 067/137] 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 068/137] 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 069/137] 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 070/137] 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 071/137] 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 072/137] 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 073/137] 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 074/137] 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 075/137] 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 215c952137f614e71b1485dfde2c3968477bef3d Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Thu, 19 Mar 2026 00:28:50 -0500 Subject: [PATCH 076/137] 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 zcmVfcXI_LP$zl z>ZVNYEd9Ux!wYi<9S1%JPU1-XD9tlzkvz}NLuTScW@et4NgkZnN5!c2D5eEdvoMiG zjHVx?L?sJ!!Xl(4SRvUE*HjXjkmpwz6b++zNn_}y6ceIB+EY*qL~)p=@(xnrNs=#A zj{M9tgYz7m%p@jGJ>n`ziMT9LlL{}SS0uy3c=6STx1_`5f#X3RNKzSugcw9USW*i^ z;wj8!Y98Vi&M6pK3P)9$vsvV8s4^c>0(xD@)+<@Kk{A^8DYIFKTFVnfhP@A(kHumj zR_6+-n8@K%G%<<7;LJ>x6IIQADNI_r&%rlE?TsK>ygF59%P~ z(Mj_h&>wP2D^f|M77D`U(6BbVN&ONf2RR)KjB`VnZ;nNgxM3U#9{SJ(Pv^j# znTgmTsg(`3gsahd$0X z36mTVsYy*BhQxGG4$pDeqfnRxN<3NiyR+KS-gL}2Ks;XqGVTy@ztQH#^BEur~;v|+s?L}G^A{oO-O-mRzKs*EHIhIyPn2CsbsnOD$BEj8i;` ziHY+~5uF2%_3=x>B+D@u3O$PFXo;d|GkI8fB7;N@b-uy?q5qWh!Ii=BfDP2)(6|gJ z4RsRfK$t_m7rD7$s+2I9s1^Cv z%rkIY93O~e4m^F7fu?p7TQVsQo5Fepr8wL~3%D>a&lHy`6-YW%@k(%P0sObySk_uOl-l9qpBQc9bH!Fq66j41uJk0|?6ecFh zF}tzEl+?#8m`p5>3{N3~uN0~fId3wt2e1cxKm!ev#6&p?&%z~|Yb>9IMbDE+2Ss6$ z1>YykEMEh}Gf;!ZhaVv17+~n~48$1|<&Y9*K2t%8i|DxDBR(wrNaaXG5lKkHqzw4`pbQU!KSfyJ<;gaMsrd!xm(=gsIIGLnMd~+8gxIFp2mal(h~n zCSH}{u~z&LqeW4g1yX19gP98s5OPRpSTOD=_(7V*f+@)`Ne0DK9L@RGT+$UxAf7=RzyTjkBN@bF zI2MCju-MB=L{UYDMWi@SVMx|=STLd%PSv0fi6lH|9`qp)NF;e;q8!SCG3rI6P*4g3 z62YXPNSuV@z9e;6mWkOuM&m)_0UroLnay*!TP?v7r$t=w>3JoIx(zI0R8tm`;i+pM zqj{-3Xg;8&9t(UvBq|pc(StGA4=w57BtIf#QKWK6PU@FN#RA8_R7`}=%#Tx|= z*D{_gFy7j?Z@9J5$!1=G(H2{4ZDW`x1!b3k*oi9JWLw~-J zX^n9YqprYk-hAI%fx&#a`#Rs|%XsqRHRlQp<@?yaQE9JazFNV&_r?WXzcnYIJjBrD zCo@n9v{67Cf4P?*S76B4JL3wBZ`iN*e+7o?)~r`Uw_bic47dWrd%O27^j_Q}$yQ)AX1sVG3Jm9FX2rgBAw$Y$>^TU>p#o#=8+0vQ$Y3nF#a@9i z<<5nSIMu(G3mFhH2YCfXT-E!=Z(PXu=AJqP1nw_nz_lEt$U6w=Av6OJeV*Y$Mx)C8 zQ!6musDGTz5n%N8=2irnJEgYyFk0dJ{;m~J*067mm1Z5JIM$7VwH;dwd zA9Q);fG%k;l2-Mfa;k;G(_q0xhkas@^F8VVKD-%emgFg9X^b95vfB&s?%JH z4q3$XONR23LDsjQ-6ET_zT#0h&75tDS5XDRmN_1ueKPZ$aiRVy>#-Ly(87(Aw;-D~v!F6

8c_Xq;3TpIT#h#-esC;Z$T%B*j`tQ(N=6i5_*i$qnWLQHSqkmALPeChM2W)bM1pWK)ZsV0xfH94rYa)AB|+r?5niy>TMCQ0DU=kN zcaURwr986r;Wf>i4v9**QXlmc9`bOusGSFW&_)4K>eM4)rz*PrR8o{w;h+#_6(N6E z&78rosN@Tf4|t%C|CKkx-i-#@mX&g(Q z#q@MRLf<^eT;SP4VaH-R6s1}tsi+kzY&yk*6H^*S#TOND77nbHWjb0}rWbug6e$ae zMY)=g25@U8g&X#6O#!SgnVgXd}8f!4AK*>DH_76QwKHKfC{K!&r5wP z%VRuQB;=sp!07_UZ&#(nAT{KmAjq@~l5_scy_8r5W&pAHfY+dv1R6LNYqABxi{za( zRycxh!k@HXEN^~Q9jeS6A69=1H!#vD#MM5s$z1rHBbkIk_XW&2&sso z#_*rli#-7FtD7)7}q=gYStJkKzTK z1XJTLh>VY1NKcA>y~qltzay6@PC*l0zz2Ll6P+H(O>(WpbQX_n)Wb~9YfUN2Kc=x) z5b7WvXJ7#}C3Q+>VKxT{6|0{+NYc3iQm72$(mQo{s7g5BJPIa(4%mjM6v#w}B=L^} zbIurJj5Edv*{vDl&;Os_`|y2llzSL!uE22L-#dM-!0_+yef=D3OCCnghrO@Y^{zI1 zRqyQW=IHb$=X0>PnXIihMk>2^_C93Ts}$^wat}kw?w!4@ds$B=FyiZdulKd-@i1P# z*ZX=Ml-;kH>*Yz%zX_b`}TfdO6b>z(f!^uutuo*zcbUez0W zo$GDh%ejIH&)UdLju>UIY^GR9bBj77#6i>y~I#v)^k zt+-b2>y@nQ>n8s&TI^Na`+8mPY94zhyM+w)m79#++e~`%>W#4lhFZ1vVGE4uExk9o zJ`5GRclNgKWj$H%>)zKZnR1c3!=Ufp`+D8{Ue!B$yH& z_cHEX+`D`(ZZfVtj0pc#t5&UgS9{f}Rm&w+d)2B{SJ)e&YSpSMth}#lg|OCItH6Nu zRaawF4MJFJtrgZAB1zx3FqXUyxrb| zl}X5}81puhEil$(-uPBv^km*Cd0{`^$Gp)UhMZDL-uW^@`J|Js{d|?l!}up9OJ2vj znzzTRn73PdGx9c*Eo0=3FTTk8uw&$n?l9J4j=ZgTSx*)tKd)r0Eo3mJDm#pBI%V`G zF#dTJ{BDP#-aqf-b-b%ZCZC}Vbs7Tn)P0=M>il^pZ#U&e-HdJp20!n6$apWOnKTdGTQg#C0Fu3Q98<`TY)hj?~D6}e7uwQ7gHuBZJr08 zqx|N06>sEy?_Q^?i)B0v=6Dz6oxIJ~^2Zxv^D1V6Q5|*DN%xMov4xD~&eBaS-)k-hsCAJ{~7)c^JyAz-W$l!F_|d<#?M>_-uajcD%8zz+k6b zZLPJ|8ZWbjjQ(dWmv_EV9OXCOXZwb0ypy-K=afyh8gG1W-FRnPfuR~R-YFR`Wu=&! zbV9@#Rh0i4W2RW8lCEd5t-zRzcfEZ>F5bx-*Fr{igTnpwF*<9b+u_Fs6hN?vxyva`mPSSHhj`yc}-)h75Op!(M?=AALI|N1iU26DeK2iy-MXjZyJ}A(QP|FbQ`-@U_9qsbNkJ`0)x5b+_hsDnW^5xP`v^} zxvj44Q^{hb6VmzMVNlsty?rCTjt{=y!J9zZJRG-P}&jFMeRQG723>RQKjZfxuT;XE4`G8d?qM)qfgx91zxK7Se*p$ty##ogWC1{cX8~Xs zaXwxjh1V>uH{*HvZW0i4+dc-1Yb$jLp+ro={yxTv zYw2TewOPNRa*AswoxN|eHtQE8^pc=VLR>3bLk8i1qf7$FwUJILA(U@XJjx^x;@V%_ zq;&K#009t~k6^$)x_n=){OD7Zxo#f=KK}JQ$00USB5CT9O zMFb>300P8BQNS*TGE&+oAc*ad@<-;&*H%V(H3kpzqoVu);s68zhdyALuccA7g|Ib-09Dz_ zwRl1WArKbsV?0~iajmS+VS_+09oOt<1?g+b5jK{dHg|RYy&C%!SN12^uZpyW; zR&Lp*Wg&3yV|=e|lxLHyx%Q^YOg?PztzGje7ocwl(DT)K?Qg3&#%mS7wJn$9UdypP zTdO&TG87?D=&2(KtoJdz*Q)toTg@?u1R~G}fhZ7m{e(g7=XoK<%M8sSR*@SLE387Sn zPG*2Wm823JumH#VyytlBb8Fp_OS&iDk|=aS6d@1@3s3-ou(dH(;iOQLR;L zBRlu;)!L^%&L|_Vop144)obJ0hrCv`wQsFk&}WnpK7%9=0U@--Yn7suIx{xobj54^ zla0DBt{&k+Mul!Fx)%E?|EcI<@Zws1qwizB5JD-{gY__=FG_u%uk&5a8>PO}LAatf z%IKor)Hgc&Dt6y@_I=Sex^{No>C^X3-{){4gHex79>()IeP2)C)x7bXzSFlGgYe&d zn@JX5`o`GotJr-X`l{#~+u8Rq-|5?YSx?5&qwj;?q_eMYd$aq-X7~MO-{%{h$Bw?! zRdip63mFvhbc$#Q$Wq>?Wt~pxl+pJ+Uxo`Ab55rS0a38uH@o{f-_^YJ`zrRn^L@Kj zSKt2-HiWPtgs|E7y>E04A%w6QLf_}~LFR7w-h2Jto2|iq&v*JZll|Ufe(yPEU-iDx zv2)J(vYyPj^L;Wh*d+TllaTqw$n3t;_hI`!=o=lgyKie>){`~g*S^o=qpwNk<|}>c zlFjZLWBV$0--qq{e4}gjeZG_VHs!rP$rwm$hPdclY%qb@eb>v9Ds^H`{kLZ|&{v{><=wbfcHrL|UCYo)aIDpx6` zlu}A5rIdD~lu}A5rIgYcA)}PeN#~q&&hC9WdtaM#(mCgxbj~RiWRub2v(Af4`@(-F;*UQNV zOaq7Rj(?8_c(mXT`=s4C`;!7p*v(HSfpQdVX@Fz7S=>$gmiA=T`LTUg)|IQk{KhA7 z1!?mD9q7A6*a3Y{oA`olw= zvMxRlla@#X{tRW{Cjwbp-C)E}T|+5?F?<|i*IY&utUIG_bl01{4Hx99Bwn+X2B|I4 z#AEjj0Segxf#u>-Cv4(@iBe$*Q2=0}Ei?thDNG|&ZNqgb8N^S@LXP4noWs53)uqc*-=PaNyyYCa?Aae=B?R$n|&%3c*k6imA zwx4tzQ(eh35%*6mkd~W+a~(dR=rqwo!1!o}64JZHLMsv?bCO3_n&K^YH+JcOBhP}G zmzgiHyrkwtXGZl=zEhd@s$8WEEyqSGJjz*D57qmStj z!u7`F`kz@t-uQ?_d2@6c(cTPr-7LHwq@`qO9%oLdbLpm&`9AyDuR+XvY&rxy zSIN0UlgP$hgAU@xC5yNV(P74l3!xI)>Atp6BS}i|za11KT3rjl3PcqZ%Nw*~M-q_4 z`3c)?SpG|GjG%lGteV7R$S5%yL=PB2@cMgoaDd10(_7OqN-8w`7iY{NvyaInr6EC>A%^y&zd z1tb8T`(0Hogaqi(cT|Y?KbKv4rX-XSn7|m;LnhnkCcwge!?D;g-;RY$C9tq@NM2|@ zOD=hb&w2WHhGIwJvw9mMZ9mV@0EvCwVV zBt39vIfOE6?6G{WQo3G>)RldbB(X;}BTeT(j%Et?2-iIXl0{t>ceLl~2LA4fL1$!0 z3byqn!Ky>-8>y+)sSdR0nv|%@)Q?sC1o?j!+hgLoujW`{te|P3(hw;q(V%yX6j?dz zZRLE7sn6n^P;o{s#BrzFejXe5z*<2|_drh;k!w4Oi+;5W4LNGpFoTWRLqIr)t3qUS z75WG!WzRZxM#>B!h{tD<-?KqJVOs6z5lBg+!t<^Yh)kGZAn#MlKsDh7me*}m?qLa^ zUc5$(Yl^4@dI{T@Ri--OzaZ6fEk6STG)DwQOB_h-;fJId#L4WidXXj8xon&hF)B=+ z?29|z!W-H%d|5(s$fBW!9FTm0`yYT903!ma09%vI2xyGhJ|DVJj!-)=6eWOQ83|x; zPM^?J3wSx`kF)aj_;V@Hrd(F$e|b;K&3hmLmReWK0(to;(m{|n(+)PZU4faQz88ou z_htn#kPc%T3~7WH&fPaUI$^PrFpt2o&LMIo;4d-? z*#w5S3If^CKzTz)w_d%)NQ~t&p`SN;xIrHRFOtQBD};Ic++Bk zPpTyDG(9GJ&+4r}zhrS@w8nF#NnN+l#j+pIMzcU}wSdtf=qQHeib#|*oCuK)*hO(@ zvz2>Xlt#Y#bP3?b1QG0#6%tQ%!G54f2YO2H4^9Ew@-{HfLNBYC_j_6mHut}pER+X* z(6e+J$Fvb?h#B30no&KGjs#^;{zC2t8`TqfkNyS4WG2Fz&*%*bXF$P7x?@ePE%zPKeVr$bm%$G|e4=k5JiPcJ zMmkyKdO$@N1836kfmIk~Ci_Vcc#vV=Yn*VXAb_Kg_SK#JTo8R)&^2YBtVn3$oS$Uf zu+E?_T^^BM?1YPalY}UB_8c;MHc>EpQ+|2?C1R$%G;t9@cMj#Y{*j2VBVxx8ahS__ zK3N30iJu&Tak9gc1hdnh6Ti%GG*>NByRNyUJ}%$}*qFpMbqzv1+Q>+N!PGzPAIv=0 z#s;-3R;krecRFVYB7XHEgv>yTL;=2KbaU5~+z&OCZeA?asX9{PS(Q+ig&(l`&w9Aaygtn?SH`{$x1|s!ho_W{VT$Ah;@i#P@(uMi~ZzOLRHKU z9Bh;O-l4RD=!+|JM^__usi+n3H5uk%V`dytkwAiL?6aZm-h4gZVP7A9hpP4Q(II_Y zDn)86)TTUe2PIa^-ZZxOg=zeSsEXMtabJWXJ3GPTXSxP?of0KZ>G>qfMa`o5TK5iS zDnua0R}&diA0Hl z=Qs85DR{F^KKZl+KH036k$vh+*y1q;MT* zsGHYA=lsWqd01#OYy0LYsQlQFL{DH5?H)hGYzU?^jGWPUy-5^Kl^6%A*OcPt749)Q zK-V9cPY4Cv77cAe!=!{Z8Ptdf!LSGi0NU1}u#lLr7Yn>6q{T61aCBGF0fs?AqR~2b_3KA+g5m)EKv25Fn;eJ6 z#pL3m0lyugB-l0p&{o<=0q^BDkBn#n{E-?vSIQpoCUS{qoHlo(}9)D*_jYqE#>rd0vNg3)IQwin~69KBBL2Ti@GXCf> zBsvNyD+T8L7f_qoI>Lvqaj@ZP2rTxtkjJj_Hk{>JB$^;i>pU-$zX<8mM{%7t2(YJ9 zT-ATT#;K|LSpf-OhU7Uq5{U`ns$!mrbVxxEBy4Y*3O%WyH7aOUCV=I8It3oVa1Lkk zQbfDNz$6bLA;m^VQSJ!L^$Kfy9)mC<1%!Lm7~FxqqD5fz)M}8)Jk4w^zx*LEErd%c z@qrRzI*EhjGcpqP`%|I0In@ zV#MM+YHVJ-(5;qGy7L(cO=3n6E&tc?#F+g7GpPWyoiK4fzdkfGg+av0%2vzbTMf0q={i57{x30=rvkJ}d|0fp zt`f?m&A~%i6x(wdD4@6q5O|19I_B0UX@M|e&H1Y+IVeV+51oHbq5_n|ei_T5kxM*i zD)vIIU%9eBS!-jlY=7}4-+HH z|F~aq^G!A{TBaKAJ`JOb=OxzhCl2PWmm}qJ{DQWvN8cuWjVF8F%}i;Kqm#b`s-lCT z2n-XD{mVp-G>nH4F>oZcgyT@`ehjGXmuV~TPak~>?NHoGX~)(g#702DMx_Kdnp@;%y=p zZ2luEckxp)DJ62(v(=e<3&Je~WUkn-aI+L5%%9^VKV6PfCOnCG=EVXNNm=ivE0^<9 z3dVQH!WC95v#tRFlH?5dsd(=@y^6FlB??-8Ah5wX{2h*#a7dcch{YwhJl<(j|#(k zT{13%7*ud`x?auv(&A#h#7`pvnCXZP|qr%wXbR@r66)AzoDpYMFL9 z3EiBx#rT=_WmD8gTrp!A#Q;S2-kYV4!GKS(l_g7JWfAZwy`v&8H6kQ1jfB&{xV8h1 zyr^IcynU<*_}r%5U6gG>ZK>0D*j732iwfh5Q+9)n1OW4u+i4eaN8JTv!~oAj>$Io$ z`q&t(Ym$O@@@X0fIQ&jsH4XkCj;ZODEiNF{B_s0X#jT+bx*Gh1#;u}o!0#;KxX@IY zTf1U6*snS=J?EJg_wz@=zIN@cbdbo4@=pXhf2J8a=Ouu6(BIFqqkc%QH4(e^o(|b7{VWI8A&;rrTqwu@@ z0p3^Bnwzy^+i_j87%&S%!ipDzUx{(DpJyF|gazFvyLxe1YcC7KpmZirm#@-#357bs zVdh7aMcQ}d5IrE$W8XK`^G%XoRF9M{K9ZuqPXNCPtq*P)qF{nh9|XF5#zM1N0^prc zm{}pDw~2GP7$*5TGf}45C&z>`o&R%B0H{Yn>xdNZ8~nh+>=FhQGcf@85IKH9z6JkSMgl>JSq6WN zS1HETo(o2(DQ+~H!P49%1)ByvD9v%9H<@p5>~of%ap09^cn&)ZTTI0&& zM5`nqRBj;Tw5x9}465ibmpZ}(^y zvtZCjEL*GRQ?__Q@3^tmCdhtvj>VZub61Jk=ZCh+5EAwb7O7q~ww1SaDM_ktQZBnqYWWrSx{8uDbrq*x?R(1#^nTxZq&+pSxt( z*mDMTrBczt0QBoVAvK^Q&8PYl2%qYCkRCo8tHLvVYJ z6k*<_>wABLub$G~_P~b+RWmWvV3HtpVT%_@F!{irY*2#hExX>8;+R-2!cB!@UIY)@ zRj;I8i=|+zj6v;GfI1e(5eekQV$Iie5I|*)NoE8diy$MxD!2rO+CJj~4&QpI5_&@A ziH5R3j{E^EoRmBOMCO{4!jfgiM|l6MWoS{KS4rb>^bZli(az5ttf>|I;nS5oHvH?A zz)1|nsdZl41a<|ALFAt_{F0`r3_g*qtE&%R>V{+YoWv=s0xAHCB?!|{cKXv&H7;r3 z{L^9G3>Y~T$hgFw$@XLLHq5LIj)N^i1d|+f0T=THU{9BDpO!9Epm$BRy|@T;%<*m8 zbM=S>*l%Lun&}0u&=WM%1&D*L#r{3{>%$yBMxLoum- z{N+1V_lncOTu^zFSxzkT)z}d@Xp3ee`FUv~eW$3fjyj@791x+G#48US%A>}8>V+h$ zNYpn;cBPmj4n8OU7lvg*STc*u+f^@sPneAiQx026FzHCuoVR(z5U*vkAb$525~jC3 z5giW)ExGCI;VUEjMv0MEtX^f)hX;Xf|FyuIU>n?*3fbY3R9tX$i-SY}+y5&-xY1ay zU#xmbibl{vFW_;mI%u&04g_}tRRJ(~oD;;dzCg<+u;{gs$XL4H)8A#uMsq!#&X-Z- zF+%aOyb|K3Kqk!r8bOh%`;0iwT;el@aF&w}J~^zzvic9A!@+uxrbVS%(y^Xy?)VZuKeq z{Z3cR&`ZUM(4bV`nB)l(y$e~8%k`8y;G!qat}Yh7OZ5<2ksMo@75%UG8KDc%^^PHSocn}}#}r(b{%El1NyLX4Q{GQyTraLZVUpH}Xf+I>&Z`6EVeJ8Zlj zxlCwZSPI(-EPR>R$zVgY412Gy-$ zM1_5Z53 yyK-veLj0bnNHqW4@W?1j_=`ecORo6k4w==YDq)pzyCRH95P3fE`uzc9Y|*2|=`Vf& From 86234a8946787798ba39530ea3c74b017efe87d2 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Thu, 19 Mar 2026 00:29:01 -0500 Subject: [PATCH 077/137] 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 078/137] 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 079/137] 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 080/137] 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 081/137] 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 082/137] 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 083/137] 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 084/137] 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 60c57b7195dabc750dfe1faca01ad936961b120b Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Sun, 22 Mar 2026 11:40:44 +0530 Subject: [PATCH 085/137] 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 086/137] 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 087/137] 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 e55bff662e18cc853d6df5c820d05e3823f00ef1 Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Wed, 25 Mar 2026 21:45:14 +0530 Subject: [PATCH 088/137] 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 3b512d1a2cfcfcb454616e3e3988fab34b1ac3c4 Mon Sep 17 00:00:00 2001 From: Keepo Date: Sat, 28 Mar 2026 21:10:15 -0400 Subject: [PATCH 089/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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/137] 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 ac62754b289569b71da33f6982c63eb927796182 Mon Sep 17 00:00:00 2001 From: yubo <3597387401@qq.com> Date: Thu, 9 Apr 2026 21:18:15 +0800 Subject: [PATCH 102/137] 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 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 103/137] 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 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 104/137] 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 105/137] 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 106/137] 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 107/137] 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 108/137] 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 109/137] 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 110/137] 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 111/137] 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 112/137] 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 113/137] 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 114/137] 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 876eaaf70f9ec9d42992849e0cb44533637a7f8c Mon Sep 17 00:00:00 2001 From: Francois Lachese Date: Wed, 20 May 2026 15:57:38 +0200 Subject: [PATCH 115/137] 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 eb3b86c4d83a8e2e54eeca77a36861b5ccc8d432 Mon Sep 17 00:00:00 2001 From: buzz Date: Thu, 21 May 2026 17:02:39 +0200 Subject: [PATCH 116/137] 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 117/137] 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 118/137] 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 119/137] 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 120/137] 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 121/137] 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 122/137] (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 123/137] 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 124/137] 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 5cce616d3d72b402dabc64aabb872e0cfb25165c Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 14:41:31 -0700 Subject: [PATCH 125/137] 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 0cccf0d8b50dd7576f2295b24065616ef4fe4d0a Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 15:23:54 -0700 Subject: [PATCH 126/137] 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 127/137] 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 128/137] ## 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 129/137] 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 130/137] 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 131/137] 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 132/137] 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 133/137] 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 134/137] 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 e37fcee35d3124a98e90c220fa6cb5c0862fd296 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Thu, 18 Jun 2026 14:28:39 +0000 Subject: [PATCH 135/137] 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 4d1a561cdcbacb9e408a534b49b567821923331a Mon Sep 17 00:00:00 2001 From: Ibrahim Issa Date: Fri, 3 Jul 2026 18:44:03 +0200 Subject: [PATCH 136/137] 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 137/137] 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);