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
+33 -4
View File
@@ -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<unsigned char> {
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<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;
}