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-<name>.<class> 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) <noreply@anthropic.com>
This commit is contained in:
Alex
2026-07-04 02:14:13 +02:00
co-authored by Claude Opus 4.8
parent 482cfab64e
commit f72f84e011
35 changed files with 377 additions and 192 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ class AGraph : public AModule {
void addValue(const int n); void addValue(const int n);
const std::chrono::seconds interval_; const std::chrono::milliseconds interval_;
bool onDraw(const Cairo::RefPtr<Cairo::Context>& cr); bool onDraw(const Cairo::RefPtr<Cairo::Context>& cr);
+2
View File
@@ -86,6 +86,8 @@ class AModule : public IModule {
Gtk::EventBox event_box_; Gtk::EventBox event_box_;
virtual void setCursor(std::string const& c); 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 handleToggle(GdkEventButton* const& ev);
virtual bool handleMouseEnter(GdkEventCrossing* const& ev); virtual bool handleMouseEnter(GdkEventCrossing* const& ev);
+3 -3
View File
@@ -38,9 +38,9 @@ class IdleInhibitor : public ALabel {
struct zwp_idle_inhibitor_v1* idle_inhibitor_; struct zwp_idle_inhibitor_v1* idle_inhibitor_;
int pid_; int pid_;
bool dynamicTimeout; bool dynamicTimeout = false;
short timeout; double timeout;
short timeout_step; double timeout_step;
bool wait_for_activity_; bool wait_for_activity_;
}; };
+1 -1
View File
@@ -76,7 +76,7 @@ screensaver, also known as "presentation mode".
typeof: double ++ typeof: double ++
The number of minutes the inhibition should last. The number of minutes the inhibition should last.
*timeout_step*: ++ *timeout-step*: ++
typeof: double ++ typeof: double ++
default: 10 ++ default: 10 ++
The number of minutes to add or subtract when scrolling (when dynamic timeouts are enabled). The number of minutes to add or subtract when scrolling (when dynamic timeouts are enabled).
+2 -2
View File
@@ -196,9 +196,9 @@ Addressed by *network*
*{bandwidthTotalBytes}*: Instant total speed in bytes/seconds. *{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. *{linkSpeed}*: Ethernet link speed.
+5
View File
@@ -40,6 +40,11 @@ Addressed by *niri/window*
default: false ++ default: false ++
Enables this module to consume all left over space dynamically. 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 # FORMAT REPLACEMENTS
See the output of "niri msg windows" for examples See the output of "niri msg windows" for examples
+5
View File
@@ -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. 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. 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: Example of disabling the cursor on a custom module:
``` ```
+8 -4
View File
@@ -3,6 +3,7 @@
#include <cairomm/context.h> #include <cairomm/context.h>
#include <fmt/format.h> #include <fmt/format.h>
#include <algorithm>
#include <cmath> #include <cmath>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
@@ -17,10 +18,13 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st
: AModule(config, name, id, : AModule(config, name, id,
config["format-alt"].isString() || config["menu"].isString() || enable_click, config["format-alt"].isString() || config["menu"].isString() || enable_click,
enable_scroll), enable_scroll),
interval_(config_["interval"] == "once" interval_(
? std::chrono::seconds::max() config_["interval"] == "once"
: std::chrono::seconds( ? std::chrono::milliseconds::max()
config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)) { : std::chrono::milliseconds(
config_["interval"].isNumeric()
? std::max(1L, static_cast<long>(config_["interval"].asDouble() * 1000))
: 1000L * static_cast<long>(interval))) {
graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw)); graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw));
graph_.set_name(name); graph_.set_name(name);
if (!id.empty()) { if (!id.empty()) {
+5
View File
@@ -90,6 +90,7 @@ auto AIconLabel::update() -> void {
label_.set_markup(cleanLabel); label_.set_markup(cleanLabel);
if (iconLabel.front() == '/') { if (iconLabel.front() == '/') {
try {
int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); 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 pixbuf = Gdk::Pixbuf::create_from_file(iconLabel, scaled_icon_size, scaled_icon_size);
@@ -97,6 +98,10 @@ auto AIconLabel::update() -> void {
image_.get_window()); image_.get_window());
image_.set(surface); image_.set(surface);
image_.set_visible(true); 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 { } else {
image_.set_from_icon_name(iconLabel, Gtk::ICON_SIZE_INVALID); image_.set_from_icon_name(iconLabel, Gtk::ICON_SIZE_INVALID);
image_.set_visible(true); image_.set_visible(true);
+25
View File
@@ -85,6 +85,13 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std::
} }
} else if (config_["cursor"].isString()) { } else if (config_["cursor"].isString()) {
setCursor(config_["cursor"].asString()); 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 { } else {
spdlog::warn("unknown cursor option configured on module {}", name_); 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) { bool AModule::handleMouseEnter(GdkEventCrossing* const& e) {
if (auto* module = event_box_.get_child(); module != nullptr) { if (auto* module = event_box_.get_child(); module != nullptr) {
module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT);
+1 -1
View File
@@ -609,7 +609,7 @@ waybar::modules::Battery::getInfos() {
if (status == "Discharging" && time_to_empty_now_exists) { if (status == "Discharging" && time_to_empty_now_exists) {
if (time_to_empty_now != 0) time_remaining = (float)time_to_empty_now / 3600.0f; 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) { } 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) { } else if (status == "Charging" && time_to_full_now_exists) {
if (time_to_full_now_exists && (time_to_full_now != 0)) if (time_to_full_now_exists && (time_to_full_now != 0))
time_remaining = -(float)time_to_full_now / 3600.0f; time_remaining = -(float)time_to_full_now / 3600.0f;
+33 -4
View File
@@ -87,14 +87,29 @@ auto isChildPath(const std::string& child, const std::string& parent) -> bool {
return child.starts_with(parent); 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<unsigned char> { auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional<unsigned char> {
GVariantBuilder builder; GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
GError* error = nullptr; 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 = GVariant* gvar =
g_dbus_proxy_call_sync(proxy_char, "ReadValue", g_variant_new("(a{sv})", &builder), 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) { if (error != nullptr) {
g_error_free(error); g_error_free(error);
return std::nullopt; return std::nullopt;
@@ -538,7 +553,9 @@ auto waybar::modules::Bluetooth::processBatteryServiceCharacteristics(
if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) { if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) {
peripheral_battery = battery_value.value(); 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(); central_battery = battery_value.value();
} }
} }
@@ -564,8 +581,20 @@ auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, Device
g_object_unref(proxy_device); g_object_unref(proxy_device);
device_info.battery_percentage = getDeviceBatteryPercentage(object); device_info.battery_percentage = getDeviceBatteryPercentage(object);
getDeviceGattBatteryLevels(object, device_info.battery_percentage, // Only perform the (synchronous, over-the-air) GATT battery scan when the peripheral battery
device_info.battery_percentage_peripheral); // 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<unsigned char> 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; return true;
} }
+6 -2
View File
@@ -568,8 +568,12 @@ void waybar::modules::Clock::tz_down() {
tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1; tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1;
} }
void waybar::modules::Clock::action_exec(const std::string& action) { void waybar::modules::Clock::action_exec(const std::string& action) {
auto cmd = action.substr(strlen("exec ")); const auto pos = action.find(" ");
pid_children_.push_back(util::command::forkExec(cmd)); 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 #ifdef HAVE_LANGINFO_1STDAY
+26 -2
View File
@@ -223,6 +223,17 @@ auto waybar::modules::Custom::update() -> void {
for (auto const& c : class_) { for (auto const& c : class_) {
style->add_class(c); style->add_class(c);
} }
// Mirror the dynamic script classes onto box_, which now carries the
// #custom-<name> widget name (see AIconLabel), so #custom-<name>.<class>
// 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("flat");
style->add_class("text-button"); style->add_class("text-button");
style->add_class(MODULE_CLASS); style->add_class(MODULE_CLASS);
@@ -230,14 +241,20 @@ auto waybar::modules::Custom::update() -> void {
image_style->add_class("image-button"); image_style->add_class("image-button");
event_box_.show(); event_box_.show();
if (!image_path_.empty()) { if (!image_path_.empty()) {
auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); try {
auto pixbuf =
Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_);
image_.set(pixbuf); 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()) { } else if (!image_name_.empty()) {
image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID); image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID);
image_.set_pixel_size(app_icon_size_); image_.set_pixel_size(app_icon_size_);
} }
image_.set_visible(!image_name_.empty() || !image_path_.empty());
label_.set_visible(!str.empty()); label_.set_visible(!str.empty());
} }
} catch (const fmt::format_error& e) { } catch (const fmt::format_error& e) {
@@ -251,6 +268,13 @@ auto waybar::modules::Custom::update() -> void {
} }
// Call parent update // Call parent update
AIconLabel::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() { void waybar::modules::Custom::parseOutputRaw() {
+21
View File
@@ -2,6 +2,11 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <algorithm>
#include <cmath>
#include <stdexcept>
#include <string>
#include "util/scope_guard.hpp" #include "util/scope_guard.hpp"
waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id,
@@ -228,6 +233,22 @@ void waybar::modules::CustomGraph::parseOutputRaw() {
tooltip_ = validated_line; tooltip_ = validated_line;
} }
class_.clear(); 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<int>(std::clamp(std::lround(parsed), 0L, 100L));
}
} catch (const std::exception&) {
percentage_ = 0;
}
} else if (i == 1) { } else if (i == 1) {
if (config_["escape"].isBool() && config_["escape"].asBool()) { if (config_["escape"].isBool() && config_["escape"].asBool()) {
tooltip_ = Glib::Markup::escape_text(validated_line); tooltip_ = Glib::Markup::escape_text(validated_line);
+1 -3
View File
@@ -138,9 +138,7 @@ void Window::handle_frame() {
box_.set_visible(true); box_.set_visible(true);
} else { } else {
box_.get_style_context()->remove_class("active"); box_.get_style_context()->remove_class("active");
if (hide_inactive_) { box_.set_visible(!hide_inactive_);
box_.set_visible(false);
}
} }
} }
} }
+13 -26
View File
@@ -53,14 +53,23 @@ auto Language::update() -> void {
std::string tooltipContent = std::string{}; std::string tooltipContent = std::string{};
bool tooltip_enabled = tooltipEnabled(); bool tooltip_enabled = tooltipEnabled();
if (tooltip_enabled) { if (tooltip_enabled) {
if (config_.isMember("tooltip-format")) { // Default to "{long}" when no tooltip-format is provided, matching the man page
auto tooltip_format = config_["tooltip-format"].asString(); auto tooltip_format =
config_.isMember("tooltip-format") ? config_["tooltip-format"].asString() : "{long}";
if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) { if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) {
const auto propName = "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()); 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)) { } else if (config_.isMember("tooltip-format-" + layout_.short_description)) {
const auto propName = "tooltip-format-" + layout_.short_description; const auto propName = "tooltip-format-" + layout_.short_description;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); 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 { } else {
tooltipContent = tooltipContent =
trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name),
@@ -68,10 +77,6 @@ auto Language::update() -> void {
fmt::arg("shortDescription", layout_.short_description), fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant))); fmt::arg("variant", layout_.variant)));
} }
} else {
// if no tooltip format is provided, use the same text as the module
tooltipContent = layoutName;
}
spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent);
} }
@@ -85,24 +90,6 @@ auto Language::update() -> void {
label_.hide(); 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(); ALabel::update();
} }
+2 -1
View File
@@ -438,7 +438,8 @@ void Workspace::update(const std::string& workspace_icon, const std::string& wor
auto windowSeparator = m_workspaceManager.getWindowSeparator(); auto windowSeparator = m_workspaceManager.getWindowSeparator();
auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold(); auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold();
auto end_it = m_workspaceManager.maxWindows() == 0 auto end_it = (m_workspaceManager.maxWindows() <= 0 ||
static_cast<size_t>(m_workspaceManager.maxWindows()) >= m_windowMap.size())
? m_windowMap.end() ? m_windowMap.end()
: m_windowMap.begin() + m_workspaceManager.maxWindows(); : m_windowMap.begin() + m_workspaceManager.maxWindows();
+18 -5
View File
@@ -44,7 +44,14 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar&
event_box_.signal_button_press_event().connect( event_box_.signal_button_press_event().connect(
sigc::mem_fun(*this, &IdleInhibitor::handleToggle)); sigc::mem_fun(*this, &IdleInhibitor::handleToggle));
// 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)); event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &IdleInhibitor::handleScroll));
}
// Add this to the modules list // Add this to the modules list
waybar::modules::IdleInhibitor::modules.push_back(this); 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) { 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 (e->button == 1) {
if (config_["dynamic-timeout"].asBool()) { if (dynamic) {
toggleStatus(1); toggleStatus(1);
} else { } else {
toggleStatus(); toggleStatus();
@@ -171,7 +181,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) {
} }
} }
} }
if (e->button == 3) { if (e->button == 3 && dynamic) {
toggleStatus(0); toggleStatus(0);
// Make all other idle inhibitor modules update // 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); toggleStatus(0);
timeout = config_["timeout"].asDouble(); timeout = config_["timeout"].asDouble();
} }
@@ -190,8 +200,11 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) {
} }
bool waybar::modules::IdleInhibitor::handleScroll(GdkEventScroll* e) { bool waybar::modules::IdleInhibitor::handleScroll(GdkEventScroll* e) {
if (!config_["dynamic-timeout"].asBool()) { // Accept both the documented "dynamic-timeouts" (plural) and the legacy
return true; // "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); auto dir = AModule::getScrollDir(e);
if (dir == SCROLL_DIR::NONE) { if (dir == SCROLL_DIR::NONE) {
+20 -5
View File
@@ -126,7 +126,12 @@ void MultipleImageStrategy::setupAndDraw() {
bool has_onclick = !data.on_click.empty(); bool has_onclick = !data.on_click.empty();
Glib::RefPtr<Gdk::Pixbuf> pixbuf; Glib::RefPtr<Gdk::Pixbuf> pixbuf;
try {
pixbuf = Gdk::Pixbuf::create_from_file(path, size_, size_); 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) { if (has_onclick) {
auto btn = images_data_[i].btn; auto btn = images_data_[i].btn;
@@ -234,22 +239,32 @@ SingleImageStrategy::SingleImageStrategy(const std::string& id, const Json::Valu
void SingleImageStrategy::update() { void SingleImageStrategy::update() {
if (config_["path"].isString()) { if (config_["path"].isString()) {
auto result = Config::tryExpandPath(config_["path"].asString(), ""); auto p = config_["path"].asString();
path_ = result.empty() ? "" : result.front(); 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()) { } else if (config_["exec"].isString()) {
output_ = util::command::exec(config_["exec"].asString(), ""); output_ = util::command::exec(config_["exec"].asString(), "");
parseOutputRaw(); parseOutputRaw();
// expand path if "~" or "$HOME" is present in original path // expand path if "~" or "$HOME" is present in original path
auto result = Config::tryExpandPath(path_, ""); auto result = Config::tryExpandPath(path_, "");
path_ = result.empty() ? "" : result.front(); path_ = (result.size() == 1) ? result.front() : path_;
} }
if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) {
Glib::RefPtr<Gdk::Pixbuf> pixbuf; Glib::RefPtr<Gdk::Pixbuf> pixbuf;
if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) {
int scaled_icon_size = size_ * image_.get_scale_factor(); int scaled_icon_size = size_ * image_.get_scale_factor();
try {
pixbuf = Gdk::Pixbuf::create_from_file(path_, scaled_icon_size, scaled_icon_size); 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(), auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(),
image_.get_window()); image_.get_window());
image_.set(surface); image_.set(surface);
+8 -17
View File
@@ -86,7 +86,6 @@ auto isCommonFormatIcons(const Json::Value& config) -> bool {
auto keyStateToIcons(const Json::Value& config) auto keyStateToIcons(const Json::Value& config)
-> std::unordered_map<std::string, std::vector<std::string>> { -> std::unordered_map<std::string, std::vector<std::string>> {
std::unordered_map<std::string, std::vector<std::string>> key_icon_states; std::unordered_map<std::string, std::vector<std::string>> key_icon_states;
std::vector<std::string> default_icons = {"unlocked", "locked"};
if (isCommonFormatIcons(config)) { if (isCommonFormatIcons(config)) {
std::vector<std::string> icons = { std::vector<std::string> icons = {
@@ -100,26 +99,18 @@ auto keyStateToIcons(const Json::Value& config)
return key_icon_states; return key_icon_states;
} }
bool found_any = false; const auto& format_icons = config["format-icons"];
for (const auto& key : std::vector<std::string>{"numlock", "capslock", "scrolllock"}) { for (const auto& key : std::vector<std::string>{"numlock", "capslock", "scrolllock"}) {
std::string map_key = key.substr(0, key.length() - 4); std::string map_key = key.substr(0, key.length() - 4);
map_key[0] = std::toupper(map_key[0]); map_key[0] = std::toupper(map_key[0]);
if (config["format-icons"].isObject() && config["format-icons"][key].isObject()) { std::string unlocked = "unlocked";
std::string unlocked = config["format-icons"][key]["unlocked"].isString() std::string locked = "locked";
? config["format-icons"][key]["unlocked"].asString() if (format_icons.isObject() && format_icons[key].isObject()) {
: "unlocked"; const auto& obj = format_icons[key];
std::string locked = config["format-icons"][key]["locked"].isString() if (obj["unlocked"].isString()) unlocked = obj["unlocked"].asString();
? config["format-icons"][key]["locked"].asString() if (obj["locked"].isString()) locked = obj["locked"].asString();
: "locked"; }
key_icon_states[map_key] = {unlocked, locked}; key_icon_states[map_key] = {unlocked, locked};
found_any = true;
}
}
if (!found_any) {
key_icon_states["Num"] = default_icons;
key_icon_states["Caps"] = default_icons;
key_icon_states["Scroll"] = default_icons;
} }
return key_icon_states; return key_icon_states;
+2 -7
View File
@@ -204,19 +204,14 @@ void IPC::parseIPC(const std::string& line) {
} }
} }
std::vector<EventHandler*> handlers_to_notify;
{ {
std::lock_guard<std::mutex> lock(callback_mutex_); std::lock_guard<std::mutex> lock(callback_mutex_);
for (auto& [ev, handler] : callbacks_) { for (auto& [ev, handler] : callbacks_) {
if (ev == "monitor") { if (ev == "monitor") {
handlers_to_notify.push_back(handler);
}
}
}
for (auto* handler : handlers_to_notify) {
handler->onEvent(root); handler->onEvent(root);
} }
}
}
return; return;
} }
+17 -14
View File
@@ -1,14 +1,14 @@
#include <cmath>
#include "modules/memory.hpp" #include "modules/memory.hpp"
namespace { namespace {
const std::unordered_map<std::string, float> kUnits = {{"kB", 1.000}, // /proc/meminfo reports values in kiB (1024 bytes) despite the 'kB' label.
{"kiB", 1.024}, // These divisors convert a kiB count to the requested unit.
{"MB", 1.000 * 1000.0}, const std::unordered_map<std::string, float> kUnits = {
{"MiB", 1.024 * 1024.0}, {"B", 1.0 / 1024.0}, {"kB", 1000.0 / 1024.0}, {"kiB", 1.0},
{"GB", 1.000 * 1000.0 * 1000.0}, {"MB", 1000000.0 / 1024.0}, {"MiB", 1024.0}, {"GB", 1000000000.0 / 1024.0},
{"GiB", 1.024 * 1024.0 * 1024.0}, {"GiB", 1024.0 * 1024.0}, {"TB", 1e12 / 1024.0}, {"TiB", 1024.0 * 1024.0 * 1024.0}};
{"TB", 1.000 * 1000.0 * 1000.0 * 1000.0},
{"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0}};
} }
waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) 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 divisor = kUnits.at(unit_);
float total_ram = memtotal / divisor; // Pre-round to 2 decimals so bare {total}/{used}/{avail} placeholders print
float total_swap = swaptotal / divisor; // cleanly, matching the 0.15.0 behavior (an explicit spec like {used:.1f}
float used_ram = (memtotal - memfree) / divisor; // still overrides this).
float used_swap = (swaptotal - swapfree) / divisor; float total_ram = 0.01f * std::round((memtotal / divisor) * 100.0f);
float available_ram = memfree / divisor; float total_swap = 0.01f * std::round((swaptotal / divisor) * 100.0f);
float available_swap = swapfree / divisor; 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 format = format_;
auto state = getState(used_ram_percentage); auto state = getState(used_ram_percentage);
+1 -1
View File
@@ -28,7 +28,7 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config)
connection_(nullptr, &mpd_connection_free), connection_(nullptr, &mpd_connection_free),
status_(nullptr, &mpd_status_free), status_(nullptr, &mpd_status_free),
song_(nullptr, &mpd_song_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()) { if (!config_["port"].isNull() && !config_["port"].isUInt()) {
spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_); spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_);
} }
+13 -1
View File
@@ -58,6 +58,7 @@ void Idle::entry() noexcept {
auto conn = ctx_->connection().get(); auto conn = ctx_->connection().get();
assert(conn != nullptr); assert(conn != nullptr);
try {
if (!mpd_send_idle_mask( if (!mpd_send_idle_mask(
conn, static_cast<mpd_idle>(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { conn, static_cast<mpd_idle>(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) {
ctx_->checkErrors(conn); ctx_->checkErrors(conn);
@@ -69,6 +70,10 @@ void Idle::entry() noexcept {
Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn),
Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); 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<Disconnected>(ctx_));
}
} }
void Idle::exit() noexcept { void Idle::exit() noexcept {
@@ -114,8 +119,10 @@ bool Idle::on_io(Glib::IOCondition const&) {
void Playing::entry() noexcept { void Playing::entry() noexcept {
timer(); timer();
idle();
spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval()); 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 { void Playing::exit() noexcept {
@@ -143,6 +150,7 @@ void Playing::idle() noexcept {
auto conn = ctx_->connection().get(); auto conn = ctx_->connection().get();
assert(conn != nullptr); assert(conn != nullptr);
try {
if (!mpd_send_idle_mask( if (!mpd_send_idle_mask(
conn, static_cast<mpd_idle>(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { conn, static_cast<mpd_idle>(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) {
ctx_->checkErrors(conn); ctx_->checkErrors(conn);
@@ -154,6 +162,10 @@ void Playing::idle() noexcept {
Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn),
Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); 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<Disconnected>(ctx_));
}
} }
bool Playing::on_timer() { bool Playing::on_timer() {
+6 -3
View File
@@ -385,6 +385,7 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye
if (mpris->player != nullptr) { if (mpris->player != nullptr) {
g_signal_handlers_disconnect_by_data(mpris->player, mpris); 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); g_clear_object(&mpris->player);
} }
mpris->player = playerctl_player_new_from_name(player_name, nullptr); mpris->player = playerctl_player_new_from_name(player_name, nullptr);
@@ -544,11 +545,12 @@ auto Mpris::getPlayerInfo() -> std::optional<PlayerInfo> {
if (error) goto errorexit; if (error) goto errorexit;
if (auto* album_artist_ = 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_); spdlog::debug("mpris[{}]: albumArtist = {}", info.name, album_artist_);
info.album_artist = album_artist_; info.album_artist = album_artist_;
g_free(album_artist_); g_free(album_artist_);
} }
if (error) goto errorexit;
if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) { if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) {
spdlog::debug("mpris[{}]: album = {}", info.name, album_); spdlog::debug("mpris[{}]: album = {}", info.name, album_);
@@ -745,7 +747,8 @@ auto Mpris::update() -> void {
if (tooltipEnabled()) { if (tooltipEnabled()) {
try { try {
auto tooltip_text = fmt::format( 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("status", info.status_string),
fmt::arg("artist", fmt::arg("artist",
std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))), std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))),
@@ -754,7 +757,7 @@ auto Mpris::update() -> void {
fmt::arg("album", fmt::arg("album",
std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))), std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))),
fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition), 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("player_icon", getIconFromJson(config_["player-icons"], info.name)),
fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string))); fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string)));
+7 -3
View File
@@ -78,7 +78,7 @@ void Window::doUpdate() {
updateAppIconName(appId, ""); updateAppIconName(appId, "");
if (tooltipEnabled()) label_.set_tooltip_markup(title); if (tooltipEnabled()) label_.set_tooltip_markup(sanitizedTitle);
const auto id = window["id"].asUInt64(); const auto id = window["id"].asUInt64();
const auto workspaceId = window["workspace_id"].asUInt64(); const auto workspaceId = window["workspace_id"].asUInt64();
@@ -93,11 +93,15 @@ void Window::doUpdate() {
oldAppId_ = appId; oldAppId_ = appId;
} }
} else { } else {
if (config_["show-empty"].asBool()) {
label_.show(); label_.show();
label_.set_markup(waybar::util::rewriteString( label_.set_markup(waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", ""), fmt::format(fmt::runtime(format_), fmt::arg("title", ""), fmt::arg("app_id", ""),
fmt::arg("app_id", "")), fmt::arg("col", -1), fmt::arg("max_col", -1)),
config_["rewrite"])); config_["rewrite"]));
} else {
label_.hide();
}
updateAppIconName("", ""); updateAppIconName("", "");
setClass("solo", false); setClass("solo", false);
+9 -3
View File
@@ -129,13 +129,15 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con
} }
if (!control_) { 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"); spdlog::error("river_control_v1 not advertised");
return;
} }
if (!seat_) { 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"); spdlog::error("wl_seat not advertised");
return;
} }
// Store the output this module belongs to; the river_output_status and // 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); button.set_relief(Gtk::RELIEF_NONE);
box_.pack_start(button, false, false, 0); 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()) if (set_tags.isArray() && !set_tags.empty())
button.signal_clicked().connect(sigc::bind( button.signal_clicked().connect(sigc::bind(
sigc::mem_fun(*this, &Tags::handle_primary_clicked), set_tags[tag].asUInt())); 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_); 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); zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this);
if (seat_) {
seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, 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_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this);
}
zriver_status_manager_v1_destroy(status_manager_); zriver_status_manager_v1_destroy(status_manager_);
status_manager_ = nullptr; status_manager_ = nullptr;
} }
void Tags::handle_primary_clicked(uint32_t tag) { void Tags::handle_primary_clicked(uint32_t tag) {
if (!control_ || !seat_) return;
// Send river command to select tag on left mouse click // Send river command to select tag on left mouse click
zriver_command_callback_v1* callback; zriver_command_callback_v1* callback;
zriver_control_v1_add_argument(control_, "set-focused-tags"); 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) { 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) { if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) {
// Send river command to toggle tag on right mouse click // Send river command to toggle tag on right mouse click
zriver_command_callback_v1* callback; zriver_command_callback_v1* callback;
+13 -1
View File
@@ -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 // Store the timeout connection so it is disconnected in ~Host, avoiding a
// use-after-free if the Host is destroyed before the retry fires. // use-after-free if the Host is destroyed before the retry fires.
host->retry_connection_ = Glib::signal_timeout().connect( host->retry_connection_ = Glib::signal_timeout().connect(
[host]() { [host]() -> bool {
if (host->watcher_ != nullptr) { if (host->watcher_ != nullptr) {
return false; return false;
} }
try {
auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION);
host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", "");
} catch (const Glib::Error& e) {
spdlog::error("Host: retry get_sync failed: {}", static_cast<std::string>(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; return false;
}, },
RETRY_DELAY_MS); RETRY_DELAY_MS);
+5 -2
View File
@@ -70,11 +70,14 @@ void Tray::onAdd(std::unique_ptr<Item>& item) {
} }
items_.push_back(item.get()); 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"); spdlog::debug("Tray::onAdd deferred check - checking ignore list");
host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); 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(); dp.emit();
} }
+4
View File
@@ -168,6 +168,7 @@ auto IPC::start() -> void {
send("window-rules/get-focused-output", {}); send("window-rules/get-focused-output", {});
std::thread([self = shared_from_this()] { std::thread([self = shared_from_this()] {
try {
auto sock = connect(); auto sock = connect();
{ {
@@ -188,6 +189,9 @@ auto IPC::start() -> void {
spdlog::debug("Wayfire IPC: received event \"{}\"", ev); spdlog::debug("Wayfire IPC: received event \"{}\"", ev);
self->root_event_handler(ev, json); self->root_event_handler(ev, json);
} }
} catch (const std::exception& e) {
spdlog::error("Wayfire IPC event thread stopped: {}", e.what());
}
}).detach(); }).detach();
} }
+16 -5
View File
@@ -118,18 +118,26 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber*
// find form-factor // find form-factor
const auto* devid = wp_properties_get(properties, "device.id"); const auto* devid = wp_properties_get(properties, "device.id");
if (devid != nullptr) {
spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid);
auto* dev = static_cast<WpDevice*>(wp_object_manager_lookup( g_autoptr(WpDevice) dev = static_cast<WpDevice*>(
self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", devid, nullptr)); wp_object_manager_lookup(self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY,
"bound-id", "=s", devid, nullptr));
if (const auto* ff = const gchar* ff = dev != nullptr ? wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev),
wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), "device.form-factor")) { "device.form-factor")
: nullptr;
if (ff != nullptr) {
self->form_factor_ = ff; self->form_factor_ = ff;
spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_);
} else { } else {
self->form_factor_ = ""; self->form_factor_ = "";
} }
} else {
self->form_factor_ = "";
}
} }
void waybar::modules::Wireplumber::updateSourceName(waybar::modules::Wireplumber* self, void waybar::modules::Wireplumber::updateSourceName(waybar::modules::Wireplumber* self,
@@ -614,7 +622,10 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) {
step = config_["scroll-step"].asDouble(); step = config_["scroll-step"].asDouble();
} }
if (config_["max-volume"].isDouble()) { 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_; double vol = volume_;
+1 -1
View File
@@ -627,7 +627,7 @@ void Task::update() {
if (markup) if (markup)
button.set_tooltip_markup(txt); button.set_tooltip_markup(txt);
else else
button.set_tooltip_markup(txt); button.set_tooltip_text(txt);
} }
} }
+6 -3
View File
@@ -3,6 +3,8 @@
#include <iomanip> #include <iomanip>
#include <regex> #include <regex>
#include <sstream> #include <sstream>
#include <stdexcept>
#include <system_error>
namespace fs = std::filesystem; namespace fs = std::filesystem;
struct TransformResult { struct TransformResult {
@@ -12,11 +14,12 @@ struct TransformResult {
TransformResult transform_8bit_to_hex(const std::string& file_path) { TransformResult transform_8bit_to_hex(const std::string& file_path) {
std::ifstream f(file_path, std::ios::in | std::ios::binary); std::ifstream f(file_path, std::ios::in | std::ios::binary);
const auto size = fs::file_size(file_path); std::error_code ec;
std::string result(size, '\0'); const auto size = fs::file_size(file_path, ec);
if (!f.is_open() || !f.good()) { if (ec || !f.is_open() || !f.good()) {
throw std::runtime_error("Cannot open file: " + file_path); throw std::runtime_error("Cannot open file: " + file_path);
} }
std::string result(size, '\0');
if (size == 0) { if (size == 0) {
return {.css = result, .was_transformed = false}; return {.css = result, .was_transformed = false};