Merge pull request #5175 from Alexays/fix/post-0150-review

fix: batch of bugs found reviewing the 0.15.0..master diff
This commit is contained in:
Alexis Rouillard
2026-07-05 10:35:13 +02:00
committed by GitHub
24 changed files with 421 additions and 154 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ class Group : public AModule {
public:
Group(const std::string&, const std::string&, const Json::Value&, bool);
~Group() override = default;
~Group() override;
auto update() -> void override;
operator Gtk::Widget&() override;
+2 -1
View File
@@ -227,8 +227,9 @@ class Workspaces : public AModule, public EventHandler {
sigc::connection m_scrollEventConnection_;
IPC& m_ipc;
// Coalesces bursts of Hyprland events into a single UI refresh. Armed and
// disconnected only on the GTK main thread (see Workspaces::update).
sigc::connection m_debounceTimer;
bool m_updatePending = false;
};
} // namespace waybar::modules::hyprland
+2
View File
@@ -1,6 +1,7 @@
// include/modules/mango/backend.hpp
#pragma once
#include <atomic>
#include <list>
#include <mutex>
#include <string>
@@ -52,6 +53,7 @@ class IPC {
static Json::Value sendCommand(const std::string& cmd);
std::atomic<bool> running_ = true;
int sockfd_ = -1;
std::thread ipc_thread_;
mutable std::mutex data_mutex_;
+6 -1
View File
@@ -16,7 +16,8 @@ class Host {
public:
Host(std::size_t id, const Json::Value&, const Bar&, const std::vector<std::string>&,
const std::function<void(std::unique_ptr<Item>&)>&,
const std::function<void(std::unique_ptr<Item>&)>&, const std::function<void()>&);
const std::function<void(std::unique_ptr<Item>&)>&, const std::function<void()>&,
const std::function<void()>&);
~Host();
void checkIgnoreList(const std::vector<std::string>& ignore_list,
@@ -55,6 +56,10 @@ class Host {
const std::vector<std::string> ignore_list_;
const std::function<void(std::unique_ptr<Item>&)> on_add_;
const std::function<void(std::unique_ptr<Item>&)> on_remove_;
// Re-applies the configured ordering to the already-added tray widgets. This
// must NOT re-run the add path (which would re-parent widgets and reconnect
// signals); it only reorders existing children.
const std::function<void()> on_reorder_;
ItemOrderMap orders_;
const std::function<void()> on_update_;
+10
View File
@@ -1,6 +1,10 @@
#pragma once
#include <fmt/format.h>
#include <sigc++/connection.h>
#include <unordered_map>
#include <utility>
#include "AModule.hpp"
#include "bar.hpp"
@@ -19,6 +23,9 @@ class Tray : public AModule {
private:
void onAdd(std::unique_ptr<Item>& item);
void onRemove(std::unique_ptr<Item>& item);
// Reorders the already-added tray widgets by their configured order. Does not
// add or remove any widget.
void reorderBox();
void checkIgnoreList(std::unique_ptr<Item>* item);
std::vector<std::string> parseIgnoreList(const Json::Value& config);
void queueUpdate();
@@ -29,6 +36,9 @@ class Tray : public AModule {
std::vector<std::string> ignore_list_;
SNI::Host host_;
std::vector<Item*> items_;
// signal_show/signal_hide connections owned per added item, so they can be
// disconnected on removal instead of leaking and accumulating.
std::unordered_map<Item*, std::pair<sigc::connection, sigc::connection>> item_connections_;
};
} // namespace waybar::modules::SNI
+16 -2
View File
@@ -238,9 +238,23 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) {
if (!format.empty()) {
const int width = gdk_window_get_width(e->window);
const int height = gdk_window_get_height(e->window);
const std::string cmd =
fmt::format(fmt::runtime(format), fmt::arg("x", (int)round(100. * e->x / width)),
// Substitute {x}/{y} with the click position. The configured command is
// arbitrary user input that may contain literal braces which are not {x}/{y}
// (e.g. `echo ${HOME}`, `awk '{print $1}'`, brace expansions). Those make
// libfmt throw fmt::format_error; since we run inside a GTK signal handler an
// uncaught exception aborts the whole bar. Only format when a placeholder is
// actually present, and fall back to the raw command if formatting throws.
std::string cmd = format;
if (format.find("{x}") != std::string::npos || format.find("{y}") != std::string::npos) {
try {
cmd = fmt::format(fmt::runtime(format), fmt::arg("x", (int)round(100. * e->x / width)),
fmt::arg("y", (int)round(100. * e->y / height)));
} catch (const fmt::format_error& err) {
spdlog::warn("Failed to format command '{}': {}. Running it unformatted.", format,
err.what());
cmd = format;
}
}
pid_children_.push_back(util::command::forkExec(cmd));
}
dp.emit();
+10 -10
View File
@@ -331,12 +331,14 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config)
* returned to the main loop, when any late initial configure has been dispatched and widgets have
* had a chance to allocate/draw.
*/
Glib::signal_idle().connect(sigc::track_obj([this] {
Glib::signal_idle().connect(sigc::track_obj(
[this] {
window.queue_resize();
window.queue_draw();
forceLayerCommit();
return false;
}, *this));
},
*this));
if (spdlog::should_log(spdlog::level::debug)) {
// Unfortunately, this function isn't in the C++ bindings, so we have to call the C version.
@@ -750,9 +752,12 @@ void waybar::Bar::onOutputGeometryChanged() {
}
void waybar::Bar::toggleSuspend(bool suspend) {
auto process_modules = [suspend](Gtk::Box& module_box) {
for (auto* widget : module_box.get_children()) {
auto* module = dynamic_cast<waybar::AModule*>(widget);
// Iterate the actual module objects. Modules are packed into the Gtk::Box via
// AModule::operator Gtk::Widget&(), which returns the member event_box_, so the
// box children are Gtk::EventBox, never AModule -- a dynamic_cast over them is
// always null and suspend()/resume() would never fire. modules_all_ holds the
// real module pointers (including group children), so use it instead.
for (auto const& module : modules_all_) {
if (module && module->shouldSuspend()) {
if (suspend) {
module->suspend();
@@ -761,9 +766,4 @@ void waybar::Bar::toggleSuspend(bool suspend) {
}
}
}
};
process_modules(left_);
process_modules(center_);
process_modules(right_);
}
+27 -6
View File
@@ -240,12 +240,33 @@ bool isValidOutput(const Json::Value& config, const std::string& name,
continue;
}
std::string str = config_output_dimension.asString();
int i = str.find(" ");
std::string dimension = str.substr(0, i);
str = str.substr(i + 1);
i = str.find(" ");
std::string comparator = str.substr(0, i);
int value = std::stoi(str.substr(i));
auto first_space = str.find(' ');
if (first_space == std::string::npos) {
spdlog::warn(
"Ignoring malformed 'output-dimensions' entry (expected '<dimension> <comparator> "
"<value>'): '{}'",
str);
continue;
}
std::string dimension = str.substr(0, first_space);
str = str.substr(first_space + 1);
auto second_space = str.find(' ');
if (second_space == std::string::npos) {
spdlog::warn(
"Ignoring malformed 'output-dimensions' entry (expected '<dimension> <comparator> "
"<value>'): '{}'",
config_output_dimension.asString());
continue;
}
std::string comparator = str.substr(0, second_space);
int value;
try {
value = std::stoi(str.substr(second_space + 1));
} catch (const std::exception& e) {
spdlog::warn("Ignoring 'output-dimensions' entry with non-integer value: '{}'",
config_output_dimension.asString());
continue;
}
int comparison_value;
if (dimension == "height") {
+8
View File
@@ -105,6 +105,14 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value&
event_box_.add(box);
}
Group::~Group() {
// Disconnect any pending reveal timeout so it cannot fire on a destroyed
// instance (the GLib source is not removed by sigc::connection's destructor).
if (reveal_timeout_.connected()) {
reveal_timeout_.disconnect();
}
}
void Group::show_group() {
box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
revealer.set_reveal_child(true);
+15
View File
@@ -220,6 +220,7 @@ auto waybar::modules::Clock::update() -> void {
if (tzInTooltip_) tzText_ = getTZtext(now.get_sys_time());
if (cldInTooltip_) cldText_ = get_calendar(today, shiftedDay, tz);
if (ordInTooltip_) ordText_ = get_ordinal_date(shiftedDay);
try {
if (tzInTooltip_ || cldInTooltip_ || ordInTooltip_) {
// std::vformat doesn't support named arguments.
m_tlpText_ =
@@ -234,6 +235,20 @@ auto waybar::modules::Clock::update() -> void {
}
m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(now));
} catch (const std::exception& e) {
// An unsupported/invalid specifier (e.g. %-I / %OI) in the tooltip-format or the
// calendar format must not take the whole module down every tick. Warn once and skip
// the tooltip for this update so the bar keeps working.
static bool tlpWarned = false;
if (!tlpWarned) {
spdlog::warn(
"Clock: could not format tooltip \"{}\": {}. Skipping tooltip; check your "
"tooltip-format/calendar format specifiers.",
m_tlpFmt_, e.what());
tlpWarned = true;
}
m_tlpText_.clear();
}
// Pango doesn't support CSS classes but to continue using it while staying
// backwards compatible this approach uses post-posting to replace fake
+28 -10
View File
@@ -99,7 +99,13 @@ void waybar::modules::CustomGraph::continuousWorker() {
thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt()));
fp_ = util::command::open(cmd, pid_, output_name_);
if (!fp_) {
throw std::runtime_error("Unable to open " + cmd);
// Letting this exception escape the SleeperThread would call
// std::terminate and kill all of Waybar. Degrade gracefully instead.
output_ = {1, ""};
dp.emit();
spdlog::error("Unable to restart {}: unable to open {}", name_, cmd);
thread_.stop();
return;
}
} else {
thread_.stop();
@@ -139,9 +145,11 @@ void waybar::modules::CustomGraph::waitingWorker() {
}
void waybar::modules::CustomGraph::refresh(int sig) {
#ifdef SIGRTMIN
if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) {
thread_.wake_up();
}
#endif
}
void waybar::modules::CustomGraph::handleEvent() {
@@ -268,22 +276,32 @@ void waybar::modules::CustomGraph::parseOutputJson() {
std::istringstream output(output_.out);
std::string line;
class_.clear();
// A script can emit invalid UTF-8; passing it unchecked to Pango/GTK aborts
// the whole bar in g_utf8_* (see parseOutputRaw, which validates the same way).
auto sanitize = [](const std::string& s) -> Glib::ustring {
Glib::ustring value = s;
if (!value.validate()) {
value = value.make_valid();
}
return value;
};
while (getline(output, line)) {
auto parsed = parser_.parse(line);
if (config_["escape"].isBool() && config_["escape"].asBool()) {
text_ = Glib::Markup::escape_text(parsed["text"].asString());
const bool escape = config_["escape"].isBool() && config_["escape"].asBool();
if (escape) {
text_ = Glib::Markup::escape_text(sanitize(parsed["text"].asString()));
} else {
text_ = parsed["text"].asString();
text_ = sanitize(parsed["text"].asString());
}
if (config_["escape"].isBool() && config_["escape"].asBool()) {
alt_ = Glib::Markup::escape_text(parsed["alt"].asString());
if (escape) {
alt_ = Glib::Markup::escape_text(sanitize(parsed["alt"].asString()));
} else {
alt_ = parsed["alt"].asString();
alt_ = sanitize(parsed["alt"].asString());
}
if (config_["escape"].isBool() && config_["escape"].asBool()) {
tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString());
if (escape) {
tooltip_ = Glib::Markup::escape_text(sanitize(parsed["tooltip"].asString()));
} else {
tooltip_ = parsed["tooltip"].asString();
tooltip_ = sanitize(parsed["tooltip"].asString());
}
if (parsed["class"].isString()) {
class_.push_back(parsed["class"].asString());
+34 -5
View File
@@ -297,11 +297,40 @@ bool IPC::isLuaProtocol() {
return *s_luaProtocolDetected_;
}
// Probe: send a harmless old-style dispatch and check the error.
// In Lua-based Hyprland (>= 0.54) the error contains "hl.dispatch".
// In older versions it returns "ok" or a different error.
auto reply = getSocket1Reply("dispatch workspace __waybar_probe__");
bool luaProto = reply.find("hl.dispatch") != std::string::npos;
// Detect the Lua-based dispatch protocol (Hyprland >= 0.54) via the read-only
// "version" query. This MUST have no side effects: an earlier probe issued a real
// "dispatch workspace __waybar_probe__", which on Hyprland < 0.54 actually switched
// the user to a junk workspace named __waybar_probe__ on the first click/scroll.
bool luaProto = false;
try {
util::JsonParser parser;
const Json::Value ver = parser.parse(getSocket1Reply("j/version"));
// Prefer the numeric "version" field ("0.54.0"); fall back to the "tag" field
// ("v0.54.0" or "v0.54.0-16-gdeadbee"), which is present on all releases.
std::string versionStr = ver["version"].asString();
if (versionStr.empty()) {
versionStr = ver["tag"].asString();
}
const size_t firstDigit = versionStr.find_first_of("0123456789");
if (firstDigit != std::string::npos) {
// std::stoi parses the leading integer and stops at the first non-digit, so it
// tolerates the trailing ".patch-commits-ghash" suffix on the tag.
const int major = std::stoi(versionStr.substr(firstDigit));
int minor = 0;
const size_t dot = versionStr.find('.', firstDigit);
if (dot != std::string::npos && dot + 1 < versionStr.size()) {
minor = std::stoi(versionStr.substr(dot + 1));
}
luaProto = major > 0 || (major == 0 && minor >= 54);
} else {
spdlog::warn("Hyprland IPC: could not parse version '{}', assuming legacy protocol",
versionStr);
}
} catch (const std::exception& e) {
spdlog::warn("Hyprland IPC: version detection failed ({}), assuming legacy protocol", e.what());
}
if (luaProto) {
spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol (Hyprland >= 0.54)");
+21 -16
View File
@@ -37,6 +37,11 @@ Workspaces::~Workspaces() {
if (m_scrollEventConnection_.connected()) {
m_scrollEventConnection_.disconnect();
}
// Cancel any pending debounce timeout so it cannot fire on a freed `this`.
// Runs on the main thread, same as where the timer is armed.
if (m_debounceTimer.connected()) {
m_debounceTimer.disconnect();
}
m_ipc.unregisterForIPC(this);
// wait for possible event handler to finish
std::lock_guard<std::mutex> lg(m_mutex);
@@ -332,23 +337,11 @@ void Workspaces::onEvent(const std::string& ev) {
}
}
if (m_debounceTimer.connected()) {
m_debounceTimer.disconnect();
m_updatePending = false;
}
m_updatePending = true;
m_debounceTimer = Glib::signal_timeout().connect(
[this]() {
if (!m_updatePending) return false;
std::lock_guard<std::mutex> lock(m_mutex);
if (m_updatePending) {
// Notify the main thread. dp (Glib::Dispatcher) is the only thread-safe way to
// hand off to the GTK main loop; GLib timer state must never be touched from the
// IPC listener thread. The debounce timer is owned entirely by the main-thread
// update() path (see Workspaces::update).
dp.emit();
m_updatePending = false;
}
return false;
},
7);
}
void Workspaces::onWorkspaceActivated(std::string const& payload) {
@@ -1041,8 +1034,20 @@ void Workspaces::setUrgentWorkspace(std::string const& windowaddress) {
}
auto Workspaces::update() -> void {
// Debounce rapid events (e.g. out-of-order create/destroy workspace events from
// Hyprland) to prevent workspace button flicker. This runs on the GTK main thread
// (invoked via the dp dispatcher), so arming/disconnecting the GLib timer here is
// thread-safe. Each event re-arms the timer, coalescing bursts into one refresh.
if (m_debounceTimer.connected()) {
m_debounceTimer.disconnect();
}
m_debounceTimer = Glib::signal_timeout().connect(
[this]() {
doUpdate();
AModule::update();
return false;
},
7);
}
void Workspaces::updateWindowCount() {
+16 -2
View File
@@ -320,7 +320,14 @@ auto waybar::modules::KeyboardState::update() -> void {
for (const auto& dev_path : dev_paths) {
try {
int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY);
auto dev = openDevice(fd);
libevdev* dev;
try {
dev = openDevice(fd);
} catch (...) {
// openDevice does not close the fd if libevdev_new_from_fd fails.
closeFile(fd);
throw;
}
numl |= libevdev_get_event_value(dev, EV_LED, LED_NUML);
capsl |= libevdev_get_event_value(dev, EV_LED, LED_CAPSL);
scrolll |= libevdev_get_event_value(dev, EV_LED, LED_SCROLLL);
@@ -376,7 +383,14 @@ auto waybar::modules::KeyboardState::update() -> void {
auto waybar::modules ::KeyboardState::tryAddDevice(const std::string& dev_path) -> void {
try {
int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY);
auto dev = openDevice(fd);
libevdev* dev;
try {
dev = openDevice(fd);
} catch (...) {
// openDevice does not close the fd if libevdev_new_from_fd fails.
closeFile(fd);
throw;
}
if (supportsLockStates(dev)) {
spdlog::info("Found device {} at '{}'", libevdev_get_name(dev), dev_path);
std::lock_guard<std::mutex> lock(devices_mutex_);
+58 -7
View File
@@ -8,6 +8,7 @@
#include <sys/un.h>
#include <unistd.h>
#include <chrono>
#include <sstream>
#include <thread>
#include <vector>
@@ -100,42 +101,77 @@ void IPC::sendAsync(const Json::Value& request) {
IPC::IPC() : sockfd_(-1), active_client_(Json::nullValue) { startIPC(); }
IPC::~IPC() {
running_ = false;
if (sockfd_ != -1) close(sockfd_);
if (ipc_thread_.joinable()) ipc_thread_.join();
}
void IPC::startIPC() {
// Connect synchronously so a missing socket (this WM isn't the active
// compositor) throws here and lets the module constructor fail, instead of
// the module always attaching with a permanently empty widget.
sockfd_ = IPC::connectToSocket();
ipc_thread_ = std::thread([this]() {
spdlog::info("Mango IPC thread started");
struct pollfd pfd;
pfd.fd = sockfd_;
pfd.events = POLLIN;
char buf[4096];
std::string buffer;
bool have_initial_fd = true;
// Reconnect loop: if the event stream drops (POLLHUP/POLLERR, read()==0 or
// an error) we back off briefly and re-establish the socket instead of
// leaving every mango module frozen forever with stale content.
while (running_) {
if (!have_initial_fd) {
try {
sockfd_ = IPC::connectToSocket();
} catch (const std::exception& e) {
spdlog::error("Mango IPC: failed to reconnect: {}", e.what());
std::this_thread::sleep_for(std::chrono::seconds(2));
continue;
}
}
have_initial_fd = false;
bool subscribed = true;
const std::vector<std::string> subs = {"watch all-monitors"};
for (const auto& cmd : subs) {
if (write(sockfd_, cmd.c_str(), cmd.size()) != (ssize_t)cmd.size() ||
write(sockfd_, "\n", 1) != 1) {
spdlog::error("Failed to subscribe to {}", cmd);
return;
subscribed = false;
break;
}
}
if (!subscribed) {
if (sockfd_ != -1) {
close(sockfd_);
sockfd_ = -1;
}
std::this_thread::sleep_for(std::chrono::seconds(2));
continue;
}
char buf[4096];
std::string buffer;
while (true) {
struct pollfd pfd;
pfd.fd = sockfd_;
pfd.events = POLLIN;
buffer.clear();
bool connected = true;
while (running_ && connected) {
int ret = poll(&pfd, 1, 1000);
if (ret == 0) continue;
if (ret < 0) {
if (errno == EINTR) continue;
spdlog::error("IPC poll error: {}", strerror(errno));
connected = false;
break;
}
if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
spdlog::info("Mango IPC socket closed or invalid");
connected = false;
break;
}
@@ -143,11 +179,13 @@ void IPC::startIPC() {
ssize_t n = read(sockfd_, buf, sizeof(buf));
if (n == 0) {
spdlog::info("Mango IPC connection closed");
connected = false;
break;
}
if (n < 0) {
if (errno == EINTR) continue;
spdlog::error("IPC read error: {}", strerror(errno));
connected = false;
break;
}
buffer.append(buf, n);
@@ -165,6 +203,19 @@ void IPC::startIPC() {
}
}
}
// On shutdown leave the socket for the destructor to close (avoids a
// double close); on a genuine disconnect close it before reconnecting.
if (!running_) break;
if (sockfd_ != -1) {
close(sockfd_);
sockfd_ = -1;
}
spdlog::warn("Mango IPC: event stream closed, reconnecting");
std::this_thread::sleep_for(std::chrono::seconds(2));
}
spdlog::info("Mango IPC thread stopping");
});
}
-2
View File
@@ -110,8 +110,6 @@ Language::Layout Language::getLayout(const std::string& fullName) {
short_description = short_name;
}
short_description = short_name;
Layout info{desc, short_name, variant, short_description};
return info;
}
+7 -1
View File
@@ -491,7 +491,13 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
continue;
}
auto* tmp = playerctl_player_new_from_name(pn, &error);
if (error || !tmp) continue;
if (error || !tmp) {
// Discard any error from this candidate so it doesn't leak into the next
// playerctl_player_new_from_name() call or the post-loop metadata calls, which
// assert that the passed GError is NULL (otherwise: GLib-CRITICAL / spurious errorexit).
g_clear_error(&error);
continue;
}
if (!first_valid_player) {
first_valid_player = tmp;
first_valid_name = name;
+6 -3
View File
@@ -90,13 +90,16 @@ uint32_t waybar::modules::Network::readLinkSpeed() const {
if (!sysfs_speed) return 0;
uint32_t speed;
// Read into a signed type: /sys/class/net/<if>/speed reports -1 when there is
// no carrier. Extracting -1 into an unsigned type would wrap to a huge value
// (and would not set failbit), so use a signed type and validate the result.
int64_t speed = 0;
sysfs_speed >> speed;
if (sysfs_speed.bad()) // read fails on incompatible devices
if (sysfs_speed.fail() || speed < 0) // read fails on incompatible devices
return 0;
return speed;
return static_cast<uint32_t>(speed);
}
waybar::modules::Network::Network(const std::string& id, const Json::Value& config)
+1 -1
View File
@@ -285,7 +285,7 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) {
Json::Value IPC::send(const Json::Value& request) {
util::ScopedFd socketfd(connectToSocket());
auto unix_istream = Gio::UnixInputStream::create(socketfd, true);
auto unix_istream = Gio::UnixInputStream::create(socketfd, false);
auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false);
auto istream = Gio::DataInputStream::create(unix_istream);
auto ostream = Gio::DataOutputStream::create(unix_ostream);
+11 -6
View File
@@ -16,7 +16,7 @@ Host::Host(std::size_t id, const Json::Value& config, const Bar& bar,
const std::vector<std::string>& ignore_list,
const std::function<void(std::unique_ptr<Item>&)>& on_add,
const std::function<void(std::unique_ptr<Item>&)>& on_remove,
const std::function<void()>& on_update)
const std::function<void()>& on_reorder, const std::function<void()>& on_update)
: bus_name_("org.kde.StatusNotifierHost-" + std::to_string(getpid()) + "-" +
std::to_string(id)),
object_path_("/StatusNotifierHost/" + std::to_string(id)),
@@ -27,6 +27,7 @@ Host::Host(std::size_t id, const Json::Value& config, const Bar& bar,
ignore_list_(ignore_list),
on_add_(on_add),
on_remove_(on_remove),
on_reorder_(on_reorder),
on_update_(on_update) {
auto orders = config["orders"];
if (!orders.isNull()) {
@@ -292,11 +293,15 @@ void Host::addRegisteredItem(const std::string& service) {
}
void Host::reorderItems() {
std::ranges::for_each(items_, on_remove_);
std::ranges::sort(items_, [](std::unique_ptr<Item>& item1, std::unique_ptr<Item>& item2) {
return item1->order_ < item2->order_;
});
std::ranges::for_each(items_, on_add_);
// Re-apply the configured ordering to the tray. This is invoked while an
// item's Id/order is first resolved (from Item::setCustomIcon), which happens
// *before* the item is marked ready and added. It must therefore only reorder
// the widgets that have already been added; re-running the full add path here
// would (a) re-parent widgets and reconnect signals for every item and (b)
// mutate items_ from within checkIgnoreList while it is being iterated,
// invalidating iterators/pointers. Delegating to on_reorder_ keeps this to a
// pure reordering of existing children.
on_reorder_();
}
} // namespace waybar::modules::SNI
+45 -4
View File
@@ -38,7 +38,7 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config)
host_((initIconsConfig(config), nb_hosts_), config, bar, ignore_list_,
std::bind(&Tray::onAdd, this, std::placeholders::_1),
std::bind(&Tray::onRemove, this, std::placeholders::_1),
std::bind(&Tray::queueUpdate, this)) {
std::bind(&Tray::reorderBox, this), std::bind(&Tray::queueUpdate, this)) {
box_.set_name("tray");
event_box_.add(box_);
if (!id.empty()) {
@@ -63,6 +63,16 @@ void Tray::onAdd(std::unique_ptr<Item>& item) {
spdlog::info("Tray::onAdd - item bus_name='{}', category='{}', icon_name='{}', title='{}'",
item->bus_name, item->category, item->icon_name, item->title);
// Idempotency guard: onAdd can be reached more than once for the same item
// (e.g. an item is processed while its Id/order is resolved and then again
// when it becomes ready). Re-adding would re-parent the event_box (GTK
// "widget already has a parent" critical), push a duplicate pointer into
// items_ and leak extra signal connections.
if (std::find(items_.begin(), items_.end(), item.get()) != items_.end()) {
spdlog::debug("Tray::onAdd - item already added, skipping");
return;
}
if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) {
box_.pack_end(item->event_box);
} else {
@@ -70,8 +80,13 @@ void Tray::onAdd(std::unique_ptr<Item>& item) {
}
items_.push_back(item.get());
item->event_box.signal_show().connect([this] { dp.emit(); });
item->event_box.signal_hide().connect([this] { dp.emit(); });
auto show_conn = item->event_box.signal_show().connect([this] { dp.emit(); });
auto hide_conn = item->event_box.signal_hide().connect([this] { dp.emit(); });
item_connections_[item.get()] = {show_conn, hide_conn};
// Position the freshly added widget according to the configured order. This
// must happen before the ignore-list check below, which may erase `item`.
reorderBox();
// After this point `item` may be erased/invalidated by the ignore-list check;
// do not touch it again below.
@@ -82,11 +97,37 @@ void Tray::onAdd(std::unique_ptr<Item>& item) {
}
void Tray::onRemove(std::unique_ptr<Item>& item) {
// May be called for items that were never added (e.g. the ignore-list check
// runs over items that are not yet ready). Only touch state we actually own.
auto it = std::find(items_.begin(), items_.end(), item.get());
if (it == items_.end()) {
return;
}
auto conn_it = item_connections_.find(item.get());
if (conn_it != item_connections_.end()) {
conn_it->second.first.disconnect();
conn_it->second.second.disconnect();
item_connections_.erase(conn_it);
}
box_.remove(item->event_box);
items_.erase(std::remove(items_.begin(), items_.end(), item.get()), items_.end());
items_.erase(it);
dp.emit();
}
void Tray::reorderBox() {
const bool reverse =
config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool();
// Stable sort keeps insertion order among items sharing the same order value.
std::stable_sort(items_.begin(), items_.end(),
[](const Item* a, const Item* b) { return a->order_ < b->order_; });
for (std::size_t i = 0; i < items_.size(); ++i) {
const int pos = reverse ? static_cast<int>(items_.size() - 1 - i) : static_cast<int>(i);
box_.reorder_child(items_[i]->event_box, pos);
}
}
auto Tray::update() -> void {
// Check if any items should be ignored now that properties have loaded
if (!ignore_list_.empty()) {
+1 -1
View File
@@ -101,7 +101,7 @@ auto Window::update() -> void {
fmt::arg("shell", shell_), fmt::arg("marks", marks_)),
config_["rewrite"]));
if (tooltipEnabled()) {
setTooltipMarkup(window_);
setTooltipMarkup(Glib::Markup::escape_text(window_));
}
// Resolve the app icon on the main thread to avoid racing with GTK draw on the
+24 -3
View File
@@ -281,21 +281,42 @@ auto SystemdFailedUnits::update() -> void {
last_status_ = overall_state_;
// A malformed user format/tooltip-format (e.g. an unknown {placeholder}) makes fmt throw a
// fmt::format_error; catch it so a bad config warns once instead of taking update() down.
try {
setLabelMarkup(fmt::format(
fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_),
fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_),
fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_),
fmt::arg("overall_state", overall_state_)));
fmt::arg("nr_failed_system", nr_failed_system_),
fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_),
fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_)));
} catch (const std::exception& e) {
static bool labelWarned = false;
if (!labelWarned) {
spdlog::warn("systemd-failed-units: invalid format, using fallback: {}", e.what());
labelWarned = true;
}
setLabelMarkup(fmt::format("{} failed", nr_failed_));
}
if (tooltipEnabled()) {
std::string failed_list = BuildTooltipFailedList();
auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_;
if (!tooltip_template.empty()) {
try {
setTooltipMarkup(fmt::format(
fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_),
fmt::arg("nr_failed_system", nr_failed_system_),
fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_),
fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_),
fmt::arg("failed_units_list", failed_list)));
} catch (const std::exception& e) {
static bool tooltipWarned = false;
if (!tooltipWarned) {
spdlog::warn("systemd-failed-units: invalid tooltip-format, skipping tooltip: {}",
e.what());
tooltipWarned = true;
}
setTooltipMarkup("");
}
} else {
setTooltipMarkup("");
}
+4 -4
View File
@@ -43,7 +43,7 @@ waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config)
if (error) {
spdlog::error("Failed to create ModemManager proxy: " + std::string(error->message));
g_error_free(error);
g_object_unref(connection);
g_clear_object(&connection);
return;
}
@@ -302,7 +302,7 @@ auto waybar::modules::Wwan::update() -> void {
}
waybar::modules::Wwan::~Wwan() {
g_object_unref(current_modem);
g_object_unref(manager);
g_object_unref(connection);
g_clear_object(&current_modem);
g_clear_object(&manager);
g_clear_object(&connection);
}