From fae2b311de9c3318e9ba2cebf933a11eb975cf6e Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:46:30 +0200 Subject: [PATCH 1/3] fix(tray): set an accel group on the dbusmenu client to stop Gtk-CRITICAL crash When a tray item exports menu accelerators (e.g. Mattermost), libdbusmenu-gtk calls gtk_widget_set_accel_path() with a NULL accel group because the DbusmenuGtkClient never had one assigned. This raises a Gtk-CRITICAL that corrupts menu state, and aborts Waybar when running under G_DEBUG=fatal-criticals. Assign a fresh GtkAccelGroup to the client right after the menu is created, before it is populated or shown. Fixes #5142. --- src/modules/sni/item.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 1c28d7af..843f386b 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -577,6 +577,15 @@ void Item::makeMenu() { if (dbus_menu != nullptr) { g_object_ref_sink(G_OBJECT(dbus_menu)); g_object_weak_ref(G_OBJECT(dbus_menu), (GWeakNotify)onMenuDestroyed, this); + // Provide an accel group to the dbusmenu client. Without one, items that export menu + // accelerators (e.g. Mattermost) trigger gtk_widget_set_accel_path() with a NULL accel group, + // which raises a Gtk-CRITICAL and corrupts menu state (or aborts under fatal-criticals). + DbusmenuGtkClient* client = dbusmenu_gtkmenu_get_client(DBUSMENU_GTKMENU(dbus_menu)); + if (client != nullptr) { + GtkAccelGroup* accel_group = gtk_accel_group_new(); + dbusmenu_gtkclient_set_accel_group(client, accel_group); + g_object_unref(accel_group); + } gtk_menu = Glib::wrap(GTK_MENU(dbus_menu)); gtk_menu->attach_to_widget(event_box); } From b7a6fa8c91e3529648cd796ad5e97a62dea9e341 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:49:01 +0200 Subject: [PATCH 2/3] fix(wlr/taskbar): dispatch built-in click actions via doAction, not the shell wlr/taskbar reads on-click* config values (close, minimize, maximize, fullscreen, minimize-raise, activate) directly as internal actions in Task::handle_clicked, but never adopted the eventActionMap_/doAction mechanism. As a result AModule::handleUserEvent additionally forkExec-ed the same value as a shell command, e.g. on-click-middle: "close" ran the action and then failed with "sh: line 1: close: command not found". Register the taskbar built-in action names in eventActionMap_ so they are recognized as module actions, and skip the shell forkExec in handleUserEvent when the configured value is a recognized module action. Non-action values are still run as user shell commands. Fixes #3284. --- include/AModule.hpp | 8 +++++++- src/AModule.cpp | 16 ++++++++++++---- src/modules/wlr/taskbar.cpp | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index e99bec9c..3fea52f6 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -98,6 +98,13 @@ class AModule : public IModule { bool disable_on_sleep_{false}; GObject* menu_ = nullptr; + // Maps a configured event name (e.g. "on-click-middle") to a built-in module + // action name. Populated from the `actions` config section, and by modules + // that interpret on-click* config values as internal actions (e.g. + // wlr/taskbar). Entries here are dispatched through doAction() instead of + // being run as shell commands. + std::map eventActionMap_; + private: bool handleUserEvent(GdkEventButton* const& ev); const bool isTooltip; @@ -106,7 +113,6 @@ class AModule : public IModule { gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; sigc::connection cursor_timeout_conn_; - std::map eventActionMap_; static const inline std::map, std::string> eventMap_{ {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, {std::make_pair(1, GdkEventType::GDK_BUTTON_RELEASE), "on-click-release"}, diff --git a/src/AModule.cpp b/src/AModule.cpp index b53f6cee..83bafbcf 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -118,7 +118,7 @@ auto AModule::update() -> void { if (config_["on-update"].isString()) { pid_children_.push_back(util::command::forkExec(config_["on-update"].asString())); } - signal_updated.emit(this); + signal_updated.emit(this); } // Get mapping between event name and module action name // Then call overridden doAction in order to call appropriate module action @@ -222,10 +222,18 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { } // Second call user scripts if (!format.empty()) { - if (config_[format].isString()) - format = config_[format].asString(); - else + // If the configured value for this event is a recognized built-in module + // action (registered in eventActionMap_), it has already been dispatched + // via doAction() above / handled by the module itself. Don't additionally + // run it as a shell command (issue #3284). Any other value is still treated + // as a user shell command. + const auto actionIt = eventActionMap_.find(format); + const bool isModuleAction = actionIt != eventActionMap_.cend() && config_[format].isString() && + config_[format].asString() == actionIt->second; + if (isModuleAction || !config_[format].isString()) format.clear(); + else + format = config_[format].asString(); } if (!format.empty()) { const int width = gdk_window_get_width(e->window); diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 62371c15..4517aec4 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -696,6 +696,21 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu box_.get_style_context()->add_class("empty"); event_box_.add(box_); + // wlr/taskbar interprets on-click* config values as built-in actions, handled + // per-task in Task::handle_clicked. Register the recognized action names so + // AModule dispatches them via doAction() instead of also running them as shell + // commands (issue #3284). Values that are not built-in actions are left alone + // and still run as user shell commands. + const auto is_builtin_action = [](const std::string& v) { + return v == "activate" || v == "minimize" || v == "minimize-raise" || v == "maximize" || + v == "fullscreen" || v == "close"; + }; + for (const auto* event : {"on-click", "on-click-middle", "on-click-right"}) { + if (config_[event].isString() && is_builtin_action(config_[event].asString())) { + eventActionMap_.insert({event, config_[event].asString()}); + } + } + // Make task buttons distribute evenly across the available width. if (config_["homogeneous"].isBool() && config_["homogeneous"].asBool()) { box_.set_homogeneous(true); From cb968c93692537b3c4291209092881e70feb1206 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:49:47 +0200 Subject: [PATCH 3/3] fix(mpd): bound the connect timeout so an unreachable server can't freeze the bar The MPD state machine drives all connection attempts from Glib::signal_timeout callbacks, which run on the GTK main loop. tryConnect() called mpd_connection_new() with the user-facing timeout_ (up to 30s by default), so an unreachable server blocked the whole bar for the full connect timeout. Bound the connect attempt to a short fixed timeout (2000 ms) so a dead server fails fast, then restore the configured timeout_ for subsequent command reads so slow-but-alive servers are unaffected. Fixes #1186. --- src/modules/mpd/mpd.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 106c54eb..8598628d 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -294,8 +294,19 @@ void waybar::modules::MPD::tryConnect() { return; } - connection_ = - detail::unique_connection(mpd_connection_new(server_, port_, timeout_), &mpd_connection_free); + // tryConnect() runs on the GTK main thread (via Glib::signal_timeout), so a + // blocking connect freezes the whole bar. Bound the connect attempt to a + // short timeout so an unreachable MPD server fails fast instead of hanging + // the loop for the full user-facing `timeout_` (up to 30s by default, #1186). + // The third argument to mpd_connection_new() is also the default command + // read timeout, so restore `timeout_` once connected to avoid shortening + // reads for slow-but-alive servers. + static constexpr unsigned kConnectTimeoutMs = 2'000; + unsigned connect_timeout = + (timeout_ != 0 && timeout_ < kConnectTimeoutMs) ? timeout_ : kConnectTimeoutMs; + + connection_ = detail::unique_connection(mpd_connection_new(server_, port_, connect_timeout), + &mpd_connection_free); if (connection_ == nullptr) { spdlog::error("{}: Failed to connect to MPD", module_name_); @@ -303,6 +314,11 @@ void waybar::modules::MPD::tryConnect() { return; } + // Restore the user-configured timeout for subsequent command reads. + if (timeout_ != 0) { + mpd_connection_set_timeout(connection_.get(), timeout_); + } + try { checkErrors(connection_.get()); spdlog::debug("{}: Connected to MPD", module_name_);