From 0729823c133e5b1c1c50d05a0923281c3ce0dd41 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 09:47:47 +0200 Subject: [PATCH 1/2] fix(image): don't crash at startup when the widget isn't realized yet The HiDPI code path builds a cairo surface from the pixbuf via Gdk::Cairo::create_surface_from_pixbuf(pixbuf, scale, image_.get_window()), which requires a realized Gtk::Image. During startup an image module can run its first update() before the widget is realized, so get_window() returns a null Gdk::Window and that path aborts before anything is logged. The more image modules are configured, the more likely at least one updates before realization, which is why >2 images reliably kills startup. Guard on get_window(): only take the surface path when a window is available, otherwise fall back to image_.set(pixbuf) (the pre-HiDPI behavior). This keeps HiDPI crispness once realized and never crashes at startup. Fixes #5051. --- src/modules/image.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 7c733880..d762f1eb 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -265,9 +265,19 @@ void SingleImageStrategy::update() { } if (pixbuf) { - auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), - image_.get_window()); - image_.set(surface); + // Building a HiDPI-aware cairo surface requires a realized widget: it reads the + // GdkWindow and its scale factor. During startup update() can run before the + // Gtk::Image is realized, in which case get_window() is null; feeding that path a + // null window aborts startup. Fall back to setting the pixbuf directly while the + // widget is unrealized, and use the crisp surface path once a window is available. + auto window = image_.get_window(); + if (window) { + auto surface = + Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), window); + image_.set(surface); + } else { + image_.set(pixbuf); + } image_.show(); if (hasTooltip_ && !tooltip_.empty()) { From 3f77c078750ba37bc589b167c903ae2e0deee33d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:20:41 +0200 Subject: [PATCH 2/2] fix(image): don't block the main loop with exec in the multiple-image path MultipleImageStrategy::update() and handleClick() ran util::command::exec (a blocking fork+exec+read) on the GTK main thread, so the whole bar froze for the script's duration on every interval and on every click. Move the exec into a new IStrategy::fetch() hook that the SleeperThread worker runs before dp.emit(); update() now only parses the cached output and draws on the main thread (mirroring how custom.cpp separates exec from formatting). handleClick() uses forkExec() so clicks fire-and-forget instead of blocking on the command's output. The entries and single-image paths are unchanged. --- include/modules/image.hpp | 6 ++++++ src/modules/image.cpp | 23 +++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/include/modules/image.hpp b/include/modules/image.hpp index 035dc4df..5b4ac818 100644 --- a/include/modules/image.hpp +++ b/include/modules/image.hpp @@ -19,6 +19,9 @@ namespace image { class IStrategy { public: virtual ~IStrategy() = default; + // Runs on the worker thread before update(). Use it for blocking work (e.g. + // spawning a user script) so the GTK main loop isn't stalled. Default no-op. + virtual void fetch() {} virtual void update() = 0; }; @@ -46,6 +49,7 @@ class MultipleImageStrategy : public IStrategy { public: MultipleImageStrategy(const std::string&, const Json::Value&, const std::string&, Gtk::EventBox&); ~MultipleImageStrategy() override = default; + void fetch() override; void update() override; private: @@ -67,6 +71,8 @@ class MultipleImageStrategy : public IStrategy { int size_; Gtk::Box box_; std::vector images_data_; + // stdout captured by fetch() on the worker thread and consumed by update() + std::string exec_output_; }; } // namespace image diff --git a/src/modules/image.cpp b/src/modules/image.cpp index d762f1eb..8c8132d7 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -45,6 +45,9 @@ auto waybar::modules::Image::getStrategy(const std::string& id, const Json::Valu void waybar::modules::Image::delayWorker() { thread_ = [this] { + // Do the blocking work (e.g. running a user script) here on the worker + // thread; update() then only parses the result and draws on the main thread. + strategy_->fetch(); dp.emit(); thread_.sleep_for(interval_); }; @@ -82,6 +85,15 @@ MultipleImageStrategy::MultipleImageStrategy(const std::string& id, const Json:: } } +void MultipleImageStrategy::fetch() { + // Run the (blocking) user script off the GTK main thread so the bar doesn't + // freeze for the script's duration on every interval. update() consumes the + // captured output. The static "entries" path takes priority and needs no exec. + if (config_["entries"].empty() && !config_["exec"].empty()) { + exec_output_ = util::command::exec(config_["exec"].asString(), "").out; + } +} + void MultipleImageStrategy::update() { // spdlog::info("update function run"); @@ -94,12 +106,12 @@ void MultipleImageStrategy::update() { if (!config_["entries"].empty()) { setImagesData(config_["entries"]); } else if (!config_["exec"].empty()) { - auto exec = util::command::exec(config_["exec"].asString(), ""); + // exec output was captured by fetch() on the worker thread Json::Value as_json; Json::Reader reader; - if (!reader.parse(exec.out, as_json)) { - spdlog::error("invalid json from exec {}", exec.out); + if (!reader.parse(exec_output_, as_json)) { + spdlog::error("invalid json from exec {}", exec_output_); return; } @@ -211,9 +223,8 @@ void MultipleImageStrategy::resetBoxAndMemory() { } void MultipleImageStrategy::handleClick(const Glib::ustring& data) { - auto msg = std::string(data); - - auto exec = util::command::exec(data, ""); + // Fire-and-forget: don't block the main loop waiting on the command's output. + util::command::forkExec(data); } SingleImageStrategy::SingleImageStrategy(const std::string& id, const Json::Value& config,