From 8ff8ceeca271dc50c2bd3343638726e868572da5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:36:31 +0200 Subject: [PATCH 1/3] fix(backlight): also match the leds subsystem for keyboard backlights The backlight module only enumerated and monitored the udev "backlight" subsystem, so keyboard-backlight LEDs in the "leds" class (e.g. white:kbd_backlight, platform::kbd_backlight) were never discovered and the module fell back to the default when pointed at one. Enumerate and monitor the "leds" subsystem in addition to "backlight". Those LEDs expose the same brightness/max_brightness attributes, so the read path is unchanged. Each device now records its subsystem so the login1 SetBrightness call targets the correct one. Automatic device selection still prefers a "backlight" device and only falls back to a "leds" device when named explicitly or when no screen backlight exists. Fixes #2848. --- include/util/backlight_backend.hpp | 8 +++-- man/waybar-backlight-slider.5.scd | 3 +- man/waybar-backlight.5.scd | 3 +- src/util/backlight_backend.cpp | 50 ++++++++++++++++++++++++------ 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/include/util/backlight_backend.hpp b/include/util/backlight_backend.hpp index ba3ccca7..8a81505c 100644 --- a/include/util/backlight_backend.hpp +++ b/include/util/backlight_backend.hpp @@ -27,9 +27,11 @@ namespace waybar::util { class BacklightDevice { public: BacklightDevice() = default; - BacklightDevice(std::string name, int actual, int max, bool powered); + BacklightDevice(std::string name, int actual, int max, bool powered, + std::string subsystem = "backlight"); std::string name() const; + std::string subsystem() const; int get_actual() const; void set_actual(int actual); int get_max() const; @@ -45,6 +47,7 @@ class BacklightDevice { int actual_ = 1; int max_ = 1; bool powered_ = true; + std::string subsystem_ = "backlight"; }; class BacklightBackend { @@ -70,7 +73,8 @@ class BacklightBackend { std::mutex udev_thread_mutex_; private: - void set_brightness_internal(const std::string& device_name, int brightness, int max_brightness); + void set_brightness_internal(const std::string& device_name, int brightness, int max_brightness, + const std::string& subsystem = "backlight"); std::function on_updated_cb_; std::chrono::milliseconds polling_interval_; diff --git a/man/waybar-backlight-slider.5.scd b/man/waybar-backlight-slider.5.scd index d357ff80..05e48b26 100644 --- a/man/waybar-backlight-slider.5.scd +++ b/man/waybar-backlight-slider.5.scd @@ -29,7 +29,8 @@ The brightness can be controlled by dragging the slider across the bar or clicki *device*: ++ typeof: string ++ - The name of the preferred device to control. If left empty, a device will be chosen automatically. + The name of the preferred device to control. If left empty, a device will be chosen automatically. ++ + Both screen backlights (the udev *backlight* subsystem) and keyboard backlights (LEDs in the udev *leds* subsystem, e.g. *white:kbd_backlight*) are supported; name such an LED here to control it. When left empty, a screen backlight is always preferred for automatic selection. *interval*: ++ typeof: uint ++ diff --git a/man/waybar-backlight.5.scd b/man/waybar-backlight.5.scd index 5a4d30db..e3414c6d 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -17,7 +17,8 @@ The *backlight* module displays the current backlight level. *device*: ++ typeof: string ++ - The name of the preferred backlight device to display. If left empty, a device will be chosen automatically. + The name of the preferred backlight device to display. If left empty, a device will be chosen automatically. ++ + Both screen backlights (the udev *backlight* subsystem) and keyboard backlights (LEDs in the udev *leds* subsystem, e.g. *white:kbd_backlight*) are supported; name such an LED here to control it. When left empty, a screen backlight is always preferred for automatic selection. *format*: ++ typeof: string ++ diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp index 61eb9b43..bf669f15 100644 --- a/src/util/backlight_backend.cpp +++ b/src/util/backlight_backend.cpp @@ -73,6 +73,7 @@ static void upsert_device(std::vector& devices, udev_device* de const char* actual = udev_device_get_sysattr_value(dev, actual_brightness_attr); const char* max = udev_device_get_sysattr_value(dev, "max_brightness"); const char* power = udev_device_get_sysattr_value(dev, "bl_power"); + const char* subsystem = udev_device_get_subsystem(dev); auto found = std::find_if(devices.begin(), devices.end(), [name](const BacklightDevice& device) { return device.name() == name; @@ -111,13 +112,18 @@ static void upsert_device(std::vector& devices, udev_device* de if (power != nullptr) power_bool = std::stoi(power) == 0; } catch (const std::exception&) { } - devices.emplace_back(name, actual_int, max_int, power_bool); + devices.emplace_back(name, actual_int, max_int, power_bool, + subsystem != nullptr ? subsystem : "backlight"); } } static void enumerate_devices(std::vector& devices, udev* udev) { std::unique_ptr enumerate{udev_enumerate_new(udev)}; udev_enumerate_add_match_subsystem(enumerate.get(), "backlight"); + // Also enumerate keyboard-backlight LEDs (e.g. "white:kbd_backlight"), which + // live in the "leds" subsystem but expose the same brightness/max_brightness + // attributes the read path uses. + udev_enumerate_add_match_subsystem(enumerate.get(), "leds"); udev_enumerate_scan_devices(enumerate.get()); udev_list_entry* enum_devices = udev_enumerate_get_list_entry(enumerate.get()); udev_list_entry* dev_list_entry; @@ -129,11 +135,18 @@ static void enumerate_devices(std::vector& devices, udev* udev) } } -BacklightDevice::BacklightDevice(std::string name, int actual, int max, bool powered) - : name_(std::move(name)), actual_(actual), max_(max), powered_(powered) {} +BacklightDevice::BacklightDevice(std::string name, int actual, int max, bool powered, + std::string subsystem) + : name_(std::move(name)), + actual_(actual), + max_(max), + powered_(powered), + subsystem_(std::move(subsystem)) {} std::string BacklightDevice::name() const { return name_; } +std::string BacklightDevice::subsystem() const { return subsystem_; } + int BacklightDevice::get_actual() const { return actual_; } void BacklightDevice::set_actual(int actual) { actual_ = actual; } @@ -178,6 +191,10 @@ BacklightBackend::BacklightBackend(std::chrono::milliseconds interval, check_nn(mon.get(), "udev monitor new failed"); check_gte(udev_monitor_filter_add_match_subsystem_devtype(mon.get(), "backlight", nullptr), 0, "udev failed to add monitor filter: "); + // Also monitor the "leds" subsystem so keyboard-backlight changes are + // reflected live, mirroring the enumeration above. + check_gte(udev_monitor_filter_add_match_subsystem_devtype(mon.get(), "leds", nullptr), 0, + "udev failed to add monitor filter: "); udev_monitor_enable_receiving(mon.get()); auto udev_fd = udev_monitor_get_fd(mon.get()); @@ -235,9 +252,22 @@ const BacklightDevice* BacklightBackend::best_device(const std::vectorget_max(); const auto abs_val = static_cast(std::round(brightness * max / 100.0F)); - set_brightness_internal(best->name(), abs_val, best->get_max()); + set_brightness_internal(best->name(), abs_val, best->get_max(), best->subsystem()); } } @@ -275,12 +305,12 @@ void BacklightBackend::set_brightness(const std::string& preferred_device, Chang const int new_brightness = change_type == ChangeType::Increase ? best->get_actual() + abs_step : best->get_actual() - abs_step; - set_brightness_internal(best->name(), new_brightness, max); + set_brightness_internal(best->name(), new_brightness, max, best->subsystem()); } } void BacklightBackend::set_brightness_internal(const std::string& device_name, int brightness, - int max_brightness) { + int max_brightness, const std::string& subsystem) { if (!login_proxy_) { spdlog::error("Login proxy not available, cannot set brightness"); return; @@ -289,7 +319,7 @@ void BacklightBackend::set_brightness_internal(const std::string& device_name, i brightness = std::clamp(brightness, 0, max_brightness); auto call_args = Glib::VariantContainerBase( - g_variant_new("(ssu)", "backlight", device_name.c_str(), brightness)); + g_variant_new("(ssu)", subsystem.c_str(), device_name.c_str(), brightness)); login_proxy_->call_sync("SetBrightness", call_args); } From b0b46ec039199d99c36a0d6637e13e292d66fbdc Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:36:40 +0200 Subject: [PATCH 2/3] fix(sway/ipc): reconnect on disconnect instead of breaking + CPU-spinning When sway's event-subscription send buffer overflows during an event flood, sway closes the client connection. The sway IPC event worker (SleeperThread running handleEvent -> recv) then threw on every iteration and the SleeperThread immediately re-invoked it, leaving the sway modules broken while busy-looping on a dead socket and pegging a CPU. Mirror the niri backend's reconnect loop: on a read/EOF/parse error from the event socket, close the old connection, back off for a couple of seconds (so we don't busy-spin), re-open the socket and replay the same subscriptions, then resume. A running_ flag set at the start of teardown makes the worker bail out cleanly instead of reconnecting to a socket that is being closed on purpose. The IPC message protocol and event parsing are unchanged. Fixes #3166. --- include/modules/sway/ipc/client.hpp | 10 ++++++ src/modules/sway/ipc/client.cpp | 52 ++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/include/modules/sway/ipc/client.hpp b/include/modules/sway/ipc/client.hpp index 2665caf9..f506b0bf 100644 --- a/include/modules/sway/ipc/client.hpp +++ b/include/modules/sway/ipc/client.hpp @@ -2,10 +2,12 @@ #include +#include #include #include #include #include +#include #include "ipc.hpp" #include "util/SafeSignal.hpp" @@ -43,6 +45,14 @@ class Ipc { struct ipc_response send(int fd, uint32_t type, const std::string& payload = ""); struct ipc_response recv(int fd); + // Re-establish the event socket and re-subscribe after sway drops us, backing + // off between attempts so we don't busy-loop while sway is unavailable. + void reconnectEvent(); + + std::string socketPath_; + std::vector subscribed_events_; + std::atomic running_{true}; + util::ScopedFd fd_; util::ScopedFd fd_event_; std::mutex mutex_; diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index dcbe7fa3..ce67279c 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -8,12 +8,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include "modules/sway/ipc/ipc.hpp" @@ -41,12 +43,15 @@ void sendAll(int fd, const char* data, size_t size, const char* what) { } // namespace Ipc::Ipc() { - const std::string socketPath = getSocketPath(); - fd_ = util::ScopedFd(open(socketPath)); - fd_event_ = util::ScopedFd(open(socketPath)); + socketPath_ = getSocketPath(); + fd_ = util::ScopedFd(open(socketPath_)); + fd_event_ = util::ScopedFd(open(socketPath_)); } Ipc::~Ipc() { + // Signal the worker before stopping it so an in-flight recv/reconnect bails + // out instead of trying to reconnect to a socket we're tearing down. + running_ = false; thread_.stop(); if (fd_ > 0) { @@ -191,11 +196,48 @@ void Ipc::subscribe(const std::string& payload) { if (res.payload != "{\"success\": true}") { throw std::runtime_error("Unable to subscribe ipc event"); } + // Remember the subscription so we can replay it if we have to reconnect. + subscribed_events_.push_back(payload); +} + +void Ipc::reconnectEvent() { + // Sway closed our event connection (typically because its send buffer filled + // up during an event flood). Re-establish the socket and re-subscribe to the + // same events, backing off between attempts so we don't busy-loop and peg a + // CPU while sway is unavailable or keeps dropping us. + while (running_) { + std::this_thread::sleep_for(std::chrono::seconds(2)); + if (!running_) { + return; + } + try { + fd_event_.reset(open(socketPath_)); + for (const auto& payload : subscribed_events_) { + const auto res = Ipc::send(fd_event_, IPC_SUBSCRIBE, payload); + if (res.payload != "{\"success\": true}") { + throw std::runtime_error("Unable to re-subscribe ipc event"); + } + } + spdlog::info("Reconnected to sway IPC event socket"); + return; + } catch (const std::exception& e) { + spdlog::warn("Failed to reconnect to sway IPC ({}), retrying", e.what()); + } + } } void Ipc::handleEvent() { - const auto res = Ipc::recv(fd_event_); - signal_event.emit(res); + try { + const auto res = Ipc::recv(fd_event_); + signal_event.emit(res); + } catch (const std::exception& e) { + if (!running_) { + // The Ipc is being torn down; the socket was closed on purpose. + return; + } + spdlog::warn("Lost sway IPC event connection ({}), reconnecting", e.what()); + reconnectEvent(); + } } } // namespace waybar::modules::sway From faf6a62bcf90124be0e65a8786ebc1c44a88c786 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:42:13 +0200 Subject: [PATCH 3/3] fix(network): detect ethernet cable unplug again (carrier/operstate) PR #4190 (merged as 93d85a0) reworked getNetworkState() so the rfkill "disabled" state is evaluated whenever the module has no carrier. Because the module always watches an RFKILL_TYPE_WLAN switch, a wired ethernet module whose cable is unplugged (carrier lost) would return "disabled" instead of "disconnected" whenever the system's WLAN radio happened to be rfkill-blocked. With no format-disabled configured, that state falls back to plain "format", so the interface kept looking connected after unplug. rfkill only concerns wireless radios, so only honor it when there is no interface at all or the current interface is actually wireless (detected via /sys/class/net//phy80211 or /wireless). A wired interface that lost its carrier now correctly reports "disconnected", while wifi rfkill display from #4190 is preserved. Fixes #4364. --- include/modules/network.hpp | 1 + src/modules/network.cpp | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/include/modules/network.hpp b/include/modules/network.hpp index abab16c2..a74ffda2 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -51,6 +51,7 @@ class Network : public ALabel { bool matchInterface(const std::string& ifname, const std::vector& altnames, std::string& matched) const; auto getInfo() -> void; + bool isWireless() const; const std::string getNetworkState() const; void clearIface(); std::optional> readBandwidthUsage(); diff --git a/src/modules/network.cpp b/src/modules/network.cpp index f69d4f33..7b4dc15f 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -288,6 +289,19 @@ void waybar::modules::Network::worker() { }; } +bool waybar::modules::Network::isWireless() const { + // The rfkill switch we monitor (and thus the "disabled" state) only applies + // to wireless radios. An interface is wireless if the kernel exposes an + // 802.11 phy (cfg80211/mac80211) or a legacy "wireless" node for it in sysfs. + if (ifname_.empty()) { + return false; + } + const auto base = "/sys/class/net/" + ifname_; + std::error_code ec; + return std::filesystem::exists(base + "/phy80211", ec) || + std::filesystem::exists(base + "/wireless", ec); +} + const std::string waybar::modules::Network::getNetworkState() const { if (ifid_ == -1 || !carrier_) { #ifdef WANT_RFKILL @@ -295,7 +309,14 @@ const std::string waybar::modules::Network::getNetworkState() const { if (config_["rfkill"].isBool()) { display_rfkill = config_["rfkill"].asBool(); } - if (rfkill_.getState() && display_rfkill) return "disabled"; + // The rfkill switch is for wireless (WLAN) radios only, so it must not mask + // a wired interface that merely lost its carrier (e.g. an unplugged ethernet + // cable): such an interface has to report "disconnected", not "disabled", + // otherwise cable-unplug detection breaks on ethernet modules (#4364). + // Only honor rfkill when there is no interface or the interface is wireless. + if (rfkill_.getState() && display_rfkill && (ifname_.empty() || isWireless())) { + return "disabled"; + } #endif return "disconnected"; }