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:
+1
-1
@@ -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<Cairo::Context>& cr);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ++
|
||||
|
||||
@@ -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:
|
||||
|
||||
```
|
||||
|
||||
+8
-4
@@ -3,6 +3,7 @@
|
||||
#include <cairomm/context.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -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<long>(config_["interval"].asDouble() * 1000))
|
||||
: 1000L * static_cast<long>(interval))) {
|
||||
graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw));
|
||||
graph_.set_name(name);
|
||||
if (!id.empty()) {
|
||||
|
||||
+11
-6
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+27
-3
@@ -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-<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("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() {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#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<int>(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);
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<size_t>(m_workspaceManager.maxWindows()) >= m_windowMap.size())
|
||||
? m_windowMap.end()
|
||||
: m_windowMap.begin() + m_workspaceManager.maxWindows();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+22
-7
@@ -126,7 +126,12 @@ void MultipleImageStrategy::setupAndDraw() {
|
||||
bool has_onclick = !data.on_click.empty();
|
||||
|
||||
Glib::RefPtr<Gdk::Pixbuf> 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<Gdk::Pixbuf> pixbuf;
|
||||
if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) {
|
||||
Glib::RefPtr<Gdk::Pixbuf> 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);
|
||||
|
||||
@@ -86,7 +86,6 @@ auto isCommonFormatIcons(const Json::Value& config) -> bool {
|
||||
auto keyStateToIcons(const Json::Value& config)
|
||||
-> std::unordered_map<std::string, std::vector<std::string>> {
|
||||
std::unordered_map<std::string, std::vector<std::string>> key_icon_states;
|
||||
std::vector<std::string> default_icons = {"unlocked", "locked"};
|
||||
|
||||
if (isCommonFormatIcons(config)) {
|
||||
std::vector<std::string> 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<std::string>{"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;
|
||||
|
||||
@@ -204,20 +204,15 @@ void IPC::parseIPC(const std::string& line) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EventHandler*> handlers_to_notify;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include <cmath>
|
||||
|
||||
#include "modules/memory.hpp"
|
||||
|
||||
namespace {
|
||||
const std::unordered_map<std::string, float> 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<std::string, float> 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);
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
|
||||
+33
-21
@@ -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>(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<bool, Glib::IOCondition const&> 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>(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<bool, Glib::IOCondition const&> 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<Disconnected>(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>(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<bool, Glib::IOCondition const&> 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>(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<bool, Glib::IOCondition const&> 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<Disconnected>(ctx_));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<PlayerInfo> {
|
||||
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)));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<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;
|
||||
},
|
||||
RETRY_DELAY_MS);
|
||||
|
||||
@@ -70,11 +70,14 @@ void Tray::onAdd(std::unique_ptr<Item>& 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<WpDevice*>(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<WpDevice*>(
|
||||
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_;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <iomanip>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
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};
|
||||
|
||||
Reference in New Issue
Block a user