From 40200afb6890f4488b9c850b6d3a98526828d212 Mon Sep 17 00:00:00 2001 From: cebem1nt Date: Fri, 6 Feb 2026 14:43:12 -0300 Subject: [PATCH 01/60] 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 02/60] 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 03/60] 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 04/60] 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 05/60] 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 06/60] 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 07/60] 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 4ac539206f932ef6c101a961bc5bb9a518e5dc21 Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Fri, 13 Feb 2026 02:00:17 +0530 Subject: [PATCH 08/60] feat: add tooltip-format config option in cpu module --- src/modules/cpu.cpp | 13 ++++++++++--- src/modules/cpu_frequency/common.cpp | 19 +++++++++++++------ src/modules/cpu_usage/common.cpp | 13 ++++++++++--- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 0703eaf7..ab75b911 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -26,9 +26,7 @@ auto waybar::modules::Cpu::update() -> void { auto [load1, load5, load15] = Load::getLoad(); auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); - if (tooltipEnabled()) { - label_.set_tooltip_text(tooltip); - } + auto format = format_; auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; auto state = getState(total_usage); @@ -56,6 +54,15 @@ auto waybar::modules::Cpu::update() -> void { store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); } label_.set_markup(fmt::vformat(format, store)); + + if (tooltipEnabled()) { + if (config_["tooltip-format"].isString()) { + tooltip = config_["tooltip-format"].asString(); + label_.set_tooltip_markup(fmt::vformat(tooltip, store)); + } else { + label_.set_tooltip_markup(tooltip); + } + } } // Call parent update diff --git a/src/modules/cpu_frequency/common.cpp b/src/modules/cpu_frequency/common.cpp index e47364ba..05adc2b3 100644 --- a/src/modules/cpu_frequency/common.cpp +++ b/src/modules/cpu_frequency/common.cpp @@ -20,12 +20,7 @@ waybar::modules::CpuFrequency::CpuFrequency(const std::string& id, const Json::V auto waybar::modules::CpuFrequency::update() -> void { // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); - if (tooltipEnabled()) { - auto tooltip = - fmt::format("Minimum frequency: {}\nAverage frequency: {}\nMaximum frequency: {}\n", - min_frequency, avg_frequency, max_frequency); - label_.set_tooltip_text(tooltip); - } + auto format = format_; auto state = getState(avg_frequency); if (!state.empty() && config_["format-" + state].isString()) { @@ -43,6 +38,18 @@ auto waybar::modules::CpuFrequency::update() -> void { store.push_back(fmt::arg("min_frequency", min_frequency)); store.push_back(fmt::arg("avg_frequency", avg_frequency)); label_.set_markup(fmt::vformat(format, store)); + + if (tooltipEnabled()) { + std::string tooltip; + if (config_["tooltip-format"].isString()) { + tooltip = config_["tooltip-format"].asString(); + label_.set_tooltip_markup(fmt::vformat(tooltip, store)); + } else { + tooltip = "Minimum frequency: {}\nAverage frequency: {}\nMaximum frequency: {}\n"; + label_.set_tooltip_markup( + fmt::format(fmt::runtime(tooltip), min_frequency, avg_frequency, max_frequency)); + } + } } // Call parent update diff --git a/src/modules/cpu_usage/common.cpp b/src/modules/cpu_usage/common.cpp index e3947967..a2bb4596 100644 --- a/src/modules/cpu_usage/common.cpp +++ b/src/modules/cpu_usage/common.cpp @@ -20,9 +20,7 @@ waybar::modules::CpuUsage::CpuUsage(const std::string& id, const Json::Value& co auto waybar::modules::CpuUsage::update() -> void { // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); - if (tooltipEnabled()) { - label_.set_tooltip_text(tooltip); - } + auto format = format_; auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; auto state = getState(total_usage); @@ -46,6 +44,15 @@ auto waybar::modules::CpuUsage::update() -> void { store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); } label_.set_markup(fmt::vformat(format, store)); + + if (tooltipEnabled()) { + if (config_["tooltip-format"].isString()) { + tooltip = config_["tooltip-format"].asString(); + label_.set_tooltip_markup(fmt::vformat(tooltip, store)); + } else { + label_.set_tooltip_markup(tooltip); + } + } } // Call parent update From 6f9dee979b4c53bcaa8ac803e6b9400fc3ac852b Mon Sep 17 00:00:00 2001 From: Ollie Stone Date: Sun, 15 Feb 2026 18:39:28 +0000 Subject: [PATCH 09/60] fix: ensure AAppIconLabel image remains not visibile if icons are disabled --- src/AAppIconLabel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AAppIconLabel.cpp b/src/AAppIconLabel.cpp index a309a6e0..3d62d69d 100644 --- a/src/AAppIconLabel.cpp +++ b/src/AAppIconLabel.cpp @@ -151,7 +151,7 @@ void AAppIconLabel::updateAppIconName(const std::string& app_identifier, } void AAppIconLabel::updateAppIcon() { - if (update_app_icon_) { + if (update_app_icon_ || (!iconEnabled() && image_.get_visible())) { update_app_icon_ = false; if (app_icon_name_.empty()) { image_.set_visible(false); From f1140f00f9a9514e000d33ce8ce5211aec767cd1 Mon Sep 17 00:00:00 2001 From: cebem1nt Date: Tue, 17 Feb 2026 11:23:27 -0300 Subject: [PATCH 10/60] niri/workspaces: change icons priority --- src/modules/niri/workspaces.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 8eb912bc..3e8a432e 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -174,11 +174,11 @@ std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) if (ws["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString(); - if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString(); + if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].asString(); - if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); + if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString(); if (ws["name"]) { const auto& name = ws["name"].asString(); From 9c871c90a7306212669c6c0a17f581d9027b97ba Mon Sep 17 00:00:00 2001 From: GG2R10 Date: Thu, 19 Feb 2026 15:12:23 -0500 Subject: [PATCH 11/60] fix: move silence_ = false inside idle lambda --- src/modules/cava/cavaRaw.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/cava/cavaRaw.cpp b/src/modules/cava/cavaRaw.cpp index d6a9e001..2b0aca72 100644 --- a/src/modules/cava/cavaRaw.cpp +++ b/src/modules/cava/cavaRaw.cpp @@ -26,6 +26,7 @@ void waybar::modules::cava::Cava::pause_resume() { backend_->doPauseResume(); } auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void { Glib::signal_idle().connect_once([this, input]() { if (silence_) { + silence_ = false; label_.get_style_context()->remove_class("silent"); if (!label_.get_style_context()->has_class("updated")) label_.get_style_context()->add_class("updated"); @@ -38,7 +39,6 @@ auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void { label_.show(); ALabel::update(); }); - silence_ = false; } auto waybar::modules::cava::Cava::onSilence() -> void { From a194b576be36423c64618148a6f789dd4362e9c1 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Feb 2026 08:52:00 -0600 Subject: [PATCH 12/60] 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 13/60] 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 14/60] 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 15/60] 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 16/60] 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 17/60] 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 18/60] 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 19/60] 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 20/60] 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 21/60] 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 22/60] 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 23/60] 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 24/60] 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 25/60] 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 26/60] 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 27/60] 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 28/60] 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 29/60] 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 30/60] 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 31/60] 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 32/60] 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 33/60] 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 34/60] 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 35/60] 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 36/60] 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 37/60] 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 38/60] 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 39/60] 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 40/60] 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 41/60] 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 42/60] 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 43/60] 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 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& 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 44/60] 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 45/60] 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 46/60] 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 47/60] 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 48/60] 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 49/60] 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 50/60] 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 51/60] 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 52/60] 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 53/60] 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 54/60] 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 55/60] 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 56/60] 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 57/60] 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 58/60] 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 59/60] 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 60/60] 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; }