From f72f84e01101ba0d38e0624e528728b7ecb3f1cf Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 02:10:39 +0200 Subject: [PATCH] fix: backward-compat + bug fixes and man-page build fix for 0.16.0 Post-0.15.0 review of the 0.15.0..HEAD range surfaced regressions and bugs. This restores backward compatibility for existing configs/CSS, fixes confirmed defects, and repairs the scdoc man-page build break on master. Pango-markup tooltips are intentional and were kept. Backward-compat restorations: - AModule: honor legacy numeric Gdk::CursorType cursor values (int overload) - memory: correct GiB divisor (was ~2.3% low); round bare {} placeholders - wireplumber: scale max-volume into the linear domain so the cap works again - idle_inhibitor: gate right/middle-click deactivate & scroll on dynamic-timeouts; accept both dynamic-timeout(s); widen timeout to double (no fractional truncation) - custom: keep #custom-. CSS selectors working (classes on box_) - image: don't wordexp-split a single path; fall back to the literal path - niri/window: restore hide-when-empty (new show-empty opt-in); escape tooltip - wlr/taskbar: plain-text tooltip when markup is disabled Bug fixes: - tray: fix use-after-free in onAdd; guard the watcher retry timeout - hyprland: clamp max-windows iterator (OOB); drop duplicate language tooltip block - niri/window: supply {col}/{max_col} args in the empty branch (fmt::format_error) - mpris: escape {dynamic}/{player} tooltip; fix dangling player; albumArtist source - mango: fix use-after-free race (dispatch under callback_mutex_) - mpd: contain throwing checkErrors in noexcept idle paths (no std::terminate/UAF) - keyboard_state: always render every lock label, with guarded defaults - bluetooth: bound GATT ReadValue timeout, opt-in + services-resolved gating, preserve authoritative Battery1 percentage - wireplumber: fix WpDevice reference leak / NULL handling - battery, clock, dwl, wayfire, graph, custom_graph, transform, river: assorted crash/logic fixes Man page / build: - niri-workspaces: fix scdoc "indented by an amount greater than 1" (workspace-taskbar sub-options were mis-indented; breaks man-page build) - document new show-empty (niri/window); correct network {txBitrate}/{rxBitrate} Not compiled locally (no gtkmm on this host); C++ build relies on CI. Man pages validated with scdoc 1.11.4. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/AGraph.hpp | 2 +- include/AModule.hpp | 2 + include/modules/idle_inhibitor.hpp | 6 +-- man/waybar-idle-inhibitor.5.scd | 2 +- man/waybar-network.5.scd | 4 +- man/waybar-niri-window.5.scd | 5 +++ man/waybar-niri-workspaces.5.scd | 16 ++++---- man/waybar-styles.5.scd.in | 5 +++ src/AGraph.cpp | 12 ++++-- src/AIconLabel.cpp | 17 ++++++--- src/AModule.cpp | 25 +++++++++++++ src/modules/battery.cpp | 2 +- src/modules/bluetooth.cpp | 37 +++++++++++++++++-- src/modules/clock.cpp | 8 +++- src/modules/custom.cpp | 30 +++++++++++++-- src/modules/custom_graph.cpp | 21 +++++++++++ src/modules/dwl/window.cpp | 4 +- src/modules/hyprland/language.cpp | 57 +++++++++++------------------ src/modules/hyprland/workspace.cpp | 3 +- src/modules/idle_inhibitor.cpp | 25 ++++++++++--- src/modules/image.cpp | 29 +++++++++++---- src/modules/keyboard_state.cpp | 25 ++++--------- src/modules/mango/backend.cpp | 7 +--- src/modules/memory/common.cpp | 31 +++++++++------- src/modules/mpd/mpd.cpp | 2 +- src/modules/mpd/state.cpp | 54 ++++++++++++++++----------- src/modules/mpris/mpris.cpp | 9 +++-- src/modules/niri/window.cpp | 16 +++++--- src/modules/river/tags.cpp | 16 +++++--- src/modules/sni/host.cpp | 18 +++++++-- src/modules/sni/tray.cpp | 7 +++- src/modules/wayfire/backend.cpp | 34 +++++++++-------- src/modules/wireplumber.cpp | 27 ++++++++++---- src/modules/wlr/taskbar.cpp | 2 +- src/util/transform_8bit_to_rgba.cpp | 9 +++-- 35 files changed, 377 insertions(+), 192 deletions(-) diff --git a/include/AGraph.hpp b/include/AGraph.hpp index e2ada835..e65cd450 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -28,7 +28,7 @@ class AGraph : public AModule { void addValue(const int n); - const std::chrono::seconds interval_; + const std::chrono::milliseconds interval_; bool onDraw(const Cairo::RefPtr& cr); diff --git a/include/AModule.hpp b/include/AModule.hpp index 659aa68e..e99bec9c 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -86,6 +86,8 @@ class AModule : public IModule { Gtk::EventBox event_box_; virtual void setCursor(std::string const& c); + // Backward-compat overload for legacy numeric Gdk::CursorType configs (pre-0.16) + virtual void setCursor(Gdk::CursorType const& c); virtual bool handleToggle(GdkEventButton* const& ev); virtual bool handleMouseEnter(GdkEventCrossing* const& ev); diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 5fbfbc5b..8fef1985 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -38,9 +38,9 @@ class IdleInhibitor : public ALabel { struct zwp_idle_inhibitor_v1* idle_inhibitor_; int pid_; - bool dynamicTimeout; - short timeout; - short timeout_step; + bool dynamicTimeout = false; + double timeout; + double timeout_step; bool wait_for_activity_; }; diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 4fd5f04f..8cbba56e 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -76,7 +76,7 @@ screensaver, also known as "presentation mode". typeof: double ++ The number of minutes the inhibition should last. -*timeout_step*: ++ +*timeout-step*: ++ typeof: double ++ default: 10 ++ The number of minutes to add or subtract when scrolling (when dynamic timeouts are enabled). diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index 2a7200fe..a7860e5c 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -196,9 +196,9 @@ Addressed by *network* *{bandwidthTotalBytes}*: Instant total speed in bytes/seconds. -*{tx_bitrate}*: Link transmit bitrate (e.g., 866.7 Mb/s). +*{txBitrate}*: Link transmit bitrate (e.g., 866.7 Mb/s). -*{rx_bitrate}*: Link receive bitrate (e.g., 866.7 Mb/s). +*{rxBitrate}*: Link receive bitrate (e.g., 866.7 Mb/s). *{linkSpeed}*: Ethernet link speed. diff --git a/man/waybar-niri-window.5.scd b/man/waybar-niri-window.5.scd index c836f057..784c3b39 100644 --- a/man/waybar-niri-window.5.scd +++ b/man/waybar-niri-window.5.scd @@ -40,6 +40,11 @@ Addressed by *niri/window* default: false ++ Enables this module to consume all left over space dynamically. +*show-empty*: ++ + typeof: bool ++ + default: false ++ + When no window is focused (empty workspace or all windows closed), keep the module visible and render *format* with empty {title}/{app_id} and {col}/{max_col} set to -1. When false (the default), the module is hidden while no window is focused. + # FORMAT REPLACEMENTS See the output of "niri msg windows" for examples diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 0627776c..d10691e8 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -96,15 +96,15 @@ If none of the sorting options are enabled, workspaces keep their output/index o typeof: object ++ Contains settings for the workspace taskbar, which displays app icons within each workspace. - *enable*: ++ - typeof: bool ++ - default: false ++ - Enables the workspace taskbar mode. + *enable*: ++ + typeof: bool ++ + default: false ++ + Enables the workspace taskbar mode. - *icon-size*: ++ - typeof: int ++ - default: 18 ++ - Size of the icons in the workspace taskbar. + *icon-size*: ++ + typeof: int ++ + default: 18 ++ + Size of the icons in the workspace taskbar. *ignore-workspaces*: ++ typeof: array ++ diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in index ca9ee0a5..f01d4cf0 100644 --- a/man/waybar-styles.5.scd.in +++ b/man/waybar-styles.5.scd.in @@ -67,6 +67,11 @@ see the cursor-shape-v1 protocol for all possible cursor types: https://wayland.app/protocols/cursor-shape-v1#wp_cursor_shape_device_v1:enum:shape. Depending on the compositor and cursor theme used, cursors not listed in the protocol may also work. +Prior to 0.16.0 the `cursor` option accepted a numeric _Gdk::CursorType_ value +(e.g. `"cursor": 8`). Numeric values are deprecated but still honored for +backward compatibility; a cursor-shape-v1 name string (e.g. `"grab"`) is now +preferred. + Example of disabling the cursor on a custom module: ``` diff --git a/src/AGraph.cpp b/src/AGraph.cpp index f79e7218..ea1f754d 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -17,10 +18,13 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st : AModule(config, name, id, config["format-alt"].isString() || config["menu"].isString() || enable_click, enable_scroll), - interval_(config_["interval"] == "once" - ? std::chrono::seconds::max() - : std::chrono::seconds( - config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)) { + interval_( + config_["interval"] == "once" + ? std::chrono::milliseconds::max() + : std::chrono::milliseconds( + config_["interval"].isNumeric() + ? std::max(1L, static_cast(config_["interval"].asDouble() * 1000)) + : 1000L * static_cast(interval))) { graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw)); graph_.set_name(name); if (!id.empty()) { diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index c17eb8d4..a54266ac 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -90,13 +90,18 @@ auto AIconLabel::update() -> void { label_.set_markup(cleanLabel); if (iconLabel.front() == '/') { - int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); - auto pixbuf = Gdk::Pixbuf::create_from_file(iconLabel, 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(iconLabel, 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 embedded icon {}: {}", iconLabel, std::string(e.what())); + image_.set_visible(false); + } } else { image_.set_from_icon_name(iconLabel, Gtk::ICON_SIZE_INVALID); image_.set_visible(true); diff --git a/src/AModule.cpp b/src/AModule.cpp index 407d5a66..b53f6cee 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -85,6 +85,13 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } } else if (config_["cursor"].isString()) { setCursor(config_["cursor"].asString()); + } else if (config_["cursor"].isInt() || config_["cursor"].isUInt()) { + // Backward-compat: legacy numeric Gdk::CursorType values (pre-0.16) + setCursor(Gdk::CursorType(config_["cursor"].asInt())); + spdlog::warn( + "Numeric 'cursor' values are deprecated; use a cursor-shape-v1 name string instead " + "(module {})", + name_); } else { spdlog::warn("unknown cursor option configured on module {}", name_); } @@ -140,6 +147,24 @@ void AModule::setCursor(std::string const& c) { } } +// Backward-compat overload: honor legacy numeric Gdk::CursorType configs (pre-0.16) +void AModule::setCursor(Gdk::CursorType const& c) { + auto gdk_window = event_box_.get_window(); + if (gdk_window) { + auto cursor = Gdk::Cursor::create(gdk_window->get_display(), c); + gdk_window->set_cursor(cursor); + } else { + // window may not be accessible yet, in this case, + // schedule another call for setting the cursor in 1 sec + cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds( + [this, c]() { + setCursor(c); + return false; + }, + 1); + } +} + bool AModule::handleMouseEnter(GdkEventCrossing* const& e) { if (auto* module = event_box_.get_child(); module != nullptr) { module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index cec8beb8..ffa6ff39 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -609,7 +609,7 @@ waybar::modules::Battery::getInfos() { if (status == "Discharging" && time_to_empty_now_exists) { if (time_to_empty_now != 0) time_remaining = (float)time_to_empty_now / 3600.0f; } else if (status == "Discharging" && total_power_exists && total_energy_exists) { - if (smooth_power_ != 0) time_remaining = (float)total_energy / smooth_power_; + if (total_power != 0) time_remaining = (float)total_energy / smooth_power_; } else if (status == "Charging" && time_to_full_now_exists) { if (time_to_full_now_exists && (time_to_full_now != 0)) time_remaining = -(float)time_to_full_now / 3600.0f; diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 7436967d..b092c29c 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -87,14 +87,29 @@ auto isChildPath(const std::string& child, const std::string& parent) -> bool { return child.starts_with(parent); } +// Returns true only if some configured format/tooltip string actually references the peripheral +// battery placeholder. The GATT battery scan issues over-the-air BLE reads, so it must stay +// opt-in: users who don't display {device_battery_percentage_peripheral} pay zero cost. +auto configUsesPeripheralBattery(const Json::Value& config) -> bool { + for (const auto& key : config.getMemberNames()) { + if (config[key].isString() && + config[key].asString().find("device_battery_percentage_peripheral") != std::string::npos) { + return true; + } + } + return false; +} + auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); GError* error = nullptr; + // Use a small finite timeout (2s) instead of the default (-1 == 25s) so a sleeping or + // dropped BLE peripheral cannot stall the Waybar main thread for ~25s on a synchronous read. GVariant* gvar = g_dbus_proxy_call_sync(proxy_char, "ReadValue", g_variant_new("(a{sv})", &builder), - G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + G_DBUS_CALL_FLAGS_NONE, 2000, nullptr, &error); if (error != nullptr) { g_error_free(error); return std::nullopt; @@ -538,7 +553,9 @@ auto waybar::modules::Bluetooth::processBatteryServiceCharacteristics( if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) { peripheral_battery = battery_value.value(); - } else { + } else if (!central_battery.has_value()) { + // Only fill the central sink once so multiple non-described 0x2a19 characteristics don't + // clobber each other (order-dependent last-writer-wins) or an already-set Battery1 value. central_battery = battery_value.value(); } } @@ -564,8 +581,20 @@ auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, Device g_object_unref(proxy_device); device_info.battery_percentage = getDeviceBatteryPercentage(object); - getDeviceGattBatteryLevels(object, device_info.battery_percentage, - device_info.battery_percentage_peripheral); + // Only perform the (synchronous, over-the-air) GATT battery scan when the peripheral battery + // placeholder is actually used and the device's services are resolved. This keeps the scan off + // the frequent property-changed hot path (RSSI/TxPower updates leave ServicesResolved false or + // unchanged) and opt-in for split-keyboard style peripherals. + if (device_info.services_resolved && configUsesPeripheralBattery(config_)) { + // Read the GATT central level into a separate local; only fall back to it when the + // authoritative org.bluez.Battery1 percentage is absent, so the GATT scan can never overwrite + // the Battery1 value used by {device_battery_percentage}. + std::optional gatt_central; + getDeviceGattBatteryLevels(object, gatt_central, device_info.battery_percentage_peripheral); + if (!device_info.battery_percentage.has_value()) { + device_info.battery_percentage = gatt_central; + } + } return true; } diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 89ae5eba..016d1d75 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -568,8 +568,12 @@ void waybar::modules::Clock::tz_down() { tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1; } void waybar::modules::Clock::action_exec(const std::string& action) { - auto cmd = action.substr(strlen("exec ")); - pid_children_.push_back(util::command::forkExec(cmd)); + const auto pos = action.find(" "); + if (pos == std::string::npos || action.find_first_not_of(" ", pos) == std::string::npos) { + spdlog::error("Clock: exec action requires a command argument"); + return; + } + pid_children_.push_back(util::command::forkExec(action.substr(pos + 1))); } #ifdef HAVE_LANGINFO_1STDAY diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index bdd9aa65..01781036 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -223,6 +223,17 @@ auto waybar::modules::Custom::update() -> void { for (auto const& c : class_) { style->add_class(c); } + // Mirror the dynamic script classes onto box_, which now carries the + // #custom- widget name (see AIconLabel), so #custom-. + // CSS selectors keep resolving as they did in 0.15.0. + auto box_style = box_.get_style_context(); + for (auto const& c : box_style->list_classes()) { + if (c == id_ || c == MODULE_CLASS) continue; + box_style->remove_class(c); + } + for (auto const& c : class_) { + box_style->add_class(c); + } style->add_class("flat"); style->add_class("text-button"); style->add_class(MODULE_CLASS); @@ -230,14 +241,20 @@ auto waybar::modules::Custom::update() -> void { image_style->add_class("image-button"); event_box_.show(); if (!image_path_.empty()) { - auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); - image_.set(pixbuf); + try { + auto pixbuf = + Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); + image_.set(pixbuf); + } catch (const Glib::Error& e) { + spdlog::warn("custom {}: failed to load image-path '{}': {}", name_, image_path_, + std::string(e.what())); + image_.clear(); + } } else if (!image_name_.empty()) { image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID); image_.set_pixel_size(app_icon_size_); } - image_.set_visible(!image_name_.empty() || !image_path_.empty()); label_.set_visible(!str.empty()); } } catch (const fmt::format_error& e) { @@ -251,6 +268,13 @@ auto waybar::modules::Custom::update() -> void { } // Call parent update AIconLabel::update(); + + // Show a configured image-path/image-name image after the base update() so + // AIconLabel::update()'s icon gate cannot re-hide it. Leave the embedded-icon + // and "icon" cases to the base class (they have no image-path/image-name). + if (!image_name_.empty() || !image_path_.empty()) { + image_.set_visible(true); + } } void waybar::modules::Custom::parseOutputRaw() { diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index 2a12f0c1..0f236304 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -2,6 +2,11 @@ #include +#include +#include +#include +#include + #include "util/scope_guard.hpp" waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, @@ -228,6 +233,22 @@ void waybar::modules::CustomGraph::parseOutputRaw() { tooltip_ = validated_line; } class_.clear(); + // Derive the graph value from the first line's leading numeric value, + // since the raw (i3blocks) format has no dedicated percentage field. + std::string value = validated_line; + if (!value.empty() && value.back() == '%') { + value.pop_back(); + } + try { + double parsed = std::stod(value); + if (std::isnan(parsed)) { + percentage_ = 0; + } else { + percentage_ = static_cast(std::clamp(std::lround(parsed), 0L, 100L)); + } + } catch (const std::exception&) { + percentage_ = 0; + } } else if (i == 1) { if (config_["escape"].isBool() && config_["escape"].asBool()) { tooltip_ = Glib::Markup::escape_text(validated_line); diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp index 15104b2e..677d2ca0 100644 --- a/src/modules/dwl/window.cpp +++ b/src/modules/dwl/window.cpp @@ -138,9 +138,7 @@ void Window::handle_frame() { box_.set_visible(true); } else { box_.get_style_context()->remove_class("active"); - if (hide_inactive_) { - box_.set_visible(false); - } + box_.set_visible(!hide_inactive_); } } } diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index dfe24b0a..9c082d3a 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -53,24 +53,29 @@ auto Language::update() -> void { std::string tooltipContent = std::string{}; bool tooltip_enabled = tooltipEnabled(); if (tooltip_enabled) { - if (config_.isMember("tooltip-format")) { - auto tooltip_format = config_["tooltip-format"].asString(); - if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) { - const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant; - tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); - } else if (config_.isMember("tooltip-format-" + layout_.short_description)) { - const auto propName = "tooltip-format-" + layout_.short_description; - tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); - } else { - tooltipContent = - trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), - fmt::arg("short", layout_.short_name), - fmt::arg("shortDescription", layout_.short_description), - fmt::arg("variant", layout_.variant))); - } + // Default to "{long}" when no tooltip-format is provided, matching the man page + auto tooltip_format = + config_.isMember("tooltip-format") ? config_["tooltip-format"].asString() : "{long}"; + if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) { + const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant; + tooltipContent = trim(fmt::format(fmt::runtime(tooltip_format), config_[propName].asString(), + fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); + } else if (config_.isMember("tooltip-format-" + layout_.short_description)) { + const auto propName = "tooltip-format-" + layout_.short_description; + tooltipContent = trim(fmt::format(fmt::runtime(tooltip_format), config_[propName].asString(), + fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); } else { - // if no tooltip format is provided, use the same text as the module - tooltipContent = layoutName; + tooltipContent = + trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); } spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); } @@ -85,24 +90,6 @@ auto Language::update() -> void { label_.hide(); } - // Tooltip support - if (tooltipEnabled()) { - std::string tooltipFormat; - if (config_["tooltip-format"].isString()) { - tooltipFormat = config_["tooltip-format"].asString(); - } else { - tooltipFormat = "{long}"; - } - auto tooltipText = - trim(fmt::format(fmt::runtime(tooltipFormat), fmt::arg("long", layout_.full_name), - fmt::arg("short", layout_.short_name), - fmt::arg("shortDescription", layout_.short_description), - fmt::arg("variant", layout_.variant))); - label_.set_tooltip_text(tooltipText); - } else { - label_.set_tooltip_text(""); - } - ALabel::update(); } diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index ade7a10f..79ff5c3d 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -438,7 +438,8 @@ void Workspace::update(const std::string& workspace_icon, const std::string& wor auto windowSeparator = m_workspaceManager.getWindowSeparator(); auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold(); - auto end_it = m_workspaceManager.maxWindows() == 0 + auto end_it = (m_workspaceManager.maxWindows() <= 0 || + static_cast(m_workspaceManager.maxWindows()) >= m_windowMap.size()) ? m_windowMap.end() : m_windowMap.begin() + m_workspaceManager.maxWindows(); diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 525095fa..170701ce 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -44,7 +44,14 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& event_box_.signal_button_press_event().connect( sigc::mem_fun(*this, &IdleInhibitor::handleToggle)); - event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &IdleInhibitor::handleScroll)); + // Only connect our own scroll handler when the user hasn't configured on-scroll-* + // commands. When on-scroll-* is set, AModule already connects a scroll handler that + // (via virtual dispatch) reaches IdleInhibitor::handleScroll; a second connection here + // would fire handleScroll twice per scroll event. + if (!(config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() || + config_["on-scroll-left"].isString() || config_["on-scroll-right"].isString())) { + event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &IdleInhibitor::handleScroll)); + } // Add this to the modules list waybar::modules::IdleInhibitor::modules.push_back(this); @@ -157,8 +164,11 @@ void waybar::modules::IdleInhibitor::toggleStatus(int force_status) { } bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { + // Accept both the documented "dynamic-timeouts" (plural) and the legacy + // "dynamic-timeout" (singular) key spellings. + const bool dynamic = config_["dynamic-timeouts"].asBool() || config_["dynamic-timeout"].asBool(); if (e->button == 1) { - if (config_["dynamic-timeout"].asBool()) { + if (dynamic) { toggleStatus(1); } else { toggleStatus(); @@ -171,7 +181,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } } } - if (e->button == 3) { + if (e->button == 3 && dynamic) { toggleStatus(0); // Make all other idle inhibitor modules update @@ -181,7 +191,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } } } - if (e->button == 2) { + if (e->button == 2 && dynamic) { toggleStatus(0); timeout = config_["timeout"].asDouble(); } @@ -190,8 +200,11 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } bool waybar::modules::IdleInhibitor::handleScroll(GdkEventScroll* e) { - if (!config_["dynamic-timeout"].asBool()) { - return true; + // Accept both the documented "dynamic-timeouts" (plural) and the legacy + // "dynamic-timeout" (singular) key spellings. + if (!(config_["dynamic-timeouts"].asBool() || config_["dynamic-timeout"].asBool())) { + // Delegate to the base handler so any configured on-scroll-* command still runs. + return ALabel::handleScroll(e); } auto dir = AModule::getScrollDir(e); if (dir == SCROLL_DIR::NONE) { diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 6372dd4e..7c733880 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -126,7 +126,12 @@ void MultipleImageStrategy::setupAndDraw() { bool has_onclick = !data.on_click.empty(); Glib::RefPtr pixbuf; - pixbuf = Gdk::Pixbuf::create_from_file(path, size_, size_); + try { + pixbuf = Gdk::Pixbuf::create_from_file(path, size_, size_); + } catch (const Glib::Error& e) { + spdlog::error("failed to load image '{}': {}", path, std::string(e.what())); + pixbuf.reset(); // fall through to the .empty branch + } if (has_onclick) { auto btn = images_data_[i].btn; @@ -234,22 +239,32 @@ SingleImageStrategy::SingleImageStrategy(const std::string& id, const Json::Valu void SingleImageStrategy::update() { if (config_["path"].isString()) { - auto result = Config::tryExpandPath(config_["path"].asString(), ""); - path_ = result.empty() ? "" : result.front(); + auto p = config_["path"].asString(); + auto result = Config::tryExpandPath(p, ""); + // Only use the expanded path when it resolves to exactly one existing match; + // otherwise keep the literal path so paths with spaces/metacharacters still work. + path_ = (result.size() == 1) ? result.front() : p; } else if (config_["exec"].isString()) { output_ = util::command::exec(config_["exec"].asString(), ""); parseOutputRaw(); // expand path if "~" or "$HOME" is present in original path auto result = Config::tryExpandPath(path_, ""); - path_ = result.empty() ? "" : result.front(); + path_ = (result.size() == 1) ? result.front() : path_; } + Glib::RefPtr pixbuf; if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) { - Glib::RefPtr pixbuf; - int scaled_icon_size = size_ * image_.get_scale_factor(); - pixbuf = Gdk::Pixbuf::create_from_file(path_, scaled_icon_size, scaled_icon_size); + try { + pixbuf = Gdk::Pixbuf::create_from_file(path_, scaled_icon_size, scaled_icon_size); + } catch (const Glib::Exception& e) { + // Existing but corrupt/non-image file: degrade to the empty state instead of crashing. + spdlog::warn("Failed to load image {}: {}", path_, std::string(e.what())); + pixbuf.reset(); + } + } + if (pixbuf) { auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), image_.get_window()); image_.set(surface); diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 49cf863f..7e43c74c 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -86,7 +86,6 @@ auto isCommonFormatIcons(const Json::Value& config) -> bool { auto keyStateToIcons(const Json::Value& config) -> std::unordered_map> { std::unordered_map> key_icon_states; - std::vector default_icons = {"unlocked", "locked"}; if (isCommonFormatIcons(config)) { std::vector icons = { @@ -100,26 +99,18 @@ auto keyStateToIcons(const Json::Value& config) return key_icon_states; } - bool found_any = false; + const auto& format_icons = config["format-icons"]; for (const auto& key : std::vector{"numlock", "capslock", "scrolllock"}) { std::string map_key = key.substr(0, key.length() - 4); map_key[0] = std::toupper(map_key[0]); - if (config["format-icons"].isObject() && config["format-icons"][key].isObject()) { - std::string unlocked = config["format-icons"][key]["unlocked"].isString() - ? config["format-icons"][key]["unlocked"].asString() - : "unlocked"; - std::string locked = config["format-icons"][key]["locked"].isString() - ? config["format-icons"][key]["locked"].asString() - : "locked"; - key_icon_states[map_key] = {unlocked, locked}; - found_any = true; + std::string unlocked = "unlocked"; + std::string locked = "locked"; + if (format_icons.isObject() && format_icons[key].isObject()) { + const auto& obj = format_icons[key]; + if (obj["unlocked"].isString()) unlocked = obj["unlocked"].asString(); + if (obj["locked"].isString()) locked = obj["locked"].asString(); } - } - - if (!found_any) { - key_icon_states["Num"] = default_icons; - key_icon_states["Caps"] = default_icons; - key_icon_states["Scroll"] = default_icons; + key_icon_states[map_key] = {unlocked, locked}; } return key_icon_states; diff --git a/src/modules/mango/backend.cpp b/src/modules/mango/backend.cpp index a8976f21..5083a7fc 100644 --- a/src/modules/mango/backend.cpp +++ b/src/modules/mango/backend.cpp @@ -204,20 +204,15 @@ void IPC::parseIPC(const std::string& line) { } } - std::vector handlers_to_notify; { std::lock_guard lock(callback_mutex_); for (auto& [ev, handler] : callbacks_) { if (ev == "monitor") { - handlers_to_notify.push_back(handler); + handler->onEvent(root); } } } - for (auto* handler : handlers_to_notify) { - handler->onEvent(root); - } - return; } diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 626376e4..610c3830 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -1,14 +1,14 @@ +#include + #include "modules/memory.hpp" namespace { -const std::unordered_map kUnits = {{"kB", 1.000}, - {"kiB", 1.024}, - {"MB", 1.000 * 1000.0}, - {"MiB", 1.024 * 1024.0}, - {"GB", 1.000 * 1000.0 * 1000.0}, - {"GiB", 1.024 * 1024.0 * 1024.0}, - {"TB", 1.000 * 1000.0 * 1000.0 * 1000.0}, - {"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0}}; +// /proc/meminfo reports values in kiB (1024 bytes) despite the 'kB' label. +// These divisors convert a kiB count to the requested unit. +const std::unordered_map kUnits = { + {"B", 1.0 / 1024.0}, {"kB", 1000.0 / 1024.0}, {"kiB", 1.0}, + {"MB", 1000000.0 / 1024.0}, {"MiB", 1024.0}, {"GB", 1000000000.0 / 1024.0}, + {"GiB", 1024.0 * 1024.0}, {"TB", 1e12 / 1024.0}, {"TiB", 1024.0 * 1024.0 * 1024.0}}; } waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) @@ -57,12 +57,15 @@ auto waybar::modules::Memory::update() -> void { } float divisor = kUnits.at(unit_); - float total_ram = memtotal / divisor; - float total_swap = swaptotal / divisor; - float used_ram = (memtotal - memfree) / divisor; - float used_swap = (swaptotal - swapfree) / divisor; - float available_ram = memfree / divisor; - float available_swap = swapfree / divisor; + // Pre-round to 2 decimals so bare {total}/{used}/{avail} placeholders print + // cleanly, matching the 0.15.0 behavior (an explicit spec like {used:.1f} + // still overrides this). + float total_ram = 0.01f * std::round((memtotal / divisor) * 100.0f); + float total_swap = 0.01f * std::round((swaptotal / divisor) * 100.0f); + float used_ram = 0.01f * std::round(((memtotal - memfree) / divisor) * 100.0f); + float used_swap = 0.01f * std::round(((swaptotal - swapfree) / divisor) * 100.0f); + float available_ram = 0.01f * std::round((memfree / divisor) * 100.0f); + float available_swap = 0.01f * std::round((swapfree / divisor) * 100.0f); auto format = format_; auto state = getState(used_ram_percentage); diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 407fa599..106c54eb 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -28,7 +28,7 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) connection_(nullptr, &mpd_connection_free), status_(nullptr, &mpd_status_free), song_(nullptr, &mpd_song_free), - ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "\u2026") { + ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "") { if (!config_["port"].isNull() && !config_["port"].isUInt()) { spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_); } diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 16cd529b..834f97c9 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -58,16 +58,21 @@ void Idle::entry() noexcept { auto conn = ctx_->connection().get(); assert(conn != nullptr); - if (!mpd_send_idle_mask( - conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { - ctx_->checkErrors(conn); - spdlog::error("mpd: Idle: failed to register for IDLE events"); - } else { - spdlog::debug("mpd: Idle: watching FD"); - sigc::slot idle_slot = sigc::mem_fun(*this, &Idle::on_io); - idle_connection_ = - Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), - Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + try { + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Idle: failed to register for IDLE events"); + } else { + spdlog::debug("mpd: Idle: watching FD"); + sigc::slot idle_slot = sigc::mem_fun(*this, &Idle::on_io); + idle_connection_ = + Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), + Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + } + } catch (std::exception const& e) { + spdlog::warn("mpd: Idle: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); } } @@ -114,8 +119,10 @@ bool Idle::on_io(Glib::IOCondition const&) { void Playing::entry() noexcept { timer(); - idle(); spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval()); + // NB: idle() may transition to Disconnected (destroying *this) on error, so it + // must be the last statement here — do not access members after this call. + idle(); } void Playing::exit() noexcept { @@ -143,16 +150,21 @@ void Playing::idle() noexcept { auto conn = ctx_->connection().get(); assert(conn != nullptr); - if (!mpd_send_idle_mask( - conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { - ctx_->checkErrors(conn); - spdlog::error("mpd: Playing: failed to register for IDLE events"); - } else if (!idle_connection_.connected()) { - spdlog::trace("mpd: Playing: watching FD"); - sigc::slot idle_slot = sigc::mem_fun(*this, &Playing::on_io); - idle_connection_ = - Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), - Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + try { + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Playing: failed to register for IDLE events"); + } else if (!idle_connection_.connected()) { + spdlog::trace("mpd: Playing: watching FD"); + sigc::slot idle_slot = sigc::mem_fun(*this, &Playing::on_io); + idle_connection_ = + Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), + Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + } + } catch (std::exception const& e) { + spdlog::warn("mpd: Playing: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); } } diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index c3e0235e..7eb11594 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -385,6 +385,7 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye if (mpris->player != nullptr) { g_signal_handlers_disconnect_by_data(mpris->player, mpris); + if (mpris->last_active_player_ == mpris->player) mpris->last_active_player_ = nullptr; g_clear_object(&mpris->player); } mpris->player = playerctl_player_new_from_name(player_name, nullptr); @@ -544,11 +545,12 @@ auto Mpris::getPlayerInfo() -> std::optional { if (error) goto errorexit; if (auto* album_artist_ = - playerctl_player_print_metadata_prop(player, "xesam:albumArtist", &error)) { + playerctl_player_print_metadata_prop(last_active_player_, "xesam:albumArtist", &error)) { spdlog::debug("mpris[{}]: albumArtist = {}", info.name, album_artist_); info.album_artist = album_artist_; g_free(album_artist_); } + if (error) goto errorexit; if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); @@ -745,7 +747,8 @@ auto Mpris::update() -> void { if (tooltipEnabled()) { try { auto tooltip_text = fmt::format( - fmt::runtime(tooltipstr), fmt::arg("player", info.name), + fmt::runtime(tooltipstr), + fmt::arg("player", std::string(Glib::Markup::escape_text(info.name))), fmt::arg("status", info.status_string), fmt::arg("artist", std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))), @@ -754,7 +757,7 @@ auto Mpris::update() -> void { fmt::arg("album", std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))), fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition), - fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)), + fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, true)), fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string))); diff --git a/src/modules/niri/window.cpp b/src/modules/niri/window.cpp index 38ce2f49..5785e6cb 100644 --- a/src/modules/niri/window.cpp +++ b/src/modules/niri/window.cpp @@ -78,7 +78,7 @@ void Window::doUpdate() { updateAppIconName(appId, ""); - if (tooltipEnabled()) label_.set_tooltip_markup(title); + if (tooltipEnabled()) label_.set_tooltip_markup(sanitizedTitle); const auto id = window["id"].asUInt64(); const auto workspaceId = window["workspace_id"].asUInt64(); @@ -93,11 +93,15 @@ void Window::doUpdate() { oldAppId_ = appId; } } else { - label_.show(); - label_.set_markup(waybar::util::rewriteString( - fmt::format(fmt::runtime(format_), fmt::arg("title", ""), - fmt::arg("app_id", "")), - config_["rewrite"])); + if (config_["show-empty"].asBool()) { + label_.show(); + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", ""), fmt::arg("app_id", ""), + fmt::arg("col", -1), fmt::arg("max_col", -1)), + config_["rewrite"])); + } else { + label_.hide(); + } updateAppIconName("", ""); setClass("solo", false); diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index f08fdac5..d7f87cf8 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -129,13 +129,15 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con } if (!control_) { + // Keep going: without river_control_v1 the tags are still displayed read-only + // (0.15.0 behavior); only the click-to-select/toggle wiring is disabled below. spdlog::error("river_control_v1 not advertised"); - return; } if (!seat_) { + // Keep going: wl_seat is only required for the focused-output ("output") class + // and for issuing control commands. Read-only tag display still works without it. spdlog::error("wl_seat not advertised"); - return; } // Store the output this module belongs to; the river_output_status and @@ -169,7 +171,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con button.set_relief(Gtk::RELIEF_NONE); box_.pack_start(button, false, false, 0); - if (!config_["disable-click"].asBool()) { + if (control_ && seat_ && !config_["disable-click"].asBool()) { if (set_tags.isArray() && !set_tags.empty()) button.signal_clicked().connect(sigc::bind( sigc::mem_fun(*this, &Tags::handle_primary_clicked), set_tags[tag].asUInt())); @@ -213,14 +215,17 @@ void Tags::handle_show() { 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); - seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); - zriver_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this); + if (seat_) { + seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); + zriver_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this); + } zriver_status_manager_v1_destroy(status_manager_); status_manager_ = nullptr; } void Tags::handle_primary_clicked(uint32_t tag) { + if (!control_ || !seat_) return; // Send river command to select tag on left mouse click zriver_command_callback_v1* callback; zriver_control_v1_add_argument(control_, "set-focused-tags"); @@ -230,6 +235,7 @@ void Tags::handle_primary_clicked(uint32_t tag) { } bool Tags::handle_button_press(GdkEventButton* event_button, uint32_t tag) { + if (!control_ || !seat_) return true; if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) { // Send river command to toggle tag on right mouse click zriver_command_callback_v1* callback; diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 6e9e95d0..272e05aa 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -143,12 +143,24 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { // Store the timeout connection so it is disconnected in ~Host, avoiding a // use-after-free if the Host is destroyed before the retry fires. host->retry_connection_ = Glib::signal_timeout().connect( - [host]() { + [host]() -> bool { if (host->watcher_ != nullptr) { return false; } - auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); - host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + try { + auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); + host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + } catch (const Glib::Error& e) { + spdlog::error("Host: retry get_sync failed: {}", static_cast(e.what())); + if (host->retry_count_ < MAX_RETRIES) { + host->retry_count_ += 1; + return true; // re-arm this timer; never let the exception escape + } + spdlog::warn("Host: giving up on watcher proxy creation after {} retries", + host->retry_count_); + } catch (const std::exception& e) { + spdlog::error("Host: retry failed: {}", e.what()); + } return false; }, RETRY_DELAY_MS); diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index e4340f77..f3e413a8 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -70,11 +70,14 @@ void Tray::onAdd(std::unique_ptr& item) { } items_.push_back(item.get()); + item->event_box.signal_show().connect([this] { dp.emit(); }); + item->event_box.signal_hide().connect([this] { dp.emit(); }); + + // After this point `item` may be erased/invalidated by the ignore-list check; + // do not touch it again below. spdlog::debug("Tray::onAdd deferred check - checking ignore list"); host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); - item->event_box.signal_show().connect([this] { dp.emit(); }); - item->event_box.signal_hide().connect([this] { dp.emit(); }); dp.emit(); } diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 037a61e1..b281b0f5 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -168,25 +168,29 @@ auto IPC::start() -> void { send("window-rules/get-focused-output", {}); std::thread([self = shared_from_this()] { - auto sock = connect(); + try { + auto sock = connect(); - { - Json::Value json; - json["method"] = "window-rules/events/watch"; + { + Json::Value json; + json["method"] = "window-rules/events/watch"; - 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"); - return; + 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"); + return; + } } - } - while (auto json = self->receive(sock)) { - auto ev = json["event"].asString(); - spdlog::debug("Wayfire IPC: received event \"{}\"", ev); - self->root_event_handler(ev, json); + while (auto json = self->receive(sock)) { + auto ev = json["event"].asString(); + spdlog::debug("Wayfire IPC: received event \"{}\"", ev); + self->root_event_handler(ev, json); + } + } catch (const std::exception& e) { + spdlog::error("Wayfire IPC event thread stopped: {}", e.what()); } }).detach(); } diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 06a3dd62..876c199d 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -118,15 +118,23 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* // find form-factor const auto* devid = wp_properties_get(properties, "device.id"); - spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); + if (devid != nullptr) { + spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); - auto* dev = static_cast(wp_object_manager_lookup( - self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", devid, nullptr)); + g_autoptr(WpDevice) dev = static_cast( + wp_object_manager_lookup(self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, + "bound-id", "=s", devid, nullptr)); - if (const auto* ff = - wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), "device.form-factor")) { - self->form_factor_ = ff; - spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); + const gchar* ff = dev != nullptr ? wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), + "device.form-factor") + : nullptr; + + if (ff != nullptr) { + self->form_factor_ = ff; + spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); + } else { + self->form_factor_ = ""; + } } else { self->form_factor_ = ""; } @@ -614,7 +622,10 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { step = config_["scroll-step"].asDouble(); } if (config_["max-volume"].isDouble()) { - maxVolume = config_["max-volume"].asDouble(); + // {volume} is displayed as cubic-percent (pow(volume_, 3) * 100), while volume_/newVol are + // linear gains. Map the documented cubic-percent ceiling into the linear domain the clamp + // operates in, restoring the 0.15.0 cap semantics (e.g. 130 -> cbrt(1.3) linear -> 130%). + maxVolume = cbrt(config_["max-volume"].asDouble() / 100.0); } double vol = volume_; diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 206a6b0e..62371c15 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -627,7 +627,7 @@ void Task::update() { if (markup) button.set_tooltip_markup(txt); else - button.set_tooltip_markup(txt); + button.set_tooltip_text(txt); } } diff --git a/src/util/transform_8bit_to_rgba.cpp b/src/util/transform_8bit_to_rgba.cpp index dc87a88e..b354a640 100644 --- a/src/util/transform_8bit_to_rgba.cpp +++ b/src/util/transform_8bit_to_rgba.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace fs = std::filesystem; struct TransformResult { @@ -12,11 +14,12 @@ struct TransformResult { TransformResult transform_8bit_to_hex(const std::string& file_path) { std::ifstream f(file_path, std::ios::in | std::ios::binary); - const auto size = fs::file_size(file_path); - std::string result(size, '\0'); - if (!f.is_open() || !f.good()) { + std::error_code ec; + const auto size = fs::file_size(file_path, ec); + if (ec || !f.is_open() || !f.good()) { throw std::runtime_error("Cannot open file: " + file_path); } + std::string result(size, '\0'); if (size == 0) { return {.css = result, .was_transformed = false};