From 0fdc4c11f3eab5b97c42116f69ac7656d8d3e194 Mon Sep 17 00:00:00 2001 From: Narice Date: Tue, 6 Dec 2022 18:40:38 +0100 Subject: [PATCH 001/416] feat(backlight): added percentexp as format option Humans don't perceive light linearly and so an exponential curve is more appropriate to gauge the perceived brightness of the screen. --- src/modules/backlight.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index aa734a4f..bf3ae226 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -179,9 +179,17 @@ auto waybar::modules::Backlight::update() -> void { if (best->get_powered()) { event_box_.show(); + const uint8_t percent = best->get_max() == 0 ? 100 : round(best->get_actual() * 100.0f / best->get_max()); + + const uint8_t percent_exp = + best->get_max() == 0 + ? 100 + : roundf(powf((float)best->get_actual() / best->get_max(), 1.0f / 2.718f) * 100); + label_.set_markup(fmt::format(format_, fmt::arg("percent", std::to_string(percent)), + fmt::arg("percentexp", std::to_string(percent_exp)), fmt::arg("icon", getIcon(percent)))); getState(percent); } else { From dbb8a881be2b44845a3dbff8cb3e10a66ecbc902 Mon Sep 17 00:00:00 2001 From: Narice Date: Tue, 6 Dec 2022 19:22:21 +0100 Subject: [PATCH 002/416] feat(backlight): Added icon_exp and changed syntax changed syntax of percentexp to percent_exp for readability --- src/modules/backlight.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index bf3ae226..2df0dc1f 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -189,8 +189,9 @@ auto waybar::modules::Backlight::update() -> void { : roundf(powf((float)best->get_actual() / best->get_max(), 1.0f / 2.718f) * 100); label_.set_markup(fmt::format(format_, fmt::arg("percent", std::to_string(percent)), - fmt::arg("percentexp", std::to_string(percent_exp)), - fmt::arg("icon", getIcon(percent)))); + fmt::arg("percent_exp", std::to_string(percent_exp)), + fmt::arg("icon", getIcon(percent)), + fmt::arg("icon_exp", getIcon(percent_exp)))); getState(percent); } else { event_box_.hide(); From 9315ccc4c6898509b64eea87f4c41d57f19a0a96 Mon Sep 17 00:00:00 2001 From: Savely Krasovsky Date: Fri, 5 May 2023 21:36:13 +0300 Subject: [PATCH 003/416] feat: added dbus handler to update current player it allows to send script a signal to shift current player after playerctld shift command --- resources/custom_modules/mediaplayer.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 1630d97c..6a6737ef 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -5,8 +5,10 @@ import sys import signal import gi import json +import dbus gi.require_version('Playerctl', '2.0') from gi.repository import Playerctl, GLib +from dbus.mainloop.glib import DBusGMainLoop logger = logging.getLogger(__name__) @@ -76,6 +78,17 @@ def signal_handler(sig, frame): sys.exit(0) +def dbus_signal_handler(*args, manager, **kwargs): + # there is no implicit way to get current player, + # so we need to create a new manager to get the right order of players + new_manager = Playerctl.PlayerManager() + if len(new_manager.props.player_names) > 0: + current_player_name = new_manager.props.player_names[0].name + for player in manager.props.players: + if player.props.player_name == current_player_name: + on_metadata(player, player.props.metadata, manager) + + def parse_arguments(): parser = argparse.ArgumentParser() @@ -111,6 +124,14 @@ def main(): signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGPIPE, signal.SIG_DFL) + signal.signal(signal.SIGUSR1, lambda *args: shift_signal_handler(*args, manager)) + + # use dbus to shift player (e.g. after playerctld shift) + DBusGMainLoop(set_as_default=True) + bus = dbus.SystemBus() + bus.add_signal_receiver(lambda *args, **kwargs: dbus_signal_handler(*args, **kwargs, manager=manager), + signal_name='Shift', + dbus_interface='org.waybar.Media') for player in manager.props.player_names: if arguments.player is not None and arguments.player != player.name: From 8ce569b114f9f149fb21376c15689e49d1617433 Mon Sep 17 00:00:00 2001 From: Savely Krasovsky Date: Fri, 5 May 2023 22:05:43 +0300 Subject: [PATCH 004/416] feat: choose another player after first was vanished --- resources/custom_modules/mediaplayer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 6a6737ef..390faaaa 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -57,6 +57,10 @@ def on_player_appeared(manager, player, selected_player=None): def on_player_vanished(manager, player): logger.info('Player has vanished') + if len(manager.props.players) > 0: + player = manager.props.players[0] + on_metadata(player, player.props.metadata, manager) + return sys.stdout.write('\n') sys.stdout.flush() From 9c161a8cbeddcb15a69ebbed0539028e3a31261d Mon Sep 17 00:00:00 2001 From: Savely Krasovsky Date: Fri, 5 May 2023 22:10:45 +0300 Subject: [PATCH 005/416] fix: shift only if there is more than one player --- resources/custom_modules/mediaplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 390faaaa..5055f806 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -86,7 +86,7 @@ def dbus_signal_handler(*args, manager, **kwargs): # there is no implicit way to get current player, # so we need to create a new manager to get the right order of players new_manager = Playerctl.PlayerManager() - if len(new_manager.props.player_names) > 0: + if len(new_manager.props.player_names) > 1: current_player_name = new_manager.props.player_names[0].name for player in manager.props.players: if player.props.player_name == current_player_name: From f5a9a4781a589a7c96eac2c0818a34fe145b9522 Mon Sep 17 00:00:00 2001 From: Savely Krasovsky Date: Fri, 5 May 2023 23:44:19 +0300 Subject: [PATCH 006/416] fix: use session bus instead of system --- resources/custom_modules/mediaplayer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 5055f806..41f6e061 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -132,10 +132,10 @@ def main(): # use dbus to shift player (e.g. after playerctld shift) DBusGMainLoop(set_as_default=True) - bus = dbus.SystemBus() + bus = dbus.SessionBus() bus.add_signal_receiver(lambda *args, **kwargs: dbus_signal_handler(*args, **kwargs, manager=manager), signal_name='Shift', - dbus_interface='org.waybar.Media') + dbus_interface='org.waybar.Player') for player in manager.props.player_names: if arguments.player is not None and arguments.player != player.name: From 70901b149e39b30a38a3d36187cdcb57303ff499 Mon Sep 17 00:00:00 2001 From: Savely Krasovsky Date: Sat, 6 May 2023 00:35:34 +0300 Subject: [PATCH 007/416] feat: register well-known name --- resources/custom_modules/mediaplayer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 41f6e061..dd3228c6 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -6,6 +6,7 @@ import signal import gi import json import dbus +import dbus.service gi.require_version('Playerctl', '2.0') from gi.repository import Playerctl, GLib from dbus.mainloop.glib import DBusGMainLoop @@ -131,11 +132,12 @@ def main(): signal.signal(signal.SIGUSR1, lambda *args: shift_signal_handler(*args, manager)) # use dbus to shift player (e.g. after playerctld shift) - DBusGMainLoop(set_as_default=True) - bus = dbus.SessionBus() + bus = dbus.SessionBus(mainloop=DBusGMainLoop()) bus.add_signal_receiver(lambda *args, **kwargs: dbus_signal_handler(*args, **kwargs, manager=manager), signal_name='Shift', dbus_interface='org.waybar.Player') + # register well-known bus name + bus.request_name('org.waybar.Player') for player in manager.props.player_names: if arguments.player is not None and arguments.player != player.name: From e1411059ab3dede8a02f3c0ac0e3b609d85f8c35 Mon Sep 17 00:00:00 2001 From: Savely Krasovsky Date: Sat, 6 May 2023 00:39:17 +0300 Subject: [PATCH 008/416] fix: remove old signal handler --- resources/custom_modules/mediaplayer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index dd3228c6..695750fa 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -129,7 +129,6 @@ def main(): signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGPIPE, signal.SIG_DFL) - signal.signal(signal.SIGUSR1, lambda *args: shift_signal_handler(*args, manager)) # use dbus to shift player (e.g. after playerctld shift) bus = dbus.SessionBus(mainloop=DBusGMainLoop()) From 0cbb280de635aa3ddcdbb416d8ebd55a1fb82b15 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 24 Feb 2024 15:00:39 +0100 Subject: [PATCH 009/416] fix title not showing for special workspaces (seperate outputs: true) --- src/modules/hyprland/window.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 1af02b55..eb659b13 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -99,7 +99,8 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{-1, 0, "", ""}; } - const int id = (*monitor)["activeWorkspace"]["id"].asInt(); + const int special_id = (*monitor)["specialWorkspace"]["id"].asInt(); + const int id = special_id != 0 ? special_id : (*monitor)["activeWorkspace"]["id"].asInt(); const auto workspaces = gIPC->getSocket1JsonReply("workspaces"); assert(workspaces.isArray()); From 9f5c27c6e06e51853b55f68043521d1677e4d048 Mon Sep 17 00:00:00 2001 From: Jannik Date: Sat, 24 Feb 2024 18:16:43 +0100 Subject: [PATCH 010/416] fix title not showing for special workspaces (seperate outputs: false --- include/modules/hyprland/window.hpp | 1 - src/modules/hyprland/window.cpp | 13 ++++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/include/modules/hyprland/window.hpp b/include/modules/hyprland/window.hpp index ea4d83b2..4cc16ffb 100644 --- a/include/modules/hyprland/window.hpp +++ b/include/modules/hyprland/window.hpp @@ -42,7 +42,6 @@ class Window : public waybar::AAppIconLabel, public EventHandler { }; auto getActiveWorkspace(const std::string&) -> Workspace; - auto getActiveWorkspace() -> Workspace; void onEvent(const std::string&) override; void queryActiveWorkspace(); void setClass(const std::string&, bool enable); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index eb659b13..fca8b9ab 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -84,17 +84,12 @@ auto Window::update() -> void { AAppIconLabel::update(); } -auto Window::getActiveWorkspace() -> Workspace { - const auto workspace = gIPC->getSocket1JsonReply("activeworkspace"); - assert(workspace.isObject()); - return Workspace::parse(workspace); -} - -auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace { +auto Window::getActiveWorkspace(const std::string& monitorName = "") -> Workspace { const auto monitors = gIPC->getSocket1JsonReply("monitors"); assert(monitors.isArray()); - auto monitor = std::find_if(monitors.begin(), monitors.end(), - [&](Json::Value monitor) { return monitor["name"] == monitorName; }); + auto monitor = std::find_if(monitors.begin(), monitors.end(), [&](Json::Value monitor) { + return monitorName == "" ? monitor["focused"].asBool() : monitor["name"] == monitorName; + }); if (monitor == std::end(monitors)) { spdlog::warn("Monitor not found: {}", monitorName); return Workspace{-1, 0, "", ""}; From 065dd877133b45baedd90044427ce3e10c1ae160 Mon Sep 17 00:00:00 2001 From: Marc Meier Date: Sun, 17 Mar 2024 18:00:13 +0100 Subject: [PATCH 011/416] Added tooltips to hyprland/workspaces Simply slam a `"tooltip": true` and a `"tooltip-format": ""` in that config --- include/modules/hyprland/workspaces.hpp | 3 ++- src/modules/hyprland/workspaces.cpp | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 42791e48..1a8fe872 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -94,7 +94,7 @@ class Workspace { bool onWindowOpened(WindowCreationPayload const& create_window_paylod); std::optional closeWindow(WindowAddress const& addr); - void update(const std::string& format, const std::string& icon); + void update(const std::string& format, const std::string& icon, const std::string &tooltipFormat); private: Workspaces& m_workspaceManager; @@ -199,6 +199,7 @@ class Workspaces : public AModule, public EventHandler { {"DEFAULT", SortMethod::DEFAULT}}; std::string m_format; + std::string m_tooltipFormat; std::map m_iconsMap; util::RegexCollection m_windowRewriteRules; diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 9e368306..1bfe6a7c 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -60,6 +60,13 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { m_format = configFormat.isString() ? configFormat.asString() : "{name}"; m_withIcon = m_format.find("{icon}") != std::string::npos; + if (tooltipEnabled()) { + const Json::Value &configTooltipFormat = config["tooltip-format"]; + m_tooltipFormat = configTooltipFormat.isString() ? configTooltipFormat.asString() : "{name}"; + } else { + m_tooltipFormat = ""; + } + if (m_withIcon && m_iconsMap.empty()) { Json::Value formatIcons = config["format-icons"]; for (std::string &name : formatIcons.getMemberNames()) { @@ -237,7 +244,7 @@ void Workspaces::doUpdate() { }); workspace->setOutput((*updated_workspace)["monitor"].asString()); - workspace->update(m_format, workspaceIcon); + workspace->update(m_format, workspaceIcon, m_tooltipFormat); } spdlog::trace("Updating window count"); @@ -866,7 +873,7 @@ void addOrRemoveClass(const Glib::RefPtr &context, bool condi } } -void Workspace::update(const std::string &format, const std::string &icon) { +void Workspace::update(const std::string &format, const std::string &icon, const std::string &tooltipFormat) { // clang-format off if (this->m_workspaceManager.activeOnly() && \ !this->isActive() && \ @@ -902,6 +909,10 @@ void Workspace::update(const std::string &format, const std::string &icon) { windows.append(window_repr); } + m_button.set_tooltip_text(fmt::format(fmt::runtime(tooltipFormat), fmt::arg("id", id()), + fmt::arg("name", name()), fmt::arg("icon", icon), + fmt::arg("windows", windows))); + m_label.set_markup(fmt::format(fmt::runtime(format), fmt::arg("id", id()), fmt::arg("name", name()), fmt::arg("icon", icon), fmt::arg("windows", windows))); From 06540347636cdb4a87123784a488032aec3d088f Mon Sep 17 00:00:00 2001 From: Marc Meier Date: Sun, 17 Mar 2024 19:41:19 +0100 Subject: [PATCH 012/416] Add man pages for hyprland/workspaces tooltips --- man/waybar-hyprland-workspaces.5.scd | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 5646df58..db306091 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -65,6 +65,16 @@ Addressed by *hyprland/workspaces* If set to id, workspaces will sort by id. If none of those, workspaces will sort with default behavior. +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: "{name}" ++ + The tooltip format. + # FORMAT REPLACEMENTS *{id}*: id of workspace assigned by compositor @@ -148,6 +158,25 @@ Additional to workspace name matching, the following *format-icons* can be set. } ``` +``` +"hyprland/workspaces": { + "format": "{icon}", + "format-window-separator": ", ", + "tooltip": true, + "tooltip-format": "{name}: {windows}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "active": "", + "default": "" + }, + // Window rewrites omitted for brevity +} +``` + # Style - *#workspaces* From fe1d49518bd63425a03f21cb585f93c013e183a8 Mon Sep 17 00:00:00 2001 From: Marc Meier Date: Wed, 20 Mar 2024 15:54:52 +0100 Subject: [PATCH 013/416] Add tooltip selection based on name and state One can set different tooltips for different workspaces. Selection works the same as it does for Icons. Also refactored the `selectIcon` function to a more general `selectString` function. Man page is updated too. --- include/modules/hyprland/workspaces.hpp | 6 +- man/waybar-hyprland-workspaces.5.scd | 35 ++++++++++-- src/modules/hyprland/workspaces.cpp | 76 ++++++++++++++----------- 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 1a8fe872..52b0850c 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -62,7 +62,7 @@ class Workspace { public: explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, const Json::Value& clients_data = Json::Value::nullRef); - std::string& selectIcon(std::map& icons_map); + std::string& selectString(std::map& string_map); Gtk::Button& button() { return m_button; }; int id() const { return m_id; }; @@ -199,7 +199,9 @@ class Workspaces : public AModule, public EventHandler { {"DEFAULT", SortMethod::DEFAULT}}; std::string m_format; - std::string m_tooltipFormat; + + std::map m_tooltipMap; + bool m_withTooltip; std::map m_iconsMap; util::RegexCollection m_windowRewriteRules; diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index db306091..f564c03f 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -70,10 +70,9 @@ Addressed by *hyprland/workspaces* default: true ++ Option to disable tooltip on hover. -*tooltip-format*: ++ - typeof: string ++ - default: "{name}" ++ - The tooltip format. +*tooltips*: ++ + typeof: array ++ + Based on the workspace ID and state, the corresponding tooltip gets selected. Selection works the same as *format-icons* do. Format replacements are supported. See *icons*. # FORMAT REPLACEMENTS @@ -163,7 +162,33 @@ Additional to workspace name matching, the following *format-icons* can be set. "format": "{icon}", "format-window-separator": ", ", "tooltip": true, - "tooltip-format": "{name}: {windows}", + "tooltips": { + "default": "{name}: {windows}", + "empty": "" // Will result in no tooltip + } + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "active": "", + "default": "" + }, + // Window rewrites omitted for brevity +} +``` + +``` +"hyprland/workspaces": { + "format": "{icon}", + "format-window-separator": ", ", + "tooltip": true, + "tooltips": { + "1": "This is the first workspace", + "2": "This is the second", + "2": "And this is the third" + } "format-icons": { "1": "", "2": "", diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8c543fd3..d7eb1f3b 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -60,11 +60,14 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { m_format = configFormat.isString() ? configFormat.asString() : "{name}"; m_withIcon = m_format.find("{icon}") != std::string::npos; - if (tooltipEnabled()) { - const Json::Value &configTooltipFormat = config["tooltip-format"]; - m_tooltipFormat = configTooltipFormat.isString() ? configTooltipFormat.asString() : "{name}"; - } else { - m_tooltipFormat = ""; + m_withTooltip = tooltipEnabled(); + + if (m_withTooltip && m_tooltipMap.empty()) { + Json::Value tooltipFormats = config["tooltips"]; + for (std::string &name : tooltipFormats.getMemberNames()) { + m_tooltipMap.emplace(name, tooltipFormats[name].asString()); + } + m_tooltipMap.emplace("", ""); } if (m_withIcon && m_iconsMap.empty()) { @@ -232,7 +235,15 @@ void Workspaces::doUpdate() { // set workspace icon std::string &workspaceIcon = m_iconsMap[""]; if (m_withIcon) { - workspaceIcon = workspace->selectIcon(m_iconsMap); + spdlog::trace("Selecting icon for workspace {}", workspace->name()); + workspaceIcon = workspace->selectString(m_iconsMap); + } + + // set tooltip + std::string &workspaceTooltip = m_tooltipMap[""]; + if (m_withTooltip) { + spdlog::trace("Selecting tooltip for workspace {}", workspace->name()); + workspaceTooltip = workspace->selectString(m_tooltipMap); } // update m_output @@ -247,7 +258,7 @@ void Workspaces::doUpdate() { workspace->setOutput((*updated_workspace)["monitor"].asString()); } - workspace->update(m_format, workspaceIcon, m_tooltipFormat); + workspace->update(m_format, workspaceIcon, workspaceTooltip); } spdlog::trace("Updating window count"); @@ -984,58 +995,57 @@ void Workspaces::sortWorkspaces() { } } -std::string &Workspace::selectIcon(std::map &icons_map) { - spdlog::trace("Selecting icon for workspace {}", name()); +std::string &Workspace::selectString(std::map &string_map) { if (isUrgent()) { - auto urgentIconIt = icons_map.find("urgent"); - if (urgentIconIt != icons_map.end()) { - return urgentIconIt->second; + auto urgentStringIt = string_map.find("urgent"); + if (urgentStringIt != string_map.end()) { + return urgentStringIt->second; } } if (isActive()) { - auto activeIconIt = icons_map.find("active"); - if (activeIconIt != icons_map.end()) { - return activeIconIt->second; + auto activeStringIt = string_map.find("active"); + if (activeStringIt != string_map.end()) { + return activeStringIt->second; } } if (isSpecial()) { - auto specialIconIt = icons_map.find("special"); - if (specialIconIt != icons_map.end()) { - return specialIconIt->second; + auto specialStringIt = string_map.find("special"); + if (specialStringIt != string_map.end()) { + return specialStringIt->second; } } - auto namedIconIt = icons_map.find(name()); - if (namedIconIt != icons_map.end()) { - return namedIconIt->second; + auto namedStringIt = string_map.find(name()); + if (namedStringIt != string_map.end()) { + return namedStringIt->second; } if (isVisible()) { - auto visibleIconIt = icons_map.find("visible"); - if (visibleIconIt != icons_map.end()) { - return visibleIconIt->second; + auto visibleStringIt = string_map.find("visible"); + if (visibleStringIt != string_map.end()) { + return visibleStringIt->second; } } if (isEmpty()) { - auto emptyIconIt = icons_map.find("empty"); - if (emptyIconIt != icons_map.end()) { - return emptyIconIt->second; + auto emptyStringIt = string_map.find("empty"); + if (emptyStringIt != string_map.end()) { + return emptyStringIt->second; } } if (isPersistent()) { - auto persistentIconIt = icons_map.find("persistent"); - if (persistentIconIt != icons_map.end()) { - return persistentIconIt->second; + auto persistentStringIt = string_map.find("persistent"); + if (persistentStringIt != string_map.end()) { + return persistentStringIt->second; } } - auto defaultIconIt = icons_map.find("default"); - if (defaultIconIt != icons_map.end()) { - return defaultIconIt->second; + auto defaultStringIt = string_map.find("default"); + if (defaultStringIt != string_map.end()) { + return defaultStringIt->second; } return m_name; From 626c52cc79f4aa4798e026f6250e8c61943a95cc Mon Sep 17 00:00:00 2001 From: lilly-lizard Date: Sat, 13 Jul 2024 14:08:48 +1200 Subject: [PATCH 014/416] add "output" class to river tags module when monitor is focused --- include/modules/river/tags.hpp | 4 +++ src/modules/river/tags.cpp | 58 ++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/include/modules/river/tags.hpp b/include/modules/river/tags.hpp index fb3eefaa..5927ac59 100644 --- a/include/modules/river/tags.hpp +++ b/include/modules/river/tags.hpp @@ -20,6 +20,8 @@ class Tags : public waybar::AModule { void handle_focused_tags(uint32_t tags); void handle_view_tags(struct wl_array *tags); void handle_urgent_tags(uint32_t tags); + void handle_focused_output(struct wl_output *output); + void handle_unfocused_output(struct wl_output *output); void handle_primary_clicked(uint32_t tag); bool handle_button_press(GdkEventButton *event_button, uint32_t tag); @@ -30,9 +32,11 @@ class Tags : public waybar::AModule { private: const waybar::Bar &bar_; + struct wl_output *output_; // stores the output this module belongs to Gtk::Box box_; std::vector buttons_; struct zriver_output_status_v1 *output_status_; + struct zriver_seat_status_v1 *seat_status_; }; } /* namespace waybar::modules::river */ diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index 9e7cd5aa..cf0adbae 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -27,12 +27,39 @@ static void listen_urgent_tags(void *data, struct zriver_output_status_v1 *zrive static_cast(data)->handle_urgent_tags(tags); } +static void listen_focused_view(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + const char *title) { + // This module doesn't care +} + +static void listen_mode(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + const char *mode) { + // This module doesn't care +} + +static void listen_focused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + struct wl_output *output) { + static_cast(data)->handle_focused_output(output); +} + +static void listen_unfocused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + struct wl_output *output) { + static_cast(data)->handle_unfocused_output(output); +} + static const zriver_output_status_v1_listener output_status_listener_impl{ .focused_tags = listen_focused_tags, .view_tags = listen_view_tags, .urgent_tags = listen_urgent_tags, }; +static const zriver_seat_status_v1_listener seat_status_listener_impl{ + .focused_output = listen_focused_output, + .unfocused_output = listen_unfocused_output, + .focused_view = listen_focused_view, + .mode = listen_mode, +}; + static void listen_command_success(void *data, struct zriver_command_callback_v1 *zriver_command_callback_v1, const char *output) { @@ -88,12 +115,15 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con seat_{nullptr}, bar_(bar), box_{bar.orientation, 0}, - output_status_{nullptr} { + output_status_{nullptr}, + seat_status_{nullptr} { struct wl_display *display = Client::inst()->wl_display; struct wl_registry *registry = wl_display_get_registry(display); wl_registry_add_listener(registry, ®istry_listener_impl, this); wl_display_roundtrip(display); + output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + if (!status_manager_) { spdlog::error("river_status_manager_v1 not advertised"); return; @@ -150,10 +180,12 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con button.show(); } - struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - 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); + 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_); } @@ -162,6 +194,10 @@ Tags::~Tags() { zriver_output_status_v1_destroy(output_status_); } + if (seat_status_) { + zriver_seat_status_v1_destroy(seat_status_); + } + if (control_) { zriver_control_v1_destroy(control_); } @@ -224,4 +260,20 @@ void Tags::handle_urgent_tags(uint32_t tags) { } } +void Tags::handle_focused_output(struct wl_output *output) { + if (output_ == output) { + for (size_t i = 0; i < buttons_.size(); ++i) { + buttons_[i].get_style_context()->add_class("output"); + } + } +} + +void Tags::handle_unfocused_output(struct wl_output *output) { + if (output_ == output) { + for (size_t i = 0; i < buttons_.size(); ++i) { + buttons_[i].get_style_context()->remove_class("output"); + } + } +} + } /* namespace waybar::modules::river */ From 79ce23a9a77f874d14fd14070a46cf9d2dba9499 Mon Sep 17 00:00:00 2001 From: lilly-lizard Date: Sat, 13 Jul 2024 14:12:54 +1200 Subject: [PATCH 015/416] docs for river tags output style --- man/waybar-river-tags.5.scd | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index f0b2b84e..d629f73d 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -40,10 +40,13 @@ Addressed by *river/tags* - *#tags button.occupied* - *#tags button.focused* - *#tags button.urgent* +- *#tags button.output* -Note that occupied/focused/urgent status may overlap. That is, a tag may be +Note that occupied/focused/urgent/output status may overlap. That is, a tag may be both occupied and focused at the same time. +The *output* style is applied when the river output (e.g. monitor) of the current bar is focused. + # SEE ALSO waybar(5), river(1) From c7f499e372edaca11c8f9163db335602698b35bf Mon Sep 17 00:00:00 2001 From: VPavliashvili Date: Sun, 21 Jul 2024 21:34:29 +0400 Subject: [PATCH 016/416] image module reworked and added multiple image rendering capability --- include/modules/image.hpp | 71 +++++++++++-- man/waybar-image.5.scd | 157 ++++++++++++++++++++++++++- src/modules/image.cpp | 216 ++++++++++++++++++++++++++++++++++---- 3 files changed, 414 insertions(+), 30 deletions(-) diff --git a/include/modules/image.hpp b/include/modules/image.hpp index 7c0d014f..56a55b63 100644 --- a/include/modules/image.hpp +++ b/include/modules/image.hpp @@ -14,9 +14,67 @@ namespace waybar::modules { +namespace image { + +class IStrategy { + public: + virtual ~IStrategy() = default; + virtual void update() = 0; +}; + +class SingleImageStrategy : public IStrategy { + public: + SingleImageStrategy(const std::string &, const Json::Value &, const std::string &, + Gtk::EventBox &, bool); + ~SingleImageStrategy() override = default; + void update() override; + + private: + void parseOutputRaw(); + + util::command::res output_; + Json::Value config_; + Gtk::Image image_; + std::string path_; + std::string tooltip_; + int size_; + Gtk::Box box_; + bool hasTooltip_; +}; + +class MultipleImageStrategy : public IStrategy { + public: + MultipleImageStrategy(const std::string &, const Json::Value &, const std::string &, + Gtk::EventBox &); + ~MultipleImageStrategy() override = default; + void update() override; + + private: + struct ImageData { + std::string path; + std::string marker; + std::string tooltip; + std::string on_click; + std::shared_ptr img; + std::shared_ptr btn; + }; + + void setImagesData(const Json::Value &); + void setupAndDraw(); + void resetBoxAndMemory(); + void handleClick(const Glib::ustring &data); + + Json::Value config_; + int size_; + Gtk::Box box_; + std::vector images_data_; +}; + +} // namespace image + class Image : public AModule { public: - Image(const std::string&, const Json::Value&); + Image(const std::string &, const Json::Value &); virtual ~Image() = default; auto update() -> void override; void refresh(int /*signal*/) override; @@ -24,16 +82,11 @@ class Image : public AModule { private: void delayWorker(); void handleEvent(); - void parseOutputRaw(); + static std::unique_ptr getStrategy(const std::string &, const Json::Value &, + const std::string &, Gtk::EventBox &, bool); - Gtk::Box box_; - Gtk::Image image_; - std::string path_; - std::string tooltip_; - int size_; int interval_; - util::command::res output_; - + std::unique_ptr strategy_; util::SleeperThread thread_; }; diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index e3a69e38..d549a618 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -6,10 +6,19 @@ waybar - image module # DESCRIPTION -The *image* module displays an image from a path. +The *image* module displays container of images(or image) from a provided paths + +# REMARK + +This module has been rewritten to add multiple image rendering functionality. +To avoid users inconvenience of breaking changes, everything related to old(only +one *image*) implementation has been left untouched including configuration part. For this +reason this wiki page is split into two parts # CONFIGURATION +For single *image* + *path*: ++ typeof: string ++ The path to the image. @@ -88,3 +97,149 @@ $path\\n$tooltip - *#image* - *#image.empty* + +# CONFIGURATION + +For multiple *image* + +*size*: ++ + typeof: string ++ + default: 16 ++ + Minumum size of the rendered image in pixels. + +*interval*: ++ + typeof: interger ++ + default: INT_MAX ++ + The interval in seconds to redraw module ++ + Default value is max value of int and changing it is only recommended ++ + if image path or other property is being changed overtime ++ + If no *interval* is provided, the module will only be rendered once. + +*multiple*: ++ + typeof: bool ++ + default: false ++ + this parameter is used to decide if old *image* implementation should be ++ + used(therefore render only one image) or to pick a new implementation to ++ + rended multiple images provided from this config. default value of this ++ + parameter is false. + +*signal*: ++ + typeof: interger ++ + default: 0 ++ + The signal number is used to redraw the module. ++ + This is used if *interval* is not provided and module should change ++ + regularly depended on external events, e.g when clicking on it. ++ + The provided value is valid between 1 and N, where SIGRTMIN+N <= SIGRTMAX. ++ + e.g if *signal* = 7, 34 + 7 = 41, which means waybar process should ++ + receive signal 41 to redraw this module, since SIGRTMIN = 34 (see 'kill -l'). + +*entries*: ++ + typeof: array ++ + default: empty array ++ + Json array of objects consisting of several fields ++ + this is a main part of configuration where per image properties are defined + + *path*: ++ + typeof: string ++ + default: empty string ++ + file path of picture to draw + + *marker*: ++ + typeof: string ++ + default: empty string ++ + user defined per image keyword(marker) to identify them, e.g in styles.css ++ + e.g if you want to draw 3 pictures and one of them dimmed, you can give ++ + *marker*: "dimmed" value and in styles.css write something like this ++ + #image .dimmed { opacity: 0.3 } + + *tooltip*: ++ + typeof: string ++ + default: empty string ++ + description of the image displayed on hover + + *on-click*: ++ + typeof: string ++ + default: empty string ++ + action to perform when clicking on the image. ++ + The action can be any system or custom binary/script ++ + e.g if { "on-click": "notify-send \"hello world\"" } ++ + "hello world" will appear as notification. + +*exec*: ++ + typeof: string ++ + default: empty string ++ + same as *entries* but provided from external script which should dynamically ++ + return json array of objects consisting of same keys as *entries*. ++ + This option is specifically useful in combination with *signal* option without *interval* provided. + +# Examples + +With entries +``` +"image#wentr": { + "entries": [ + { + "path": "/home/user/Pictures/idk1.png", + "marker": "dimmed", + "tooltip": "dimmed image", + "on-click": "notify-send \"hello world\"" + }, + { + "path": "/home/user/Pictures/idk2.png", + "marker": "normal", + "on-click": "myCustomScript.py" + } + ], + "size": 32, + "interval": 1 // will redraw every 5 seconds +} +``` + +With exec +``` +"image#wexec": { + "exec": "imageProvider.py", + "size": 32, + "signal": 7 // e.g 'pkill -n waybar --signal 41' will trigger redraw +} +``` + +where imageProvider.py looks like this +``` +#!/usr/bin/python + +import json + +dir = "/home/user/Pictures/" +out = [ + {"path": dir + "idk1.png", "marker": "clickable", "on-click": "echo 'hello world'"}, + {"path": dir + "idk2.png", "marker": "normal", "tooltip": "sample tooltip"}, + {"path": dir + "idk3.png", "marker": "important"}, +] +res = json.dumps(out) +print(res) +``` + +## Example styles +``` +#image .important { + opacity: 0.9; +} + +#image button.important { + opacity: 0.2 +} + +#image .normal { + opacity: 0.3 +} +``` + +# STYLE + +- *#image* +- *#image*.empty /\*when image could not be rendered\*/ +- *#image*.button /\*when on-click is provided\*/ +- *#image*.*marker* /\*where *marker* is parameter from config\*/ +- *#image*.button.*marker* + diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 8274d323..ff704ab9 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -1,25 +1,13 @@ #include "modules/image.hpp" +#include + waybar::modules::Image::Image(const std::string& id, const Json::Value& config) - : AModule(config, "image", id), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { - box_.pack_start(image_); - box_.set_name("image"); - if (!id.empty()) { - box_.get_style_context()->add_class(id); - } - box_.get_style_context()->add_class(MODULE_CLASS); - event_box_.add(box_); - - dp.emit(); - - size_ = config["size"].asInt(); + : AModule(config, "image", id) { + strategy_ = getStrategy(id, config, MODULE_CLASS, event_box_, tooltipEnabled()); interval_ = config_["interval"].asInt(); - if (size_ == 0) { - size_ = 16; - } - if (interval_ == 0) { interval_ = INT_MAX; } @@ -27,6 +15,20 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config) delayWorker(); } +auto waybar::modules::Image::getStrategy( + const std::string& id, const Json::Value& cfg, const std::string& module, Gtk::EventBox& evbox, + bool hasTooltip) -> std::unique_ptr { + std::unique_ptr strat; + if (!cfg["multiple"].empty() && cfg["multiple"].asBool()) { + strat = std::make_unique(id, cfg, module, evbox); + } else { + strat = std::make_unique(id, cfg, module, evbox, + hasTooltip); + } + + return strat; +} + void waybar::modules::Image::delayWorker() { thread_ = [this] { dp.emit(); @@ -42,6 +44,180 @@ void waybar::modules::Image::refresh(int sig) { } auto waybar::modules::Image::update() -> void { + strategy_->update(); + + AModule::update(); +} + +namespace waybar::modules::image { + +MultipleImageStrategy::MultipleImageStrategy(const std::string& id, const Json::Value& config, + const std::string& module, Gtk::EventBox& evbox) + : IStrategy(), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + config_ = config; + + box_.set_name("image"); + box_.get_style_context()->add_class(id); + box_.get_style_context()->add_class(module); + evbox.add(box_); + + size_ = config["size"].asInt(); + if (size_ == 0) { + size_ = 16; + } +} + +void MultipleImageStrategy::update() { + // spdlog::info("update function run"); + + // clear box_, previous css classes and memory + if (box_.get_children().size() > 0) { + resetBoxAndMemory(); + } + + // set new images from config script + if (!config_["entries"].empty()) { + setImagesData(config_["entries"]); + } else if (!config_["exec"].empty()) { + auto exec = util::command::exec(config_["exec"].asString(), ""); + Json::Value as_json; + Json::Reader reader; + + if (!reader.parse(exec.out, as_json)) { + spdlog::error("invalid json from exec {}", exec.out); + return; + } + + setImagesData(as_json); + } else { + spdlog::error("no image files provded in config"); + return; + } + + setupAndDraw(); +} + +void MultipleImageStrategy::setupAndDraw() { + for (unsigned int i = 0; i < images_data_.size(); i++) { + images_data_[i].img = std::make_shared(); + images_data_[i].btn = std::make_shared(); + + auto img = images_data_[i].img; + auto data = images_data_[i]; + + auto path = data.path; + auto marker = data.marker; + auto tooltip = data.tooltip; + bool has_onclick = !data.on_click.empty(); + + Glib::RefPtr pixbuf; + pixbuf = Gdk::Pixbuf::create_from_file(path, size_, size_); + + if (has_onclick) { + auto btn = images_data_[i].btn; + btn->set_name("button_" + path); + btn->get_style_context()->add_class(marker); + btn->set_tooltip_text(tooltip); + btn->set_image(*img); + box_.pack_start(*btn); + + btn->add_events(Gdk::BUTTON_PRESS_MASK); + btn->signal_clicked().connect( + sigc::bind(sigc::mem_fun(*this, &MultipleImageStrategy::handleClick), data.on_click)); + + if (pixbuf) { + btn->show_all(); + img->set(pixbuf); + box_.get_style_context()->remove_class("empty"); + } else { + btn->hide(); + img->clear(); + img->hide(); + box_.get_style_context()->add_class("empty"); + } + } else { + img->set_name(path); + img->get_style_context()->add_class(marker); + img->set_tooltip_text(tooltip); + box_.pack_start(*img); + // spdlog::info("added image -> {}:{}", marker, path); + + if (pixbuf) { + img->set(pixbuf); + img->show(); + box_.get_style_context()->remove_class("empty"); + } else { + img->clear(); + img->hide(); + box_.get_style_context()->add_class("empty"); + } + } + } +} + +void MultipleImageStrategy::setImagesData(const Json::Value& entries) { + for (unsigned int i = 0; i < entries.size(); i++) { + auto path = entries[i]["path"]; + auto marker = entries[i]["marker"]; + auto tooltip = entries[i]["tooltip"]; + auto onclick = entries[i]["on-click"]; + + bool has_tooltip_err = !tooltip.empty() && !tooltip.isString(); + bool has_onclick_err = !onclick.empty() && !onclick.isString(); + + if (!path.isString() || !marker.isString() || has_tooltip_err || has_onclick_err || + !Glib::file_test(path.asString(), Glib::FILE_TEST_EXISTS)) { + spdlog::error("invalid input in images config -> {}", entries[i]); + return; + } + ImageData data; + data.path = path.asString(); + data.marker = marker.asString(); + data.tooltip = !tooltip.empty() ? tooltip.asString() : ""; + data.on_click = onclick.asString(); + + images_data_.push_back(data); + } +} + +void MultipleImageStrategy::resetBoxAndMemory() { + auto children = box_.get_children(); + for (auto child : children) { + box_.remove(*child); + // spdlog::info("child removed with name -> {}", std::string(child->get_name())); + } + + images_data_.clear(); +} + +void MultipleImageStrategy::handleClick(const Glib::ustring& data) { + auto msg = std::string(data); + + auto exec = util::command::exec(data, ""); +} + +SingleImageStrategy::SingleImageStrategy(const std::string& id, const Json::Value& config, + const std::string& module, Gtk::EventBox& evbox, + bool tooltipEnabled) + : IStrategy(), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + config_ = config; + hasTooltip_ = tooltipEnabled; + + box_.pack_start(image_); + box_.set_name("image"); + if (!id.empty()) { + box_.get_style_context()->add_class(id); + } + box_.get_style_context()->add_class(module); + evbox.add(box_); + + size_ = config["size"].asInt(); + if (size_ == 0) { + size_ = 16; + } +} + +void SingleImageStrategy::update() { Glib::RefPtr pixbuf; if (config_["path"].isString()) { path_ = config_["path"].asString(); @@ -57,7 +233,7 @@ auto waybar::modules::Image::update() -> void { pixbuf = {}; if (pixbuf) { - if (tooltipEnabled() && !tooltip_.empty()) { + if (hasTooltip_ && !tooltip_.empty()) { if (box_.get_tooltip_markup() != tooltip_) { box_.set_tooltip_markup(tooltip_); } @@ -70,11 +246,9 @@ auto waybar::modules::Image::update() -> void { image_.hide(); box_.get_style_context()->add_class("empty"); } - - AModule::update(); } -void waybar::modules::Image::parseOutputRaw() { +void SingleImageStrategy::parseOutputRaw() { std::istringstream output(output_.out); std::string line; int i = 0; @@ -89,3 +263,5 @@ void waybar::modules::Image::parseOutputRaw() { i++; } } + +} // namespace waybar::modules::image From f1acea521bbacb034ae5bb7380e78c51853faa34 Mon Sep 17 00:00:00 2001 From: Lauri Niskanen Date: Sun, 7 Jul 2024 06:47:06 +0300 Subject: [PATCH 017/416] pulseaudio: Add 'sink-mapping' config option This commit adds a new optional config to the pulseaudio modules. It maps sink names to other sink names so that if the current sink is a key, the sink named by the value is considered to be the current sink instead of it. E.g. "sink-mapping": { "easyeffects_sink": "speakers_sink" } --- include/util/audio_backend.hpp | 4 +++- man/waybar-pulseaudio.5.scd | 4 ++++ src/modules/pulseaudio.cpp | 1 + src/modules/pulseaudio_slider.cpp | 3 ++- src/util/audio_backend.cpp | 17 +++++++++++++++++ 5 files changed, 27 insertions(+), 2 deletions(-) diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp index 2f53103e..b9a9e0ff 100644 --- a/include/util/audio_backend.hpp +++ b/include/util/audio_backend.hpp @@ -48,6 +48,7 @@ class AudioBackend { std::string default_source_name_; std::vector ignored_sinks_; + std::map sink_mapping_; std::function on_updated_cb_ = NOOP; @@ -69,6 +70,7 @@ class AudioBackend { void changeVolume(ChangeType change_type, double step = 1, uint16_t max_volume = 100); void setIgnoredSinks(const Json::Value& config); + void setSinkMapping(const Json::Value& config); std::string getSinkPortName() const { return port_name_; } std::string getFormFactor() const { return form_factor_; } @@ -93,4 +95,4 @@ class AudioBackend { bool isBluetooth(); }; -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index 232e84a0..4f585d4e 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -113,6 +113,10 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c typeof: array ++ Sinks in this list will not be shown as active sink by Waybar. Entries should be the sink's description field. +*sink-mapping*: ++ + typeof: object ++ + Sinks named by the values of this mapping will be considered to be the current sink instead of the sinks named by the respective keys. + *menu*: ++ typeof: string ++ Action that popups the menu. diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index 255ca571..0189daaa 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -7,6 +7,7 @@ waybar::modules::Pulseaudio::Pulseaudio(const std::string &id, const Json::Value backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); backend->setIgnoredSinks(config_["ignored-sinks"]); + backend->setSinkMapping(config_["sink-mapping"]); } bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll *e) { diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp index bf85584e..7e642d29 100644 --- a/src/modules/pulseaudio_slider.cpp +++ b/src/modules/pulseaudio_slider.cpp @@ -6,6 +6,7 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con : ASlider(config, "pulseaudio-slider", id) { backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); backend->setIgnoredSinks(config_["ignored-sinks"]); + backend->setSinkMapping(config_["sink-mapping"]); if (config_["target"].isString()) { std::string target = config_["target"].asString(); @@ -79,4 +80,4 @@ void PulseaudioSlider::onValueChanged() { backend->changeVolume(volume, min_, max_); } -} // namespace waybar::modules \ No newline at end of file +} // namespace waybar::modules diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 3d90b6d5..c267816f 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -155,6 +155,13 @@ void AudioBackend::sinkInfoCb(pa_context * /*context*/, const pa_sink_info *i, i } } + if (const auto mapping = backend->sink_mapping_.find(backend->current_sink_name_); + mapping != backend->sink_mapping_.end()) { + if (i->name == mapping->second) { + backend->current_sink_name_ = i->name; + } + } + if (backend->current_sink_name_ == i->name) { backend->current_sink_running_ = i->state == PA_SINK_RUNNING; } @@ -292,4 +299,14 @@ void AudioBackend::setIgnoredSinks(const Json::Value &config) { } } +void AudioBackend::setSinkMapping(const Json::Value &config) { + if (config.isObject()) { + for (auto it = config.begin(); it != config.end(); ++it) { + if (it.key().isString() && it->isString()) { + sink_mapping_.emplace(it.key().asString(), it->asString()); + } + } + } +} + } // namespace waybar::util From 9f2684c26ed2478f794690c4a4e9a533381cd017 Mon Sep 17 00:00:00 2001 From: Justus Rossmeier Date: Tue, 23 Apr 2024 21:05:03 +0200 Subject: [PATCH 018/416] add click coordinates to script Allow the current relative mouse position {x} and {y} relative to the widget in percent to be used as an argument to custom scripts --- src/AModule.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/AModule.cpp b/src/AModule.cpp index c180b480..08656d72 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -1,5 +1,6 @@ #include "AModule.hpp" +#include #include #include @@ -181,7 +182,12 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { format.clear(); } if (!format.empty()) { - pid_.push_back(util::command::forkExec(format)); + const int width = gdk_window_get_width(e->window); + const int height = gdk_window_get_height(e->window); + const std::string cmd = + fmt::format(fmt::runtime(format), fmt::arg("x", (int)round(100. * e->x / width)), + fmt::arg("y", (int)round(100. * e->y / height))); + pid_.push_back(util::command::forkExec(cmd)); } dp.emit(); return true; From 696f7e1542ee2786d27e6a6e4e86bf2359132673 Mon Sep 17 00:00:00 2001 From: Victor Perez Cano Date: Fri, 18 Oct 2024 20:41:13 +0200 Subject: [PATCH 019/416] tooltip functionality on hyprland/language module --- include/modules/hyprland/language.hpp | 1 + src/modules/hyprland/language.cpp | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index 47a4d69c..ce5b9262 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -35,6 +35,7 @@ class Language : public waybar::ALabel, public EventHandler { std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; + const bool tooltip_format_enabled_; Layout layout_; }; diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index d86393af..901749a0 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -10,7 +10,9 @@ namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { + : ALabel(config, "language", id, "{}", 0, true), + bar_(bar), + tooltip_format_enabled_{config_["tooltip-format"].isString()} { modulesReady = true; if (!gIPC) { @@ -57,9 +59,27 @@ auto Language::update() -> void { spdlog::debug("hyprland language formatted layout name {}", layoutName); + std::string tooltipContent = std::string{}; + if (tooltipEnabled()) { + if (tooltip_format_enabled_) { + auto tooltip_format = config_["tooltip-format"].asString(); + tooltipContent = 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)); + + } else { + tooltipContent = layoutName; + } + } + + spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); + if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); + label_.set_tooltip_markup(tooltipContent); } else { label_.hide(); } From 550d108a03a4d86dced4eef8e2db095a294b23f1 Mon Sep 17 00:00:00 2001 From: Victor Perez Cano Date: Sat, 19 Oct 2024 17:30:23 +0200 Subject: [PATCH 020/416] added tooltip-format- on hyprland/language tooltip --- include/modules/hyprland/language.hpp | 1 - src/modules/hyprland/language.cpp | 37 +++++++++++++++++---------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index ce5b9262..47a4d69c 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -35,7 +35,6 @@ class Language : public waybar::ALabel, public EventHandler { std::mutex mutex_; const Bar& bar_; util::JsonParser parser_; - const bool tooltip_format_enabled_; Layout layout_; }; diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 901749a0..ecb3ea2c 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -11,8 +11,7 @@ namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "language", id, "{}", 0, true), - bar_(bar), - tooltip_format_enabled_{config_["tooltip-format"].isString()} { + bar_(bar) { modulesReady = true; if (!gIPC) { @@ -56,30 +55,40 @@ auto Language::update() -> void { fmt::arg("shortDescription", layout_.short_description), fmt::arg("variant", layout_.variant))); } - spdlog::debug("hyprland language formatted layout name {}", layoutName); std::string tooltipContent = std::string{}; - if (tooltipEnabled()) { - if (tooltip_format_enabled_) { + bool tooltip_enabled = tooltipEnabled(); + if (tooltip_enabled) { + if (config_.isMember("tooltip-format")) { auto tooltip_format = config_["tooltip-format"].asString(); - tooltipContent = 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)); - - } else { + 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))); + } + } + 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); if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); - label_.set_tooltip_markup(tooltipContent); + if (tooltip_enabled) { + label_.set_tooltip_markup(tooltipContent); + } } else { label_.hide(); } From cb0ecfbddc6d4e2d305608b01fd03945e167706b Mon Sep 17 00:00:00 2001 From: Victor Perez Cano Date: Sat, 19 Oct 2024 18:04:07 +0200 Subject: [PATCH 021/416] updated man for hyprland/language --- man/waybar-hyprland-language.5.scd | 41 +++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index 33b28ae4..9ebc8801 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -38,6 +38,19 @@ Addressed by *hyprland/language* typeof: array ++ The actions corresponding to the buttons of the menu. +*tooltip*: ++ + typeof: boolean ++ + default: true ++ + Enables or disables the tooltip for the language module. By default, the tooltip is enabled. Set to *false* to disable. + +*tooltip-format*: ++ + typeof: string ++ + default: {long} ++ + Specifies the format of the tooltip when it is enabled. It follows the same format replacement rules as the *format* key. + +*tooltip-format-*: ++ + typeof: string ++ + Allows specifying a different tooltip format for each language. The ** should be replaced with the language code. This can be used to provide a custom tooltip for each language. # FORMAT REPLACEMENTS @@ -54,13 +67,35 @@ Addressed by *hyprland/language* ``` "hyprland/language": { - "format": "Lang: {long}" - "format-en": "AMERICA, HELL YEAH!" - "format-tr": "As bayrakları" + "format": "Lang: {long}", + "format-en": "AMERICA, HELL YEAH!", + "format-tr": "As bayrakları", "keyboard-name": "at-translated-set-2-keyboard" } ``` +``` +"hyprland/language": { + "format": "{}", + "format-en": "US", + "format-es": "ES", + "tooltip": true, + "tooltip-format": "{long}" +} +``` + +``` +"hyprland/language": { + "format": "{}", + "format-en": "US", + "format-es": "ES", + "tooltip": true, + "tooltip-format": "{}", + "tooltip-format-es": "{Español}", + "tooltip-format-en": "{English (american)}" +} +``` + # STYLE - *#language* From 6e4160dc532069e3fe5136f8424d036ad0abbb86 Mon Sep 17 00:00:00 2001 From: ico277 Date: Tue, 22 Oct 2024 03:49:52 +0200 Subject: [PATCH 022/416] add an option to seach by hwmon name for the temperature module --- src/modules/temperature.cpp | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index 30287763..9ae5c1fb 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -1,6 +1,7 @@ #include "modules/temperature.hpp" #include +#include #include #if defined(__FreeBSD__) @@ -20,12 +21,42 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val if (check_set_path(item.asString())) break; }; - // if hwmon_path is an array, loop to find first valid item - traverseAsArray(config_["hwmon-path"], [this](const std::string& path) { - if (!std::filesystem::exists(path)) return false; - file_path_ = path; - return true; - }); + if (config_["hwmon-by-name"].isString() && config_["input-filename"].isString()) { + auto name = config_["hwmon-by-name"].asString(); + auto input_filename = config_["input-filename"].asString(); + for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon/")) { + if (std::filesystem::is_directory(entry) && file_path_.empty()) { + std::string name_filepath = entry.path().string() + "/name"; + std::string input_filepath = entry.path().string() + "/" + input_filename; + + if (std::filesystem::exists(name_filepath) && std::filesystem::exists(input_filepath)) { + std::ifstream name_file(name_filepath); + if (!name_file.is_open()) { + throw std::runtime_error("Error: Could not open file " + name_filepath); + } + + std::string line; + while (std::getline(name_file, line)) { + if (line.find(name) != std::string::npos) { + file_path_ = input_filepath; + break; + } + } + } + } + } + if (file_path_.empty()) + throw std::runtime_error("Could not find hwmon by name " + name); + } + + if (file_path_.empty()) { + // if hwmon_path is an array, loop to find first valid item + traverseAsArray(config_["hwmon-path"], [this](const std::string& path) { + if (!std::filesystem::exists(path)) return false; + file_path_ = path; + return true; + }); + } if (file_path_.empty() && config_["input-filename"].isString()) { // fallback to hwmon_paths-abs From 8cae35d49f2da063af422d5b3538b962768da078 Mon Sep 17 00:00:00 2001 From: ico277 Date: Tue, 22 Oct 2024 18:23:24 +0200 Subject: [PATCH 023/416] try to adhere more to the rest of the coding style --- src/modules/temperature.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index 9ae5c1fb..fae1c1f6 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -26,8 +26,8 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val auto input_filename = config_["input-filename"].asString(); for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon/")) { if (std::filesystem::is_directory(entry) && file_path_.empty()) { - std::string name_filepath = entry.path().string() + "/name"; - std::string input_filepath = entry.path().string() + "/" + input_filename; + auto name_filepath = entry.path().string() + "/name"; + auto input_filepath = entry.path().string() + "/" + input_filename; if (std::filesystem::exists(name_filepath) && std::filesystem::exists(input_filepath)) { std::ifstream name_file(name_filepath); From b92394cd4dd51183d0184937cedc8f1802009802 Mon Sep 17 00:00:00 2001 From: ico277 Date: Tue, 22 Oct 2024 18:53:13 +0200 Subject: [PATCH 024/416] man page edit for the configuration addition --- man/waybar-temperature.5.scd | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 541bf3af..4ca17410 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -29,7 +29,12 @@ Addressed by *temperature* *input-filename*: ++ typeof: string ++ - The temperature filename of your *hwmon-path-abs*, e.g. *temp1_input* + The temperature filename of your *hwmon-path-abs* (also used by *hwmon-by-name*), e.g. *temp1_input* + +*hwmon-by-name*: ++ + typeof: string ++ + The substring to search for in */sys/class/hwmon/hwmonX/name* (where hwmonX is any folder in */sys/class/hwmon/*). + Waybar will search for every directory in */sys/class/hwmon/* and uses the directory in which the *name* matches *hwmon-by-name*. *critical-threshold*: ++ typeof: integer ++ From 00e5cb7568107d6416536dc484f4a7ccb293ec8f Mon Sep 17 00:00:00 2001 From: ico277 Date: Tue, 22 Oct 2024 18:56:10 +0200 Subject: [PATCH 025/416] clang-format run --- src/modules/temperature.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index fae1c1f6..ec0f585e 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -43,10 +43,9 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val } } } - } + } } - if (file_path_.empty()) - throw std::runtime_error("Could not find hwmon by name " + name); + if (file_path_.empty()) throw std::runtime_error("Could not find hwmon by name " + name); } if (file_path_.empty()) { From 38cf981f78896888a51d4785af05b75c83015ec2 Mon Sep 17 00:00:00 2001 From: Victor Perez Cano Date: Fri, 25 Oct 2024 18:58:55 +0200 Subject: [PATCH 026/416] fixed clang format for language.cpp --- src/modules/hyprland/language.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index ecb3ea2c..26985fcb 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -10,8 +10,7 @@ namespace waybar::modules::hyprland { Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) - : ALabel(config, "language", id, "{}", 0, true), - bar_(bar) { + : ALabel(config, "language", id, "{}", 0, true), bar_(bar) { modulesReady = true; if (!gIPC) { @@ -69,20 +68,19 @@ auto Language::update() -> void { 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))); + 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))); } - } - else { + } else { // if no tooltip format is provided, use the same text as the module tooltipContent = layoutName; } spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); } - if (!format_.empty()) { label_.show(); label_.set_markup(layoutName); From 8270b55e1c080e330676863c20adc3bf84227c6c Mon Sep 17 00:00:00 2001 From: Pavel Date: Sun, 8 Dec 2024 18:50:18 +0300 Subject: [PATCH 027/416] feat: add way to use environment value for 'output' option --- src/config.cpp | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 375dc4cb..2ada69d1 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -123,20 +123,37 @@ void Config::mergeConfig(Json::Value &a_config_, Json::Value &b_config_) { } bool isValidOutput(const Json::Value &config, const std::string &name, const std::string &identifier) { + const auto isOutputMatches = [&](const std::string &output) -> bool { + if (output.substr(0, 1) == "$") { + auto *const environment_value = std::getenv(output.substr(1).c_str()); + if (environment_value != nullptr) { + const std::string output_from_env = environment_value; + return output_from_env == name || output_from_env == identifier; + } + + spdlog::warn("The environment value is unknown: {}", output); + } + + return output == name || output == identifier; + }; + if (config["output"].isArray()) { for (auto const &output_conf : config["output"]) { if (output_conf.isString()) { auto config_output = output_conf.asString(); + if (config_output.substr(0, 1) == "!") { - if (config_output.substr(1) == name || config_output.substr(1) == identifier) { + if (isOutputMatches(config_output.substr(1))) { return false; } continue; } - if (config_output == name || config_output == identifier) { + + if (isOutputMatches(config_output)) { return true; } + if (config_output.substr(0, 1) == "*") { return true; } @@ -147,11 +164,13 @@ bool isValidOutput(const Json::Value &config, const std::string &name, if (config["output"].isString()) { auto config_output = config["output"].asString(); + if (!config_output.empty()) { if (config_output.substr(0, 1) == "!") { - return config_output.substr(1) != name && config_output.substr(1) != identifier; + return !isOutputMatches(config_output.substr(1)); } - return config_output == name || config_output == identifier; + + return isOutputMatches(config_output); } } From fe1a2c208d5918b30e9f33f91f0901d54c11edcf Mon Sep 17 00:00:00 2001 From: tS Date: Mon, 9 Dec 2024 21:15:52 +0100 Subject: [PATCH 028/416] Added source_desc argument to format_source in pulseaudio.cpp Maybe you want hide name and volume on mute :) --- src/modules/pulseaudio.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index 255ca571..9b56fae1 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -115,7 +115,7 @@ auto waybar::modules::Pulseaudio::update() -> void { auto sink_desc = backend->getSinkDesc(); auto source_desc = backend->getSourceDesc(); - format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume)); + format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume), fmt::arg("source_desc", source_desc)); auto text = fmt::format( fmt::runtime(format), fmt::arg("desc", sink_desc), fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume), From 807208cba8c52b56c5c8430a4fc7c88d9666c098 Mon Sep 17 00:00:00 2001 From: Alex Denes Date: Sat, 8 Feb 2025 03:53:28 +0000 Subject: [PATCH 029/416] dwl/tags: add hide-vacant option --- include/modules/dwl/tags.hpp | 1 + man/waybar-dwl-tags.5.scd | 5 +++++ src/modules/dwl/tags.cpp | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/include/modules/dwl/tags.hpp b/include/modules/dwl/tags.hpp index 53dff989..8f2a158a 100644 --- a/include/modules/dwl/tags.hpp +++ b/include/modules/dwl/tags.hpp @@ -28,6 +28,7 @@ class Tags : public waybar::AModule { const waybar::Bar &bar_; Gtk::Box box_; std::vector buttons_; + bool hide_vacant_; struct zdwl_ipc_output_v2 *output_status_; }; diff --git a/man/waybar-dwl-tags.5.scd b/man/waybar-dwl-tags.5.scd index a2146dfd..d81cc546 100644 --- a/man/waybar-dwl-tags.5.scd +++ b/man/waybar-dwl-tags.5.scd @@ -21,6 +21,11 @@ Addressed by *dwl/tags* typeof: array ++ The label to display for each tag. +*hide-vacant*: ++ + typeof: bool ++ + default: false ++ + If set to true, tags without clients and that are not active will be hidden. + *disable-click*: ++ typeof: bool ++ default: false ++ diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp index f8b250c8..ce4c2324 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -94,7 +94,11 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con seat_{nullptr}, bar_(bar), box_{bar.orientation, 0}, + hide_vacant_(false), output_status_{nullptr} { + if (config_["hide-vacant"].asBool()) { + hide_vacant_ = config_["hide-vacant"].asBool(); + } struct wl_display *display = Client::inst()->wl_display; struct wl_registry *registry = wl_display_get_registry(display); @@ -198,6 +202,12 @@ void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint } else { button.get_style_context()->remove_class("urgent"); } + + if (hide_vacant_ && !clients && !(state & TAG_ACTIVE)) { + button.set_visible(false); + } else { + button.set_visible(true); + } } } /* namespace waybar::modules::dwl */ From 8685e88ec439ac4bbb1c745a643f8c63f64375c1 Mon Sep 17 00:00:00 2001 From: Alex Denes Date: Sat, 8 Feb 2025 01:35:32 +0000 Subject: [PATCH 030/416] dwl/window: add .active styling class for active windows --- include/modules/dwl/window.hpp | 2 ++ man/waybar-dwl-window.5.scd | 4 ++++ src/modules/dwl/window.cpp | 13 +++++++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/include/modules/dwl/window.hpp b/include/modules/dwl/window.hpp index 43586399..619360a5 100644 --- a/include/modules/dwl/window.hpp +++ b/include/modules/dwl/window.hpp @@ -19,6 +19,7 @@ class Window : public AAppIconLabel, public sigc::trackable { void handle_layout(const uint32_t layout); void handle_title(const char *title); void handle_appid(const char *ppid); + void handle_active(const uint32_t active); void handle_layout_symbol(const char *layout_symbol); void handle_frame(); @@ -30,6 +31,7 @@ class Window : public AAppIconLabel, public sigc::trackable { std::string title_; std::string appid_; std::string layout_symbol_; + bool active_; uint32_t layout_; struct zdwl_ipc_output_v2 *output_status_; diff --git a/man/waybar-dwl-window.5.scd b/man/waybar-dwl-window.5.scd index 9ac33d94..c7f70908 100644 --- a/man/waybar-dwl-window.5.scd +++ b/man/waybar-dwl-window.5.scd @@ -109,6 +109,10 @@ If no expression matches, the format output is left unchanged. Invalid expressions (e.g., mismatched parentheses) are skipped. +# STYLE + +- *#window.active* + # EXAMPLES ``` diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp index a960a1f0..82d92003 100644 --- a/src/modules/dwl/window.cpp +++ b/src/modules/dwl/window.cpp @@ -19,7 +19,7 @@ static void toggle_visibility(void *data, zdwl_ipc_output_v2 *zdwl_output_v2) { } static void active(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t active) { - // Intentionally empty + static_cast(data)->handle_active(active); } static void set_tag(void *data, zdwl_ipc_output_v2 *zdwl_output_v2, uint32_t tag, uint32_t state, @@ -74,7 +74,9 @@ static const wl_registry_listener registry_listener_impl = {.global = handle_glo .global_remove = handle_global_remove}; Window::Window(const std::string &id, const Bar &bar, const Json::Value &config) - : AAppIconLabel(config, "window", id, "{}", 0, true), bar_(bar) { + : AAppIconLabel(config, "window", id, "{}", 0, true), + bar_(bar), + active_(false) { struct wl_display *display = Client::inst()->wl_display; struct wl_registry *registry = wl_display_get_registry(display); @@ -102,6 +104,8 @@ void Window::handle_title(const char *title) { title_ = Glib::Markup::escape_tex void Window::handle_appid(const char *appid) { appid_ = Glib::Markup::escape_text(appid); } +void Window::handle_active(const uint32_t active) { active_ = active != 0; } + void Window::handle_layout_symbol(const char *layout_symbol) { layout_symbol_ = Glib::Markup::escape_text(layout_symbol); } @@ -118,6 +122,11 @@ void Window::handle_frame() { if (tooltipEnabled()) { label_.set_tooltip_text(title_); } + if (active_) { + box_.get_style_context()->add_class("active"); + } else { + box_.get_style_context()->remove_class("active"); + } } } // namespace waybar::modules::dwl From feb6dfb9d66006889018ba250807e1e0dfa69ff8 Mon Sep 17 00:00:00 2001 From: Jeevitha Kannan K S Date: Fri, 21 Mar 2025 16:23:39 +0530 Subject: [PATCH 031/416] feat(power-profiles-daemon): split driver into cpu driver and platform driver --- include/modules/power_profiles_daemon.hpp | 5 +++-- man/waybar-power-profiles-daemon.5.scd | 6 ++--- resources/config.jsonc | 2 +- src/modules/power_profiles_daemon.cpp | 27 ++++++++++++++++++----- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/include/modules/power_profiles_daemon.hpp b/include/modules/power_profiles_daemon.hpp index a2bd3858..c43ccd78 100644 --- a/include/modules/power_profiles_daemon.hpp +++ b/include/modules/power_profiles_daemon.hpp @@ -9,9 +9,10 @@ namespace waybar::modules { struct Profile { std::string name; - std::string driver; + std::string cpuDriver; + std::string platformDriver; - Profile(std::string n, std::string d) : name(std::move(n)), driver(std::move(d)) {} + Profile(std::string n, std::string cd, std::string pd) : name(std::move(n)), cpuDriver(std::move(cd)), platformDriver(std::move(pd)) {} }; class PowerProfilesDaemon : public ALabel { diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd index 6488767b..c1b2f5b8 100644 --- a/man/waybar-power-profiles-daemon.5.scd +++ b/man/waybar-power-profiles-daemon.5.scd @@ -25,7 +25,7 @@ $XDG_CONFIG_HOME/waybar/config :[ Message displayed on the bar. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. |[ *tooltip-format* :[ string -:[ "Power profile: {profile}\\nDriver: {driver}" +:[ "Power profile: {profile}\\nCPU driver: {cpu_driver}\\nPlatform driver: {platform_driver}" :[ Messaged displayed in the module tooltip. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. |[ *tooltip* :[ bool @@ -51,7 +51,7 @@ Compact display (default config): ``` "power-profiles-daemon": { "format": "{icon}", - "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip-format": "Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}", "tooltip": true, "format-icons": { "default": "", @@ -67,7 +67,7 @@ Display the full profile name: ``` "power-profiles-daemon": { "format": "{icon} {profile}", - "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip-format": "Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}", "tooltip": true, "format-icons": { "default": "", diff --git a/resources/config.jsonc b/resources/config.jsonc index 6ac1aa50..3b1ae160 100644 --- a/resources/config.jsonc +++ b/resources/config.jsonc @@ -151,7 +151,7 @@ }, "power-profiles-daemon": { "format": "{icon}", - "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip-format": "Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}", "tooltip": true, "format-icons": { "default": "", diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index 3ae3ae83..b2876c9e 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -12,7 +12,7 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu if (config_["tooltip-format"].isString()) { tooltipFormat_ = config_["tooltip-format"].asString(); } else { - tooltipFormat_ = "Power profile: {profile}\nDriver: {driver}"; + tooltipFormat_ = "Power profile: {profile}\nCpuDriver: {cpu_driver}"; } // Fasten your seatbelt, we're up for quite a ride. The rest of the // init is performed asynchronously. There's 2 callbacks involved. @@ -95,15 +95,29 @@ void PowerProfilesDaemon::populateInitState() { powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles"); for (auto& variantDict : profilesVariant.get()) { Glib::ustring name; - Glib::ustring driver; + Glib::ustring cpuDriver; + Glib::ustring platformDriver; if (auto p = variantDict.find("Profile"); p != variantDict.end()) { name = p->second.get(); } - if (auto d = variantDict.find("Driver"); d != variantDict.end()) { - driver = d->second.get(); + if (auto cd = variantDict.find("CpuDriver"); cd != variantDict.end()) { + cpuDriver = cd->second.get(); } + if (auto pd = variantDict.find("PlatformDriver"); pd != variantDict.end()) { + platformDriver = pd->second.get(); + } + + if (cpuDriver.empty()) { + cpuDriver = "Unavailable"; + spdlog::warn("Cannot find power profiles daemon cpu driver."); + } + if (platformDriver.empty()) { + platformDriver = "Unavailable"; + spdlog::warn("Cannot find power profiles daemon platform driver."); + } + if (!name.empty()) { - availableProfiles_.emplace_back(std::move(name), std::move(driver)); + availableProfiles_.emplace_back(std::move(name), std::move(cpuDriver), std::move(platformDriver)); } else { spdlog::error( "Power profiles daemon: power-profiles-daemon sent us an empty power profile name. " @@ -153,7 +167,8 @@ auto PowerProfilesDaemon::update() -> void { // Set label fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("profile", profile.name)); - store.push_back(fmt::arg("driver", profile.driver)); + store.push_back(fmt::arg("cpu_driver", profile.cpuDriver)); + store.push_back(fmt::arg("platform_driver", profile.platformDriver)); store.push_back(fmt::arg("icon", getIcon(0, profile.name))); label_.set_markup(fmt::vformat(format_, store)); if (tooltipEnabled()) { From 9556bdabda987acb8bb8959cbfbe1e6f157cccf2 Mon Sep 17 00:00:00 2001 From: hansi Date: Mon, 24 Mar 2025 11:47:29 +0400 Subject: [PATCH 032/416] feat: add display-condition configuration option --- man/waybar-niri-workspaces.5.scd | 10 ++++++++++ src/modules/niri/workspaces.cpp | 19 ++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 0c0249ca..12d79cdf 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -41,6 +41,16 @@ Addressed by *niri/workspaces* default: false ++ If set to true, only the active or focused workspace will be shown. +*display-condition*: ++ + typeof: string ++ + oneof: "show-all", "keep-named", "only-populated" ++ + default: "show-all" ++ + Specifies the conditions for showing workspace widgets. ++ + ++ + *show-all*: displays widgets for all workspaces. ++ + *keep-named*: always displays named workspaces, irrespective of whether they contain windows or not. ++ + *only-populated*: only displays widgets for workspaces that have windows. + *on-update*: ++ typeof: string ++ Command to execute when the module is updated. diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index d2fcad5d..6141e9e9 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -32,13 +32,22 @@ void Workspaces::doUpdate() { auto ipcLock = gIPC->lockData(); const auto alloutputs = config_["all-outputs"].asBool(); + const auto display_cond = config_["display-condition"].asString(); std::vector my_workspaces; const auto &workspaces = gIPC->workspaces(); - std::copy_if(workspaces.cbegin(), workspaces.cend(), std::back_inserter(my_workspaces), - [&](const auto &ws) { - if (alloutputs) return true; - return ws["output"].asString() == bar_.output->name; - }); + std::copy_if( + workspaces.cbegin(), workspaces.cend(), std::back_inserter(my_workspaces), + [&](const auto &ws) { + if (display_cond == "only-populated") { + if (ws["active_window_id"].isNull() && !ws["is_active"].asBool()) return false; + } else if (display_cond == "keep-named") { + if (ws["name"].isNull() && ws["active_window_id"].isNull() && !ws["is_active"].asBool()) + return false; + } + + if (alloutputs) return true; + return ws["output"].asString() == bar_.output->name; + }); // Remove buttons for removed workspaces. for (auto it = buttons_.begin(); it != buttons_.end();) { From 6a7b4af78a1f1b8bca0123497823477f4e1387f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Piwo=C5=84ski?= Date: Fri, 28 Mar 2025 20:56:22 +0000 Subject: [PATCH 033/416] feat(Group): add dynamic drawer hiding modules based on module's class. Add `always-visible-class` to config. Update the drawer on `hide_group`. --- include/group.hpp | 2 ++ src/group.cpp | 48 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/include/group.hpp b/include/group.hpp index 5ce331a8..99419b45 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -26,12 +26,14 @@ class Group : public AModule { bool is_first_widget = true; bool is_drawer = false; bool click_to_reveal = false; + std::string always_visible_class; std::string add_class_to_drawer_children; bool handleMouseEnter(GdkEventCrossing *const &ev) override; bool handleMouseLeave(GdkEventCrossing *const &ev) override; bool handleToggle(GdkEventButton *const &ev) override; void show_group(); void hide_group(); + void update_always_visible_modules(); }; } // namespace waybar diff --git a/src/group.cpp b/src/group.cpp index 50841efd..83a1846e 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -63,6 +63,9 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& ? drawer_config["transition-left-to-right"].asBool() : true); click_to_reveal = drawer_config["click-to-reveal"].asBool(); + always_visible_class = + (drawer_config["always-visible-class"].isString() ? drawer_config["always-visible-class"].asString() + : ""); auto transition_type = getPreferredTransitionType(vertical); @@ -89,9 +92,52 @@ void Group::show_group() { revealer.set_reveal_child(true); } +void Group::update_always_visible_modules() { + if (always_visible_class.empty()) { + return; + } + + for (auto* event_box : revealer_box.get_children()) { + if (auto container = dynamic_cast(event_box)) { + for (auto* base_element : container->get_children()) { + if (base_element->get_style_context()->has_class("active")) { + event_box->get_style_context()->remove_class(add_class_to_drawer_children); + + revealer_box.remove(*event_box); + box.pack_end(*event_box, false, false); + event_box->show(); + } + } + } + } + + for (auto* event_box : box.get_children()) { + if (event_box == &revealer) { + continue; + } + if (box.get_children().size() <= 2) { + break; + } + if (auto container = dynamic_cast(event_box)) { + for (auto* base_element : container->get_children()) { + if (!base_element->get_style_context()->has_class("active")) { + event_box->get_style_context()->add_class(add_class_to_drawer_children); + + box.remove(*event_box); + revealer_box.pack_start(*event_box, false, false); + event_box->show(); + } + } + } + } + +} + void Group::hide_group() { box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); revealer.set_reveal_child(false); + + Group::update_always_visible_modules(); } bool Group::handleMouseEnter(GdkEventCrossing* const& e) { @@ -121,7 +167,7 @@ bool Group::handleToggle(GdkEventButton* const& e) { } auto Group::update() -> void { - // noop + Group::update_always_visible_modules(); } Gtk::Box& Group::getBox() { return is_drawer ? (is_first_widget ? box : revealer_box) : box; } From 653732dfc6c4b8d79985b5f7348bcdc933a0dd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Piwo=C5=84ski?= Date: Fri, 28 Mar 2025 21:30:36 +0000 Subject: [PATCH 034/416] fix(Group): update always visible modules logic to use dynamic class reference --- src/group.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/group.cpp b/src/group.cpp index 83a1846e..5af1febb 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -100,7 +100,7 @@ void Group::update_always_visible_modules() { for (auto* event_box : revealer_box.get_children()) { if (auto container = dynamic_cast(event_box)) { for (auto* base_element : container->get_children()) { - if (base_element->get_style_context()->has_class("active")) { + if (base_element->get_style_context()->has_class(always_visible_class)) { event_box->get_style_context()->remove_class(add_class_to_drawer_children); revealer_box.remove(*event_box); @@ -120,7 +120,7 @@ void Group::update_always_visible_modules() { } if (auto container = dynamic_cast(event_box)) { for (auto* base_element : container->get_children()) { - if (!base_element->get_style_context()->has_class("active")) { + if (!base_element->get_style_context()->has_class(always_visible_class)) { event_box->get_style_context()->add_class(add_class_to_drawer_children); box.remove(*event_box); From 67df5f58384678e1ef9523bc17bee957551bea04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Piwo=C5=84ski?= Date: Sun, 30 Mar 2025 14:49:07 +0000 Subject: [PATCH 035/416] feat(Group): enhance widget visibility management - set visibility instantly on module update using dynamic signal handling. Improve performance - manage the visibility of updated module only instead of iterating over all modules on `hide_group`. --- include/AModule.hpp | 1 + include/group.hpp | 7 +++-- src/AModule.cpp | 1 + src/bar.cpp | 2 +- src/group.cpp | 75 ++++++++++++++++++++++++++------------------- 5 files changed, 51 insertions(+), 35 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index 2ffc429c..f31b4354 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -15,6 +15,7 @@ class AModule : public IModule { static constexpr const char *MODULE_CLASS = "module"; ~AModule() override; + sigc::signal signal_updated; auto update() -> void override; virtual auto refresh(int shouldRefresh) -> void {}; operator Gtk::Widget &() override; diff --git a/include/group.hpp b/include/group.hpp index 99419b45..462c05c2 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -17,7 +17,7 @@ class Group : public AModule { operator Gtk::Widget &() override; virtual Gtk::Box &getBox(); - void addWidget(Gtk::Widget &widget); + void addWidget(AModule* module); protected: Gtk::Box box; @@ -33,7 +33,10 @@ class Group : public AModule { bool handleToggle(GdkEventButton *const &ev) override; void show_group(); void hide_group(); - void update_always_visible_modules(); + void manage_visibility(AModule* module); + void show_widget(Gtk::Widget& widget); + void hide_widget(Gtk::Widget& widget); + void hide_current_widget_if_inactive(); }; } // namespace waybar diff --git a/src/AModule.cpp b/src/AModule.cpp index 5abb779a..0d5aaaba 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -95,6 +95,7 @@ auto AModule::update() -> void { if (config_["on-update"].isString()) { pid_.push_back(util::command::forkExec(config_["on-update"].asString())); } + signal_updated.emit(this); } // Get mapping between event name and module action name // Then call overrided doAction in order to call appropriate module action diff --git a/src/bar.cpp b/src/bar.cpp index b7737d36..db3402bb 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -506,7 +506,7 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, std::shared_ptr module_sp(module); modules_all_.emplace_back(module_sp); if (group != nullptr) { - group->addWidget(*module); + group->addWidget(module); } else { if (pos == "modules-left") { modules_left_.emplace_back(module_sp); diff --git a/src/group.cpp b/src/group.cpp index 5af1febb..40208714 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -92,52 +92,57 @@ void Group::show_group() { revealer.set_reveal_child(true); } -void Group::update_always_visible_modules() { - if (always_visible_class.empty()) { - return; - } +void Group::hide_widget(Gtk::Widget& widget) { + widget.get_style_context()->add_class(add_class_to_drawer_children); + box.remove(widget); + revealer_box.pack_start(widget, false, false); +} - for (auto* event_box : revealer_box.get_children()) { - if (auto container = dynamic_cast(event_box)) { - for (auto* base_element : container->get_children()) { - if (base_element->get_style_context()->has_class(always_visible_class)) { - event_box->get_style_context()->remove_class(add_class_to_drawer_children); - - revealer_box.remove(*event_box); - box.pack_end(*event_box, false, false); - event_box->show(); - } - } - } - } +void Group::show_widget(Gtk::Widget& widget) { + widget.get_style_context()->remove_class(add_class_to_drawer_children); + revealer_box.remove(widget); + box.pack_end(widget, false, false); +} +void Group::hide_current_widget_if_inactive() { for (auto* event_box : box.get_children()) { if (event_box == &revealer) { continue; } - if (box.get_children().size() <= 2) { - break; - } - if (auto container = dynamic_cast(event_box)) { - for (auto* base_element : container->get_children()) { - if (!base_element->get_style_context()->has_class(always_visible_class)) { - event_box->get_style_context()->add_class(add_class_to_drawer_children); - - box.remove(*event_box); - revealer_box.pack_start(*event_box, false, false); - event_box->show(); + if (auto event_box_container = dynamic_cast(event_box)) { + for (auto* the_only_visible : event_box_container->get_children()) { + if (!the_only_visible->get_style_context()->has_class(always_visible_class)) { + hide_widget(*event_box); } } } } +} +void Group::manage_visibility(AModule* module) { + Gtk::Widget& widget = *module; + + if (auto container = dynamic_cast(&widget)) { + for (auto* base_element : container->get_children()) { + if (base_element->get_style_context()->has_class(always_visible_class)) { + if (box.get_children().size() == 2) { + Group::hide_current_widget_if_inactive(); + } + show_widget(widget); + } else { + // Do not hide if it's the only widget + revealer + if (box.get_children().size() <= 2) { + return; + } + hide_widget(widget); + } + } + } } void Group::hide_group() { box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); revealer.set_reveal_child(false); - - Group::update_always_visible_modules(); } bool Group::handleMouseEnter(GdkEventCrossing* const& e) { @@ -167,12 +172,14 @@ bool Group::handleToggle(GdkEventButton* const& e) { } auto Group::update() -> void { - Group::update_always_visible_modules(); + // noop } Gtk::Box& Group::getBox() { return is_drawer ? (is_first_widget ? box : revealer_box) : box; } -void Group::addWidget(Gtk::Widget& widget) { +void Group::addWidget(AModule* module) { + Gtk::Widget& widget = *module; + getBox().pack_start(widget, false, false); if (is_drawer && !is_first_widget) { @@ -180,6 +187,10 @@ void Group::addWidget(Gtk::Widget& widget) { } is_first_widget = false; + + if (!always_visible_class.empty()) { + module->signal_updated.connect(sigc::mem_fun(*this, &Group::manage_visibility)); + } } Group::operator Gtk::Widget&() { return event_box_; } From 2faad27cadb6d86c064debc82fcea90dcaf1d0a2 Mon Sep 17 00:00:00 2001 From: fkobi Date: Mon, 7 Apr 2025 14:45:31 +0000 Subject: [PATCH 036/416] meson_options: improve sndio's description Correct me if I am wrong but I think that is it's only use. If that is the case the description would be less generic and thus better. --- meson_options.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson_options.txt b/meson_options.txt index d83fe01f..42827c02 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -12,7 +12,7 @@ option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable supp option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') option('mpd', type: 'feature', value: 'auto', description: 'Enable support for the Music Player Daemon') option('rfkill', type: 'feature', value: 'auto', description: 'Enable support for RFKILL') -option('sndio', type: 'feature', value: 'auto', description: 'Enable support for sndio') +option('sndio', type: 'feature', value: 'auto', description: 'Enable support for volume control via sndio') option('logind', type: 'feature', value: 'auto', description: 'Enable support for logind') option('tests', type: 'feature', value: 'auto', description: 'Enable tests') option('experimental', type : 'boolean', value : false, description: 'Enable experimental features') From 9b7a8a8c21a43f94a421a0db65244e8277b4a603 Mon Sep 17 00:00:00 2001 From: hecate cantus Date: Sun, 27 Apr 2025 20:30:14 -0700 Subject: [PATCH 037/416] implement albumArtist todo testing --- include/modules/mpris/mpris.hpp | 3 +++ src/modules/mpris/mpris.cpp | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/include/modules/mpris/mpris.hpp b/include/modules/mpris/mpris.hpp index ad4dac1e..a07a6358 100644 --- a/include/modules/mpris/mpris.hpp +++ b/include/modules/mpris/mpris.hpp @@ -38,6 +38,7 @@ class Mpris : public ALabel { std::optional artist; std::optional album; + std::optional album_artist; std::optional title; std::optional length; // as HH:MM:SS std::optional position; // same format @@ -76,6 +77,8 @@ class Mpris : public ALabel { std::string player_; std::vector ignored_players_; + bool prefer_album_artist_; + PlayerctlPlayerManager* manager; PlayerctlPlayer* player; std::string lastStatus; diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index ed383b0c..2492f3b5 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -31,6 +31,7 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) dynamic_separator_(" - "), truncate_hours_(true), tooltip_len_limits_(false), + prefer_album_artist_(false), // this character is used in Gnome so it's fine to use it here ellipsis_("\u2026"), player_("playerctld"), @@ -68,6 +69,9 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) if (config_["enable-tooltip-len-limits"].isBool()) { tooltip_len_limits_ = config["enable-tooltip-len-limits"].asBool(); } + if (config_["prefer-album-artist"].isBool()) { + prefer_album_artist_ = config["prefer-album-artist"].asBool(); + } } if (config["artist-len"].isUInt()) { @@ -244,6 +248,12 @@ void truncate(std::string& s, const std::string& ellipsis, size_t max_len) { auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string { auto artist = info.artist.value_or(std::string()); + if (prefer_album_artist_) { + auto album_artist = info.album_artist.value_or(std::string()); + if (!album_artist.empty()) { + artist = album_artist; + } + } if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_); return artist; } @@ -516,6 +526,7 @@ auto Mpris::getPlayerInfo() -> std::optional { .status_string = player_status, .artist = std::nullopt, .album = std::nullopt, + .album_artist = std::nullopt, .title = std::nullopt, .length = std::nullopt, }; @@ -527,6 +538,12 @@ auto Mpris::getPlayerInfo() -> std::optional { } if (error) goto errorexit; + if (auto* album_artist_ = playerctl_player_print_metadata_prop(player, "xesam:albumArtist", &error)) { + spdlog::debug("mpris[{}]: albumArtist = {}", info.name, album_artist_); + info.album_artist = album_artist_; + g_free(album_artist_); + } + if (auto* album_ = playerctl_player_get_album(player, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); info.album = album_; From 5ab852ff5245596f5f153b584d4c9557bb3a2112 Mon Sep 17 00:00:00 2001 From: Alex Denes Date: Sat, 8 Feb 2025 01:35:32 +0000 Subject: [PATCH 038/416] dwl/window: allow hiding of title when either inactive or empty --- include/modules/dwl/window.hpp | 2 ++ man/waybar-dwl-window.5.scd | 10 ++++++++++ src/modules/dwl/window.cpp | 24 ++++++++++++++++++++---- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/include/modules/dwl/window.hpp b/include/modules/dwl/window.hpp index 619360a5..212fb513 100644 --- a/include/modules/dwl/window.hpp +++ b/include/modules/dwl/window.hpp @@ -32,6 +32,8 @@ class Window : public AAppIconLabel, public sigc::trackable { std::string appid_; std::string layout_symbol_; bool active_; + bool hide_inactive_; + bool hide_empty_; uint32_t layout_; struct zdwl_ipc_output_v2 *output_status_; diff --git a/man/waybar-dwl-window.5.scd b/man/waybar-dwl-window.5.scd index c7f70908..907d5332 100644 --- a/man/waybar-dwl-window.5.scd +++ b/man/waybar-dwl-window.5.scd @@ -17,6 +17,16 @@ Addressed by *dwl/window* default: {title} ++ The format, how information should be displayed. +*hide-empty*: ++ + typeof: bool ++ + default: false ++ + Option to hide the module when the content would be empty. + +*hide-inactive*: ++ + typeof: bool ++ + default: false ++ + Option to hide the module when the window is unfocused. + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label (in 90 degree increments). diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp index 82d92003..b5dd377c 100644 --- a/src/modules/dwl/window.cpp +++ b/src/modules/dwl/window.cpp @@ -76,7 +76,15 @@ static const wl_registry_listener registry_listener_impl = {.global = handle_glo Window::Window(const std::string &id, const Bar &bar, const Json::Value &config) : AAppIconLabel(config, "window", id, "{}", 0, true), bar_(bar), - active_(false) { + active_(false), + hide_inactive_(false), + hide_empty_(false) { + if (config_["hide-inactive"].isBool()) { + hide_inactive_ = config["hide-inactive"].asBool(); + } + if (config_["hide-empty"].isBool()) { + hide_empty_ = config["hide-empty"].asBool(); + } struct wl_display *display = Client::inst()->wl_display; struct wl_registry *registry = wl_display_get_registry(display); @@ -122,10 +130,18 @@ void Window::handle_frame() { if (tooltipEnabled()) { label_.set_tooltip_text(title_); } - if (active_) { - box_.get_style_context()->add_class("active"); + if (hide_empty_ && title_.empty()) { + box_.set_visible(false); } else { - box_.get_style_context()->remove_class("active"); + if (active_) { + box_.get_style_context()->add_class("active"); + box_.set_visible(true); + } else { + box_.get_style_context()->remove_class("active"); + if (hide_inactive_) { + box_.set_visible(false); + } + } } } From f3ae13e44600f3585b32bf9b50258b3c3f19a1dc Mon Sep 17 00:00:00 2001 From: Duncan Overbruck Date: Tue, 13 May 2025 21:40:39 +0200 Subject: [PATCH 039/416] cpu: add load{1,5,15} format variables --- man/waybar-cpu.5.scd | 6 ++++++ src/modules/cpu.cpp | 3 +++ 2 files changed, 9 insertions(+) diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index 287bf123..3e06f35c 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -91,6 +91,12 @@ The *cpu* module displays the current CPU utilization. *{load}*: Current CPU load. +*{load1}*: CPU load average over the last minute. + +*{load5}*: CPU load average over the last 5 minutes. + +*{load15}*: CPU load average over the last 15 minutes. + *{usage}*: Current overall CPU usage. *{usage*{n}*}*: Current CPU core n usage. Cores are numbered from zero, so first core will be {usage0} and 4th will be {usage3}. diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 0703eaf7..45035a81 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -43,6 +43,9 @@ auto waybar::modules::Cpu::update() -> void { auto icons = std::vector{state}; fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("load", load1)); + store.push_back(fmt::arg("load1", load1)); + store.push_back(fmt::arg("load5", load5)); + store.push_back(fmt::arg("load15", load15)); store.push_back(fmt::arg("usage", total_usage)); store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); store.push_back(fmt::arg("max_frequency", max_frequency)); From ba1988e0c24090180056839a47ae5d45b9b0f8d6 Mon Sep 17 00:00:00 2001 From: Marcin Fabrykowski Date: Thu, 4 Apr 2024 23:27:06 +0200 Subject: [PATCH 040/416] allow dynamic timeout setting Signed-off-by: Marcin Fabrykowski --- include/modules/idle_inhibitor.hpp | 9 +++- man/waybar-idle-inhibitor.5.scd | 37 ++++++++++++-- src/modules/idle_inhibitor.cpp | 80 +++++++++++++++++++++++++----- 3 files changed, 107 insertions(+), 19 deletions(-) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 22bd808f..6fad6706 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -17,14 +17,21 @@ class IdleInhibitor : public ALabel { auto update() -> void override; static std::list modules; static bool status; + static long deactivationTime; private: bool handleToggle(GdkEventButton* const& e) override; - void toggleStatus(); + bool handleScroll(GdkEventScroll* e) override; + + void toggleStatus(int force_status = -1); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; int pid_; + + bool dynamicTimeout; + short timeout; + short timeout_step; }; } // namespace waybar::modules diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 6d5a2170..3043fa67 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -41,15 +41,15 @@ screensaver, also known as "presentation mode". *on-click*: ++ typeof: string ++ - Command to execute when clicked on the module. A click also toggles the state + Command to execute when clicked on the module. A click also toggles the state (enable only if dynamic timeouts are enabled). *on-click-middle*: ++ typeof: string ++ - Command to execute when middle-clicked on the module using mousewheel. + Command to execute when middle-clicked on the module using mousewheel. (reset the timeout to the initial value if dynamic timeouts are enabled). *on-click-right*: ++ typeof: string ++ - Command to execute when you right-click on the module. + Command to execute when you right-click on the module. (deactivate the inhibit if dynamic timeouts are enabled). *on-update*: ++ typeof: string ++ @@ -57,11 +57,11 @@ screensaver, also known as "presentation mode". *on-scroll-up*: ++ typeof: string ++ - Command to execute when scrolling up on the module. + Command to execute when scrolling up on the module. (increase the timeout if dynamic timeouts are enabled). *on-scroll-down*: ++ typeof: string ++ - Command to execute when scrolling down on the module. + Command to execute when scrolling down on the module. (decrease the timeout if dynamic timeouts are enabled). *smooth-scrolling-threshold*: ++ typeof: double ++ @@ -76,6 +76,16 @@ screensaver, also known as "presentation mode". typeof: double ++ The number of minutes the inhibition should last. +*timeout_step*: ++ + typeof: double ++ + default: 10 ++ + The number of minutes to add or subtract when scrolling (when dynamic timeouts are enabled). + +*dynamic-timeouts*: ++ + typeof: bool ++ + default: false ++ + Option to enable dynamic timeouts. + *tooltip*: ++ typeof: bool ++ default: true ++ @@ -113,6 +123,10 @@ screensaver, also known as "presentation mode". *{icon}*: Icon, as defined in *format-icons* +*{timeout}*: Timeout in minutes + +*{timeleft}*: Time left in minutes + # EXAMPLES ``` @@ -125,3 +139,16 @@ screensaver, also known as "presentation mode". "timeout": 30.5 } ``` + +``` +"idle_inhibitor": { + "format": "{status} {timeleft}/{timeout}", + "format-icons": { + "activated": "", + "deactivated": "" + }, + "timeout": 480, + "dynamic-timeouts": true, + "timeout-step": 10 +} +``` diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index a5fc9ac7..56568e7e 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -5,13 +5,16 @@ std::list waybar::modules::IdleInhibitor::modules; bool waybar::modules::IdleInhibitor::status = false; +long waybar::modules::IdleInhibitor::deactivationTime = time(nullptr); waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "idle_inhibitor", id, "{status}", 0, false, true), bar_(bar), idle_inhibitor_(nullptr), - pid_(-1) { + pid_(-1), + timeout(config_["timeout"].asDouble()), + timeout_step(config_["timeout-step"].isDouble() ? config_["timeout-step"].asDouble() : 10) { if (waybar::Client::inst()->idle_inhibit_manager == nullptr) { throw std::runtime_error("idle-inhibit not available"); } @@ -21,10 +24,14 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& toggleStatus(); } - event_box_.add_events(Gdk::BUTTON_PRESS_MASK); + deactivationTime = time(nullptr) + timeout * 60; + + event_box_.add_events(Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); 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)); + // Add this to the modules list waybar::modules::IdleInhibitor::modules.push_back(this); @@ -63,7 +70,9 @@ auto waybar::modules::IdleInhibitor::update() -> void { } std::string status_text = status ? "activated" : "deactivated"; + int timeleft = (deactivationTime - time(nullptr)) / 60; label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("status", status_text), + fmt::arg("timeout", timeout), fmt::arg("timeleft", timeleft), fmt::arg("icon", getIcon(0, status_text)))); label_.get_style_context()->add_class(status_text); if (tooltipEnabled()) { @@ -77,38 +86,49 @@ auto waybar::modules::IdleInhibitor::update() -> void { ALabel::update(); } -void waybar::modules::IdleInhibitor::toggleStatus() { +void waybar::modules::IdleInhibitor::toggleStatus(int force_status) { status = !status; + if (force_status != -1) { + status = force_status; + } if (timeout_.connected()) { /* cancel any already active timeout handler */ timeout_.disconnect(); } - if (status && config_["timeout"].isNumeric()) { - auto timeoutMins = config_["timeout"].asDouble(); - int timeoutSecs = timeoutMins * 60; - + if (status && timeout) { + deactivationTime = time(nullptr) + timeout * 60; timeout_ = Glib::signal_timeout().connect_seconds( []() { /* intentionally not tied to a module instance lifetime * as the output with `this` can be disconnected */ - spdlog::info("deactivating idle_inhibitor by timeout"); - status = false; + bool continueRunning = true; + int timeleft = (deactivationTime - time(nullptr)) / 60; + spdlog::info("updating timeleft. deactivation timestamp: {}, minutes left: {}", + deactivationTime, timeleft); + if (timeleft <= 0) { + spdlog::info("deactivating idle_inhibitor by timeout"); + status = false; + continueRunning = false; + } for (auto const& module : waybar::modules::IdleInhibitor::modules) { module->update(); } - /* disconnect */ - return false; + return continueRunning; }, - timeoutSecs); + 60); } } bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { if (e->button == 1) { - toggleStatus(); + if (config_["dynamic-timeout"].asBool()) { + toggleStatus(1); + } else { + toggleStatus(); + } // Make all other idle inhibitor modules update for (auto const& module : waybar::modules::IdleInhibitor::modules) { @@ -117,7 +137,41 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } } } + if (e->button == 3) { + toggleStatus(0); + // Make all other idle inhibitor modules update + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + if (module != this) { + module->update(); + } + } + } + if (e->button == 2) { + toggleStatus(0); + timeout = config_["timeout"].asDouble(); + } ALabel::handleToggle(e); return true; } + +bool waybar::modules::IdleInhibitor::handleScroll(GdkEventScroll* e) { + if (!config_["dynamic-timeout"].asBool()) { + return true; + } + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + toggleStatus(0); + double step = dir == SCROLL_DIR::UP ? 1 : -1; + step *= timeout_step; + timeout += step; + if (timeout < 0) { + timeout = 0; + } + deactivationTime = time(nullptr) + timeout * 60; + + ALabel::handleScroll(e); + return true; +} From e3e80773422b4b56166967f9aca0f8fc4a1fcc63 Mon Sep 17 00:00:00 2001 From: gred Date: Sat, 7 Jun 2025 12:11:22 +0300 Subject: [PATCH 041/416] Add hide-active --- include/modules/hyprland/workspaces.hpp | 2 ++ src/modules/hyprland/workspace.cpp | 9 +++++++++ src/modules/hyprland/workspaces.cpp | 1 + 3 files changed, 12 insertions(+) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 6b33baea..7618f35c 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -36,6 +36,7 @@ class Workspaces : public AModule, public EventHandler { auto allOutputs() const -> bool { return m_allOutputs; } auto showSpecial() const -> bool { return m_showSpecial; } auto activeOnly() const -> bool { return m_activeOnly; } + auto hideActive() const -> bool { return m_hideActive; } auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } auto moveToMonitor() const -> bool { return m_moveToMonitor; } @@ -121,6 +122,7 @@ class Workspaces : public AModule, public EventHandler { bool m_allOutputs = false; bool m_showSpecial = false; bool m_activeOnly = false; + bool m_hideActive = false; bool m_specialVisibleOnly = false; bool m_moveToMonitor = false; Json::Value m_persistentWorkspaceConfig; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 4655096f..e0fed6cc 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -173,6 +173,15 @@ std::string &Workspace::selectIcon(std::map &icons_map } void Workspace::update(const std::string &format, const std::string &icon) { + // clang-format off + if (this->m_workspaceManager.hideActive() && \ + this->isActive() && \ + !this->isPersistent() && \ + !this->isSpecial()) { + // clang-format on + m_button.hide(); + return; + } // clang-format off if (this->m_workspaceManager.activeOnly() && \ !this->isActive() && \ diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 0e225935..ebfc942b 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -601,6 +601,7 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { populateBoolConfig(config, "show-special", m_showSpecial); populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly); populateBoolConfig(config, "active-only", m_activeOnly); + populateBoolConfig(config, "hide-active", m_hideActive); populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); m_persistentWorkspaceConfig = config.get("persistent-workspaces", Json::Value()); From 35b0d591a60440828add7bfdc6b24d44acead5b0 Mon Sep 17 00:00:00 2001 From: gred Date: Sat, 7 Jun 2025 12:14:40 +0300 Subject: [PATCH 042/416] Update man page --- man/waybar-hyprland-workspaces.5.scd | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 18c39898..79ee1d26 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -58,6 +58,11 @@ Addressed by *hyprland/workspaces* default: false ++ If set to true, only the active workspace will be shown. +*hide-active*: ++ + typeof: bool ++ + default: false ++ + If set to true, the active workspace will be hidded. Unless a workspace is persistent or special. + *move-to-monitor*: ++ typeof: bool ++ default: false ++ From 5a075646d27255cb66358e84ef3f86e01b7693a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= <88423841+Roc25@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:19:52 +0300 Subject: [PATCH 043/416] Update man Co-authored-by: Tuur Vanhoutte <4633209+zjeffer@users.noreply.github.com> --- man/waybar-hyprland-workspaces.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 79ee1d26..73ccd9f3 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -61,7 +61,7 @@ Addressed by *hyprland/workspaces* *hide-active*: ++ typeof: bool ++ default: false ++ - If set to true, the active workspace will be hidded. Unless a workspace is persistent or special. + If set to true, the active workspace will be hidden. Unless a workspace is persistent or special. *move-to-monitor*: ++ typeof: bool ++ From 9b6590b972590672d465b1823c050b56fb4c288e Mon Sep 17 00:00:00 2001 From: burbschat Date: Sat, 14 Jun 2025 22:30:46 +0900 Subject: [PATCH 044/416] Add CSS classes for sway workspace indicators depending on associated output Allows for different styles of workspace indicators (buttons) depending on the output the workspace is located at. Using this one can for example color the indicators depending on the output making it easy to infer on which output a workspace is located. Example config: "sway/workspaces": { "all-outputs": true, "format": "{name}", "output-classes": { "DP-1": "output1", "LVDS-2": "output2" } } Example CSS: #workspaces button.output1{ background-color: green; } #workspaces button.output2{ background-color: blue; } --- man/waybar-sway-workspaces.5.scd | 7 +++++++ src/modules/sway/workspaces.cpp | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 1c4360e9..e4041f5a 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -105,6 +105,12 @@ warp-on-scroll: ++ default: false ++ Enables this module to consume all left over space dynamically. +*output-classes*: ++ + typeof: array ++ + Specify additional CSS classes to be added to workspace indicators based on the output on which the associated workspace is located. + Keys are output names and values are class names like *${output}: {output-class}*. + Assignment in config is used to keep the stylesheet independent of the available outputs. + # FORMAT REPLACEMENTS @@ -193,3 +199,4 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge - *#workspaces button.empty* - *#workspaces button.current_output* - *#workspaces button#sway-workspace-${name}* +- *#workspaces button.${output-class}* diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index b8ed73d4..fc122a03 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -321,6 +321,17 @@ auto Workspaces::update() -> void { button.get_style_context()->remove_class("empty"); } if ((*it)["output"].isString()) { + // Simply attempt to remove all output classes every time to reset output classes. This works + // even if a class has not been previously added to the style context. + for (const auto & oclass : config_["output-classes"]) { + button.get_style_context()->remove_class(oclass.asString()); + } + // If output-classes contains a class for output associated with current workspace button, add + // the class to its style context. + std::string output_name = (*it)["output"].asString(); + if (config_["output-classes"].isMember(output_name) && config_["output-classes"][output_name].isString()){ + button.get_style_context()->add_class(config_["output-classes"][output_name].asString()); + } if (((*it)["output"].asString()) == bar_.output->name) { button.get_style_context()->add_class("current_output"); } else { From d6f9dc911a9fd549893778e3e605cbea89ea3a98 Mon Sep 17 00:00:00 2001 From: burbschat Date: Sat, 14 Jun 2025 22:59:32 +0900 Subject: [PATCH 045/416] Apply format according to .clang-format --- src/modules/sway/workspaces.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index fc122a03..3b319623 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -323,13 +323,14 @@ auto Workspaces::update() -> void { if ((*it)["output"].isString()) { // Simply attempt to remove all output classes every time to reset output classes. This works // even if a class has not been previously added to the style context. - for (const auto & oclass : config_["output-classes"]) { + for (const auto &oclass : config_["output-classes"]) { button.get_style_context()->remove_class(oclass.asString()); } // If output-classes contains a class for output associated with current workspace button, add // the class to its style context. std::string output_name = (*it)["output"].asString(); - if (config_["output-classes"].isMember(output_name) && config_["output-classes"][output_name].isString()){ + if (config_["output-classes"].isMember(output_name) && + config_["output-classes"][output_name].isString()) { button.get_style_context()->add_class(config_["output-classes"][output_name].asString()); } if (((*it)["output"].asString()) == bar_.output->name) { From ad0d1350fdf058dfd718192611f1f4320148d2ca Mon Sep 17 00:00:00 2001 From: AtomToast Date: Wed, 29 May 2024 17:48:21 +0200 Subject: [PATCH 046/416] feat(river/layout): add format-icons --- man/waybar-river-layout.5.scd | 35 +++++++++++++++++++++++++++++++---- src/modules/river/layout.cpp | 4 +++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/man/waybar-river-layout.5.scd b/man/waybar-river-layout.5.scd index 78e03634..7f4ff2e5 100644 --- a/man/waybar-river-layout.5.scd +++ b/man/waybar-river-layout.5.scd @@ -16,9 +16,13 @@ Addressed by *river/layout* *format*: ++ typeof: string ++ - default: {} ++ + default: {layout} ++ The format, how information should be displayed. On {} data gets inserted. +*format-icons*: ++ + typeof: array ++ + Based on the current layout, the corresponding icon gets selected. + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label (in 90 degree increments). @@ -69,16 +73,39 @@ Addressed by *river/layout* default: false ++ Enables this module to consume all left over space dynamically. -# EXAMPLE +# FORMAT REPLACEMENTS + +*{layout}*: Layout, as defined by your layout generator. By default this will be *rivertile(1)* + +*{icon}*: Icon, as defined in *format-icons* + +# EXAMPLES + +With layout name: ``` "river/layout": { - "format": "{}", + "format": "{layout}", "min-length": 4, "align": "right" } ``` +With icons, this sets icons for the 4 layouts available in rivertile: + +``` +"river/layout": { + "format": "{icon}", + "min-length": 3, + "format-icons": { + "left": "[]=", + "bottom": "TTT", + "top": "ꓕꓕꓕ", + "right": "=[]", + } +} +``` + # STYLE - *#layout* @@ -86,4 +113,4 @@ Addressed by *river/layout* # SEE ALSO -waybar(5), river(1) +waybar(5), river(1), rivertile(1) diff --git a/src/modules/river/layout.cpp b/src/modules/river/layout.cpp index e938400f..589ca29f 100644 --- a/src/modules/river/layout.cpp +++ b/src/modules/river/layout.cpp @@ -146,7 +146,9 @@ void Layout::handle_name(const char *name) { label_.hide(); // hide empty labels or labels with empty format } else { label_.show(); - label_.set_markup(fmt::format(fmt::runtime(format_), Glib::Markup::escape_text(name).raw())); + std::string escaped_name=Glib::Markup::escape_text(name).raw(); + label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("layout", name), + fmt::arg("icon", getIcon(0, name)))); } ALabel::update(); } From bcac5ffc23897b1d988b9b05e53924944698bb04 Mon Sep 17 00:00:00 2001 From: AtomToast Date: Wed, 29 May 2024 17:48:54 +0200 Subject: [PATCH 047/416] doc(README): update river feature list --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5266e916..2cc8bff4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ #### Current features - Sway (Workspaces, Binding mode, Focused window name) -- River (Mapping mode, Tags, Focused window name) +- River (Mapping mode, Tags, Focused window name, Layout) - Hyprland (Window Icons, Workspaces, Focused window name) - Niri (Workspaces, Focused window name, Language) - DWL (Tags, Focused window name) [requires dwl ipc patch](https://codeberg.org/dwl/dwl-patches/src/branch/main/patches/ipc) From b4c247a4372cfc4fad1f8524284785d9178163ad Mon Sep 17 00:00:00 2001 From: AtomToast Date: Tue, 29 Apr 2025 17:26:17 +0200 Subject: [PATCH 048/416] remove unused variable --- src/modules/river/layout.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/river/layout.cpp b/src/modules/river/layout.cpp index 589ca29f..8109dadc 100644 --- a/src/modules/river/layout.cpp +++ b/src/modules/river/layout.cpp @@ -146,7 +146,6 @@ void Layout::handle_name(const char *name) { label_.hide(); // hide empty labels or labels with empty format } else { label_.show(); - std::string escaped_name=Glib::Markup::escape_text(name).raw(); label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("layout", name), fmt::arg("icon", getIcon(0, name)))); } From 383949795eb23a15809b39ae79f5c1849e09af87 Mon Sep 17 00:00:00 2001 From: LoricAndre Date: Wed, 23 Jul 2025 17:38:25 +0200 Subject: [PATCH 049/416] feat(hyprland/workspaces): add uniqueIcons option --- include/modules/hyprland/workspaces.hpp | 2 ++ src/modules/hyprland/workspace.cpp | 6 +++++- src/modules/hyprland/workspaces.cpp | 3 ++- test/config.cpp | 1 + test/config/hyprland-workspaces.json | 1 + 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 9a44e594..9cc81ec9 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -39,6 +39,7 @@ class Workspaces : public AModule, public EventHandler { auto specialVisibleOnly() const -> bool { return m_specialVisibleOnly; } auto persistentOnly() const -> bool { return m_persistentOnly; } auto moveToMonitor() const -> bool { return m_moveToMonitor; } + auto uniqueIcons() const -> bool { return m_uniqueIcons; } auto getBarOutput() const -> std::string { return m_bar.output->name; } @@ -126,6 +127,7 @@ class Workspaces : public AModule, public EventHandler { bool m_specialVisibleOnly = false; bool m_persistentOnly = false; bool m_moveToMonitor = false; + bool m_uniqueIcons = false; Json::Value m_persistentWorkspaceConfig; // Map for windows stored in workspaces not present in the current bar. diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index b58c1719..01678a54 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -95,7 +96,10 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) { if (!create_window_payload.isEmpty(m_workspaceManager)) { auto repr = create_window_payload.repr(m_workspaceManager); - if (!repr.empty()) { + if (!repr.empty() && (!m_workspaceManager.uniqueIcons() || + std::ranges::find_if(m_windowMap, [&repr](const auto &x) { + return x.second == repr; + }) == m_windowMap.end())) { m_windowMap[create_window_payload.getAddress()] = repr; } } diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index cfc62025..1c5d6e38 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -609,6 +609,7 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void { populateBoolConfig(config, "persistent-only", m_persistentOnly); populateBoolConfig(config, "active-only", m_activeOnly); populateBoolConfig(config, "move-to-monitor", m_moveToMonitor); + populateBoolConfig(config, "unique-icons", m_uniqueIcons); m_persistentWorkspaceConfig = config.get("persistent-workspaces", Json::Value()); populateSortByConfig(config); @@ -1038,4 +1039,4 @@ std::optional Workspaces::parseWorkspaceId(std::string const &workspaceIdSt } } -} // namespace waybar::modules::hyprland \ No newline at end of file +} // namespace waybar::modules::hyprland diff --git a/test/config.cpp b/test/config.cpp index c60519ce..22d0b07e 100644 --- a/test/config.cpp +++ b/test/config.cpp @@ -139,6 +139,7 @@ TEST_CASE("Load Hyprland Workspaces bar config", "[config]") { REQUIRE(hyprland["active-only"].asBool() == true); REQUIRE(hyprland["all-outputs"].asBool() == false); REQUIRE(hyprland["move-to-monitor"].asBool() == true); + REQUIRE(hyprland["unique-icons"].asBool() == true); REQUIRE(hyprland["format"].asString() == "{icon} {windows}"); REQUIRE(hyprland["format-window-separator"].asString() == " "); REQUIRE(hyprland["on-scroll-down"].asString() == "hyprctl dispatch workspace e-1"); diff --git a/test/config/hyprland-workspaces.json b/test/config/hyprland-workspaces.json index dd733897..903e94f9 100644 --- a/test/config/hyprland-workspaces.json +++ b/test/config/hyprland-workspaces.json @@ -29,6 +29,7 @@ "window-rewrite": { "title": "" }, + "unique-icons": true, "window-rewrite-default": "", "window-rewrite-separator": " ", "sort-by": "number" From d1f3c0ef95a0cc9db346642cefbd7ce125e7e953 Mon Sep 17 00:00:00 2001 From: LoricAndre Date: Wed, 23 Jul 2025 17:46:46 +0200 Subject: [PATCH 050/416] docs: update man page --- man/waybar-hyprland-workspaces.5.scd | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index f48d6f78..9119e7de 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -70,6 +70,11 @@ Addressed by *hyprland/workspaces* Otherwise, the workspace will open on the monitor where it was previously assigned. Analog to using `focusworkspaceoncurrentmonitor` dispatcher instead of `workspace` in Hyprland. +*unique-icons*: ++ + typeof: bool ++ + default: false ++ + If set to true, only one instance of each window icon will be shown per workspace + *ignore-workspaces*: ++ typeof: array ++ default: [] ++ From af1ccf7d865133aad55ecfc6bab7ed3f14c43f69 Mon Sep 17 00:00:00 2001 From: LoricAndre Date: Wed, 23 Jul 2025 17:48:53 +0200 Subject: [PATCH 051/416] docs: add missing final period --- man/waybar-hyprland-workspaces.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 9119e7de..61646deb 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -73,7 +73,7 @@ Addressed by *hyprland/workspaces* *unique-icons*: ++ typeof: bool ++ default: false ++ - If set to true, only one instance of each window icon will be shown per workspace + If set to true, only one instance of each window icon will be shown per workspace. *ignore-workspaces*: ++ typeof: array ++ From 03eba632158fabcc17cd2847ad10a08e67ad6487 Mon Sep 17 00:00:00 2001 From: Theo Ratkin Date: Tue, 29 Jul 2025 14:04:00 -0400 Subject: [PATCH 052/416] feat(mpd): add playing-interval option --- include/modules/mpd/mpd.hpp | 3 +++ include/modules/mpd/state.hpp | 1 + include/modules/mpd/state.inl.hpp | 1 + src/modules/mpd/mpd.cpp | 5 +++++ src/modules/mpd/state.cpp | 6 +++--- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 32d526e9..699ff2cd 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -28,6 +28,8 @@ class MPD : public ALabel { unsigned timeout_; + unsigned playing_interval_; + detail::unique_connection connection_; detail::unique_status status_; @@ -59,6 +61,7 @@ class MPD : public ALabel { inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } inline bool paused() const { return connection_ && state_ == MPD_STATE_PAUSE; } + inline unsigned playing_interval() const { return playing_interval_; } }; #if !defined(MPD_NOINLINE) diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 2c9071b4..93246f9f 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -194,6 +194,7 @@ class Context { bool is_paused() const; bool is_stopped() const; constexpr std::size_t interval() const; + unsigned playing_interval() const; void tryConnect() const; void checkErrors(mpd_connection*) const; void do_update(); diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp index 895970e6..76d48bb4 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -8,6 +8,7 @@ inline bool Context::is_paused() const { return mpd_module_->paused(); } inline bool Context::is_stopped() const { return mpd_module_->stopped(); } constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_.count(); } +inline unsigned Context::playing_interval() const { return mpd_module_->playing_interval(); } inline void Context::tryConnect() const { mpd_module_->tryConnect(); } inline unique_connection& Context::connection() { return mpd_module_->connection_; } constexpr inline mpd_state Context::state() const { return mpd_module_->state_; } diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 192e6c1a..479eb09e 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -22,6 +22,7 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) port_(config_["port"].isUInt() ? config["port"].asUInt() : 0), password_(config_["password"].empty() ? "" : config_["password"].asString()), timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000), + playing_interval_(config_["playing-interval"].isUInt() ? config_["playing-interval"].asUInt() : 1'000), connection_(nullptr, &mpd_connection_free), status_(nullptr, &mpd_status_free), song_(nullptr, &mpd_song_free) { @@ -33,6 +34,10 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) spdlog::warn("{}: `timeout` configuration should be an unsigned int", module_name_); } + if (!config_["playing-interval"].isNull() && !config_["playing-interval"].isUInt()) { + spdlog::warn("{}: `playing-interval` configuration should be an unsigned int", module_name_); + } + if (!config["server"].isNull()) { if (!config_["server"].isString()) { spdlog::warn("{}:`server` configuration should be a string", module_name_); diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 3d7c8561..4f10f7c3 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -119,14 +119,14 @@ bool Idle::on_io(Glib::IOCondition const&) { void Playing::entry() noexcept { sigc::slot timer_slot = sigc::mem_fun(*this, &Playing::on_timer); - timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1); - spdlog::debug("mpd: Playing: enabled 1 second periodic timer."); + timer_connection_ = Glib::signal_timeout().connect(timer_slot, ctx_->playing_interval()); + spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval()); } void Playing::exit() noexcept { if (timer_connection_.connected()) { timer_connection_.disconnect(); - spdlog::debug("mpd: Playing: disabled 1 second periodic timer."); + spdlog::debug("mpd: Playing: disabled {}ms periodic timer.", ctx_->playing_interval()); } } From 929c817eb40f48bff74ce83fbef92e7d6627cf6f Mon Sep 17 00:00:00 2001 From: Theo Ratkin Date: Fri, 8 Aug 2025 07:55:28 -0400 Subject: [PATCH 053/416] chore(man): add playing-interval option to mpd man page --- man/waybar-mpd.5.scd | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index 84abc2e8..9ce01c4d 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -29,6 +29,11 @@ Addressed by *mpd* default: 5 ++ The interval in which the connection to the MPD server is retried +*playing-interval*: ++ + typeof: integer++ + default: 1000 ++ + The interval (in milliseconds) in which the playing state is updated. + *timeout*: ++ typeof: integer++ default: 30 ++ From ca8f51a39f67a0a5c90be2d12fd24b1110899b76 Mon Sep 17 00:00:00 2001 From: shibjyoti555 Date: Sat, 9 Aug 2025 03:11:21 +0530 Subject: [PATCH 054/416] added a "fallback" option to hyprland/window when no window in focus --- src/modules/hyprland/window.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 815fbad8..13c4a924 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -13,6 +13,8 @@ #include "util/rewrite_string.hpp" #include "util/sanitize_str.hpp" +#include + namespace waybar::modules::hyprland { std::shared_mutex windowIpcSmtx; @@ -54,8 +56,15 @@ auto Window::update() -> void { std::string label_text; if (!format_.empty()) { label_.show(); + + // If the focused window name is empty and fallback is configured, use fallback text + std::string displayTitle = windowName; + if (displayTitle.empty() && config_["fallback"].isString()) { + displayTitle = config_["fallback"].asString(); + } + label_text = waybar::util::rewriteString( - fmt::format(fmt::runtime(format_), fmt::arg("title", windowName), + fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle), fmt::arg("initialTitle", windowData_.initial_title), fmt::arg("class", windowData_.class_name), fmt::arg("initialClass", windowData_.initial_class_name)), From e355b40c66279bd62411c388f26ffde26ebdee3c Mon Sep 17 00:00:00 2001 From: winkelnp <68015877+winkelnp@users.noreply.github.com> Date: Mon, 11 Aug 2025 17:34:50 +0100 Subject: [PATCH 055/416] Add `format-bluetooth` support to wireplumber module --- src/modules/wireplumber.cpp | 44 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index a43ad29b..4b25f817 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -14,16 +14,16 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val mixer_api_(nullptr), def_nodes_api_(nullptr), default_node_name_(nullptr), - default_source_name_(nullptr), pending_plugins_(0), muted_(false), - source_muted_(false), volume_(0.0), - source_volume_(0.0), min_step_(0.0), node_id_(0), + type_(nullptr), source_node_id_(0), - type_(nullptr) { + source_muted_(false), + source_volume_(0.0), + default_source_name_(nullptr) { waybar::modules::Wireplumber::modules.push_back(this); wp_init(WP_INIT_PIPEWIRE); @@ -418,10 +418,33 @@ void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { auto waybar::modules::Wireplumber::update() -> void { auto format = format_; std::string tooltipFormat; + std::string format_name = "format"; + + // Handle sink bluetooth state + const std::string name = default_node_name_ != nullptr ? default_node_name_ : ""; + + auto bt = name.find("bluez") != std::string::npos || name.find("a2dp-sink") != std::string::npos; + if (bt) { + // format = + // config_["format-bluetooth"].isString() ? config_["format-bluetooth"].asString() : format; + format_name += "-bluetooth"; + label_.get_style_context()->add_class("bluetooth"); + } else { + label_.get_style_context()->remove_class("bluetooth"); + } // Handle sink mute state if (muted_) { - format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; + // if (bt) + // format = config_["format-bluetooth-muted"].isString() + // ? config_["format-bluetooth-muted"].asString() + // : format; + // else + // format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; + // Check muted bluetooth format exists, otherwise fall back to default muted format. + if (format_name != "format" && !config_[format_name + "-muted"].isString()) + format_name = "format"; + format_name += "-muted"; label_.get_style_context()->add_class("muted"); label_.get_style_context()->add_class("sink-muted"); } else { @@ -441,13 +464,10 @@ auto waybar::modules::Wireplumber::update() -> void { // Get the state and apply state-specific format if available auto state = getState(vol); - if (!state.empty()) { - std::string format_name = muted_ ? "format-muted" : "format"; - std::string state_format_name = format_name + "-" + state; - if (config_[state_format_name].isString()) { - format = config_[state_format_name].asString(); - } - } + if (!state.empty() && config_[format_name + "-" + state].isString()) + format = config_[format_name + "-" + state].asString(); + else if (config_[format_name].isString()) + format = config_[format_name].asString(); // Prepare source format string (similar to PulseAudio) std::string format_source = "{volume}%"; From 2e231472159e2b2a5dfc2e4bed69855b88e8fab3 Mon Sep 17 00:00:00 2001 From: ErrorNoInternet Date: Thu, 17 Apr 2025 16:45:47 -0400 Subject: [PATCH 056/416] feat(group): add reveal-by-default option --- man/waybar.5.scd.in | 5 +++++ src/group.cpp | 11 ++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 5bb62724..e817e26a 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -322,6 +322,11 @@ A group may hide all but one element, showing them only on mouse hover. In order Defines the direction of the transition animation. If true, the hidden elements will slide from left to right. If false, they will slide from right to left. When the bar is vertical, it reads as top-to-bottom. +*reveal-by-default*: ++ + typeof: bool ++ + default: false ++ + Whether the child should be revealed when Waybar starts up. This has to be used with click-to-reveal to take effect. + ``` "group/power": { "orientation": "inherit", diff --git a/src/group.cpp b/src/group.cpp index 50841efd..31d60285 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -62,13 +62,22 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& const bool left_to_right = (drawer_config["transition-left-to-right"].isBool() ? drawer_config["transition-left-to-right"].asBool() : true); + const bool reveal_by_default = + (drawer_config["reveal-by-default"].isBool() ? drawer_config["reveal-by-default"].asBool() + : false); + click_to_reveal = drawer_config["click-to-reveal"].asBool(); auto transition_type = getPreferredTransitionType(vertical); revealer.set_transition_type(transition_type); revealer.set_transition_duration(transition_duration); - revealer.set_reveal_child(false); + if (click_to_reveal && reveal_by_default) { + box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); + revealer.set_reveal_child(true); + } else { + revealer.set_reveal_child(false); + } revealer.get_style_context()->add_class("drawer"); From 4a7da0cab2747981bdab15acb0fc3af07c48a6e3 Mon Sep 17 00:00:00 2001 From: Steffen Kothe Date: Fri, 15 Aug 2025 10:55:21 +0000 Subject: [PATCH 057/416] fix: Fail early when neither user nor system is requested The module can be configured to neither request the user nor the system session. In fact does this display non-valuable information to the user and can be considered as abuse case of the configuration. Therefore throw an exception when neither user nor system session are requested. Signed-off-by: Steffen Kothe --- src/modules/systemd_failed_units.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 90f33be7..ac389257 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -47,6 +47,9 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& user_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); } + if (!user_proxy && !system_proxy) + throw std::runtime_error("Neither system nor user status is requested."); + updateData(); /* Always update for the first time. */ dp.emit(); From 3060141a65fad5a79c7f8375c1e363d1e6e34f58 Mon Sep 17 00:00:00 2001 From: Steffen Kothe Date: Fri, 15 Aug 2025 10:58:23 +0000 Subject: [PATCH 058/416] fix: Fix hide_on_ok when user or system session is ignored In case of an user ignoring the failed system or user units, hiding of the waybar module did not work. Ensure that the ignore case of a system or user bus is reflected correctly by determining the configuration. Fixes [1] where regression was introduced in [2]. Links: [1] https://github.com/Alexays/Waybar/issues/4241 [2] https://github.com/Alexays/Waybar/issues/4240 Signed-off-by: Steffen Kothe --- src/modules/systemd_failed_units.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index ac389257..9e3f16f3 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -74,7 +74,7 @@ auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, void SystemdFailedUnits::RequestSystemState() { auto load = [](const char* kind, Glib::RefPtr& proxy) -> std::string { try { - if (!proxy) return "unknown"; + if (!proxy) return "ignored"; auto parameters = Glib::VariantContainerBase( g_variant_new("(ss)", "org.freedesktop.systemd1.Manager", "SystemState")); Glib::VariantContainerBase data = proxy->call_sync("Get", parameters); @@ -93,7 +93,9 @@ void SystemdFailedUnits::RequestSystemState() { system_state = load("systemwide", system_proxy); user_state = load("user", user_proxy); - if (system_state == "running" && user_state == "running") + + if ((system_state == "running" || system_state == "ignored") && + (user_state == "running" || user_state == "ignored")) overall_state = "ok"; else overall_state = "degraded"; From aaffebed4c0c7f3940e206319d7fc0ef8ca9f6b8 Mon Sep 17 00:00:00 2001 From: Marc Bornand Date: Fri, 24 Jan 2025 14:46:25 +0100 Subject: [PATCH 059/416] wireplumber: add more volume formats - Add the possibility to show the volume in linear, cubic, db, and cubic percent (default for backward compatibility). - Add the ability to change the scale for the scrolling action. - Make the minimal step flexible to fit any scale. --- man/waybar-wireplumber.5.scd | 25 ++++++++++- src/modules/wireplumber.cpp | 81 +++++++++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 18 deletions(-) diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index ae78f184..45f1b92a 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -63,6 +63,11 @@ The *wireplumber* module displays the current volume reported by WirePlumber. default: 1.0 ++ The speed at which to change the volume when scrolling. +*scroll-scale*: ++ + typeof: string ++ + default: cubic_percent + The scale to use for the scrolling volume change. Options are 'linear', 'db', 'cubic', and 'cubic_percent'. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. @@ -107,7 +112,13 @@ The *wireplumber* module displays the current volume reported by WirePlumber. # FORMAT REPLACEMENTS -*{volume}*: Volume in percentage. +*{volume}*: Volume in percentage, cubic scale as integer. + +*{volume_linear}*: Volume in linear scale as float. + +*{volume_cubic}*: Volume in cubic scale as float. + +*{volume_db}*: Volume in decibel scale as float. *{node_name}*: The node's nickname as reported by WirePlumber (*node.nick* property) @@ -123,7 +134,7 @@ The *wireplumber* module displays the current volume reported by WirePlumber. } ``` -## Separate Sink and Source Widgets +## Separate Sink and Source Widgets ``` "wireplumber#sink": { @@ -143,6 +154,16 @@ The *wireplumber* module displays the current volume reported by WirePlumber. } ``` +## Use a Different Scale + +``` +"wireplumber": { + "format": "{volume_db:.2f}dB", + "scroll-scale": "db", + "scroll-step": 2.5 +} +``` + # STYLE - *#wireplumber* diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index a43ad29b..f37445c7 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -1,6 +1,8 @@ #include "modules/wireplumber.hpp" #include +#include +#include bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; } @@ -396,7 +398,7 @@ void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* r spdlog::debug("[{}]: loaded mixer API", self->name_); g_ptr_array_add(self->apis_, ({ WpPlugin* p = wp_plugin_find(self->wp_core_, "mixer-api"); - g_object_set(G_OBJECT(p), "scale", 1 /* cubic */, nullptr); + g_object_set(G_OBJECT(p), "scale", 0 /* linear */, nullptr); p; })); @@ -436,8 +438,16 @@ auto waybar::modules::Wireplumber::update() -> void { label_.get_style_context()->remove_class("source-muted"); } - int vol = round(volume_ * 100.0); - int source_vol = round(source_volume_ * 100.0); + + double vol_cube = pow(volume_, 3); + double source_vol_cube = pow(source_volume_, 3); + + int vol = round(vol_cube * 100.0); + int source_vol = round(source_vol_cube * 100.0); + + double vol_db = 20.0 * log10(volume_); + double source_vol_db = 20.0 * log10(source_volume_); + // Get the state and apply state-specific format if available auto state = getState(vol); @@ -468,7 +478,10 @@ auto waybar::modules::Wireplumber::update() -> void { std::string markup = fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), - fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_)); + fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_), + fmt::arg("volume_linear", volume_), fmt::arg("volume_cubic", vol_cube), + fmt::arg("volume_db", vol_db),fmt::arg("source_volume_linear", source_volume_), + fmt::arg("source_volume_cubic", source_vol_cube), fmt::arg("source_volume_db", source_vol_db)); label_.set_markup(markup); if (tooltipEnabled()) { @@ -480,7 +493,10 @@ auto waybar::modules::Wireplumber::update() -> void { label_.set_tooltip_text(fmt::format( fmt::runtime(tooltipFormat), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), - fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_))); + fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_), + fmt::arg("volume_linear", volume_), fmt::arg("volume_cubic", vol_cube), + fmt::arg("volume_db", vol_db), fmt::arg("source_volume_linear", source_volume_), + fmt::arg("source_volume_cubic", source_vol_cube), fmt::arg("source_volume_db", source_vol_db))); } else { label_.set_tooltip_text(node_name_); } @@ -499,28 +515,61 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { return true; } double maxVolume = 1; - double step = 1.0 / 100.0; + double step = 1.0; if (config_["scroll-step"].isDouble()) { - step = config_["scroll-step"].asDouble() / 100.0; + step = config_["scroll-step"].asDouble(); } if (config_["max-volume"].isDouble()) { - maxVolume = config_["max-volume"].asDouble() / 100.0; + maxVolume = config_["max-volume"].asDouble(); } - if (step < min_step_) step = min_step_; + double vol = volume_; + std::string scale = "cubic_percent"; + if (config_["scroll-scale"].isString()) { + scale = config_["scroll-scale"].asString(); + } - double newVol = volume_; + if (scale == "cubic") { + vol = pow(vol, 3); + } + else if (scale == "db") { + vol = log10(vol) * 20.0; + } + else if (scale == "cubic_percent") { + vol = pow(vol, 3) * 100.0; + } + + + double newVol = vol; if (dir == SCROLL_DIR::UP) { - if (volume_ < maxVolume) { - newVol = volume_ + step; - if (newVol > maxVolume) newVol = maxVolume; + newVol = vol + step; + } else if (dir == SCROLL_DIR::DOWN) { + newVol = vol - step; + } + + if (scale == "cubic") { + newVol = cbrt(newVol); + } + else if (scale == "db") { + newVol = exp10(newVol / 20.0); + } + else if (scale == "cubic_percent") { + newVol = cbrt(newVol / 100.0); + } + + if (dir == SCROLL_DIR::UP) { + if (volume_ + min_step_ > newVol) { + newVol = volume_ + min_step_; } } else if (dir == SCROLL_DIR::DOWN) { - if (volume_ > 0) { - newVol = volume_ - step; - if (newVol < 0) newVol = 0; + if (volume_ - min_step_ < newVol) { + newVol = volume_ - min_step_; } } + + if (newVol < 0) newVol = 0; + else if (newVol > maxVolume) newVol = maxVolume; + if (newVol != volume_) { GVariant* variant = g_variant_new_double(newVol); gboolean ret; From 946f175aa287f978163341e6df2ddd85bb53dacb Mon Sep 17 00:00:00 2001 From: Diego Velez Date: Sat, 13 Sep 2025 15:17:01 -0400 Subject: [PATCH 060/416] feat: Show Niri/window component when workspace is empty --- src/modules/niri/window.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/niri/window.cpp b/src/modules/niri/window.cpp index 6e6fd36f..18f5d8a4 100644 --- a/src/modules/niri/window.cpp +++ b/src/modules/niri/window.cpp @@ -82,7 +82,12 @@ void Window::doUpdate() { oldAppId_ = appId; } } else { - label_.hide(); + label_.show(); + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", ""), + fmt::arg("app_id", "")), + config_["rewrite"])); + updateAppIconName("", ""); setClass("solo", false); if (!oldAppId_.empty()) setClass(oldAppId_, false); From eee0362367be8a574b2e9fcd9faa7d594248527e Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Wed, 17 Sep 2025 22:09:49 +0000 Subject: [PATCH 061/416] Fix clang build error --- src/modules/clock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index a7d57437..e4c48207 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -375,7 +375,7 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea } } os << Glib::ustring::format( - (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(L' '), + (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(' '), std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), calendarLine); From d5e45615bb8b535a3b4406d63a7a53876f4377b5 Mon Sep 17 00:00:00 2001 From: Noak Palander Date: Sat, 2 Nov 2024 03:01:54 +0100 Subject: [PATCH 062/416] added a taskbar squash option --- include/modules/wlr/taskbar.hpp | 6 +++++ src/modules/wlr/taskbar.cpp | 47 ++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 8dc4dadd..31b53a7e 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -68,6 +68,7 @@ class Task { Glib::RefPtr app_info_; bool button_visible_ = false; bool ignored_ = false; + bool squashed_ = false; bool with_icon_ = false; bool with_name_ = false; @@ -91,6 +92,7 @@ class Task { void set_minimize_hint(); void on_button_size_allocated(Gtk::Allocation &alloc); void hide_if_ignored(); + void hide_if_duplicate(); public: /* Getter functions */ @@ -153,6 +155,7 @@ class Taskbar : public waybar::AModule { IconLoader icon_loader_; std::unordered_set ignore_list_; + std::unordered_set squash_list_; std::map app_ids_replace_map_; struct zwlr_foreign_toplevel_manager_v1 *manager_; @@ -178,7 +181,10 @@ class Taskbar : public waybar::AModule { const IconLoader &icon_loader() const; const std::unordered_set &ignore_list() const; + const std::unordered_set &squash_list() const; const std::map &app_ids_replace_map() const; + std::size_t task_id_count(std::string_view id) const; + std::size_t task_title_count(std::string_view title) const; }; } /* namespace waybar::modules::wlr */ diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index c5c522d8..85cf0256 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -197,6 +197,7 @@ void Task::handle_title(const char *title) { } title_ = title; hide_if_ignored(); + hide_if_duplicate(); if (!with_icon_ && !with_name_ || app_info_) { return; @@ -221,6 +222,26 @@ void Task::set_minimize_hint() { minimize_hint.y, minimize_hint.w, minimize_hint.h); } +void Task::hide_if_duplicate() { + const auto &squash_list = tbar_->squash_list(); + bool contains_app = + squash_list.contains("*") || squash_list.contains(title_) || squash_list.contains(app_id_); + + // Squashes if the app is in the squash list and more than 1 instance is open + if (contains_app && (tbar_->task_id_count(app_id_) > 1 || tbar_->task_title_count(title_) > 1)) { + squashed_ = true; + if (button_visible_) { + auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + handle_output_leave(output); + } + } + + if (!squashed_ && !ignored_) { + auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + handle_output_enter(output); + } +} + void Task::hide_if_ignored() { if (tbar_->ignore_list().count(app_id_) || tbar_->ignore_list().count(title_)) { ignored_ = true; @@ -228,7 +249,9 @@ void Task::hide_if_ignored() { auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); handle_output_leave(output); } - } else { + } + + if (!ignored_ && !squashed_) { bool is_was_ignored = ignored_; ignored_ = false; if (is_was_ignored) { @@ -246,6 +269,7 @@ void Task::handle_app_id(const char *app_id) { } app_id_ = app_id; hide_if_ignored(); + hide_if_duplicate(); auto ids_replace_map = tbar_->app_ids_replace_map(); if (ids_replace_map.count(app_id_)) { @@ -285,6 +309,10 @@ void Task::handle_output_enter(struct wl_output *output) { spdlog::debug("{} is ignored", repr()); return; } + if (squashed_) { + spdlog::debug("{} was squashed", repr()); + return; + } spdlog::debug("{} entered output {}", repr(), (void *)output); @@ -606,6 +634,13 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu } } + // Load squash-list + if (config_["squash-list"].isArray()) { + for (auto &app_name : config_["squash-list"]) { + squash_list_.emplace(app_name.asString()); + } + } + // Load app_id remappings if (config_["app_ids-mapping"].isObject()) { const Json::Value &mapping = config_["app_ids-mapping"]; @@ -753,8 +788,18 @@ const IconLoader &Taskbar::icon_loader() const { return icon_loader_; } const std::unordered_set &Taskbar::ignore_list() const { return ignore_list_; } +const std::unordered_set &Taskbar::squash_list() const { return squash_list_; } + const std::map &Taskbar::app_ids_replace_map() const { return app_ids_replace_map_; } +std::size_t Taskbar::task_id_count(std::string_view id) const { + return std::ranges::count_if(tasks_, [=](auto &&task) { return id == task->app_id(); }); +} + +std::size_t Taskbar::task_title_count(std::string_view title) const { + return std::ranges::count_if(tasks_, [=](auto &&task) { return title == task->title(); }); +} + } /* namespace waybar::modules::wlr */ From ae3420813cdf24416d490ce5717d69322faf8c5c Mon Sep 17 00:00:00 2001 From: Noak Palander Date: Sun, 3 Nov 2024 01:01:36 +0100 Subject: [PATCH 063/416] fixed bug; when the shown task is removed it promotes another one --- include/modules/wlr/taskbar.hpp | 5 +++++ src/modules/wlr/taskbar.cpp | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 31b53a7e..3f3510d5 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -185,6 +186,10 @@ class Taskbar : public waybar::AModule { const std::map &app_ids_replace_map() const; std::size_t task_id_count(std::string_view id) const; std::size_t task_title_count(std::string_view title) const; + + auto tasks() { + return tasks_ | std::views::transform([](auto &task) -> Task & { return *task; }); + } }; } /* namespace waybar::modules::wlr */ diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 85cf0256..4f494539 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -392,6 +392,28 @@ void Task::handle_closed() { tbar_->remove_button(button); button_visible_ = false; } + + const auto &squash_list = tbar_->squash_list(); + const bool in_squash_list = + squash_list.contains("*") || squash_list.contains(title_) || squash_list.contains(app_id_); + if (in_squash_list && !squashed_ && + (tbar_->task_id_count(app_id_) > 1 || tbar_->task_title_count(title_) > 1)) { + // Find next squashed task with same title or id (excluding ourselves) + auto tasks = tbar_->tasks(); + const auto it = std::ranges::find_if(tasks, [this](auto &&task) { + return &task != this && task.squashed_ && + (task.app_id() == app_id_ || task.title() == title_); + }); + + if (it != tasks.end() && !(*it).ignored_) { + Task &task = *it; + task.squashed_ = false; + tbar_->add_button(task.button); + task.button.show(); + task.button_visible_ = true; + } + } + tbar_->remove_task(id_); } From 41c34b15d6f4bdd77d650c9adc5a59b368eb9b13 Mon Sep 17 00:00:00 2001 From: Aleksi Hannula Date: Fri, 3 Oct 2025 09:15:47 +0300 Subject: [PATCH 064/416] Hide niri/language widget if formatted string is empty --- src/modules/niri/language.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp index 496e5404..64b8c99d 100644 --- a/src/modules/niri/language.cpp +++ b/src/modules/niri/language.cpp @@ -84,7 +84,7 @@ void Language::doUpdate() { spdlog::debug("niri language formatted layout name {}", layoutName); - if (!format_.empty()) { + if (!layoutName.empty()) { label_.show(); label_.set_markup(layoutName); } else { From 192baa4556b681f68ccc698000be62c37be671ab Mon Sep 17 00:00:00 2001 From: dimti Date: Tue, 7 Oct 2025 10:41:19 +0300 Subject: [PATCH 065/416] Add css class to hyprland language module of short_name language code --- src/modules/hyprland/language.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 25f6789d..b1e88521 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -103,7 +103,9 @@ void Language::initLanguage() { searcher = waybar::util::sanitize_string(searcher); + label_.get_style_context()->remove_class(layout_.short_name); layout_ = getLayout(searcher); + label_.get_style_context()->add_class(layout_.short_name); spdlog::debug("hyprland language initLanguage found {}", layout_.full_name); From 632e30eb9ff2e26b491c0ec7c672befa18cc1a15 Mon Sep 17 00:00:00 2001 From: Dominique Martinet Date: Tue, 7 Oct 2025 22:46:22 +0900 Subject: [PATCH 066/416] niri/window: add col/max_col parameters niri v25.08 added window positions to IPC, so waybar actually has the information stored in json around. This handles the associated WindowLayoutsChanged IPC message to keep the position up to date in memory, and adds a couple of format messages to show the current column, so one can dislpay where we are in the workspace (in the hope of improving this feeling of being lost...) Ideally a visual representation of the workspace like the one illustrated in the PR[1] would be even better but this will probably do for now. Link: https://github.com/YaLTeR/niri/pull/1265#issuecomment-2724621758 [1] --- man/waybar-niri-window.5.scd | 4 ++++ src/modules/niri/backend.cpp | 12 ++++++++++++ src/modules/niri/window.cpp | 13 ++++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/man/waybar-niri-window.5.scd b/man/waybar-niri-window.5.scd index 8e886c29..c836f057 100644 --- a/man/waybar-niri-window.5.scd +++ b/man/waybar-niri-window.5.scd @@ -48,6 +48,10 @@ See the output of "niri msg windows" for examples *{app_id}*: The current app ID of the focused window. +*{col}*: The current column of the focused window in the workspace. + +*{max_col}*: The maximum column inside the workspace of the focused window. + # REWRITE RULES *rewrite* is an object where keys are regular expressions and values are diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index fa4dc287..528b7b5c 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -202,6 +202,18 @@ void IPC::parseIPC(const std::string &line) { for (auto &win : windows_) { win["is_focused"] = focused && win["id"].asUInt64() == id; } + } else if (const auto &payload = ev["WindowLayoutsChanged"]) { + const auto &values = payload["changes"]; + for (const auto &changed : values) { + const auto id = changed[0].asUInt64(); + const auto &change = changed[1]; + for (auto &win : windows_) { + if (win["id"].asUInt64() == id) { + win["layout"] = change; + break; + } + } + } } } diff --git a/src/modules/niri/window.cpp b/src/modules/niri/window.cpp index 6e6fd36f..e9b11908 100644 --- a/src/modules/niri/window.cpp +++ b/src/modules/niri/window.cpp @@ -17,6 +17,7 @@ Window::Window(const std::string &id, const Bar &bar, const Json::Value &config) gIPC->registerForIPC("WindowOpenedOrChanged", this); gIPC->registerForIPC("WindowClosed", this); gIPC->registerForIPC("WindowFocusChanged", this); + gIPC->registerForIPC("WindowLayoutsChanged", this); dp.emit(); } @@ -54,15 +55,25 @@ void Window::doUpdate() { if (it != windows.cend()) { const auto &window = *it; + auto max_col = -1; + for (const auto &win : windows) { + if (win["workspace_id"].asUInt64() != window["workspace_id"].asUInt64()) { + continue; + } + const auto col = win["layout"]["pos_in_scrolling_layout"][0].asInt64(); + if (col > max_col) max_col = col; + } const auto title = window["title"].asString(); const auto appId = window["app_id"].asString(); + const auto col = window["layout"]["pos_in_scrolling_layout"][0].asInt64(); const auto sanitizedTitle = waybar::util::sanitize_string(title); const auto sanitizedAppId = waybar::util::sanitize_string(appId); label_.show(); label_.set_markup(waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", sanitizedTitle), - fmt::arg("app_id", sanitizedAppId)), + fmt::arg("app_id", sanitizedAppId), fmt::arg("col", col), + fmt::arg("max_col", max_col)), config_["rewrite"])); updateAppIconName(appId, ""); From 730b502c769603415611da1315f27f000f7d57c4 Mon Sep 17 00:00:00 2001 From: Maik Broemme Date: Wed, 8 Oct 2025 08:52:19 +0200 Subject: [PATCH 067/416] wlr/taskbar: add `justify` option for Gtk::Box horizontal alignment The `wlr/taskbar` module had no way to control its container's horizontal alignment. This change adds a `justify` config key (left|center|right) that maps to Gtk::Align::START/CENTER/END and is applied via set_halign() on the module's Gtk::Box. Default behavior remains unchanged (left). --- man/waybar-wlr-taskbar.5.scd | 4 ++++ src/modules/wlr/taskbar.cpp | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index af1ba97f..ef24a1aa 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -57,6 +57,10 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, group tasks by their app_id. Cannot be used with 'active-first'. +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's box, allowing options 'left', 'right', or 'center' to define the positioning. + *on-click*: ++ typeof: string ++ The action which should be triggered when clicking on the application button with the left mouse button. diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index c5c522d8..925cec09 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -98,6 +98,17 @@ Task::Task(const waybar::Bar &bar, const Json::Value &config, Taskbar *tbar, content_.add(icon_); content_.add(text_after_); + if (config_["justify"].isString()) { + auto justify_str = config_["justify"].asString(); + if (justify_str == "left") { + content_.set_halign(Gtk::ALIGN_START); + } else if (justify_str == "right") { + content_.set_halign(Gtk::ALIGN_END); + } else if (justify_str == "center") { + content_.set_halign(Gtk::ALIGN_CENTER); + } + } + content_.show(); button.add(content_); From e1d4967d8e346e2beb3991ed4ed5a0c91d943b92 Mon Sep 17 00:00:00 2001 From: Thomas Crha Date: Thu, 9 Oct 2025 17:51:16 +1100 Subject: [PATCH 068/416] feat(niri/worspaces): add ignore-workspaces configuration option --- include/modules/niri/workspaces.hpp | 5 ++++ man/waybar-niri-workspaces.5.scd | 5 ++++ src/modules/niri/workspaces.cpp | 37 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index a6850ed1..1dc6837c 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "AModule.hpp" #include "bar.hpp" @@ -20,11 +22,14 @@ class Workspaces : public AModule, public EventHandler { void doUpdate(); Gtk::Button &addButton(const Json::Value &ws); std::string getIcon(const std::string &value, const Json::Value &ws); + bool isWorkspaceIgnored(const std::string &name); const Bar &bar_; Gtk::Box box_; // Map from niri workspace id to button. std::unordered_map buttons_; + // Vec of regex rules to ignore workspaces. + std::vector ignoreWorkspaces_; }; } // namespace waybar::modules::niri diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 4be85eb3..f1fc6775 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -50,6 +50,11 @@ Addressed by *niri/workspaces* default: false ++ Enables this module to consume all left over space dynamically. +*ignore-workspaces*: ++ + typeof: array ++ + default: [] ++ + Regexes to match against workspaces names and index's. If there's a match, the workspace will not be shown. + # FORMAT REPLACEMENTS *{value}*: Name of the workspace, or index for unnamed workspaces, diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 7dfc0b35..6c17beb3 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -15,6 +15,24 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value box_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(box_); + // Parse ignore-workspaces rules. + auto ignoreWorkspaces = config["ignore-workspaces"]; + if (ignoreWorkspaces.isArray()) { + for (const auto &workspaceRegex : ignoreWorkspaces) { + if (workspaceRegex.isString()) { + std::string ruleString = workspaceRegex.asString(); + try { + const std::regex rule{ruleString, std::regex_constants::icase}; + ignoreWorkspaces_.emplace_back(rule); + } catch (const std::regex_error &e) { + spdlog::error("Invalid ignore-workspaces rule {}: {}", ruleString, e.what()); + } + } else { + spdlog::error("Not a string in ignore-workspaces: '{}'", workspaceRegex); + } + } + } + if (!gIPC) gIPC = std::make_unique(); gIPC->registerForIPC("WorkspacesChanged", this); @@ -37,6 +55,16 @@ void Workspaces::doUpdate() { const auto &workspaces = gIPC->workspaces(); std::copy_if(workspaces.cbegin(), workspaces.cend(), std::back_inserter(my_workspaces), [&](const auto &ws) { + std::string name; + if (ws["name"]) { + name = ws["name"].asString(); + } else { + name = std::to_string(ws["idx"].asUInt()); + } + if (isWorkspaceIgnored(name)) { + return false; + } + if (alloutputs) return true; return ws["output"].asString() == bar_.output->name; }); @@ -193,4 +221,13 @@ std::string Workspaces::getIcon(const std::string &value, const Json::Value &ws) return value; } +bool Workspaces::isWorkspaceIgnored(const std::string &name) { + for (auto &rule : ignoreWorkspaces_) { + if (std::regex_match(name, rule)) { + return true; + } + } + return false; +} + } // namespace waybar::modules::niri From cd820a100dfbbf6908f33ac4495d2e982f5ee85a Mon Sep 17 00:00:00 2001 From: dimti Date: Fri, 10 Oct 2025 11:14:30 +0300 Subject: [PATCH 069/416] also remove/add css class in onEvent method --- src/modules/hyprland/language.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index b1e88521..dd966b17 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -82,7 +82,9 @@ void Language::onEvent(const std::string& ev) { layoutName = waybar::util::sanitize_string(layoutName); + label_.get_style_context()->remove_class(layout_.short_name); layout_ = getLayout(layoutName); + label_.get_style_context()->add_class(layout_.short_name); spdlog::debug("hyprland language onevent with {}", layoutName); From bdf501a8a5d7377491f00acabe2df85449697cbd Mon Sep 17 00:00:00 2001 From: dimti Date: Fri, 10 Oct 2025 11:35:47 +0300 Subject: [PATCH 070/416] make getLayout is not static and set css classes for short name keyboard layout in getLayout method --- include/modules/hyprland/language.hpp | 2 +- src/modules/hyprland/language.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index ec59e5c3..d1469f60 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -30,7 +30,7 @@ class Language : public waybar::ALabel, public EventHandler { std::string short_description; }; - static auto getLayout(const std::string&) -> Layout; + auto getLayout(const std::string&) -> Layout; std::mutex mutex_; const Bar& bar_; diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index dd966b17..9aa97e81 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -82,9 +82,7 @@ void Language::onEvent(const std::string& ev) { layoutName = waybar::util::sanitize_string(layoutName); - label_.get_style_context()->remove_class(layout_.short_name); layout_ = getLayout(layoutName); - label_.get_style_context()->add_class(layout_.short_name); spdlog::debug("hyprland language onevent with {}", layoutName); @@ -105,9 +103,7 @@ void Language::initLanguage() { searcher = waybar::util::sanitize_string(searcher); - label_.get_style_context()->remove_class(layout_.short_name); layout_ = getLayout(searcher); - label_.get_style_context()->add_class(layout_.short_name); spdlog::debug("hyprland language initLanguage found {}", layout_.full_name); @@ -122,6 +118,8 @@ auto Language::getLayout(const std::string& fullName) -> Layout { rxkb_context_parse_default_ruleset(context); rxkb_layout* layout = rxkb_layout_first(context); + label_.get_style_context()->remove_class(layout_.short_name); + spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); while (layout != nullptr) { std::string nameOfLayout = rxkb_layout_get_description(layout); @@ -141,6 +139,8 @@ auto Language::getLayout(const std::string& fullName) -> Layout { rxkb_context_unref(context); + label_.get_style_context()->add_class(info.short_name); + spdlog::debug("hyprland language add new short_name css class {}", info.short_name); return info; } From 2e6bf549a05d1f4c72aed4b473626ccf9cd83cd0 Mon Sep 17 00:00:00 2001 From: dimti Date: Fri, 10 Oct 2025 11:52:59 +0300 Subject: [PATCH 071/416] Change label css classes in initial and on event language class method. Used in getLabel - make a strange behaviour --- src/modules/hyprland/language.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 9aa97e81..3dd5aef7 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -82,7 +82,11 @@ void Language::onEvent(const std::string& ev) { layoutName = waybar::util::sanitize_string(layoutName); + label_.get_style_context()->remove_class(layout_.short_name); + spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); layout_ = getLayout(layoutName); + label_.get_style_context()->add_class(layout_.short_name); + spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name); spdlog::debug("hyprland language onevent with {}", layoutName); @@ -103,7 +107,11 @@ void Language::initLanguage() { searcher = waybar::util::sanitize_string(searcher); + label_.get_style_context()->remove_class(layout_.short_name); + spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); layout_ = getLayout(searcher); + label_.get_style_context()->add_class(layout_.short_name); + spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name); spdlog::debug("hyprland language initLanguage found {}", layout_.full_name); @@ -118,8 +126,6 @@ auto Language::getLayout(const std::string& fullName) -> Layout { rxkb_context_parse_default_ruleset(context); rxkb_layout* layout = rxkb_layout_first(context); - label_.get_style_context()->remove_class(layout_.short_name); - spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); while (layout != nullptr) { std::string nameOfLayout = rxkb_layout_get_description(layout); @@ -139,8 +145,6 @@ auto Language::getLayout(const std::string& fullName) -> Layout { rxkb_context_unref(context); - label_.get_style_context()->add_class(info.short_name); - spdlog::debug("hyprland language add new short_name css class {}", info.short_name); return info; } From a36a5e89a9871773b24dba9e51ac056877a7902a Mon Sep 17 00:00:00 2001 From: dimti Date: Fri, 10 Oct 2025 11:57:44 +0300 Subject: [PATCH 072/416] Approach to dry pattern - separate remove/add css-classes logic to methods --- include/modules/hyprland/language.hpp | 4 +++- src/modules/hyprland/language.cpp | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index d1469f60..199356c1 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -30,7 +30,9 @@ class Language : public waybar::ALabel, public EventHandler { std::string short_description; }; - auto getLayout(const std::string&) -> Layout; + auto removeXkbLayoutCssClass() -> void; + auto addXkbLayoutCssClass() -> void; + static auto getLayout(const std::string&) -> Layout; std::mutex mutex_; const Bar& bar_; diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 3dd5aef7..33e56720 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -82,11 +82,9 @@ void Language::onEvent(const std::string& ev) { layoutName = waybar::util::sanitize_string(layoutName); - label_.get_style_context()->remove_class(layout_.short_name); - spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); + removeXkbLayoutCssClass(); layout_ = getLayout(layoutName); - label_.get_style_context()->add_class(layout_.short_name); - spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name); + addXkbLayoutCssClass(); spdlog::debug("hyprland language onevent with {}", layoutName); @@ -107,11 +105,9 @@ void Language::initLanguage() { searcher = waybar::util::sanitize_string(searcher); - label_.get_style_context()->remove_class(layout_.short_name); - spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); + removeXkbLayoutCssClass(); layout_ = getLayout(searcher); - label_.get_style_context()->add_class(layout_.short_name); - spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name); + addXkbLayoutCssClass(); spdlog::debug("hyprland language initLanguage found {}", layout_.full_name); @@ -121,6 +117,15 @@ void Language::initLanguage() { } } +auto Language::removeXkbLayoutCssClass() -> void { + label_.get_style_context()->remove_class(layout_.short_name); + spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); +} +auto Language::addXkbLayoutCssClass() -> void { + label_.get_style_context()->add_class(layout_.short_name); + spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name); +} + auto Language::getLayout(const std::string& fullName) -> Layout { auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); rxkb_context_parse_default_ruleset(context); From 4c12001569f54cb11596324cff68c52aeaa72a67 Mon Sep 17 00:00:00 2001 From: dimti Date: Fri, 10 Oct 2025 11:58:56 +0300 Subject: [PATCH 073/416] do not need to remove css class from gtk label on init phase --- src/modules/hyprland/language.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 33e56720..1ece0713 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -105,7 +105,6 @@ void Language::initLanguage() { searcher = waybar::util::sanitize_string(searcher); - removeXkbLayoutCssClass(); layout_ = getLayout(searcher); addXkbLayoutCssClass(); From 382b9daadae2c11e1628285fdea47f9d16cf1399 Mon Sep 17 00:00:00 2001 From: Maik Broemme Date: Fri, 10 Oct 2025 15:23:01 +0200 Subject: [PATCH 074/416] wlr/taskbar: add `homogeneous` to evenly distribute button widths --- man/waybar-wlr-taskbar.5.scd | 5 +++++ src/modules/wlr/taskbar.cpp | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index af1ba97f..c0e24ef3 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -57,6 +57,11 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, group tasks by their app_id. Cannot be used with 'active-first'. +*homogeneous*: ++ + typeof: bool ++ + default: false ++ + If set to true, distribute every task button evenly across the taskbar's allocated width. Buttons will automatically resize so that 'N' visible tasks each take '1/N' of the width. + *on-click*: ++ typeof: string ++ The action which should be triggered when clicking on the application button with the left mouse button. diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index c5c522d8..b39ab16a 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -575,6 +575,12 @@ Taskbar::Taskbar(const std::string &id, const waybar::Bar &bar, const Json::Valu box_.get_style_context()->add_class("empty"); event_box_.add(box_); + // Make task buttons distribute evenly across the available width. + if (config_["homogeneous"].isBool() && config_["homogeneous"].asBool()) { + box_.set_homogeneous(true); + box_.set_hexpand(true); + } + struct wl_display *display = Client::inst()->wl_display; struct wl_registry *registry = wl_display_get_registry(display); @@ -716,7 +722,14 @@ void Taskbar::handle_finished() { } void Taskbar::add_button(Gtk::Button &bt) { - box_.pack_start(bt, false, false); + + // When homogeneous is enabled, allow children to expand and fill so they divide space equally. + const bool homogeneous = (config_["homogeneous"].isBool() && config_["homogeneous"].asBool()); + box_.pack_start(bt, homogeneous, homogeneous); + if (homogeneous) { + bt.set_hexpand(true); + bt.set_halign(Gtk::ALIGN_FILL); + } box_.get_style_context()->remove_class("empty"); } From a96f1caa197f5f83e6e98c47669885bf6fbfe0c1 Mon Sep 17 00:00:00 2001 From: lilly-lizard Date: Sat, 11 Oct 2025 10:25:50 +1300 Subject: [PATCH 075/416] fixed bug after merge with master duplicating code in handle_show() --- src/modules/river/tags.cpp | 57 ++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index c50fcdeb..5712e9a0 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -27,15 +27,11 @@ static void listen_urgent_tags(void *data, struct zriver_output_status_v1 *zrive static_cast(data)->handle_urgent_tags(tags); } -static void listen_focused_view(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, - const char *title) { - // This module doesn't care -} - -static void listen_mode(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, - const char *mode) { - // This module doesn't care -} +static const zriver_output_status_v1_listener output_status_listener_impl{ + .focused_tags = listen_focused_tags, + .view_tags = listen_view_tags, + .urgent_tags = listen_urgent_tags, +}; static void listen_focused_output(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, struct wl_output *output) { @@ -47,11 +43,15 @@ static void listen_unfocused_output(void *data, struct zriver_seat_status_v1 *zr static_cast(data)->handle_unfocused_output(output); } -static const zriver_output_status_v1_listener output_status_listener_impl{ - .focused_tags = listen_focused_tags, - .view_tags = listen_view_tags, - .urgent_tags = listen_urgent_tags, -}; +static void listen_focused_view(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + const char *title) { + // This module doesn't care +} + +static void listen_mode(void *data, struct zriver_seat_status_v1 *zriver_seat_status_v1, + const char *mode) { + // This module doesn't care +} static const zriver_seat_status_v1_listener seat_status_listener_impl{ .focused_output = listen_focused_output, @@ -115,6 +115,7 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con seat_{nullptr}, bar_(bar), box_{bar.orientation, 0}, + output_{nullptr}, output_status_{nullptr}, seat_status_{nullptr} { struct wl_display *display = Client::inst()->wl_display; @@ -122,8 +123,6 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con wl_registry_add_listener(registry, ®istry_listener_impl, this); wl_display_roundtrip(display); - output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - if (!status_manager_) { spdlog::error("river_status_manager_v1 not advertised"); return; @@ -137,6 +136,10 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con spdlog::error("wl_seat not advertised"); } + output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output_); + seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); + box_.set_name("tags"); if (!id.empty()) { box_.get_style_context()->add_class(id); @@ -180,14 +183,6 @@ Tags::Tags(const std::string &id, const waybar::Bar &bar, const Json::Value &con button.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); - - zriver_status_manager_v1_destroy(status_manager_); - box_.signal_show().connect(sigc::mem_fun(*this, &Tags::handle_show)); } @@ -196,10 +191,6 @@ Tags::~Tags() { zriver_output_status_v1_destroy(output_status_); } - if (seat_status_) { - zriver_seat_status_v1_destroy(seat_status_); - } - if (control_) { zriver_control_v1_destroy(control_); } @@ -210,12 +201,18 @@ Tags::~Tags() { } void Tags::handle_show() { - struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output); + spdlog::info("bruh1"); + + 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); + zriver_status_manager_v1_destroy(status_manager_); status_manager_ = nullptr; + + spdlog::info("bruh2"); } void Tags::handle_primary_clicked(uint32_t tag) { From 644a3575416e457286ffb691d0ee941f52d5a175 Mon Sep 17 00:00:00 2001 From: winkelnp <68015877+winkelnp@users.noreply.github.com> Date: Mon, 13 Oct 2025 23:27:45 +0200 Subject: [PATCH 076/416] [wireplumber] Add support for device.form-factor --- include/modules/wireplumber.hpp | 2 + src/modules/wireplumber.cpp | 72 ++++++++++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index 0565704f..9b7d5a7b 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -33,6 +33,7 @@ class Wireplumber : public ALabel { static void onDefaultNodesApiChanged(waybar::modules::Wireplumber* self); bool handleScroll(GdkEventScroll* e) override; + std::vector getWPIcon(); static std::list modules; @@ -54,6 +55,7 @@ class Wireplumber : public ALabel { bool source_muted_; double source_volume_; gchar* default_source_name_; + std::string form_factor_; }; } // namespace waybar::modules diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 4b25f817..d4962e68 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -23,7 +23,8 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val source_node_id_(0), source_muted_(false), source_volume_(0.0), - default_source_name_(nullptr) { + default_source_name_(nullptr), + form_factor_("") { waybar::modules::Wireplumber::modules.push_back(this); wp_init(WP_INIT_PIPEWIRE); @@ -97,6 +98,36 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* : description != nullptr ? description : "Unknown node name"; spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_); + + // find form-factor only if sink + if (g_strcmp0(self->type_, "Audio/Sink") == 0) { + const auto* devid = wp_properties_get(properties, "device.id"); + spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); + + auto* devproxy = static_cast( + wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, + "bound-id", "=s", devid, nullptr)); + + if (devproxy == nullptr) { + auto err = fmt::format("Object '{}' not found\n", devid); + spdlog::error("[{}]: {}", self->name_, err); + throw std::runtime_error(err); + } + + g_autoptr(WpProperties) devprop = + WP_IS_PIPEWIRE_OBJECT(devproxy) != 0 + ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(devproxy)) + : wp_properties_new_empty(); + devprop = wp_properties_ensure_unique_owner(devprop); + + if (const auto* ff = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(devproxy), "device.form-factor")) { + self->form_factor_ = ff; + spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); + } else { + self->form_factor_ = ""; + } + } } void waybar::modules::Wireplumber::updateSourceName(waybar::modules::Wireplumber* self, @@ -358,6 +389,8 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { "=s", self->type_, nullptr); wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", "Audio/Source", nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", + "=s", "Audio/Device", nullptr); } void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, @@ -415,6 +448,33 @@ void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { this); } +static const std::array ports = { + "headphone", "speaker", "headset", "hands-free", "portable", "car", "hifi", +}; + +std::vector waybar::modules::Wireplumber::getWPIcon() { + std::vector res; + if (muted_) { + res.emplace_back(node_name_ + "-muted"); + } + res.push_back(node_name_); + res.push_back(source_name_); + std::transform(form_factor_.begin(), form_factor_.end(), form_factor_.begin(), ::tolower); + for (auto const& port : ports) { + if (form_factor_.find(port) != std::string::npos) { + if (muted_) { + res.emplace_back(port + "-muted"); + } + res.push_back(port); + break; + } + } + if (muted_) { + res.emplace_back("default-muted"); + } + return res; +} + auto waybar::modules::Wireplumber::update() -> void { auto format = format_; std::string tooltipFormat; @@ -485,10 +545,10 @@ auto waybar::modules::Wireplumber::update() -> void { std::string formatted_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_vol)); - std::string markup = - fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), - fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), - fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_)); + std::string markup = fmt::format( + fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), + fmt::arg("icon", getIcon(vol, getWPIcon())), fmt::arg("format_source", formatted_source), + fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_)); label_.set_markup(markup); if (tooltipEnabled()) { @@ -499,7 +559,7 @@ auto waybar::modules::Wireplumber::update() -> void { if (!tooltipFormat.empty()) { label_.set_tooltip_text(fmt::format( fmt::runtime(tooltipFormat), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), - fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), + fmt::arg("icon", getIcon(vol, getWPIcon())), fmt::arg("format_source", formatted_source), fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_))); } else { label_.set_tooltip_text(node_name_); From deb8a56eb1cc284925c1f238b04c1e011293a6a7 Mon Sep 17 00:00:00 2001 From: winkelnp <68015877+winkelnp@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:40:56 +0200 Subject: [PATCH 077/416] [wireplumber] remove old version (bluetooth) and unnecessary checks (form-factor) --- src/modules/wireplumber.cpp | 45 +++++++++---------------------------- 1 file changed, 11 insertions(+), 34 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index d4962e68..cec7968d 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -99,34 +99,19 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* : "Unknown node name"; spdlog::debug("[{}]: Updating '{}' node name to: {}", self->name_, self->type_, self->node_name_); - // find form-factor only if sink - if (g_strcmp0(self->type_, "Audio/Sink") == 0) { - const auto* devid = wp_properties_get(properties, "device.id"); - spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); + // find form-factor + const auto* devid = wp_properties_get(properties, "device.id"); + spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); - auto* devproxy = static_cast( - wp_object_manager_lookup(self->om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, - "bound-id", "=s", devid, nullptr)); + auto* dev = static_cast(wp_object_manager_lookup( + self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", devid, nullptr)); - if (devproxy == nullptr) { - auto err = fmt::format("Object '{}' not found\n", devid); - spdlog::error("[{}]: {}", self->name_, err); - throw std::runtime_error(err); - } - - g_autoptr(WpProperties) devprop = - WP_IS_PIPEWIRE_OBJECT(devproxy) != 0 - ? wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(devproxy)) - : wp_properties_new_empty(); - devprop = wp_properties_ensure_unique_owner(devprop); - - if (const auto* ff = - wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(devproxy), "device.form-factor")) { - self->form_factor_ = ff; - spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); - } else { - self->form_factor_ = ""; - } + 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_); + } else { + self->form_factor_ = ""; } } @@ -485,8 +470,6 @@ auto waybar::modules::Wireplumber::update() -> void { auto bt = name.find("bluez") != std::string::npos || name.find("a2dp-sink") != std::string::npos; if (bt) { - // format = - // config_["format-bluetooth"].isString() ? config_["format-bluetooth"].asString() : format; format_name += "-bluetooth"; label_.get_style_context()->add_class("bluetooth"); } else { @@ -495,12 +478,6 @@ auto waybar::modules::Wireplumber::update() -> void { // Handle sink mute state if (muted_) { - // if (bt) - // format = config_["format-bluetooth-muted"].isString() - // ? config_["format-bluetooth-muted"].asString() - // : format; - // else - // format = config_["format-muted"].isString() ? config_["format-muted"].asString() : format; // Check muted bluetooth format exists, otherwise fall back to default muted format. if (format_name != "format" && !config_[format_name + "-muted"].isString()) format_name = "format"; From c831a352f2dce8feb4b606d136f06e8d29c03e7e Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 18:11:50 -0500 Subject: [PATCH 078/416] Added signal support and wait-for-activity bool. --- include/modules/idle_inhibitor.hpp | 10 +++ man/waybar-idle-inhibitor.5.scd | 44 +++++++++- src/modules/idle_inhibitor.cpp | 130 +++++++++++++++++++++++++---- 3 files changed, 167 insertions(+), 17 deletions(-) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 22bd808f..4bc9124a 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -10,21 +10,31 @@ namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; + sigc::connection activity_timeout_; + sigc::connection motion_connection_; + sigc::connection key_connection_; public: IdleInhibitor(const std::string&, const waybar::Bar&, const Json::Value&); virtual ~IdleInhibitor(); auto update() -> void override; + auto refresh(int) -> void override; static std::list modules; static bool status; private: bool handleToggle(GdkEventButton* const& e) override; + bool handleMotion(GdkEventMotion* const& e); + bool handleKey(GdkEventKey* const& e); void toggleStatus(); + void resetActivityTimeout(); + void setupActivityMonitoring(); + void teardownActivityMonitoring(); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; int pid_; + bool wait_for_activity_; }; } // namespace waybar::modules diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 405c8fc5..cd6980f2 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -76,6 +76,17 @@ screensaver, also known as "presentation mode". typeof: double ++ The number of minutes the inhibition should last. +*wait-for-activity*: ++ + typeof: bool ++ + default: *false* ++ + When enabled, the idle inhibitor remains active as long as there is keyboard or mouse activity on the bar. If there is no activity for the duration specified in *timeout*, the inhibitor will automatically toggle off. This option requires *timeout* to be set. + +*signal*: ++ + typeof: integer ++ + The signal number used to toggle the idle inhibitor externally. ++ + The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ + Use `pkill -SIGRTMIN+N waybar` to toggle the idle inhibitor from scripts or keybindings. + *tooltip*: ++ typeof: bool ++ default: true ++ @@ -115,17 +126,46 @@ screensaver, also known as "presentation mode". # EXAMPLES +Basic usage with timeout: + ``` "idle_inhibitor": { "format": "{icon}", "format-icons": { - "activated": "", - "deactivated": "" + "activated": "", + "deactivated": "" }, "timeout": 30.5 } ``` +With external control via signals (can be toggled with `pkill -SIGRTMIN+8 waybar`): + +``` +"idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "", + "deactivated": "" + }, + "signal": 8 +} +``` + +With wait-for-activity feature: + +``` +"idle_inhibitor": { + "format": "{icon}", + "format-icons": { + "activated": "", + "deactivated": "" + }, + "timeout": 5.0, + "wait-for-activity": true +} +``` + # STYLE - *#idle_inhibitor* diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index a5fc9ac7..6012bb4c 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -11,11 +11,17 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& : ALabel(config, "idle_inhibitor", id, "{status}", 0, false, true), bar_(bar), idle_inhibitor_(nullptr), - pid_(-1) { + pid_(-1), + wait_for_activity_(false) { if (waybar::Client::inst()->idle_inhibit_manager == nullptr) { throw std::runtime_error("idle-inhibit not available"); } + // Read the wait-for-activity config option + if (config_["wait-for-activity"].isBool()) { + wait_for_activity_ = config_["wait-for-activity"].asBool(); + } + if (waybar::modules::IdleInhibitor::modules.empty() && config_["start-activated"].isBool() && config_["start-activated"].asBool() != status) { toggleStatus(); @@ -32,6 +38,8 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& } waybar::modules::IdleInhibitor::~IdleInhibitor() { + teardownActivityMonitoring(); + if (idle_inhibitor_ != nullptr) { zwp_idle_inhibitor_v1_destroy(idle_inhibitor_); idle_inhibitor_ = nullptr; @@ -77,6 +85,17 @@ auto waybar::modules::IdleInhibitor::update() -> void { ALabel::update(); } +auto waybar::modules::IdleInhibitor::refresh(int sig) -> void { + if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { + toggleStatus(); + + // Make all other idle inhibitor modules update + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } + } +} + void waybar::modules::IdleInhibitor::toggleStatus() { status = !status; @@ -89,20 +108,30 @@ void waybar::modules::IdleInhibitor::toggleStatus() { auto timeoutMins = config_["timeout"].asDouble(); int timeoutSecs = timeoutMins * 60; - timeout_ = Glib::signal_timeout().connect_seconds( - []() { - /* intentionally not tied to a module instance lifetime - * as the output with `this` can be disconnected - */ - spdlog::info("deactivating idle_inhibitor by timeout"); - status = false; - for (auto const& module : waybar::modules::IdleInhibitor::modules) { - module->update(); - } - /* disconnect */ - return false; - }, - timeoutSecs); + // If wait-for-activity is enabled, set up activity monitoring + if (wait_for_activity_) { + setupActivityMonitoring(); + resetActivityTimeout(); + } else { + // Original behavior: simple timeout + timeout_ = Glib::signal_timeout().connect_seconds( + []() { + /* intentionally not tied to a module instance lifetime + * as the output with `this` can be disconnected + */ + spdlog::info("deactivating idle_inhibitor by timeout"); + status = false; + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } + /* disconnect */ + return false; + }, + timeoutSecs); + } + } else { + // When deactivated, tear down activity monitoring + teardownActivityMonitoring(); } } @@ -121,3 +150,74 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { ALabel::handleToggle(e); return true; } + +bool waybar::modules::IdleInhibitor::handleMotion(GdkEventMotion* const& e) { + if (wait_for_activity_ && status) { + resetActivityTimeout(); + } + return false; +} + +bool waybar::modules::IdleInhibitor::handleKey(GdkEventKey* const& e) { + if (wait_for_activity_ && status) { + resetActivityTimeout(); + } + return false; +} + +void waybar::modules::IdleInhibitor::resetActivityTimeout() { + if (!config_["timeout"].isNumeric()) { + return; + } + + if (activity_timeout_.connected()) { + activity_timeout_.disconnect(); + } + + auto timeoutMins = config_["timeout"].asDouble(); + int timeoutSecs = timeoutMins * 60; + + activity_timeout_ = Glib::signal_timeout().connect_seconds( + []() { + spdlog::info("deactivating idle_inhibitor due to inactivity"); + status = false; + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); + } + return false; + }, + timeoutSecs); +} + +void waybar::modules::IdleInhibitor::setupActivityMonitoring() { + // Don't set up if already connected + if (motion_connection_.connected() || key_connection_.connected()) { + return; + } + + // Enable motion and key event monitoring on the bar window + auto window = bar_.window.get_window(); + if (window) { + window->set_events(window->get_events() | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); + } + + // Connect to the bar window's event signals + motion_connection_ = bar_.window.signal_motion_notify_event().connect( + sigc::mem_fun(*this, &IdleInhibitor::handleMotion)); + key_connection_ = bar_.window.signal_key_press_event().connect( + sigc::mem_fun(*this, &IdleInhibitor::handleKey)); +} + +void waybar::modules::IdleInhibitor::teardownActivityMonitoring() { + if (activity_timeout_.connected()) { + activity_timeout_.disconnect(); + } + + if (motion_connection_.connected()) { + motion_connection_.disconnect(); + } + + if (key_connection_.connected()) { + key_connection_.disconnect(); + } +} From 262f8d96f31d1ac608beadee680e5f7b63d76353 Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 18:26:06 -0500 Subject: [PATCH 079/416] fixed the compilation errors in the `idle_inhibitor.cpp` file --- src/modules/idle_inhibitor.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 6012bb4c..619c52f1 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -195,16 +195,20 @@ void waybar::modules::IdleInhibitor::setupActivityMonitoring() { return; } + // Get non-const reference to the window to set up event monitoring + // This is safe because we're only setting up signal handlers, not modifying the Bar itself + auto& window = const_cast(bar_.window); + // Enable motion and key event monitoring on the bar window - auto window = bar_.window.get_window(); - if (window) { - window->set_events(window->get_events() | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); + auto gdk_window = window.get_window(); + if (gdk_window) { + gdk_window->set_events(gdk_window->get_events() | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); } // Connect to the bar window's event signals - motion_connection_ = bar_.window.signal_motion_notify_event().connect( + motion_connection_ = window.signal_motion_notify_event().connect( sigc::mem_fun(*this, &IdleInhibitor::handleMotion)); - key_connection_ = bar_.window.signal_key_press_event().connect( + key_connection_ = window.signal_key_press_event().connect( sigc::mem_fun(*this, &IdleInhibitor::handleKey)); } From 173e7306d0a968debedd934d24a9d9513df4c721 Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 19:42:53 -0500 Subject: [PATCH 080/416] fix using the `ext-idle-notify-v1` protocol --- include/client.hpp | 2 + include/modules/idle_inhibitor.hpp | 14 ++- protocol/ext-idle-notify-v1.xml | 131 +++++++++++++++++++++++++++++ protocol/meson.build | 1 + src/client.cpp | 4 + src/modules/idle_inhibitor.cpp | 116 ++++++++++++------------- 6 files changed, 197 insertions(+), 71 deletions(-) create mode 100644 protocol/ext-idle-notify-v1.xml diff --git a/include/client.hpp b/include/client.hpp index 0e68f002..a4bc5332 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -12,6 +12,7 @@ struct zwp_idle_inhibitor_v1; struct zwp_idle_inhibit_manager_v1; +struct ext_idle_notifier_v1; namespace waybar { @@ -27,6 +28,7 @@ class Client { struct wl_registry *registry = nullptr; struct zxdg_output_manager_v1 *xdg_output_manager = nullptr; struct zwp_idle_inhibit_manager_v1 *idle_inhibit_manager = nullptr; + struct ext_idle_notifier_v1 *idle_notifier = nullptr; std::vector> bars; Config config; std::string bar_id; diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 4bc9124a..523426c6 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -10,9 +10,8 @@ namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; - sigc::connection activity_timeout_; - sigc::connection motion_connection_; - sigc::connection key_connection_; + struct ext_idle_notification_v1* idle_notification_; + uint32_t idle_timeout_ms_; public: IdleInhibitor(const std::string&, const waybar::Bar&, const Json::Value&); @@ -24,12 +23,11 @@ class IdleInhibitor : public ALabel { private: bool handleToggle(GdkEventButton* const& e) override; - bool handleMotion(GdkEventMotion* const& e); - bool handleKey(GdkEventKey* const& e); void toggleStatus(); - void resetActivityTimeout(); - void setupActivityMonitoring(); - void teardownActivityMonitoring(); + void setupIdleNotification(); + void teardownIdleNotification(); + static void handleIdled(void* data, struct ext_idle_notification_v1* notification); + static void handleResumed(void* data, struct ext_idle_notification_v1* notification); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; diff --git a/protocol/ext-idle-notify-v1.xml b/protocol/ext-idle-notify-v1.xml new file mode 100644 index 00000000..db7d9c16 --- /dev/null +++ b/protocol/ext-idle-notify-v1.xml @@ -0,0 +1,131 @@ + + + + Copyright © 2015 Martin Gräßlin + Copyright © 2022 Simon Ser + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This interface allows clients to monitor user idle status. + + After binding to this global, clients can create ext_idle_notification_v1 + objects to get notified when the user is idle for a given amount of time. + + + + + Destroy the manager object. All objects created via this interface + remain valid. + + + + + + Create a new idle notification object. + + The notification object has a minimum timeout duration and is tied to a + seat. The client will be notified if the seat is inactive for at least + the provided timeout. See ext_idle_notification_v1 for more details. + + A zero timeout is valid and means the client wants to be notified as + soon as possible when the seat is inactive. + + + + + + + + + + + Create a new idle notification object to track input from the + user, such as keyboard and mouse movement. Because this object is + meant to track user input alone, it ignores idle inhibitors. + + The notification object has a minimum timeout duration and is tied to a + seat. The client will be notified if the seat is inactive for at least + the provided timeout. See ext_idle_notification_v1 for more details. + + A zero timeout is valid and means the client wants to be notified as + soon as possible when the seat is inactive. + + + + + + + + + + + This interface is used by the compositor to send idle notification events + to clients. + + Initially the notification object is not idle. The notification object + becomes idle when no user activity has happened for at least the timeout + duration, starting from the creation of the notification object. User + activity may include input events or a presence sensor, but is + compositor-specific. + + How this notification responds to idle inhibitors depends on how + it was constructed. If constructed from the + get_idle_notification request, then if an idle inhibitor is + active (e.g. another client has created a zwp_idle_inhibitor_v1 + on a visible surface), the compositor must not make the + notification object idle. However, if constructed from the + get_input_idle_notification request, then idle inhibitors are + ignored, and only input from the user, e.g. from a keyboard or + mouse, counts as activity. + + When the notification object becomes idle, an idled event is sent. When + user activity starts again, the notification object stops being idle, + a resumed event is sent and the timeout is restarted. + + + + + Destroy the notification object. + + + + + + This event is sent when the notification object becomes idle. + + It's a compositor protocol error to send this event twice without a + resumed event in-between. + + + + + + This event is sent when the notification object stops being idle. + + It's a compositor protocol error to send this event twice without an + idled event in-between. It's a compositor protocol error to send this + event prior to any idled event. + + + + diff --git a/protocol/meson.build b/protocol/meson.build index b16113b2..3da6ee47 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -29,6 +29,7 @@ client_protocols = [ ['river-status-unstable-v1.xml'], ['river-control-unstable-v1.xml'], ['dwl-ipc-unstable-v2.xml'], + ['ext-idle-notify-v1.xml'], ] if wayland_protos.version().version_compare('>=1.39') diff --git a/src/client.cpp b/src/client.cpp index 946780db..a71d1c7a 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -7,6 +7,7 @@ #include #include "gtkmm/icontheme.h" +#include "ext-idle-notify-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" #include "util/format.hpp" @@ -26,6 +27,9 @@ void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint } else if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) { client->idle_inhibit_manager = static_cast( wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1)); + } else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) { + client->idle_notifier = static_cast( + wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, 1)); } } diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 619c52f1..7d5fdd01 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -1,5 +1,6 @@ #include "modules/idle_inhibitor.hpp" +#include "ext-idle-notify-v1-client-protocol.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/command.hpp" @@ -11,6 +12,8 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& : ALabel(config, "idle_inhibitor", id, "{status}", 0, false, true), bar_(bar), idle_inhibitor_(nullptr), + idle_notification_(nullptr), + idle_timeout_ms_(0), pid_(-1), wait_for_activity_(false) { if (waybar::Client::inst()->idle_inhibit_manager == nullptr) { @@ -20,6 +23,11 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& // Read the wait-for-activity config option if (config_["wait-for-activity"].isBool()) { wait_for_activity_ = config_["wait-for-activity"].asBool(); + + // Check if ext-idle-notify protocol is available when wait-for-activity is enabled + if (wait_for_activity_ && waybar::Client::inst()->idle_notifier == nullptr) { + throw std::runtime_error("wait-for-activity requires ext-idle-notify-v1 protocol support"); + } } if (waybar::modules::IdleInhibitor::modules.empty() && config_["start-activated"].isBool() && @@ -38,7 +46,7 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& } waybar::modules::IdleInhibitor::~IdleInhibitor() { - teardownActivityMonitoring(); + teardownIdleNotification(); if (idle_inhibitor_ != nullptr) { zwp_idle_inhibitor_v1_destroy(idle_inhibitor_); @@ -107,11 +115,11 @@ void waybar::modules::IdleInhibitor::toggleStatus() { if (status && config_["timeout"].isNumeric()) { auto timeoutMins = config_["timeout"].asDouble(); int timeoutSecs = timeoutMins * 60; + idle_timeout_ms_ = timeoutSecs * 1000; - // If wait-for-activity is enabled, set up activity monitoring + // If wait-for-activity is enabled, set up idle notification if (wait_for_activity_) { - setupActivityMonitoring(); - resetActivityTimeout(); + setupIdleNotification(); } else { // Original behavior: simple timeout timeout_ = Glib::signal_timeout().connect_seconds( @@ -130,8 +138,8 @@ void waybar::modules::IdleInhibitor::toggleStatus() { timeoutSecs); } } else { - // When deactivated, tear down activity monitoring - teardownActivityMonitoring(); + // When deactivated, tear down idle notification + teardownIdleNotification(); } } @@ -151,77 +159,59 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { return true; } -bool waybar::modules::IdleInhibitor::handleMotion(GdkEventMotion* const& e) { - if (wait_for_activity_ && status) { - resetActivityTimeout(); +void waybar::modules::IdleInhibitor::handleIdled(void* data, + struct ext_idle_notification_v1* /*notification*/) { + spdlog::info("deactivating idle_inhibitor due to user inactivity"); + status = false; + for (auto const& module : waybar::modules::IdleInhibitor::modules) { + module->update(); } - return false; } -bool waybar::modules::IdleInhibitor::handleKey(GdkEventKey* const& e) { - if (wait_for_activity_ && status) { - resetActivityTimeout(); - } - return false; +void waybar::modules::IdleInhibitor::handleResumed(void* data, + struct ext_idle_notification_v1* /*notification*/) { + // User became active again - notification will continue monitoring + spdlog::debug("user activity detected, idle_inhibitor still active"); } -void waybar::modules::IdleInhibitor::resetActivityTimeout() { - if (!config_["timeout"].isNumeric()) { +void waybar::modules::IdleInhibitor::setupIdleNotification() { + // Don't set up if already exists + if (idle_notification_ != nullptr) { return; } - if (activity_timeout_.connected()) { - activity_timeout_.disconnect(); - } - - auto timeoutMins = config_["timeout"].asDouble(); - int timeoutSecs = timeoutMins * 60; - - activity_timeout_ = Glib::signal_timeout().connect_seconds( - []() { - spdlog::info("deactivating idle_inhibitor due to inactivity"); - status = false; - for (auto const& module : waybar::modules::IdleInhibitor::modules) { - module->update(); - } - return false; - }, - timeoutSecs); -} - -void waybar::modules::IdleInhibitor::setupActivityMonitoring() { - // Don't set up if already connected - if (motion_connection_.connected() || key_connection_.connected()) { + auto* client = waybar::Client::inst(); + if (client->idle_notifier == nullptr) { + spdlog::error("ext-idle-notify protocol not available"); return; } - // Get non-const reference to the window to set up event monitoring - // This is safe because we're only setting up signal handlers, not modifying the Bar itself - auto& window = const_cast(bar_.window); - - // Enable motion and key event monitoring on the bar window - auto gdk_window = window.get_window(); - if (gdk_window) { - gdk_window->set_events(gdk_window->get_events() | Gdk::POINTER_MOTION_MASK | Gdk::KEY_PRESS_MASK); + // Get the wayland seat from the display + auto* gdk_seat = gdk_display_get_default_seat(client->gdk_display->gobj()); + if (gdk_seat == nullptr) { + spdlog::error("failed to get default seat"); + return; } + auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat); - // Connect to the bar window's event signals - motion_connection_ = window.signal_motion_notify_event().connect( - sigc::mem_fun(*this, &IdleInhibitor::handleMotion)); - key_connection_ = window.signal_key_press_event().connect( - sigc::mem_fun(*this, &IdleInhibitor::handleKey)); + // Create idle notification that monitors all input (not just when inhibitor is active) + // We use get_idle_notification instead of get_input_idle_notification to respect + // idle inhibitors from other applications + idle_notification_ = ext_idle_notifier_v1_get_idle_notification( + client->idle_notifier, idle_timeout_ms_, wl_seat); + + static const struct ext_idle_notification_v1_listener idle_notification_listener = { + .idled = &IdleInhibitor::handleIdled, + .resumed = &IdleInhibitor::handleResumed, + }; + + ext_idle_notification_v1_add_listener(idle_notification_, &idle_notification_listener, this); + wl_display_roundtrip(client->wl_display); } -void waybar::modules::IdleInhibitor::teardownActivityMonitoring() { - if (activity_timeout_.connected()) { - activity_timeout_.disconnect(); - } - - if (motion_connection_.connected()) { - motion_connection_.disconnect(); - } - - if (key_connection_.connected()) { - key_connection_.disconnect(); +void waybar::modules::IdleInhibitor::teardownIdleNotification() { + if (idle_notification_ != nullptr) { + ext_idle_notification_v1_destroy(idle_notification_); + idle_notification_ = nullptr; } } From d68f168e09cfbaf782bd5ef73ac737649a1014e6 Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 19:50:38 -0500 Subject: [PATCH 081/416] fixed the compilation error --- include/modules/idle_inhibitor.hpp | 6 +++--- src/modules/idle_inhibitor.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 523426c6..8ccb0d33 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -10,7 +10,7 @@ namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; - struct ext_idle_notification_v1* idle_notification_; + struct ::ext_idle_notification_v1* idle_notification_; uint32_t idle_timeout_ms_; public: @@ -26,8 +26,8 @@ class IdleInhibitor : public ALabel { void toggleStatus(); void setupIdleNotification(); void teardownIdleNotification(); - static void handleIdled(void* data, struct ext_idle_notification_v1* notification); - static void handleResumed(void* data, struct ext_idle_notification_v1* notification); + static void handleIdled(void* data, struct ::ext_idle_notification_v1* notification); + static void handleResumed(void* data, struct ::ext_idle_notification_v1* notification); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 7d5fdd01..78cc5ab8 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -160,7 +160,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } void waybar::modules::IdleInhibitor::handleIdled(void* data, - struct ext_idle_notification_v1* /*notification*/) { + struct ::ext_idle_notification_v1* /*notification*/) { spdlog::info("deactivating idle_inhibitor due to user inactivity"); status = false; for (auto const& module : waybar::modules::IdleInhibitor::modules) { @@ -169,7 +169,7 @@ void waybar::modules::IdleInhibitor::handleIdled(void* data, } void waybar::modules::IdleInhibitor::handleResumed(void* data, - struct ext_idle_notification_v1* /*notification*/) { + struct ::ext_idle_notification_v1* /*notification*/) { // User became active again - notification will continue monitoring spdlog::debug("user activity detected, idle_inhibitor still active"); } From 3e2ea1a8705b47e3967b5ae54bf5cd134ce24bfc Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 19:58:43 -0500 Subject: [PATCH 082/416] fixed the type declarations --- include/modules/idle_inhibitor.hpp | 6 +++--- src/modules/idle_inhibitor.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 8ccb0d33..515d7f5a 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -10,7 +10,7 @@ namespace waybar::modules { class IdleInhibitor : public ALabel { sigc::connection timeout_; - struct ::ext_idle_notification_v1* idle_notification_; + ext_idle_notification_v1* idle_notification_; uint32_t idle_timeout_ms_; public: @@ -26,8 +26,8 @@ class IdleInhibitor : public ALabel { void toggleStatus(); void setupIdleNotification(); void teardownIdleNotification(); - static void handleIdled(void* data, struct ::ext_idle_notification_v1* notification); - static void handleResumed(void* data, struct ::ext_idle_notification_v1* notification); + static void handleIdled(void* data, ext_idle_notification_v1* notification); + static void handleResumed(void* data, ext_idle_notification_v1* notification); const Bar& bar_; struct zwp_idle_inhibitor_v1* idle_inhibitor_; diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 78cc5ab8..95ef94bd 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -160,7 +160,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } void waybar::modules::IdleInhibitor::handleIdled(void* data, - struct ::ext_idle_notification_v1* /*notification*/) { + ext_idle_notification_v1* /*notification*/) { spdlog::info("deactivating idle_inhibitor due to user inactivity"); status = false; for (auto const& module : waybar::modules::IdleInhibitor::modules) { @@ -169,7 +169,7 @@ void waybar::modules::IdleInhibitor::handleIdled(void* data, } void waybar::modules::IdleInhibitor::handleResumed(void* data, - struct ::ext_idle_notification_v1* /*notification*/) { + ext_idle_notification_v1* /*notification*/) { // User became active again - notification will continue monitoring spdlog::debug("user activity detected, idle_inhibitor still active"); } From 26922c7fbc4296c27ae92384f0cfd5fd8283641c Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 20:07:28 -0500 Subject: [PATCH 083/416] Update idle_inhibitor.hpp --- include/modules/idle_inhibitor.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 515d7f5a..40c5227c 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -6,6 +6,8 @@ #include "bar.hpp" #include "client.hpp" +struct ext_idle_notification_v1; + namespace waybar::modules { class IdleInhibitor : public ALabel { From e1e99716802d40814964f3e3a4351cad18a9062f Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 20:49:24 -0500 Subject: [PATCH 084/416] fixed stale notification object issue --- src/modules/idle_inhibitor.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 95ef94bd..7ce192af 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -119,6 +119,9 @@ void waybar::modules::IdleInhibitor::toggleStatus() { // If wait-for-activity is enabled, set up idle notification if (wait_for_activity_) { + spdlog::debug("idle_inhibitor: wait-for-activity enabled, timeout: {} ms", idle_timeout_ms_); + // Tear down any existing notification first to ensure fresh setup + teardownIdleNotification(); setupIdleNotification(); } else { // Original behavior: simple timeout @@ -163,6 +166,13 @@ void waybar::modules::IdleInhibitor::handleIdled(void* data, ext_idle_notification_v1* /*notification*/) { spdlog::info("deactivating idle_inhibitor due to user inactivity"); status = false; + + // Clean up the notification since we're deactivating + auto* self = static_cast(data); + if (self != nullptr) { + self->teardownIdleNotification(); + } + for (auto const& module : waybar::modules::IdleInhibitor::modules) { module->update(); } @@ -175,9 +185,12 @@ void waybar::modules::IdleInhibitor::handleResumed(void* data, } void waybar::modules::IdleInhibitor::setupIdleNotification() { - // Don't set up if already exists + spdlog::debug("idle_inhibitor: setting up idle notification"); + + // Clean up any existing notification first if (idle_notification_ != nullptr) { - return; + spdlog::debug("idle_inhibitor: cleaning up existing notification before setup"); + teardownIdleNotification(); } auto* client = waybar::Client::inst(); @@ -197,9 +210,15 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { // Create idle notification that monitors all input (not just when inhibitor is active) // We use get_idle_notification instead of get_input_idle_notification to respect // idle inhibitors from other applications + spdlog::debug("idle_inhibitor: creating notification with timeout {} ms", idle_timeout_ms_); idle_notification_ = ext_idle_notifier_v1_get_idle_notification( client->idle_notifier, idle_timeout_ms_, wl_seat); + if (idle_notification_ == nullptr) { + spdlog::error("idle_inhibitor: failed to create idle notification"); + return; + } + static const struct ext_idle_notification_v1_listener idle_notification_listener = { .idled = &IdleInhibitor::handleIdled, .resumed = &IdleInhibitor::handleResumed, @@ -207,10 +226,12 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { ext_idle_notification_v1_add_listener(idle_notification_, &idle_notification_listener, this); wl_display_roundtrip(client->wl_display); + spdlog::debug("idle_inhibitor: idle notification setup complete"); } void waybar::modules::IdleInhibitor::teardownIdleNotification() { if (idle_notification_ != nullptr) { + spdlog::debug("idle_inhibitor: tearing down idle notification"); ext_idle_notification_v1_destroy(idle_notification_); idle_notification_ = nullptr; } From 19afad86751c022cc1a6690aea6be11b7da1ee21 Mon Sep 17 00:00:00 2001 From: guttermonk Date: Fri, 17 Oct 2025 21:21:48 -0500 Subject: [PATCH 085/416] updated client to bind version 2 of the ext-idle-notifier protocol --- src/client.cpp | 6 +++++- src/modules/idle_inhibitor.cpp | 26 ++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index a71d1c7a..d4c7aca2 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -28,8 +29,11 @@ void waybar::Client::handleGlobal(void *data, struct wl_registry *registry, uint client->idle_inhibit_manager = static_cast( wl_registry_bind(registry, name, &zwp_idle_inhibit_manager_v1_interface, 1)); } else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) { + // Bind version 2 if available (for get_input_idle_notification), otherwise version 1 + auto bind_version = std::min(version, 2u); client->idle_notifier = static_cast( - wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, 1)); + wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, bind_version)); + spdlog::debug("Bound ext-idle-notifier-v1 at version {}", bind_version); } } diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 7ce192af..8dfe0b85 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -207,12 +207,26 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { } auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat); - // Create idle notification that monitors all input (not just when inhibitor is active) - // We use get_idle_notification instead of get_input_idle_notification to respect - // idle inhibitors from other applications - spdlog::debug("idle_inhibitor: creating notification with timeout {} ms", idle_timeout_ms_); - idle_notification_ = ext_idle_notifier_v1_get_idle_notification( - client->idle_notifier, idle_timeout_ms_, wl_seat); + // Check protocol version to determine which function to use + uint32_t version = wl_proxy_get_version(reinterpret_cast(client->idle_notifier)); + + spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})", + idle_timeout_ms_, version); + + if (version >= 2) { + // Version 2+: Use get_input_idle_notification which ignores idle inhibitors + // This allows us to detect actual user inactivity even while the inhibitor is active + spdlog::debug("idle_inhibitor: using get_input_idle_notification (ignores inhibitors)"); + idle_notification_ = ext_idle_notifier_v1_get_input_idle_notification( + client->idle_notifier, idle_timeout_ms_, wl_seat); + } else { + // Version 1: Fall back to get_idle_notification + // WARNING: This respects idle inhibitors, so it won't fire while inhibitor is active + spdlog::warn("idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support get_input_idle_notification, " + "wait-for-activity may not work correctly", version); + idle_notification_ = ext_idle_notifier_v1_get_idle_notification( + client->idle_notifier, idle_timeout_ms_, wl_seat); + } if (idle_notification_ == nullptr) { spdlog::error("idle_inhibitor: failed to create idle notification"); From 16d7c9d8497465a360e214f40944a2624852ea30 Mon Sep 17 00:00:00 2001 From: zspher <66728045+zspher@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:35:48 -0600 Subject: [PATCH 086/416] feat(image): expand `~` & `$HOME` in path --- src/modules/image.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 173aabd3..8b9888bc 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -1,5 +1,7 @@ #include "modules/image.hpp" +#include + waybar::modules::Image::Image(const std::string& id, const Json::Value& config) : AModule(config, "image", id), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { box_.pack_start(image_); @@ -56,6 +58,10 @@ auto waybar::modules::Image::update() -> void { path_ = ""; } + // expand path if "~" or "$HOME" is present in original path + auto result = Config::tryExpandPath(path_, ""); + path_ = result.empty() ? "" : result.front(); + if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) { Glib::RefPtr pixbuf; From d61bc859ae7a086abed0f9b93cf69ce7f4e8b3bf Mon Sep 17 00:00:00 2001 From: zspher <66728045+zspher@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:07:36 -0600 Subject: [PATCH 087/416] perf(image): set image from `path` only once --- src/modules/image.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 8b9888bc..02b6d336 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -28,6 +28,13 @@ waybar::modules::Image::Image(const std::string& id, const Json::Value& config) size_ = 16; } + if (config_["path"].isString()) { + auto result = Config::tryExpandPath(config_["path"].asString(), ""); + path_ = result.empty() ? "" : result.front(); + } else { + path_.clear(); + } + if (interval_.count() == 0) { interval_ = std::chrono::milliseconds::max(); } @@ -49,19 +56,14 @@ void waybar::modules::Image::refresh(int sig) { } auto waybar::modules::Image::update() -> void { - if (config_["path"].isString()) { - path_ = config_["path"].asString(); - } else if (config_["exec"].isString()) { + if (config_["exec"].isString()) { output_ = util::command::exec(config_["exec"].asString(), ""); parseOutputRaw(); - } else { - path_ = ""; + // expand path if "~" or "$HOME" is present in original path + auto result = Config::tryExpandPath(path_, ""); + path_ = result.empty() ? "" : result.front(); } - // expand path if "~" or "$HOME" is present in original path - auto result = Config::tryExpandPath(path_, ""); - path_ = result.empty() ? "" : result.front(); - if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) { Glib::RefPtr pixbuf; From 5b5b80a03146751174572aaa5a2023ae731fcce3 Mon Sep 17 00:00:00 2001 From: OlivierNeyret Date: Sat, 22 Nov 2025 17:47:03 +0100 Subject: [PATCH 088/416] Fix indentation formatting in power_menu.xml --- resources/custom_modules/power_menu.xml | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/resources/custom_modules/power_menu.xml b/resources/custom_modules/power_menu.xml index aa2a42ca..972499d7 100644 --- a/resources/custom_modules/power_menu.xml +++ b/resources/custom_modules/power_menu.xml @@ -2,27 +2,27 @@ - - Suspend - - - - - Hibernate - - + + Suspend + + - - Shutdown - + + Hibernate + + + + + Shutdown + - - Reboot - + + Reboot + From bbb7f377612ba66bcd2d11541cea127ff5b0b284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Wed, 5 Nov 2025 12:43:25 +0000 Subject: [PATCH 089/416] support hiding tray icons --- include/modules/sni/icon_manager.hpp | 11 ++++++++++- include/modules/sni/item.hpp | 2 ++ man/waybar-tray.5.scd | 16 ++++++++++++---- src/modules/sni/item.cpp | 14 +++++++++++--- src/modules/sni/tray.cpp | 12 ++++++++---- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/include/modules/sni/icon_manager.hpp b/include/modules/sni/icon_manager.hpp index 614d42d9..9f603d16 100644 --- a/include/modules/sni/icon_manager.hpp +++ b/include/modules/sni/icon_manager.hpp @@ -5,6 +5,7 @@ #include #include +#include class IconManager { public: @@ -19,7 +20,10 @@ class IconManager { std::string app_name = key; const Json::Value& icon_value = icons_config[key]; - if (icon_value.isString()) { + if (icon_value.isBool() && !icon_value.asBool()) { + // false value means hide this app + hidden_apps_.insert(app_name); + } else if (icon_value.isString()) { std::string icon_path = icon_value.asString(); icons_map_[app_name] = icon_path; } @@ -37,7 +41,12 @@ class IconManager { return ""; } + bool isHidden(const std::string& app_name) const { + return hidden_apps_.find(app_name) != hidden_apps_.end(); + } + private: IconManager() = default; std::unordered_map icons_map_; + std::unordered_set hidden_apps_; }; diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 503ab637..3430aba3 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -86,6 +86,8 @@ class Item : public sigc::trackable { gdouble distance_scrolled_y_ = 0; // visibility of items with Status == Passive bool show_passive_ = false; + // hidden via config + bool is_hidden_ = false; const Bar& bar_; diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index dec5347f..1d787e50 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -42,16 +42,24 @@ Addressed by *tray* default: false ++ Enables this module to consume all left over space dynamically. +*icons*: ++ + typeof: object ++ + Per-application icon customization. Keys are application IDs (e.g., "spotify", "blueman"). ++ + Values can be: ++ + - *string*: Custom icon name or path to icon file ++ + - *false*: Hide this application's tray icon completely + # EXAMPLES ``` "tray": { "icon-size": 21, "spacing": 10, - "icons": { - "blueman": "bluetooth", - "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png" - } + "icons": { + "blueman": "bluetooth", + "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png", + "spotify": false + } } ``` diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 398afac6..8fba7447 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -156,15 +156,23 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { * I still haven't found a way for it to pick from theme automatically, although * it might be my theme. */ + std::string icon_id = id; if (id == "chrome_status_icon_1") { Glib::VariantBase value; this->proxy_->get_cached_property(value, "ToolTip"); tooltip = get_variant(value); if (!tooltip.text.empty()) { - setCustomIcon(tooltip.text.lowercase()); + icon_id = tooltip.text.lowercase(); + setCustomIcon(icon_id); } } else { - setCustomIcon(id); + setCustomIcon(icon_id); + } + + // Check if this item should be hidden + if (IconManager::instance().isHidden(icon_id)) { + spdlog::debug("Hiding tray item with ID: {}", icon_id); + is_hidden_ = true; } } else if (name == "Title") { title = get_variant(value); @@ -214,7 +222,7 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { void Item::setStatus(const Glib::ustring& value) { Glib::ustring lower = value.lowercase(); - event_box.set_visible(show_passive_ || lower.compare("passive") != 0); + event_box.set_visible(!is_hidden_ && (show_passive_ || lower.compare("passive") != 0)); auto style = event_box.get_style_context(); for (const auto& class_name : style->list_classes()) { diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 34a3c05f..eb33291f 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -8,11 +8,18 @@ namespace waybar::modules::SNI { +static void initIconsConfig(const Json::Value& config) { + if (config["icons"].isObject()) { + IconManager::instance().setIconsConfig(config["icons"]); + } +} + Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) : AModule(config, "tray", id), box_(bar.orientation, 0), watcher_(SNI::Watcher::getInstance()), - host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1), + host_((initIconsConfig(config), nb_hosts_), config, bar, + std::bind(&Tray::onAdd, this, std::placeholders::_1), std::bind(&Tray::onRemove, this, std::placeholders::_1)) { box_.set_name("tray"); event_box_.add(box_); @@ -27,9 +34,6 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) show_passive_ = config["show-passive-items"].asBool(); } nb_hosts_ += 1; - if (config_["icons"].isObject()) { - IconManager::instance().setIconsConfig(config_["icons"]); - } dp.emit(); } From b277b639991318e1b99d146fa5c9059ae1664bb0 Mon Sep 17 00:00:00 2001 From: Mental-Vortex <206187961+Mental-Vortex@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:31:51 +0800 Subject: [PATCH 090/416] fix: Return when ID doesn't exist, we only care about the interest list. --- src/modules/wireplumber.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index a43ad29b..b30961cb 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -200,6 +200,7 @@ void waybar::modules::Wireplumber::onMixerChanged(waybar::modules::Wireplumber* return; } } + return; } spdlog::warn("[{}]: (onMixerChanged: {}) - Object with id {} not found", self->name_, From 85a8fca0de0b2488ed81b4f3b88eca300da051e7 Mon Sep 17 00:00:00 2001 From: Raphael Kimmig Date: Sun, 7 Dec 2025 12:18:24 +0100 Subject: [PATCH 091/416] feat: allow sorting niri workspaces --- include/modules/niri/workspaces.hpp | 6 +++ man/waybar-niri-workspaces.5.scd | 17 ++++++ src/modules/niri/workspaces.cpp | 84 ++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 2 deletions(-) diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index a6850ed1..09956267 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "AModule.hpp" #include "bar.hpp" @@ -18,6 +19,7 @@ class Workspaces : public AModule, public EventHandler { private: void onEvent(const Json::Value &ev) override; void doUpdate(); + void sortWorkspaces(std::vector &workspaces) const; Gtk::Button &addButton(const Json::Value &ws); std::string getIcon(const std::string &value, const Json::Value &ws); @@ -25,6 +27,10 @@ class Workspaces : public AModule, public EventHandler { Gtk::Box box_; // Map from niri workspace id to button. std::unordered_map buttons_; + + bool sort_by_id_ = false; + bool sort_by_name_ = false; + bool sort_by_coordinates_ = false; }; } // namespace waybar::modules::niri diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 4be85eb3..b8fd15ec 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -17,6 +17,23 @@ Addressed by *niri/workspaces* default: false ++ If set to false, workspaces will only be shown on the output they are on. If set to true all workspaces will be shown on every output. +*sort-by-name*: ++ + typeof: bool ++ + default: false ++ + Sort workspaces by name (numeric sort when all names are numbers). Unnamed workspaces fall back to their index on the output. + +*sort-by-coordinates*: ++ + typeof: bool ++ + default: false ++ + Sort workspaces by output and index. If both *sort-by-name* and *sort-by-coordinates* are true, sorting by name is applied. + +*sort-by-id*: ++ + typeof: bool ++ + default: false ++ + Sort workspaces by id, taking precedence over other sort options. + +If none of the sorting options are enabled, workspaces keep their output/index order. + *format*: ++ typeof: string ++ default: {value} ++ diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 7dfc0b35..ee9963b4 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -4,10 +4,34 @@ #include #include +#include +#include + namespace waybar::modules::niri { Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config) : AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) { + const auto config_sort_by_number = config_["sort-by-number"]; + if (config_sort_by_number.isBool()) { + spdlog::warn("[niri/workspaces]: Prefer sort-by-id instead of sort-by-number"); + sort_by_id_ = config_sort_by_number.asBool(); + } + + const auto config_sort_by_id = config_["sort-by-id"]; + if (config_sort_by_id.isBool()) { + sort_by_id_ = config_sort_by_id.asBool(); + } + + const auto config_sort_by_name = config_["sort-by-name"]; + if (config_sort_by_name.isBool()) { + sort_by_name_ = config_sort_by_name.asBool(); + } + + const auto config_sort_by_coordinates = config_["sort-by-coordinates"]; + if (config_sort_by_coordinates.isBool()) { + sort_by_coordinates_ = config_sort_by_coordinates.asBool(); + } + box_.set_name("workspaces"); if (!id.empty()) { box_.get_style_context()->add_class(id); @@ -41,6 +65,8 @@ void Workspaces::doUpdate() { return ws["output"].asString() == bar_.output->name; }); + sortWorkspaces(my_workspaces); + // Remove buttons for removed workspaces. for (auto it = buttons_.begin(); it != buttons_.end();) { auto ws = std::find_if(my_workspaces.begin(), my_workspaces.end(), @@ -123,8 +149,7 @@ void Workspaces::doUpdate() { for (auto it = my_workspaces.cbegin(); it != my_workspaces.cend(); ++it) { const auto &ws = *it; - auto pos = ws["idx"].asUInt() - 1; - if (alloutputs) pos = it - my_workspaces.cbegin(); + const auto pos = static_cast(std::distance(my_workspaces.cbegin(), it)); auto &button = buttons_[ws["id"].asUInt64()]; box_.reorder_child(button, pos); @@ -193,4 +218,59 @@ std::string Workspaces::getIcon(const std::string &value, const Json::Value &ws) return value; } +void Workspaces::sortWorkspaces(std::vector &workspaces) const { + auto get_name = [](const Json::Value &ws) -> std::string { + if (ws["name"]) return ws["name"].asString(); + return std::to_string(ws["idx"].asUInt()); + }; + + auto is_numeric = [](const std::string &value) { + return !value.empty() && + std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c); }); + }; + + const bool names_are_numeric = + std::all_of(workspaces.begin(), workspaces.end(), + [&](const auto &ws) { return is_numeric(get_name(ws)); }); + + auto compare_numeric_strings = [](const std::string &a, const std::string &b) { + if (a.size() != b.size()) return a.size() < b.size(); + return a < b; + }; + + std::sort(workspaces.begin(), workspaces.end(), [&](const auto &a, const auto &b) { + if (sort_by_id_) { + return a["id"].asUInt64() < b["id"].asUInt64(); + } + + if (sort_by_name_) { + const auto a_name = get_name(a); + const auto b_name = get_name(b); + if (a_name == b_name) return a["id"].asUInt64() < b["id"].asUInt64(); + if (names_are_numeric) return compare_numeric_strings(a_name, b_name); + return a_name < b_name; + } + + if (sort_by_coordinates_) { + const auto &a_output = a["output"].asString(); + const auto &b_output = b["output"].asString(); + if (a_output == b_output) { + const auto a_idx = a["idx"].asUInt(); + const auto b_idx = b["idx"].asUInt(); + if (a_idx == b_idx) return a["id"].asUInt64() < b["id"].asUInt64(); + return a_idx < b_idx; + } + return a_output < b_output; + } + + // Default to sorting by workspace index on each output. + const auto &a_output = a["output"].asString(); + const auto &b_output = b["output"].asString(); + const auto a_idx = a["idx"].asUInt(); + const auto b_idx = b["idx"].asUInt(); + if (a_output == b_output) return a_idx < b_idx; + return a_output < b_output; + }); +} + } // namespace waybar::modules::niri From 7777be0da6567a834f599f3f54b43924d98e21bc Mon Sep 17 00:00:00 2001 From: Axel Dahlberg Date: Sun, 7 Dec 2025 19:43:25 +0100 Subject: [PATCH 092/416] feat: allow not switching output when clicking sway/workspace --- man/waybar-sway-workspaces.5.scd | 4 ++++ src/modules/sway/workspaces.cpp | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 25b59eb9..2347cc39 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -78,6 +78,10 @@ Addressed by *sway/workspaces* typeof: bool ++ Whether to disable *workspace_auto_back_and_forth* when clicking on workspaces. If this is set to *true*, clicking on a workspace you are already on won't do anything, even if *workspace_auto_back_and_forth* is enabled in the Sway configuration. +*no-switch-output*: ++ + typeof: bool ++ + Whether to disable switching output when clicking on workspaces. If this is set to *true*, then clicking on a workspace on a different output will bring it to the current output instead of switching to that output. + *alphabetical_sort*: ++ typeof: bool ++ Whether to sort workspaces alphabetically. Please note this can make "swaymsg workspace prev/next" move to workspaces inconsistent with the ordering shown in Waybar. diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 86e7349e..608ecf01 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -369,11 +369,20 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { node["name"].asString(), node["target_output"].asString(), "--no-auto-back-and-forth", node["name"].asString())); } else { - ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"", - config_["disable-auto-back-and-forth"].asBool() - ? "--no-auto-back-and-forth" - : "", - node["name"].asString())); + if (config_["no-switch-output"].asBool()) { + ipc_.sendCmd(IPC_COMMAND, fmt::format("[workspace=\"^{}$\"] move workspace to output current; workspace number {} \"{}\"", + node["name"].asString(), + config_["disable-auto-back-and-forth"].asBool() + ? "--no-auto-back-and-forth" + : "", + node["name"].asString())); + } else { + ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"", + config_["disable-auto-back-and-forth"].asBool() + ? "--no-auto-back-and-forth" + : "", + node["name"].asString())); + } } } catch (const std::exception &e) { spdlog::error("Workspaces: {}", e.what()); From 268e33e8927537498f37b6b4b34c82eab192ef72 Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Tue, 9 Dec 2025 19:13:24 -0500 Subject: [PATCH 093/416] Unique icons for active and named workspaces Allows you to set active icons for named workspaces: "active:" = "" --- src/modules/hyprland/workspace.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index ae66bc84..298ba590 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -161,6 +161,13 @@ std::string &Workspace::selectIcon(std::map &icons_map } } + if (isActive() && isSpecial()) { + auto activeIconIt = icons_map.find("active:" + name()); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; + } + } + if (isActive()) { auto activeIconIt = icons_map.find("active"); if (activeIconIt != icons_map.end()) { From 55e1c4c4a93061ce7ed5ad7b2922e91b89848676 Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Tue, 9 Dec 2025 20:47:04 -0500 Subject: [PATCH 094/416] Adds name of hyprland workspace to button class Allows you to target named hyprland special workspaces\n #workspaces button.special.\ {} --- src/modules/hyprland/workspace.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 298ba590..d84b973b 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -241,6 +241,7 @@ void Workspace::update(const std::string &workspace_icon) { auto styleContext = m_button.get_style_context(); addOrRemoveClass(styleContext, isActive(), "active"); addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isSpecial(), name()); addOrRemoveClass(styleContext, isEmpty(), "empty"); addOrRemoveClass(styleContext, isPersistent(), "persistent"); addOrRemoveClass(styleContext, isUrgent(), "urgent"); From a28fe0a9c437f1c591144e16d1a21b1849ca0da8 Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Tue, 9 Dec 2025 23:45:45 -0500 Subject: [PATCH 095/416] bugfix(hyprland/workspaces): Special workspaces being opened on different monitors now updates correctly Previously, if you were to open a special workspace on a different monitor than it was created, the display for workspaces wouldn't update properly\nI've implemented a somewhat hacky solution by just reloading hyprland everytime onSpecialWorkspaceActivated is called. --- src/modules/hyprland/workspaces.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8360137f..670b1b11 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -366,6 +366,7 @@ void Workspaces::onWorkspaceActivated(std::string const &payload) { void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) { std::string name(begin(payload), begin(payload) + payload.find_first_of(',')); m_activeSpecialWorkspaceName = (!name.starts_with("special:") ? name : name.substr(8)); + m_ipc.getSocket1JsonReply("reload"); } void Workspaces::onWorkspaceDestroyed(std::string const &payload) { From a2f277a0b1efd94fc191cad3e06e60dc4e69ce55 Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Thu, 11 Dec 2025 12:04:10 -0500 Subject: [PATCH 096/416] Active special workspaces fallback on non-active icon before default --- src/modules/hyprland/workspace.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index d84b973b..82247020 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -166,6 +166,10 @@ std::string &Workspace::selectIcon(std::map &icons_map if (activeIconIt != icons_map.end()) { return activeIconIt->second; } + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; + } } if (isActive()) { From 85aac3fee7e8eea8bb359febfec79eed4dd668a2 Mon Sep 17 00:00:00 2001 From: mike b Date: Thu, 11 Dec 2025 14:46:32 -0500 Subject: [PATCH 097/416] feature to limit the number of icons displayed --- include/modules/hyprland/workspaces.hpp | 2 + man/waybar-hyprland-workspaces.5.scd | 5 +++ src/modules/hyprland/workspace.cpp | 59 +++++++++++++++++-------- src/modules/hyprland/workspaces.cpp | 3 ++ subprojects/.wraplock | 0 5 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 subprojects/.wraplock diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index a5d94bbf..bb0d27f4 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -50,6 +50,7 @@ class Workspaces : public AModule, public EventHandler { auto taskbarFormatBefore() const -> std::string { return m_taskbarFormatBefore; } auto taskbarFormatAfter() const -> std::string { return m_taskbarFormatAfter; } auto taskbarIconSize() const -> int { return m_taskbarIconSize; } + auto taskbarMaxIcons() const -> int { return m_taskbarMaxIcons; } auto taskbarOrientation() const -> Gtk::Orientation { return m_taskbarOrientation; } auto taskbarReverseDirection() const -> bool { return m_taskbarReverseDirection; } auto onClickWindow() const -> std::string { return m_onClickWindow; } @@ -186,6 +187,7 @@ class Workspaces : public AModule, public EventHandler { std::string m_taskbarFormatBefore; std::string m_taskbarFormatAfter; int m_taskbarIconSize = 16; + int m_taskbarMaxIcons = 0; // 0 means unlimited Gtk::Orientation m_taskbarOrientation = Gtk::ORIENTATION_HORIZONTAL; bool m_taskbarReverseDirection = false; util::EnumParser m_activeWindowEnumParser; diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 1d04157b..1aed0787 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -75,6 +75,11 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. default: 16 ++ Size of the icons in the workspace taskbar. + *max-icons*: ++ + typeof: int ++ + default: 0 (unlimited) ++ + Maximum number of icons to show per workspace. When set, duplicate icons (windows with the same class) are removed first, then the list is trimmed to this limit. Set to 0 for unlimited icons. + *icon-theme*: ++ typeof: string | array ++ default: [] ++ diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index ae66bc84..c14a8545 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -286,11 +287,41 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) { } } - bool isFirst = true; - auto processWindow = [&](const WindowRepr &window_repr) { + // Build a list of windows to display, removing duplicates by window_class + // and respecting max-icons limit + std::vector windowsToShow; + std::set seenClasses; + + auto addWindowIfUnique = [&](const WindowRepr &window_repr) { if (shouldSkipWindow(window_repr)) { - return; // skip + return; } + // Deduplicate by window_class + if (seenClasses.find(window_repr.window_class) != seenClasses.end()) { + return; + } + seenClasses.insert(window_repr.window_class); + windowsToShow.push_back(&window_repr); + }; + + if (m_workspaceManager.taskbarReverseDirection()) { + for (auto it = m_windowMap.rbegin(); it != m_windowMap.rend(); ++it) { + addWindowIfUnique(*it); + } + } else { + for (const auto &window_repr : m_windowMap) { + addWindowIfUnique(window_repr); + } + } + + // Apply max-icons limit if configured + int maxIcons = m_workspaceManager.taskbarMaxIcons(); + if (maxIcons > 0 && static_cast(windowsToShow.size()) > maxIcons) { + windowsToShow.resize(maxIcons); + } + + bool isFirst = true; + for (const auto *window_repr : windowsToShow) { if (isFirst) { isFirst = false; } else if (m_workspaceManager.getWindowSeparator() != "") { @@ -300,27 +331,27 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) { } auto window_box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); - window_box->set_tooltip_text(window_repr.window_title); + window_box->set_tooltip_text(window_repr->window_title); window_box->get_style_context()->add_class("taskbar-window"); - if (window_repr.isActive) { + if (window_repr->isActive) { window_box->get_style_context()->add_class("active"); } auto event_box = Gtk::manage(new Gtk::EventBox()); event_box->add(*window_box); if (m_workspaceManager.onClickWindow() != "") { event_box->signal_button_press_event().connect( - sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address)); + sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr->address)); } auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), - fmt::arg("title", window_repr.window_title)); + fmt::arg("title", window_repr->window_title)); if (!text_before.empty()) { auto window_label_before = Gtk::make_managed(text_before); window_box->pack_start(*window_label_before, true, true); } if (m_workspaceManager.taskbarWithIcon()) { - auto app_info_ = IconLoader::get_app_info_from_app_id_list(window_repr.window_class); + auto app_info_ = IconLoader::get_app_info_from_app_id_list(window_repr->window_class); int icon_size = m_workspaceManager.taskbarIconSize(); auto window_icon = Gtk::make_managed(); m_workspaceManager.iconLoader().image_load_icon(*window_icon, app_info_, icon_size); @@ -328,7 +359,7 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) { } auto text_after = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatAfter()), - fmt::arg("title", window_repr.window_title)); + fmt::arg("title", window_repr->window_title)); if (!text_after.empty()) { auto window_label_after = Gtk::make_managed(text_after); window_box->pack_start(*window_label_after, true, true); @@ -336,16 +367,6 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) { m_content.pack_start(*event_box, true, false); event_box->show_all(); - }; - - if (m_workspaceManager.taskbarReverseDirection()) { - for (auto it = m_windowMap.rbegin(); it != m_windowMap.rend(); ++it) { - processWindow(*it); - } - } else { - for (const auto &window_repr : m_windowMap) { - processWindow(window_repr); - } } auto formatAfter = m_workspaceManager.formatAfter(); diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8360137f..1f29ac9b 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -756,6 +756,9 @@ auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value &config) -> vo if (workspaceTaskbar["icon-size"].isInt()) { m_taskbarIconSize = workspaceTaskbar["icon-size"].asInt(); } + if (workspaceTaskbar["max-icons"].isInt()) { + m_taskbarMaxIcons = workspaceTaskbar["max-icons"].asInt(); + } if (workspaceTaskbar["orientation"].isString() && toLower(workspaceTaskbar["orientation"].asString()) == "vertical") { m_taskbarOrientation = Gtk::ORIENTATION_VERTICAL; diff --git a/subprojects/.wraplock b/subprojects/.wraplock new file mode 100644 index 00000000..e69de29b From 2c9e83441a01d5de926a4313a8d1198002369381 Mon Sep 17 00:00:00 2001 From: Max Gautier Date: Sat, 31 May 2025 23:05:55 +0200 Subject: [PATCH 098/416] log: use native journald protocol for when running as a service When running waybar as a systemd service, allows a slighty more readable and searchable (in particular, by log level) using journal metadata. --- meson.build | 5 +++++ src/main.cpp | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/meson.build b/meson.build index 0675a963..0f553d84 100644 --- a/meson.build +++ b/meson.build @@ -111,6 +111,10 @@ gtk_layer_shell = dependency('gtk-layer-shell-0', version: ['>=0.9.0'], default_options: ['introspection=false', 'vapi=false'], fallback: ['gtk-layer-shell', 'gtk_layer_shell']) systemd = dependency('systemd', required: get_option('systemd')) +libsystemd = dependency('libsystemd', required: get_option('systemd')) +if libsystemd.found() + add_project_arguments('-DHAVE_LIBSYSTEMD', language: 'cpp') +endif cpp_lib_chrono = compiler.compute_int('__cpp_lib_chrono', prefix : '#include ') have_chrono_timezones = cpp_lib_chrono >= 201611 @@ -541,6 +545,7 @@ executable( upower_glib, pipewire, playerctl, + libsystemd, libpulse, libjack, libwireplumber, diff --git a/src/main.cpp b/src/main.cpp index 021f8e1a..fa27ee98 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,17 @@ #include #include +#ifdef HAVE_LIBSYSTEMD +#include +#include + +#include +#include +#include +#include +#include +#endif + #include "bar.hpp" #include "client.hpp" #include "util/SafeSignal.hpp" @@ -154,7 +165,48 @@ static void handleSignalMainThread(int signum, bool& reload) { } } +static void logToJournalIfRunAsService() { +#ifdef HAVE_LIBSYSTEMD + /* Implementation of automatic protocol upgrading (from stderr to journal) + ** as described in https://systemd.io/JOURNAL_NATIVE_PROTOCOL */ + char const* journal_stream = std::getenv("JOURNAL_STREAM"); + + if (journal_stream != nullptr) { + dev_t device; + ino_t inode; + size_t len = std::strlen(journal_stream); + + auto result = std::from_chars(journal_stream, journal_stream + len, device); + if (result.ec == std::errc{}) + result = std::from_chars(result.ptr + 1, journal_stream + len, inode); + if (result.ec != std::errc{}) { + spdlog::warn("malformed JOURNAL_STREAM (\"{}\"): {}, logging to console", journal_stream, + std::make_error_condition(result.ec).message()); + } + + struct stat f_stderr; + + if (fstat(STDERR_FILENO, &f_stderr) != 0) { + spdlog::warn("unable to check stderr device and inode numbers: {}", strerror(errno)); + } else if (device == f_stderr.st_dev && inode == f_stderr.st_ino) { + auto journald = spdlog::systemd_logger_st("native_journal", "waybar", false); + /* systemd_logger_st is thread-safe with enable_formatter = false + ** thanks to underlying sd_journal_send being thread-safe + ** https://github.com/gabime/spdlog/issues/2320#issuecomment-1079766037 + */ + spdlog::set_default_logger(journald); + } else { + spdlog::info("JOURNAL_STREAM does not point to stderr, logging to console"); + } + } else { + spdlog::info("no JOURNAL_STREAM, logging to console"); + } +#endif +} + int main(int argc, char* argv[]) { + logToJournalIfRunAsService(); + try { auto* client = waybar::Client::inst(); From 5c9a1b08d9d635066a7315976525d32bc6c75388 Mon Sep 17 00:00:00 2001 From: PickNicko13 Date: Sun, 21 Dec 2025 23:46:15 +0100 Subject: [PATCH 099/416] Implemented "only-physical" parameter in the wireplumber module. It makes it find the first node that has adevice.id linked to the default output if the default output doesn't have it (i.e. it is a virtual output). Useful if you want to control (and see) the volume of the actual output and not the effect sink. May need some polishing and/or style fixing, but it works. --- include/modules/wireplumber.hpp | 6 ++ src/modules/wireplumber.cpp | 176 +++++++++++++++++++++++++++++--- 2 files changed, 169 insertions(+), 13 deletions(-) diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index 0565704f..264b3ebe 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -36,6 +36,10 @@ class Wireplumber : public ALabel { static std::list modules; + uint32_t resolvePhysicalSink(uint32_t start_id); + uint32_t findPlaybackNodeId(const gchar* description); + static uint32_t get_linked_sink_id(WpObjectManager* om, uint32_t from_node_id, const gchar* media_class); + WpCore* wp_core_; GPtrArray* apis_; WpObjectManager* om_; @@ -54,6 +58,8 @@ class Wireplumber : public ALabel { bool source_muted_; double source_volume_; gchar* default_source_name_; + bool only_physical_; + bool resolved_physical_; }; } // namespace waybar::modules diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index bb3005bd..70320fd1 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -1,4 +1,5 @@ #include "modules/wireplumber.hpp" +#include #include @@ -25,7 +26,8 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val source_node_id_(0), source_muted_(false), source_volume_(0.0), - default_source_name_(nullptr) { + default_source_name_(nullptr), + only_physical_(false) { waybar::modules::Wireplumber::modules.push_back(this); wp_init(WP_INIT_PIPEWIRE); @@ -35,6 +37,7 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val type_ = g_strdup(config_["node-type"].isString() ? config_["node-type"].asString().c_str() : "Audio/Sink"); + only_physical_ = config_["only-physical"].isBool() ? config_["only-physical"].asBool() : false; prepare(this); @@ -226,28 +229,34 @@ void waybar::modules::Wireplumber::onDefaultNodesApiChanged(waybar::modules::Wir spdlog::debug("[{}]: (onDefaultNodesApiChanged: {})", self->name_, self->type_); // Handle sink - uint32_t defaultNodeId; + uint32_t defaultNodeId = 0; g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &defaultNodeId); if (isValidNodeId(defaultNodeId)) { + uint32_t effective_id = self->only_physical_ ? self->resolvePhysicalSink(defaultNodeId) : defaultNodeId; + + if (self->only_physical_ && effective_id != defaultNodeId) { + spdlog::info("[{}]: only-physical enabled: using sink {} instead of default {}", self->name_, effective_id, defaultNodeId); + } + g_autoptr(WpNode) node = static_cast( wp_object_manager_lookup(self->om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", - "=u", defaultNodeId, nullptr)); + "=u", effective_id, nullptr)); if (node != nullptr) { - const gchar* defaultNodeName = + const gchar* effectiveNodeName = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name"); - if (g_strcmp0(self->default_node_name_, defaultNodeName) != 0 || - self->node_id_ != defaultNodeId) { - spdlog::debug("[{}]: Default sink changed to -> Node(name: {}, id: {})", self->name_, - defaultNodeName, defaultNodeId); + if (g_strcmp0(self->default_node_name_, effectiveNodeName) != 0 || + self->node_id_ != effective_id) { + spdlog::debug("[{}]: Default sink resolved to -> Node(name: {}, id: {})", self->name_, + effectiveNodeName, effective_id); g_free(self->default_node_name_); - self->default_node_name_ = g_strdup(defaultNodeName); - self->node_id_ = defaultNodeId; - updateVolume(self, defaultNodeId); - updateNodeName(self, defaultNodeId); + self->default_node_name_ = g_strdup(effectiveNodeName); + self->node_id_ = effective_id; + updateVolume(self, effective_id); + updateNodeName(self, effective_id); } } } @@ -300,7 +309,16 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir // Get default sink g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", self->type_, &self->default_node_name_); - g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &self->node_id_); + uint32_t initial_sink_id = 0; + g_signal_emit_by_name(self->def_nodes_api_, "get-default-node", self->type_, &initial_sink_id); + + if (self->only_physical_ && isValidNodeId(initial_sink_id)) { + self->node_id_ = self->resolvePhysicalSink(initial_sink_id); + spdlog::info("[{}]: only-physical enabled: initial physical sink {} (default was {})", + self->name_, self->node_id_, initial_sink_id); + } else { + self->node_id_ = initial_sink_id; + } // Get default source g_signal_emit_by_name(self->def_nodes_api_, "get-default-configured-node-name", "Audio/Source", @@ -360,6 +378,10 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { "=s", self->type_, nullptr); wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", "Audio/Source", nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", + "=s", "Stream/Output/Audio", nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_LINK, nullptr); + wp_object_manager_request_object_features(om_, WP_TYPE_GLOBAL_PROXY, WP_OBJECT_FEATURES_ALL); } void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, @@ -530,3 +552,131 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { } return true; } + +uint32_t waybar::modules::Wireplumber::findPlaybackNodeId(const gchar* description) { + if (!description || *description == '\0') { + return 0; + } + + spdlog::debug("[{}]: Searching playback node with node.description = {}", name_, description); + + g_autoptr(WpIterator) it = wp_object_manager_new_filtered_iterator( + om_, WP_TYPE_NODE, + WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.description", "=s", description, + WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", "Stream/Output/Audio", + nullptr); + + uint32_t playback_id = 0; + + g_auto(GValue) item = G_VALUE_INIT; + if(wp_iterator_next(it, &item)) { + WpNode* output_node = WP_NODE(g_value_get_object(&item)); + playback_id = wp_proxy_get_bound_id(WP_PROXY(output_node)); + + spdlog::debug("[{}]: Found matching playback node id {}", name_, playback_id); + } + g_value_unset(&item); + + if (playback_id == 0) { + spdlog::debug("[{}]: No playback node found with description '{}'", name_, description); + } + + return playback_id; +} + +uint32_t waybar::modules::Wireplumber::get_linked_sink_id(WpObjectManager* om, uint32_t from_node_id, const gchar* media_class) { + spdlog::debug("DEBUG: Searching links connected to node {}", from_node_id); + + g_autoptr(WpIterator) out_it = wp_object_manager_new_filtered_iterator( + om, WP_TYPE_LINK, + WP_CONSTRAINT_TYPE_PW_PROPERTY, "link.output.node", "=u", from_node_id, + nullptr); + + g_auto(GValue) item = G_VALUE_INIT; + if(wp_iterator_next(out_it, &item)) { + WpLink* link = WP_LINK(g_value_get_object(&item)); + guint32 out_node, out_port, in_node, in_port; + wp_link_get_linked_object_ids(link, &out_node, &out_port, &in_node, &in_port); + + spdlog::debug("Found outgoing link {} -> {}", out_node, in_node); + + g_value_unset(&item); + return in_node; + } + g_value_unset(&item); + + spdlog::debug("No link found from node {}", from_node_id); + return 0; +} + +uint32_t waybar::modules::Wireplumber::resolvePhysicalSink(uint32_t start_id) { + if (!isValidNodeId(start_id) || !only_physical_) { + return start_id; + } + + std::unordered_set visited; + uint32_t current_id = start_id; + int depth = 0; + const int max_depth = 10; + + spdlog::debug("[{}]: Starting physical sink resolution from id {}", name_, start_id); + + while (visited.insert(current_id).second && depth++ < max_depth) { + g_autoptr(WpProxy) proxy = static_cast(wp_object_manager_lookup( + om_, WP_TYPE_GLOBAL_PROXY, + WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", current_id, + WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", type_, + nullptr)); + + if (!proxy || !WP_IS_PIPEWIRE_OBJECT(proxy)) { + spdlog::warn("[{}]: Node {} not found during resolution", name_, current_id); + break; + } + + g_autoptr(WpProperties) props = wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy)); + if (!props) props = wp_properties_new_empty(); + + const gchar* device_id = wp_properties_get(props, "device.id"); + if (device_id != nullptr) { + spdlog::debug("[{}]: Found physical sink {} (device.id = {})", name_, current_id, device_id); + return current_id; + } + + const gchar* description = wp_properties_get(props, "node.description"); + if (!description || *description == '\0') { + description = wp_properties_get(props, "node.nick"); + } + if (!description || *description == '\0') { + spdlog::debug("[{}]: Virtual node {} has no description/nick, stopping", name_, current_id); + break; + } + + spdlog::debug("[{}]: Node {} is virtual (description: {}), searching playback node", name_, current_id, description); + + uint32_t playback_id = findPlaybackNodeId(description); + if (playback_id == 0) { + spdlog::debug("[{}]: No playback node found, cannot resolve further", name_, current_id); + break; + } + + // Follow outgoing link from playback node + uint32_t next_id; + next_id = get_linked_sink_id(om_, playback_id, type_); + if (next_id != 0) { + spdlog::debug("[{}]: Found linked node {} via link traversal", name_, next_id); + } else { + spdlog::debug("[{}]: No links found from/to playback node {}", name_, playback_id); + break; + } + + current_id = next_id; + spdlog::debug("[{}]: Resolved next sink id {}", name_, current_id); + } + + if (depth >= max_depth || !visited.insert(current_id).second) { + spdlog::warn("[{}]: Max depth or cycle reached, stopping at {}", name_, current_id); + } + + spdlog::info("[{}]: Final resolved sink id {}", name_, current_id); + return current_id; +} From c2c1a5d6148ad15a893ec2b1ecd2486d101e9eb1 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Tue, 23 Dec 2025 11:09:31 +0700 Subject: [PATCH 100/416] Add labelCOntainsIcon in AIconLabel --- include/AIconLabel.hpp | 2 ++ src/AIconLabel.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/include/AIconLabel.hpp b/include/AIconLabel.hpp index 054d031a..78a4fcf3 100644 --- a/include/AIconLabel.hpp +++ b/include/AIconLabel.hpp @@ -19,6 +19,8 @@ class AIconLabel : public ALabel { Gtk::Image image_; Gtk::Box box_; + bool labelContainsIcon; + bool iconEnabled() const; }; diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index a20c22e9..5ee12b1c 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -61,7 +61,7 @@ auto AIconLabel::update() -> void { } bool AIconLabel::iconEnabled() const { - return config_["icon"].isBool() ? config_["icon"].asBool() : false; + return labelContainsIcon || (config_["icon"].isBool() ? config_["icon"].asBool() : false); } } // namespace waybar From c0b9635e28a4d1286b0c936f1eb4e2065982c8d2 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Tue, 23 Dec 2025 11:10:34 +0700 Subject: [PATCH 101/416] Parse label_ to extract embedded icon markup --- src/AIconLabel.cpp | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index 5ee12b1c..7c5dc2cb 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include namespace waybar { @@ -56,6 +58,42 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const } auto AIconLabel::update() -> void { + std::string iconName = ""; + labelContainsIcon = false; + try { + std::string inputLabel = label_.get_label().c_str(); + + const std::regex iconSearch(R"((?=\\0icon\\1f).+?(?=\\n))"); + std::smatch iconMatch; + if (std::regex_search(inputLabel, iconMatch, iconSearch)) { + iconName = iconMatch[0].str().substr(9); + + const std::regex cleanLabelPattern(R"(\\0icon\\1f.+?\\n)"); + std::string labelResult = std::regex_replace(inputLabel, cleanLabelPattern, ""); + + label_.set_markup(labelResult); + labelContainsIcon = true; + } + } catch (const std::exception& e) { + spdlog::warn("Error while parsing icon from label. {}", e.what()); + } + + if (labelContainsIcon) { + if (iconName.front() == '/') { + auto pixbuf = Gdk::Pixbuf::create_from_file(iconName); + int scaled_icon_size = 24 * image_.get_scale_factor(); + pixbuf = Gdk::Pixbuf::create_from_file(iconName, 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); + } else { + image_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + image_.set_visible(true); + } + } + image_.set_visible(image_.get_visible() && iconEnabled()); ALabel::update(); } From b8da9ee94c7fffc6c46acd0295bacfc58836e678 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Wed, 24 Dec 2025 07:45:06 +0700 Subject: [PATCH 102/416] Move extract icon into its own method --- include/AIconLabel.hpp | 1 + src/AIconLabel.cpp | 38 ++++++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/include/AIconLabel.hpp b/include/AIconLabel.hpp index 78a4fcf3..2e0956e5 100644 --- a/include/AIconLabel.hpp +++ b/include/AIconLabel.hpp @@ -14,6 +14,7 @@ class AIconLabel : public ALabel { bool enable_click = false, bool enable_scroll = false); virtual ~AIconLabel() = default; auto update() -> void override; + static std::tuple extractIcon(const std::string& input); protected: Gtk::Image image_; diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index 7c5dc2cb..532ed433 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -57,39 +57,45 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const event_box_.add(box_); } -auto AIconLabel::update() -> void { - std::string iconName = ""; - labelContainsIcon = false; +std::tuple AIconLabel::extractIcon(const std::string& input) { + std::string iconResult = ""; + std::string labelResult = input; try { - std::string inputLabel = label_.get_label().c_str(); - const std::regex iconSearch(R"((?=\\0icon\\1f).+?(?=\\n))"); std::smatch iconMatch; - if (std::regex_search(inputLabel, iconMatch, iconSearch)) { - iconName = iconMatch[0].str().substr(9); + if (std::regex_search(input, iconMatch, iconSearch)) { + iconResult = iconMatch[0].str().substr(9); const std::regex cleanLabelPattern(R"(\\0icon\\1f.+?\\n)"); - std::string labelResult = std::regex_replace(inputLabel, cleanLabelPattern, ""); - - label_.set_markup(labelResult); - labelContainsIcon = true; + labelResult = std::regex_replace(input, cleanLabelPattern, ""); } } catch (const std::exception& e) { spdlog::warn("Error while parsing icon from label. {}", e.what()); } + return std::make_tuple(iconResult, labelResult); +} + +auto AIconLabel::update() -> void { + labelContainsIcon = false; + + auto [iconLabel, cleanLabel] = extractIcon(label_.get_label().c_str()); + labelContainsIcon = iconLabel.length() > 0; + if (labelContainsIcon) { - if (iconName.front() == '/') { - auto pixbuf = Gdk::Pixbuf::create_from_file(iconName); - int scaled_icon_size = 24 * image_.get_scale_factor(); - pixbuf = Gdk::Pixbuf::create_from_file(iconName, scaled_icon_size, scaled_icon_size); + label_.set_markup(cleanLabel); + + if (iconLabel.front() == '/') { + auto pixbuf = Gdk::Pixbuf::create_from_file(iconLabel); + int scaled_icon_size = 6 * image_.get_scale_factor(); + 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); } else { - image_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + image_.set_from_icon_name(iconLabel, Gtk::ICON_SIZE_INVALID); image_.set_visible(true); } } From eb2090688ccbf66cfdecba63c2459a54090b1591 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Wed, 24 Dec 2025 08:25:51 +0700 Subject: [PATCH 103/416] AIconLabel: support icon-size config --- include/AIconLabel.hpp | 1 + src/AIconLabel.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/include/AIconLabel.hpp b/include/AIconLabel.hpp index 2e0956e5..91b022a4 100644 --- a/include/AIconLabel.hpp +++ b/include/AIconLabel.hpp @@ -19,6 +19,7 @@ class AIconLabel : public ALabel { protected: Gtk::Image image_; Gtk::Box box_; + unsigned app_icon_size_{24}; bool labelContainsIcon; diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index 532ed433..81953d7b 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -11,6 +11,10 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const const std::string &format, uint16_t interval, bool ellipsize, bool enable_click, bool enable_scroll) : ALabel(config, name, id, format, interval, ellipsize, enable_click, enable_scroll) { + if (config["icon-size"].isUInt()) { + app_icon_size_ = config["icon-size"].asUInt(); + } + image_.set_pixel_size(app_icon_size_); event_box_.remove(); label_.unset_name(); label_.get_style_context()->remove_class(MODULE_CLASS); @@ -86,9 +90,8 @@ auto AIconLabel::update() -> void { label_.set_markup(cleanLabel); if (iconLabel.front() == '/') { - auto pixbuf = Gdk::Pixbuf::create_from_file(iconLabel); - int scaled_icon_size = 6 * image_.get_scale_factor(); - pixbuf = Gdk::Pixbuf::create_from_file(iconLabel, scaled_icon_size, scaled_icon_size); + 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()); From 11533ecf0a5b78171a95bce304cd9f905b4d7ff6 Mon Sep 17 00:00:00 2001 From: Adam Schwalm Date: Fri, 26 Dec 2025 17:16:50 -0600 Subject: [PATCH 104/416] Add '-' support to wifi config Most modules support config fields like "format--", so the user can, for example, set a different format for batter when unplugged _and_ under a given level. Wifi didn't support this. Change the module to support format state based on signal strength. --- src/modules/network.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index d0b7970c..951b099e 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -291,19 +291,27 @@ auto waybar::modules::Network::update() -> void { bandwidth_up_total_ = up_octets; } + auto threshold_state = getState(signal_strength_); + if (!alt_) { auto state = getNetworkState(); if (!state_.empty() && label_.get_style_context()->has_class(state_)) { label_.get_style_context()->remove_class(state_); } - if (config_["format-" + state].isString()) { + if (!threshold_state.empty() && + config_["format-" + state + "-" + threshold_state].isString()) { + default_format_ = config_["format-" + state + "-" + threshold_state].asString(); + } else if (config_["format-" + state].isString()) { default_format_ = config_["format-" + state].asString(); } else if (config_["format"].isString()) { default_format_ = config_["format"].asString(); } else { default_format_ = DEFAULT_FORMAT; } - if (config_["tooltip-format-" + state].isString()) { + if (!threshold_state.empty() && + config_["tooltip-format-" + state + "-" + threshold_state].isString()) { + tooltip_format = config_["tooltip-format-" + state + "-" + threshold_state].asString(); + } else if (config_["tooltip-format-" + state].isString()) { tooltip_format = config_["tooltip-format-" + state].asString(); } if (!label_.get_style_context()->has_class(state)) { @@ -312,7 +320,6 @@ auto waybar::modules::Network::update() -> void { format_ = default_format_; state_ = state; } - getState(signal_strength_); std::string final_ipaddr_; if (addr_pref_ == ip_addr_pref::IPV4) { From ce029c341939b88a91ec167ef1968bba2876158c Mon Sep 17 00:00:00 2001 From: Kaosu Date: Sat, 27 Dec 2025 08:16:03 +0100 Subject: [PATCH 105/416] temperature: add hwmon-name option for stable sensor selection Allow selecting a hwmon device for the temperature module by its name instead of a fixed hwmon path. This avoids fragile configurations that break when new hwmon devices are added and the kernel changes hwmon ordering. The hwmon-name option is mutually exclusive with hwmon-path and hwmon-path-abs to prevent ambiguous configurations. --- man/waybar-temperature.5.scd | 6 ++++++ src/modules/temperature.cpp | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 1e19ec82..672a2518 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -27,6 +27,12 @@ Addressed by *temperature* The path of the hwmon-directory of the device, e.g. */sys/devices/pci0000:00/0000:00:18.3/hwmon*. (Note that the subdirectory *hwmon/hwmon#*, where *#* is a number is not part of the path!) Has to be used together with *input-filename*. This can also be an array of strings, for which, it just works like *hwmon-path*. +*hwmon-name*: ++ + typeof: string ++ + Select a hwmon device by its name (from /sys/class/hwmon/*/name), e.g. *amdgpu* + Requires *input-filename* to be set. + Cannot be used together with *hwmon-path* or *hwmon-path-abs*. + *input-filename*: ++ typeof: string ++ The temperature filename of your *hwmon-path-abs*, e.g. *temp1_input* diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index fa23ef56..d8108577 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -20,6 +20,24 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val if (check_set_path(item.asString())) break; }; + auto find_hwmon_by_name = [](const std::string& name) -> std::optional { + for (const auto& entry : std::filesystem::directory_iterator("/sys/class/hwmon")) { + std::ifstream f(entry.path() / "name"); + std::string hwname; + if (f >> hwname && hwname == name) { + return entry.path(); + } + } + return std::nullopt; + }; + + // ensure either hwmon-name OR old paths are used, not both + if (config_["hwmon-name"].isString() && + (!config_["hwmon-path"].isNull() || !config_["hwmon-path-abs"].isNull())) { + throw std::runtime_error( + "hwmon-name cannot be used together with hwmon-path or hwmon-path-abs"); + } + // if hwmon_path is an array, loop to find first valid item traverseAsArray(config_["hwmon-path"], [this](const std::string& path) { if (!std::filesystem::exists(path)) return false; @@ -40,6 +58,20 @@ waybar::modules::Temperature::Temperature(const std::string& id, const Json::Val }); } + if (file_path_.empty() && config_["hwmon-name"].isString()) { + if (!config_["input-filename"].isString()) { + throw std::runtime_error("hwmon-name requires input-filename to be set"); + } + + auto hwmon = find_hwmon_by_name(config_["hwmon-name"].asString()); + + if (!hwmon) { + throw std::runtime_error("hwmon-name '" + config_["hwmon-name"].asString() + "' not found"); + } + + file_path_ = hwmon->string() + "/" + config_["input-filename"].asString(); + } + if (file_path_.empty()) { auto zone = config_["thermal-zone"].isInt() ? config_["thermal-zone"].asInt() : 0; file_path_ = fmt::format("/sys/class/thermal/thermal_zone{}/temp", zone); From 61af3e95faa77afc699aa142846e9feee793a862 Mon Sep 17 00:00:00 2001 From: Kaosu Date: Sat, 27 Dec 2025 08:38:14 +0100 Subject: [PATCH 106/416] fix the doc --- man/waybar-temperature.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 672a2518..0783331e 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -29,7 +29,7 @@ Addressed by *temperature* *hwmon-name*: ++ typeof: string ++ - Select a hwmon device by its name (from /sys/class/hwmon/*/name), e.g. *amdgpu* + Select a hwmon device by its name (from /sys/class/hwmon/\*/name), e.g. *amdgpu*. Requires *input-filename* to be set. Cannot be used together with *hwmon-path* or *hwmon-path-abs*. From 8e9ebb8bb14c461b9b5175bc842d7f284c43bf30 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Sat, 27 Dec 2025 17:57:20 +0700 Subject: [PATCH 107/416] use snake_case for variable name --- include/AIconLabel.hpp | 2 +- src/AIconLabel.cpp | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/include/AIconLabel.hpp b/include/AIconLabel.hpp index 91b022a4..34beb9b2 100644 --- a/include/AIconLabel.hpp +++ b/include/AIconLabel.hpp @@ -21,7 +21,7 @@ class AIconLabel : public ALabel { Gtk::Box box_; unsigned app_icon_size_{24}; - bool labelContainsIcon; + bool label_contains_icon; bool iconEnabled() const; }; diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index 81953d7b..c053aa97 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -62,31 +62,31 @@ AIconLabel::AIconLabel(const Json::Value &config, const std::string &name, const } std::tuple AIconLabel::extractIcon(const std::string& input) { - std::string iconResult = ""; - std::string labelResult = input; + std::string icon_result = ""; + std::string label_result = input; try { - const std::regex iconSearch(R"((?=\\0icon\\1f).+?(?=\\n))"); - std::smatch iconMatch; - if (std::regex_search(input, iconMatch, iconSearch)) { - iconResult = iconMatch[0].str().substr(9); + const std::regex icon_search(R"((?=\\0icon\\1f).+?(?=\\n))"); + std::smatch icon_match; + if (std::regex_search(input, icon_match, icon_search)) { + icon_result = icon_match[0].str().substr(9); - const std::regex cleanLabelPattern(R"(\\0icon\\1f.+?\\n)"); - labelResult = std::regex_replace(input, cleanLabelPattern, ""); + const std::regex clean_label_pattern(R"(\\0icon\\1f.+?\\n)"); + label_result = std::regex_replace(input, clean_label_pattern, ""); } } catch (const std::exception& e) { spdlog::warn("Error while parsing icon from label. {}", e.what()); } - return std::make_tuple(iconResult, labelResult); + return std::make_tuple(icon_result, label_result); } auto AIconLabel::update() -> void { - labelContainsIcon = false; + label_contains_icon = false; auto [iconLabel, cleanLabel] = extractIcon(label_.get_label().c_str()); - labelContainsIcon = iconLabel.length() > 0; + label_contains_icon = iconLabel.length() > 0; - if (labelContainsIcon) { + if (label_contains_icon) { label_.set_markup(cleanLabel); if (iconLabel.front() == '/') { @@ -108,7 +108,7 @@ auto AIconLabel::update() -> void { } bool AIconLabel::iconEnabled() const { - return labelContainsIcon || (config_["icon"].isBool() ? config_["icon"].asBool() : false); + return label_contains_icon || (config_["icon"].isBool() ? config_["icon"].asBool() : false); } } // namespace waybar From 9dfcd206d70c2e3be5785e262c39871aca430f39 Mon Sep 17 00:00:00 2001 From: Hallvard Lavik Date: Sat, 27 Dec 2025 12:11:43 +0100 Subject: [PATCH 108/416] Allow tooltip formatting the CPU module. --- src/modules/cpu.cpp | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 0703eaf7..b0519f8d 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -26,15 +26,17 @@ auto waybar::modules::Cpu::update() -> void { auto [load1, load5, load15] = Load::getLoad(); auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); - if (tooltipEnabled()) { - label_.set_tooltip_text(tooltip); - } + auto format = format_; + std::string tooltip_format; auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; auto state = getState(total_usage); if (!state.empty() && config_["format-" + state].isString()) { format = config_["format-" + state].asString(); } + if (config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } if (format.empty()) { event_box_.hide(); @@ -58,6 +60,38 @@ auto waybar::modules::Cpu::update() -> void { label_.set_markup(fmt::vformat(format, store)); } + if (tooltipEnabled()) { + if (tooltip_format.empty() && config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + tooltip_format = config_["tooltip-format"].asString(); + + auto icons = std::vector{state}; + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("load", load1)); + store.push_back(fmt::arg("usage", total_usage)); + store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); + store.push_back(fmt::arg("max_frequency", max_frequency)); + store.push_back(fmt::arg("min_frequency", min_frequency)); + store.push_back(fmt::arg("avg_frequency", avg_frequency)); + for (size_t i = 1; i < cpu_usage.size(); ++i) { + auto core_i = i - 1; + auto core_format = fmt::format("usage{}", core_i); + store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i])); + auto icon_format = fmt::format("icon{}", core_i); + store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); + } + auto tooltip_text = fmt::vformat(tooltip_format, store); + + if (label_.get_tooltip_text() != tooltip_text) { + label_.set_tooltip_markup(tooltip_text); + } + } else { + label_.set_tooltip_text(tooltip); + } + } + // Call parent update ALabel::update(); } From 0a5da13fbf5fa5e0c30de4d6d42d23c574b4450a Mon Sep 17 00:00:00 2001 From: Mathis Raguin Date: Wed, 31 Dec 2025 14:44:49 +0100 Subject: [PATCH 109/416] fix(workspaces/sway): handle workspaces with quotes --- include/modules/sway/workspaces.hpp | 2 +- src/modules/sway/workspaces.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index d8a9e18a..fcc91ea1 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -23,7 +23,7 @@ class Workspaces : public AModule, public sigc::trackable { auto update() -> void override; private: - static constexpr std::string_view workspace_switch_cmd_ = "workspace {} \"{}\""; + static constexpr std::string_view workspace_switch_cmd_ = "workspace {} {}"; static constexpr std::string_view persistent_workspace_switch_cmd_ = R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 182bf631..cb29b587 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -369,7 +369,7 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { node["name"].asString(), node["target_output"].asString(), "--no-auto-back-and-forth", node["name"].asString())); } else { - ipc_.sendCmd(IPC_COMMAND, fmt::format("workspace {} \"{}\"", + ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, config_["disable-auto-back-and-forth"].asBool() ? "--no-auto-back-and-forth" : "", From da4367df2f37fd30eaee5f4bc57c7c9179a5917b Mon Sep 17 00:00:00 2001 From: Mathis Raguin Date: Wed, 31 Dec 2025 15:35:38 +0100 Subject: [PATCH 110/416] fix(sway/workspaces): use number when available to avoid parsing issues --- include/modules/sway/workspaces.hpp | 3 ++- src/modules/sway/workspaces.cpp | 13 ++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index fcc91ea1..f68223be 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -23,7 +23,8 @@ class Workspaces : public AModule, public sigc::trackable { auto update() -> void override; private: - static constexpr std::string_view workspace_switch_cmd_ = "workspace {} {}"; + static constexpr std::string_view workspace_switch_cmd_ = "workspace {} \"{}\""; + static constexpr std::string_view workspace_switch_number_cmd_ = "workspace {} number {}"; static constexpr std::string_view persistent_workspace_switch_cmd_ = R"(workspace {} "{}"; move workspace to output "{}"; workspace {} "{}")"; diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index cb29b587..4efbeb74 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -369,11 +369,14 @@ Gtk::Button &Workspaces::addButton(const Json::Value &node) { node["name"].asString(), node["target_output"].asString(), "--no-auto-back-and-forth", node["name"].asString())); } else { - ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, - config_["disable-auto-back-and-forth"].asBool() - ? "--no-auto-back-and-forth" - : "", - node["name"].asString())); + std::string flag = config_["disable-auto-back-and-forth"].asBool() + ? "--no-auto-back-and-forth" + : ""; + if (node["num"].asInt() >= 0) { + ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_number_cmd_, flag, node["num"].asInt())); + } else { + ipc_.sendCmd(IPC_COMMAND, fmt::format(workspace_switch_cmd_, flag, node["name"].asString())); + } } } catch (const std::exception &e) { spdlog::error("Workspaces: {}", e.what()); From c2a038c222d4961b6f5f9ba1d6df9dc5682df45d Mon Sep 17 00:00:00 2001 From: Mathis Raguin Date: Wed, 31 Dec 2025 16:15:54 +0100 Subject: [PATCH 111/416] fix(sway/workspaces): escape markup in workspace if enabled --- src/modules/sway/workspaces.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 182bf631..450f08a9 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -329,7 +329,12 @@ auto Workspaces::update() -> void { } else { button.get_style_context()->remove_class("current_output"); } - std::string output = (*it)["name"].asString(); + std::string output; + if (!config_["disable-markup"].asBool()) { + output = g_markup_escape_text((*it)["name"].asString().c_str(), -1); + } else { + output = (*it)["name"].asString(); + } std::string windows = ""; if (config_["window-rewrite"].isObject()) { updateWindows((*it), windows); From 9e53174ef59eee7d70bb0aee650818e344e6582f Mon Sep 17 00:00:00 2001 From: Dennis Weiershaeuser Date: Wed, 31 Dec 2025 18:05:43 +0100 Subject: [PATCH 112/416] feat(sway/workspaces): add format for negative indices --- man/waybar-sway-workspaces.5.scd | 12 ++++++++++ src/modules/sway/workspaces.cpp | 39 ++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 3d4e3ad0..9bffda3d 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -22,6 +22,11 @@ Addressed by *sway/workspaces* default: {value} ++ The format, how information should be displayed. +*format-for-negative-index*: ++ + typeof: string ++ + default: *format* ++ + An alternative format, which will be used for workspaces with no explict or negative index ("num" in sway terms). Requires *format* to be set. + *format-icons*: ++ typeof: array ++ Based on the workspace name and state, the corresponding icon gets selected. See *icons*. @@ -187,6 +192,13 @@ n.b.: the list of outputs can be obtained from command line using *swaymsg -t ge } ``` +``` +"sway/workspaces": { + "format": "{index} - {name}", + "format-for-negative-index": "{name}" +} +``` + # Style - *#workspaces button* diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 182bf631..88c090c5 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -329,24 +329,43 @@ auto Workspaces::update() -> void { } else { button.get_style_context()->remove_class("current_output"); } - std::string output = (*it)["name"].asString(); + std::string windows = ""; if (config_["window-rewrite"].isObject()) { updateWindows((*it), windows); } + + auto index = (*it)["num"].asInt(); + auto full_name = (*it)["name"].asString(); + if (config_["format"].isString()) { - auto format = config_["format"].asString(); - output = fmt::format( - fmt::runtime(format), fmt::arg("icon", getIcon(output, *it)), fmt::arg("value", output), - fmt::arg("name", trimWorkspaceName(output)), fmt::arg("index", (*it)["num"].asString()), - fmt::arg("windows", - windows.substr(0, windows.length() - m_formatWindowSeparator.length())), - fmt::arg("output", (*it)["output"].asString())); + std::string format; + if(config_["format-for-negative-index"].isString() && index < 0) { + format = config_["format-for-negative-index"].asString(); + } else { + format = config_["format"].asString(); + } + + auto name = trimWorkspaceName(full_name); + auto output = (*it)["output"].asString(); + auto icon = getIcon(full_name, *it); + auto separated_windows = windows.substr(0, windows.length() - m_formatWindowSeparator.length()); + + full_name = fmt::format( + fmt::runtime(format), + fmt::arg("index", index), + fmt::arg("name", name), + fmt::arg("value", full_name), + fmt::arg("output", output), + fmt::arg("icon", icon), + fmt::arg("windows", separated_windows) + ); } + if (!config_["disable-markup"].asBool()) { - static_cast(button.get_children()[0])->set_markup(output); + static_cast(button.get_children()[0])->set_markup(full_name); } else { - button.set_label(output); + button.set_label(full_name); } onButtonReady(*it, button); } From a2d597f17fada1edd479f8ade866aaa66514d7d4 Mon Sep 17 00:00:00 2001 From: PickNicko13 Date: Thu, 1 Jan 2026 16:54:54 +0100 Subject: [PATCH 113/416] Added better logic for different kinds of filter nodes for only-physical. Added fallback in case the final node doesn't have the volume property. --- include/modules/wireplumber.hpp | 3 +- src/modules/wireplumber.cpp | 148 ++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 37 deletions(-) diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index 264b3ebe..f227145c 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -38,7 +38,8 @@ class Wireplumber : public ALabel { uint32_t resolvePhysicalSink(uint32_t start_id); uint32_t findPlaybackNodeId(const gchar* description); - static uint32_t get_linked_sink_id(WpObjectManager* om, uint32_t from_node_id, const gchar* media_class); + uint32_t get_linked_sink_id(WpObjectManager* om, uint32_t from_node_id); + uint32_t get_linked_node_from_output_ports(WpObjectManager* om, uint32_t from_node_id); WpCore* wp_core_; GPtrArray* apis_; diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 70320fd1..bbce1b53 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -374,14 +374,17 @@ void waybar::modules::Wireplumber::activatePlugins() { void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { spdlog::debug("[{}]: preparing object manager: '{}'", name_, self->type_); - wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", - "=s", self->type_, nullptr); - wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", - "=s", "Audio/Source", nullptr); - wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", - "=s", "Stream/Output/Audio", nullptr); - wp_object_manager_add_interest(om_, WP_TYPE_LINK, nullptr); - wp_object_manager_request_object_features(om_, WP_TYPE_GLOBAL_PROXY, WP_OBJECT_FEATURES_ALL); + if(only_physical_){ + wp_object_manager_add_interest(om_, WP_TYPE_NODE, nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_LINK, nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_PORT, nullptr); + wp_object_manager_request_object_features(om_, WP_TYPE_GLOBAL_PROXY, WP_OBJECT_FEATURES_ALL); + } else { + wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", + "=s", self->type_, nullptr); + wp_object_manager_add_interest(om_, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", + "=s", "Audio/Source", nullptr); + } } void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, @@ -553,12 +556,14 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { return true; } +// Finds the output node for filter chains defined in pipewire, +// since their input nodes are NOT providing actual outputs uint32_t waybar::modules::Wireplumber::findPlaybackNodeId(const gchar* description) { if (!description || *description == '\0') { return 0; } - spdlog::debug("[{}]: Searching playback node with node.description = {}", name_, description); + spdlog::debug("[{}]: Searching for playback node with node.description = {}", name_, description); g_autoptr(WpIterator) it = wp_object_manager_new_filtered_iterator( om_, WP_TYPE_NODE, @@ -584,8 +589,8 @@ uint32_t waybar::modules::Wireplumber::findPlaybackNodeId(const gchar* descripti return playback_id; } -uint32_t waybar::modules::Wireplumber::get_linked_sink_id(WpObjectManager* om, uint32_t from_node_id, const gchar* media_class) { - spdlog::debug("DEBUG: Searching links connected to node {}", from_node_id); +uint32_t waybar::modules::Wireplumber::get_linked_sink_id(WpObjectManager* om, uint32_t from_node_id) { + spdlog::debug("[{}]: Searching for links connected to node {}", name_, from_node_id); g_autoptr(WpIterator) out_it = wp_object_manager_new_filtered_iterator( om, WP_TYPE_LINK, @@ -598,14 +603,75 @@ uint32_t waybar::modules::Wireplumber::get_linked_sink_id(WpObjectManager* om, u guint32 out_node, out_port, in_node, in_port; wp_link_get_linked_object_ids(link, &out_node, &out_port, &in_node, &in_port); - spdlog::debug("Found outgoing link {} -> {}", out_node, in_node); + spdlog::debug("[{}]: Found outgoing link {} -> {}", name_, out_node, in_node); g_value_unset(&item); return in_node; } g_value_unset(&item); - spdlog::debug("No link found from node {}", from_node_id); + spdlog::debug("[{}]: No links found connected to node {}", name_, from_node_id); + return 0; +} + +// Follow non-monitor output ports to the next node +uint32_t waybar::modules::Wireplumber::get_linked_node_from_output_ports(WpObjectManager* om, uint32_t from_node_id) { + spdlog::debug("[{}]: Searching for non-monitor output ports on node {}", name_, from_node_id); + + g_autoptr(WpIterator) port_it = wp_object_manager_new_filtered_iterator( + om, WP_TYPE_PORT, + WP_CONSTRAINT_TYPE_PW_PROPERTY, "node.id", "=u", from_node_id, + WP_CONSTRAINT_TYPE_PW_PROPERTY, "port.direction", "=s", "out", + nullptr); + + g_auto(GValue) port_item = G_VALUE_INIT; + while (wp_iterator_next(port_it, &port_item)) { + WpPort* port = WP_PORT(g_value_get_object(&port_item)); + + g_autoptr(WpProperties) port_props = wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(port)); + if (!port_props) { + g_value_unset(&port_item); + continue; + } + + const gchar* name = wp_properties_get(port_props, "port.name"); + + // WP_CONSTRAINT_VERB_MATCHES uses GPatternSpec and it is glob-like. Unfortunately, there is no way to + // express "not beginning with a string" in glob-style regex. Or at least I didn't figure out how to do that. + // Therefore, just filter them out with a conditional. Performance difference should be negligible anyway. + if (!name || g_str_has_prefix(name, "monitor_")) { + g_value_unset(&port_item); + continue; + } + + spdlog::debug("[{}]: Found non-monitor output port with name '{}'", name_, name); + + // Find outgoing link from this port + uint32_t port_id = wp_proxy_get_bound_id(WP_PROXY(port)); + + g_autoptr(WpIterator) link_it = wp_object_manager_new_filtered_iterator( + om, WP_TYPE_LINK, + WP_CONSTRAINT_TYPE_PW_PROPERTY, "link.output.port", "=u", port_id, + nullptr); + + g_auto(GValue) link_item = G_VALUE_INIT; + if (wp_iterator_next(link_it, &link_item)) { + WpLink* link = WP_LINK(g_value_get_object(&link_item)); + guint32 out_node, out_port, in_node, in_port; + wp_link_get_linked_object_ids(link, &out_node, &out_port, &in_node, &in_port); + + spdlog::debug("[{}]: Found link from port {} (node {}) -> node {}", name_, port_id, from_node_id, in_node); + g_value_unset(&link_item); + g_value_unset(&port_item); + return in_node; + } + g_value_unset(&link_item); + + g_value_unset(&port_item); + } + g_value_unset(&port_item); + + spdlog::debug("[{}]: No non-monitor output ports with links on node {}", name_, from_node_id); return 0; } @@ -621,11 +687,11 @@ uint32_t waybar::modules::Wireplumber::resolvePhysicalSink(uint32_t start_id) { spdlog::debug("[{}]: Starting physical sink resolution from id {}", name_, start_id); + // Follow the output node chain until a physical device is found while (visited.insert(current_id).second && depth++ < max_depth) { g_autoptr(WpProxy) proxy = static_cast(wp_object_manager_lookup( om_, WP_TYPE_GLOBAL_PROXY, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=u", current_id, - WP_CONSTRAINT_TYPE_PW_PROPERTY, "media.class", "=s", type_, nullptr)); if (!proxy || !WP_IS_PIPEWIRE_OBJECT(proxy)) { @@ -633,50 +699,60 @@ uint32_t waybar::modules::Wireplumber::resolvePhysicalSink(uint32_t start_id) { break; } + // 1: If it has a device.id, we found the physical sink g_autoptr(WpProperties) props = wp_pipewire_object_get_properties(WP_PIPEWIRE_OBJECT(proxy)); if (!props) props = wp_properties_new_empty(); const gchar* device_id = wp_properties_get(props, "device.id"); if (device_id != nullptr) { spdlog::debug("[{}]: Found physical sink {} (device.id = {})", name_, current_id, device_id); - return current_id; - } - - const gchar* description = wp_properties_get(props, "node.description"); - if (!description || *description == '\0') { - description = wp_properties_get(props, "node.nick"); - } - if (!description || *description == '\0') { - spdlog::debug("[{}]: Virtual node {} has no description/nick, stopping", name_, current_id); break; } - spdlog::debug("[{}]: Node {} is virtual (description: {}), searching playback node", name_, current_id, description); + spdlog::debug("[{}]: Node {} is virtual, trying direct output ports", name_, current_id); + + // 2: Try following non-monitor output ports + uint32_t next_id = get_linked_node_from_output_ports(om_, current_id); + if (next_id != 0) { + spdlog::debug("[{}]: Following direct output port link to node {}", name_, next_id); + current_id = next_id; + continue; + } + + // 3: Search for audio stream node + // (pipewire filter chains create a node for input and a separate node for output) + const gchar* description = wp_properties_get(props, "node.description"); + if (!description || *description == '\0') { + spdlog::warn("[{}]: Virtual node {} has no description/nick - cannot search playback node", name_, current_id); + break; + } + + spdlog::debug("[{}]: No direct output ports, searching playback node for description '{}'", name_, description); uint32_t playback_id = findPlaybackNodeId(description); if (playback_id == 0) { - spdlog::debug("[{}]: No playback node found, cannot resolve further", name_, current_id); + spdlog::warn("[{}]: No playback node found for virtual sink {} - stopping at virtual sink", name_, current_id); break; } - // Follow outgoing link from playback node - uint32_t next_id; - next_id = get_linked_sink_id(om_, playback_id, type_); - if (next_id != 0) { - spdlog::debug("[{}]: Found linked node {} via link traversal", name_, next_id); - } else { - spdlog::debug("[{}]: No links found from/to playback node {}", name_, playback_id); + next_id = get_linked_sink_id(om_, playback_id); + if (next_id == 0) { + spdlog::warn("[{}]: Playback node {} has no outgoing links - stopping at virtual sink {}", name_, playback_id, current_id); break; } + spdlog::debug("[{}]: Following playback node link to node {}", name_, next_id); current_id = next_id; - spdlog::debug("[{}]: Resolved next sink id {}", name_, current_id); } - if (depth >= max_depth || !visited.insert(current_id).second) { - spdlog::warn("[{}]: Max depth or cycle reached, stopping at {}", name_, current_id); - } + GVariant* variant = nullptr; + g_signal_emit_by_name(mixer_api_, "get-volume", current_id, &variant); + + if (variant == nullptr) { + spdlog::warn("[{}]: Node {} does not support volume - fallback to default sink id", name_, current_id); + current_id = start_id; + } spdlog::info("[{}]: Final resolved sink id {}", name_, current_id); return current_id; } From 49083131d18fc0d11e315b7c046fa1cb167149a1 Mon Sep 17 00:00:00 2001 From: Ansari Date: Sat, 3 Jan 2026 11:16:03 +0530 Subject: [PATCH 114/416] fix(AModule): improve cursor configuration handling --- src/AModule.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/AModule.cpp b/src/AModule.cpp index c6fdff3e..f4a9470e 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -72,8 +72,12 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: // Respect user configuration of cursor if (config_.isMember("cursor")) { - if (config_["cursor"].isBool() && config_["cursor"].asBool()) { - setCursor(Gdk::HAND2); + if (config_["cursor"].isBool()) { + if (config_["cursor"].asBool()) { + setCursor(Gdk::HAND2); + } else { + setCursor(Gdk::ARROW); + } } else if (config_["cursor"].isInt()) { setCursor(Gdk::CursorType(config_["cursor"].asInt())); } else { From a8a41bd17cf68ff0ae83910147f7785f82da687d Mon Sep 17 00:00:00 2001 From: Ansari Date: Sat, 3 Jan 2026 11:20:03 +0530 Subject: [PATCH 115/416] fix(workspaces): skip ignored workspaces in persistent rules --- src/modules/hyprland/workspaces.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8360137f..4a2c0e17 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -306,6 +306,10 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &c // 2. the rule's monitor is the current monitor // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { + // => skip ignore-workspaces even if its a persistent + if(isWorkspaceIgnored(workspace)) { + continue; + } // => persistent workspace should be shown on this monitor auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); workspaceData["persistent-rule"] = true; From 5870c4ba0e85911160b04d99793989de2586eefc Mon Sep 17 00:00:00 2001 From: Aksel Lunde Aase Date: Fri, 9 Jan 2026 13:22:48 +0100 Subject: [PATCH 116/416] feat(clock): Add action to execute external command --- include/modules/clock.hpp | 3 +++ man/waybar-clock.5.scd | 2 ++ src/modules/clock.cpp | 8 ++++++++ 3 files changed, 13 insertions(+) diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index c3548063..4ac91d61 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -80,6 +80,7 @@ class Clock final : public ALabel { void cldShift_reset(); void tz_up(); void tz_down(); + void action_exec(const std::string& action); // Module Action Map static inline std::map actionMap_{ {"mode", &waybar::modules::Clock::cldModeSwitch}, @@ -88,6 +89,8 @@ class Clock final : public ALabel { {"shift_reset", &waybar::modules::Clock::cldShift_reset}, {"tz_up", &waybar::modules::Clock::tz_up}, {"tz_down", &waybar::modules::Clock::tz_down}}; + static inline std::map actionWithArgsMap_{ + {"exec", &waybar::modules::Clock::action_exec}}; }; } // namespace waybar::modules diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index b4b5d5b7..440c3447 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -180,6 +180,8 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ Switch to the next calendar month/year |[ *shift_down* :[ Switch to the previous calendar month/year +|[ *exec * +:[ Execute the specified command # FORMAT REPLACEMENTS diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 5fe5407b..2bc99764 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -9,6 +9,7 @@ #include #include +#include "util/command.hpp" #include "util/ustring_clen.hpp" #ifdef HAVE_LANGINFO_1STDAY @@ -472,6 +473,8 @@ auto waybar::modules::Clock::local_zone() -> const time_zone* { auto waybar::modules::Clock::doAction(const std::string& name) -> void { if (actionMap_[name]) { (this->*actionMap_[name])(); + } else if (auto key = name.substr(0, name.find(" ")); actionWithArgsMap_[key]) { + (this->*actionWithArgsMap_[key])(name); } else spdlog::error("Clock. Unsupported action \"{0}\"", name); } @@ -498,6 +501,11 @@ void waybar::modules::Clock::tz_down() { if (tzSize == 1) return; 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)); +} + #ifdef HAVE_LANGINFO_1STDAY template From 7f63a905a13a8a2cfe911932216fbec529b6da1d Mon Sep 17 00:00:00 2001 From: Benji-clm Date: Fri, 9 Jan 2026 13:02:28 +0000 Subject: [PATCH 117/416] feat(battery): add option to smooth power for more stable time_remaining estimates --- include/modules/battery.hpp | 6 ++++++ src/modules/battery.cpp | 30 +++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/include/modules/battery.hpp b/include/modules/battery.hpp index ffc82aba..357cbfb3 100644 --- a/include/modules/battery.hpp +++ b/include/modules/battery.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -51,6 +52,11 @@ class Battery : public ALabel { bool warnFirstTime_{true}; bool weightedAverage_{true}; const Bar& bar_; + bool smoothPowerEnable_{false}; + double time_constant_s_{260.0}; + double smooth_power_{0.0}; // µW + std::chrono::steady_clock::time_point last_t_{std::chrono::steady_clock::now()}; + std::string old_status_raw_{""}; util::SleeperThread thread_; util::SleeperThread thread_battery_update_; diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index c706eb3c..0ff15ac6 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include "util/command.hpp" #if defined(__FreeBSD__) @@ -33,6 +36,11 @@ waybar::modules::Battery::Battery(const std::string& id, const Bar& bar, const J } udev_monitor_enable_receiving(mon_.get()); + if (config_["smooth-power"].isBool()) { + smoothPowerEnable_ = config_["smooth-power"].asBool(); + if (smoothPowerEnable_ && config_["smooth-power-time-constant"].isNumeric()) + time_constant_s_ = std::max(1.0, config_["smooth-power-time-constant"].asDouble()); + } if (config_["weighted-average"].isBool()) weightedAverage_ = config_["weighted-average"].asBool(); #endif spdlog::debug("battery: worker interval is {}", interval_.count()); @@ -562,11 +570,31 @@ waybar::modules::Battery::getInfos() { if (online && current_status != "Discharging") status = "Plugged"; } + if (total_energy_exists && total_power_exists && total_power != 0) { + if (!smoothPowerEnable_) { + smooth_power_ = total_power; + } else { + if (status != old_status_raw_) { + smooth_power_ = total_power; + last_t_ = std::chrono::steady_clock::now(); + } else { + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + double dt_s = + std::chrono::duration_cast >(now - last_t_).count(); + smooth_power_ = smooth_power_ + ((1 - std::exp(-dt_s / time_constant_s_)) * + (total_power - smooth_power_)); + last_t_ = now; + } + + old_status_raw_ = status; + } + } + float time_remaining{0.0f}; 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 (total_power != 0) time_remaining = (float)total_energy / total_power; + if (smooth_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; From c38024bdecc818ecafecbdaf77456d8013dd4b4e Mon Sep 17 00:00:00 2001 From: szbmrk Date: Thu, 15 Jan 2026 00:29:49 +0100 Subject: [PATCH 118/416] feat(hyprland/submap): add config option to specify different icons for submaps --- include/modules/hyprland/submap.hpp | 2 ++ src/modules/hyprland/submap.cpp | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/include/modules/hyprland/submap.hpp b/include/modules/hyprland/submap.hpp index 7e3425ef..14faffd8 100644 --- a/include/modules/hyprland/submap.hpp +++ b/include/modules/hyprland/submap.hpp @@ -26,8 +26,10 @@ class Submap : public waybar::ALabel, public EventHandler { const Bar& bar_; util::JsonParser parser_; std::string submap_; + std::string icon_; bool always_on_ = false; std::string default_submap_ = "Default"; + std::unordered_map icons_; IPC& m_ipc; }; diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp index eb9b1de8..a4591149 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -21,6 +21,18 @@ Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config) // register for hyprland ipc m_ipc.registerForIPC("submap", this); dp.emit(); + + if (config["icons"].isObject()) { + const Json::Value& icons = config["icons"]; + + for (const std::string& key : icons.getMemberNames()) { + const Json::Value& value = icons[key]; + + if (value.isString()) { + icons_[key] = value.asString(); + } + } + } } Submap::~Submap() { @@ -47,7 +59,8 @@ auto Submap::update() -> void { if (submap_.empty()) { event_box_.hide(); } else { - label_.set_markup(fmt::format(fmt::runtime(format_), submap_)); + label_.set_markup( + fmt::format(fmt::runtime(format_), fmt::arg("submap", submap_), fmt::arg("icon", icon_))); if (tooltipEnabled()) { label_.set_tooltip_text(submap_); } @@ -72,6 +85,12 @@ void Submap::onEvent(const std::string& ev) { submap_ = submapName; + if (!icons_[submap_].empty()) { + icon_ = icons_[submap_]; + } else { + icon_ = ""; + } + if (submap_.empty() && always_on_) { submap_ = default_submap_; } From f3c36282549a609061995810f32c0240732796d7 Mon Sep 17 00:00:00 2001 From: szbmrk Date: Thu, 15 Jan 2026 00:37:04 +0100 Subject: [PATCH 119/416] docs(hyprland/submap): add config option to specify different icons for submaps --- man/waybar-hyprland-submap.5.scd | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/man/waybar-hyprland-submap.5.scd b/man/waybar-hyprland-submap.5.scd index f6cdff94..0e540a93 100644 --- a/man/waybar-hyprland-submap.5.scd +++ b/man/waybar-hyprland-submap.5.scd @@ -15,7 +15,7 @@ Addressed by *hyprland/submap* *format*: ++ typeof: string ++ default: {} ++ - The format, how information should be displayed. On {} the currently active submap is displayed. + The format, how information should be displayed. This format supports two placeholders: *{submap}* for the currently active submap name and *{icon}* for the icon associated with the submap. *rotate*: ++ typeof: integer ++ @@ -80,6 +80,10 @@ Addressed by *hyprland/submap* default: Default ++ Option to set the submap name to display when not in an active submap. +*icons*: ++ + typeof: object ++ + Based on the submap name, the corresponding icon will be selected from this map and made available as the *{icon}* placeholder in the format string. The keys are submap names, and the values are the icons or strings to display for those submaps. + *menu*: ++ typeof: string ++ Action that popups the menu. @@ -103,9 +107,13 @@ Addressed by *hyprland/submap* ``` "hyprland/submap": { - "format": "✌️ {}", + "format": "{icon} {submap}", "max-length": 8, - "tooltip": false + "tooltip": false, + "icons": { + "resize": "", + "pause": "", + } } ``` From b192244f50ab34885f011cadec327400c12b07cb Mon Sep 17 00:00:00 2001 From: Aaron Gerbert Date: Fri, 23 Jan 2026 11:27:38 -0500 Subject: [PATCH 120/416] Commit Squash --- src/modules/hyprland/workspace.cpp | 12 ++++++++++++ src/modules/hyprland/workspaces.cpp | 1 + 2 files changed, 13 insertions(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index ae66bc84..82247020 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -161,6 +161,17 @@ std::string &Workspace::selectIcon(std::map &icons_map } } + if (isActive() && isSpecial()) { + auto activeIconIt = icons_map.find("active:" + name()); + if (activeIconIt != icons_map.end()) { + return activeIconIt->second; + } + auto namedIconIt = icons_map.find(name()); + if (namedIconIt != icons_map.end()) { + return namedIconIt->second; + } + } + if (isActive()) { auto activeIconIt = icons_map.find("active"); if (activeIconIt != icons_map.end()) { @@ -234,6 +245,7 @@ void Workspace::update(const std::string &workspace_icon) { auto styleContext = m_button.get_style_context(); addOrRemoveClass(styleContext, isActive(), "active"); addOrRemoveClass(styleContext, isSpecial(), "special"); + addOrRemoveClass(styleContext, isSpecial(), name()); addOrRemoveClass(styleContext, isEmpty(), "empty"); addOrRemoveClass(styleContext, isPersistent(), "persistent"); addOrRemoveClass(styleContext, isUrgent(), "urgent"); diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8360137f..670b1b11 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -366,6 +366,7 @@ void Workspaces::onWorkspaceActivated(std::string const &payload) { void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) { std::string name(begin(payload), begin(payload) + payload.find_first_of(',')); m_activeSpecialWorkspaceName = (!name.starts_with("special:") ? name : name.substr(8)); + m_ipc.getSocket1JsonReply("reload"); } void Workspaces::onWorkspaceDestroyed(std::string const &payload) { From ca0b068ad86ed3b6ba949a699fbfe676d1d3f320 Mon Sep 17 00:00:00 2001 From: szbmrk Date: Fri, 9 Jan 2026 17:59:11 +0100 Subject: [PATCH 121/416] feat(disk)!: handle multiple disks at once --- include/modules/disk.hpp | 5 +- man/waybar-disk.5.scd | 45 ++++++++-- src/modules/disk.cpp | 175 +++++++++++++++++++++++++-------------- 3 files changed, 152 insertions(+), 73 deletions(-) diff --git a/include/modules/disk.hpp b/include/modules/disk.hpp index 1b4f3176..ea6f89c8 100644 --- a/include/modules/disk.hpp +++ b/include/modules/disk.hpp @@ -4,6 +4,7 @@ #include #include +#include #include "ALabel.hpp" #include "util/format.hpp" @@ -19,7 +20,9 @@ class Disk : public ALabel { private: util::SleeperThread thread_; - std::string path_; + std::string header_; + std::vector paths_; + std::string separator_; std::string unit_; float calc_specific_divisor(const std::string divisor); diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index 00af2b90..dab6af7e 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -6,17 +6,12 @@ waybar - disk module # DESCRIPTION -The *disk* module displays the current disk space used. +The *disk* module displays information of multiple disks. # CONFIGURATION Addressed by *disk* -*path*: ++ - typeof: string ++ - default: "/" ++ - Any path residing in the filesystem or mountpoint for which the information should be displayed. - *interval*: ++ typeof: integer++ default: 30 ++ @@ -25,7 +20,7 @@ Addressed by *disk* *format*: ++ typeof: string ++ default: "{percentage_used}%" ++ - The format, how information should be displayed. + The format, how information for each disk should be displayed. *rotate*: ++ typeof: integer ++ @@ -75,6 +70,26 @@ Addressed by *disk* typeof: string ++ Command to execute when scrolling down on the module. +*path*: ++ + typeof: string ++ + default: "/" ++ + Deprecated path of filesystem or mountpoint to monitor. + +*paths*: ++ + typeof: array ++ + default: ["/"] ++ + Array of paths residing in the filesystem or mountpoint for which the information should be displayed. + +*header*: ++ + typeof: string ++ + default: "" ++ + Text to appear before the disk information defined in the format. + +*separator*: ++ + typeof: string ++ + default: " " ++ + Separator string between multiple disk information. + *smooth-scrolling-threshold*: ++ typeof: double ++ Threshold to be used when scrolling. @@ -123,7 +138,7 @@ Addressed by *disk* *{free}*: Amount of available disk space for normal users. Automatically selects unit based on size remaining. -*{path}*: The path specified in the configuration. +*{path}*: The path for each disk specified in the configuration. *{specific_total}*: Total amount of space on the disk, partition, or mountpoint in a specific unit. Defaults to bytes. @@ -143,10 +158,22 @@ Addressed by *disk* ``` "disk": { "interval": 30, + "format": "{percentage_free}% free on {path}", + "header": "Disks: ", + "paths": ["/", "/home"], + "separator": " ", +} +``` + +``` +"disk": { + "interval": 30, + "paths": ["/"], "format": "{specific_free:0.2f} GB out of {specific_total:0.2f} GB available. Alternatively {free} out of {total} available", "unit": "GB" - // 1434.25 GB out of 2000.00 GB available. Alternatively 1.4TiB out of 1.9TiB available. } + + // 1434.25 GB out of 2000.00 GB available. Alternatively 1.4TiB out of 1.9TiB available. ``` # STYLE diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index ef257b72..695ae157 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -1,15 +1,32 @@ #include "modules/disk.hpp" +#include + using namespace waybar::util; waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config) - : ALabel(config, "disk", id, "{}%", 30), path_("/") { + : ALabel(config, "disk", id, "{}%", 30), header_(""), paths_(), separator_(" ") { thread_ = [this] { dp.emit(); thread_.sleep_for(interval_); }; - if (config["path"].isString()) { - path_ = config["path"].asString(); + if (config["header"].isString()) { + header_ = config["header"].asString(); + } + if (config["path"].isString() && !config["paths"].isArray()) { + spdlog::warn("Disk: path is deprecated use paths instead!"); + paths_.push_back(config["path"].asString()); + } + if (config["paths"].isArray()) { + for (const auto& path : config["paths"]) { + paths_.push_back(path.asString()); + } + } + if (!config["path"].isString() && !config["paths"].isArray()) { + paths_.emplace_back("/"); + } + if (config["separator"].isString()) { + separator_ = config["separator"].asString(); } if (config["unit"].isString()) { unit_ = config["unit"].asString(); @@ -17,76 +34,108 @@ waybar::modules::Disk::Disk(const std::string& id, const Json::Value& config) } auto waybar::modules::Disk::update() -> void { - struct statvfs /* { - unsigned long f_bsize; // filesystem block size - unsigned long f_frsize; // fragment size - fsblkcnt_t f_blocks; // size of fs in f_frsize units - fsblkcnt_t f_bfree; // # free blocks - fsblkcnt_t f_bavail; // # free blocks for unprivileged users - fsfilcnt_t f_files; // # inodes - fsfilcnt_t f_ffree; // # free inodes - fsfilcnt_t f_favail; // # free inodes for unprivileged users - unsigned long f_fsid; // filesystem ID - unsigned long f_flag; // mount flags - unsigned long f_namemax; // maximum filename length - }; */ - stats; - int err = statvfs(path_.c_str(), &stats); + std::string tooltip_label; + std::string label = header_; - /* Conky options - fs_bar - Bar that shows how much space is used - fs_free - Free space on a file system - fs_free_perc - Free percentage of space - fs_size - File system size - fs_used - File system used space - */ + bool had_valid_disk = false; - if (err != 0) { - event_box_.hide(); - return; - } + for (size_t i = 0; i < paths_.size(); ++i) { + const auto& path = paths_[i]; - float specific_free, specific_used, specific_total, divisor; + struct statvfs /* { + unsigned long f_bsize; // filesystem block size + unsigned long f_frsize; // fragment size + fsblkcnt_t f_blocks; // size of fs in f_frsize units + fsblkcnt_t f_bfree; // # free blocks + fsblkcnt_t f_bavail; // # free blocks for unprivileged users + fsfilcnt_t f_files; // # inodes + fsfilcnt_t f_ffree; // # free inodes + fsfilcnt_t f_favail; // # free inodes for unprivileged users + unsigned long f_fsid; // filesystem ID + unsigned long f_flag; // mount flags + unsigned long f_namemax; // maximum filename length + }; */ + stats; - divisor = calc_specific_divisor(unit_); - specific_free = (stats.f_bavail * stats.f_frsize) / divisor; - specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor; - specific_total = (stats.f_blocks * stats.f_frsize) / divisor; + int err = statvfs(path.c_str(), &stats); - auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true); - auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true); - auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true); - auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks; + /* Conky options + fs_bar - Bar that shows how much space is used + fs_free - Free space on a file system + fs_free_perc - Free percentage of space + fs_size - File system size + fs_used - File system used space + */ - auto format = format_; - auto state = getState(percentage_used); - if (!state.empty() && config_["format-" + state].isString()) { - format = config_["format-" + state].asString(); - } + if (err != 0) { + spdlog::warn("Disk: statvfs failed for path '{}' (errno={})", path, errno); + continue; + } - if (format.empty()) { - event_box_.hide(); - } else { - event_box_.show(); - label_.set_markup(fmt::format( - fmt::runtime(format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), - fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used), - fmt::arg("percentage_used", percentage_used), fmt::arg("total", total), - fmt::arg("path", path_), fmt::arg("specific_free", specific_free), - fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total))); - } + float specific_free, specific_used, specific_total, divisor; + + divisor = calc_specific_divisor(unit_); + specific_free = (stats.f_bavail * stats.f_frsize) / divisor; + specific_used = ((stats.f_blocks - stats.f_bfree) * stats.f_frsize) / divisor; + specific_total = (stats.f_blocks * stats.f_frsize) / divisor; + + auto free = pow_format(stats.f_bavail * stats.f_frsize, "B", true); + auto used = pow_format((stats.f_blocks - stats.f_bfree) * stats.f_frsize, "B", true); + auto total = pow_format(stats.f_blocks * stats.f_frsize, "B", true); + auto percentage_used = (stats.f_blocks - stats.f_bfree) * 100 / stats.f_blocks; + + std::string disk_format = format_; + auto state = getState(percentage_used); + if (!state.empty() && config_["format-" + state].isString()) { + disk_format = config_["format-" + state].asString(); + } + + if (!disk_format.empty()) { + if (had_valid_disk) { + label += separator_; + } + + label += fmt::format( + fmt::runtime(disk_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), + fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), + fmt::arg("used", used), fmt::arg("percentage_used", percentage_used), + fmt::arg("total", total), fmt::arg("path", path), + fmt::arg("specific_free", specific_free), fmt::arg("specific_used", specific_used), + fmt::arg("specific_total", specific_total)); + } - if (tooltipEnabled()) { std::string tooltip_format = "{used} used out of {total} on {path} ({percentage_used}%)"; if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_text(fmt::format( - fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), - fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used), - fmt::arg("percentage_used", percentage_used), fmt::arg("total", total), - fmt::arg("path", path_), fmt::arg("specific_free", specific_free), - fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total))); + + if (!tooltip_format.empty()) { + if (had_valid_disk) { + tooltip_label += "\n"; + } + + tooltip_label += fmt::format( + fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks, + fmt::arg("free", free), + fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), + fmt::arg("used", used), fmt::arg("percentage_used", percentage_used), + fmt::arg("total", total), fmt::arg("path", path), + fmt::arg("specific_free", specific_free), fmt::arg("specific_used", specific_used), + fmt::arg("specific_total", specific_total)); + } + + had_valid_disk = true; + } + if (had_valid_disk) { + event_box_.show(); + } else { + event_box_.hide(); + } + + label_.set_markup(label); + + if (tooltipEnabled() && !tooltip_label.empty()) { + label_.set_tooltip_text(tooltip_label); } // Call parent update ALabel::update(); @@ -109,7 +158,7 @@ float waybar::modules::Disk::calc_specific_divisor(std::string divisor) { return 1000.0 * 1000.0 * 1000.0 * 1000.0; } else if (divisor == "TiB") { return 1024.0 * 1024.0 * 1024.0 * 1024.0; - } else { // default to Bytes if it is anything that we don't recongnise + } else { // default to Bytes if it is anything that we don't recognise return 1.0; } -} \ No newline at end of file +} From 3655d735254411b44031cddc9eb460f4e923220f Mon Sep 17 00:00:00 2001 From: anton <1711399+AntGarSil@users.noreply.github.com> Date: Sun, 8 Feb 2026 00:44:14 +0000 Subject: [PATCH 122/416] Ignore Tray entries by service name and when dbus properties load --- include/modules/sni/host.hpp | 5 ++++ include/modules/sni/tray.hpp | 3 +++ src/modules/sni/host.cpp | 48 ++++++++++++++++++++++++++++++++++++ src/modules/sni/tray.cpp | 38 +++++++++++++++++++++++++++- 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index 6c62ac31..5b475708 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -15,9 +15,13 @@ namespace waybar::modules::SNI { class Host { public: Host(const std::size_t id, const Json::Value&, const Bar&, + const std::vector&, const std::function&)>&, const std::function&)>&); ~Host(); + + void checkIgnoreList(const std::vector& ignore_list, + const std::function&)>& on_remove); private: void busAcquired(const Glib::RefPtr&, Glib::ustring); @@ -41,6 +45,7 @@ class Host { SnWatcher* watcher_ = nullptr; const Json::Value& config_; const Bar& bar_; + const std::vector ignore_list_; const std::function&)> on_add_; const std::function&)> on_remove_; }; diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 5f12d7f2..9288ec9a 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -19,11 +19,14 @@ class Tray : public AModule { private: void onAdd(std::unique_ptr& item); void onRemove(std::unique_ptr& item); + void checkIgnoreList(std::unique_ptr* item); + std::vector parseIgnoreList(const Json::Value& config); static inline std::size_t nb_hosts_ = 0; bool show_passive_ = false; Gtk::Box box_; SNI::Watcher::singleton watcher_; + std::vector ignore_list_; SNI::Host host_; }; diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 54faa16c..e9893465 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -7,6 +7,7 @@ namespace waybar::modules::SNI { Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, + const std::vector& ignore_list, const std::function&)>& on_add, const std::function&)>& on_remove) : bus_name_("org.kde.StatusNotifierHost-" + std::to_string(getpid()) + "-" + @@ -16,6 +17,7 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, sigc::mem_fun(*this, &Host::busAcquired))), config_(config), bar_(bar), + ignore_list_(ignore_list), on_add_(on_add), on_remove_(on_remove) {} @@ -33,6 +35,39 @@ Host::~Host() { g_clear_object(&watcher_); } +void Host::checkIgnoreList(const std::vector& ignore_list, + const std::function&)>& on_remove) { + spdlog::debug("Host::checkIgnoreList - checking {} items against {} patterns", items_.size(), ignore_list.size()); + + for (auto it = items_.begin(); it != items_.end(); ) { + auto& item = *it; + spdlog::debug(" Checking item: bus_name='{}', category='{}', icon_name='{}', title='{}'", + item->bus_name, item->category, item->icon_name, item->title); + + bool should_remove = false; + + for (const auto& ignored : ignore_list) { + if (item->bus_name.find(ignored) != std::string::npos || + item->category.find(ignored) != std::string::npos || + item->icon_name.find(ignored) != std::string::npos || + item->id.find(ignored) != std::string::npos || + item->title.find(ignored) != std::string::npos) { + spdlog::info("Host: Ignoring item bus_name='{}', category='{}', icon_name='{}', title='{}' - matched pattern '{}'", + item->bus_name, item->category, item->icon_name, item->title, ignored); + on_remove(item); + should_remove = true; + break; + } + } + + if (should_remove) { + it = items_.erase(it); + } else { + ++it; + } + } +} + void Host::busAcquired(const Glib::RefPtr& conn, Glib::ustring name) { watcher_id_ = Gio::DBus::watch_name(conn, "org.kde.StatusNotifierWatcher", sigc::mem_fun(*this, &Host::nameAppeared), @@ -100,7 +135,9 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { g_signal_connect(host->watcher_, "item-unregistered", G_CALLBACK(&Host::itemUnregistered), data); auto items = sn_watcher_dup_registered_items(host->watcher_); if (items != nullptr) { + spdlog::info("Host: Found {} pre-registered SNI items", g_strv_length(items)); for (uint32_t i = 0; items[i] != nullptr; i += 1) { + spdlog::info("Host: Processing pre-registered item: {}", items[i]); host->addRegisteredItem(items[i]); } } @@ -109,7 +146,9 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { void Host::itemRegistered(SnWatcher* watcher, const gchar* service, gpointer data) { auto host = static_cast(data); + spdlog::info("Host::itemRegistered called with service: {}", service); host->addRegisteredItem(service); + //host->checkIgnoreList(host->ignore_list_, std::bind(&Host::itemUnregistered, host, std::placeholders::_1, std::placeholders::_2, data)); } void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer data) { @@ -133,12 +172,21 @@ std::tuple Host::getBusNameAndObjectPath(const std::st } void Host::addRegisteredItem(std::string service) { + // Check service string directly before parsing + for (const auto& ignored : ignore_list_) { + if (service.find(ignored) != std::string::npos) { + spdlog::info("Host: Ignoring service '{}' - matched pattern '{}'", service, ignored); + return; + } + } std::string bus_name, object_path; std::tie(bus_name, object_path) = getBusNameAndObjectPath(service); + spdlog::debug("SNI item registered: bus_name={}, object_path={}, full_service={}", bus_name, object_path, service); auto it = std::find_if(items_.begin(), items_.end(), [&bus_name, &object_path](const auto& item) { return bus_name == item->bus_name && object_path == item->object_path; }); if (it == items_.end()) { + spdlog::debug("Adding SNI item: {}", bus_name); items_.emplace_back(new Item(bus_name, object_path, config_, bar_)); on_add_(items_.back()); } diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 34a3c05f..248ddf8e 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -8,11 +8,29 @@ namespace waybar::modules::SNI { +std::vector Tray::parseIgnoreList(const Json::Value& config) { + std::vector ignore_list; + if (config["ignore-list"].isArray()) { + spdlog::info("Tray: Found ignore-list with {} items", config["ignore-list"].size()); + for (const auto& item : config["ignore-list"]) { + if (item.isString()) { + ignore_list.push_back(item.asString()); + spdlog::info("Tray: Adding to ignore list: {}", item.asString()); + } + } + } else { + spdlog::info("Tray: No ignore-list configured"); + } + return ignore_list; +} + Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) : AModule(config, "tray", id), box_(bar.orientation, 0), watcher_(SNI::Watcher::getInstance()), - host_(nb_hosts_, config, bar, std::bind(&Tray::onAdd, this, std::placeholders::_1), + ignore_list_(parseIgnoreList(config)), + host_(nb_hosts_, config, bar, ignore_list_, + std::bind(&Tray::onAdd, this, std::placeholders::_1), std::bind(&Tray::onRemove, this, std::placeholders::_1)) { box_.set_name("tray"); event_box_.add(box_); @@ -33,12 +51,24 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) dp.emit(); } +void Tray::checkIgnoreList(std::unique_ptr* item_ptr) { + // Delegate to Host's checkIgnoreList method + host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); +} + void Tray::onAdd(std::unique_ptr& item) { + spdlog::info("Tray::onAdd - item bus_name='{}', category='{}', icon_name='{}', title='{}'", + item->bus_name, item->category, item->icon_name, item->title); + if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) { box_.pack_end(item->event_box); } else { box_.pack_start(item->event_box); } + + spdlog::debug("Tray::onAdd deferred check - checking ignore list"); + host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); + dp.emit(); } @@ -48,6 +78,12 @@ void Tray::onRemove(std::unique_ptr& item) { } auto Tray::update() -> void { + // Check if any items should be ignored now that properties have loaded + if (!ignore_list_.empty()) { + spdlog::debug("Tray::update() - checking ignore list"); + host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); + } + // Show tray only when items are available std::vector children = box_.get_children(); if (show_passive_) { From e67b230dbe5d0f58546a0b052e2c752f5dc53586 Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Tue, 10 Feb 2026 01:52:59 +0530 Subject: [PATCH 123/416] fix: prioritize plugged status over default when battery is full --- src/modules/battery.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index c706eb3c..bb1bb6fa 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -685,13 +685,20 @@ auto waybar::modules::Battery::update() -> void { if (status == "Unknown") { status = getAdapterStatus(capacity); } + auto state = getState(capacity, true); + bool adapter_online = false; + if (!adapter_.empty()) { + std::ifstream(adapter_ / "online") >> adapter_online; + } + if (status == "Full" && adapter_online) { + status = "Plugged"; + } auto status_pretty = status; puts(status.c_str()); // Transform to lowercase and replace space with dash std::ranges::transform(status.begin(), status.end(), status.begin(), [](char ch) { return ch == ' ' ? '-' : std::tolower(ch); }); auto format = format_; - auto state = getState(capacity, true); processEvents(state, status, capacity); setBarClass(state); auto time_remaining_formatted = formatTimeRemaining(time_remaining); From 736c05335020d5e8165806d0bbcba9094021f91f Mon Sep 17 00:00:00 2001 From: eonphi <259135569+eonphi@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:37:35 +0100 Subject: [PATCH 124/416] feat(hyprland/workspaces): grouping icons --- include/modules/hyprland/workspaces.hpp | 4 ++ src/modules/hyprland/workspace.cpp | 51 ++++++++++++++++++++++--- src/modules/hyprland/workspaces.cpp | 10 +++++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 8bf88888..a240549a 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -61,6 +61,8 @@ class Workspaces : public AModule, public EventHandler { std::string getRewrite(std::string window_class, std::string window_title); std::string& getWindowSeparator() { return m_formatWindowSeparator; } + auto windowRewriteGroupThreshold() const -> int { return m_windowRewriteGroupThreshold; } + auto const& getWindowRewriteGroupFormat() const { return m_windowRewriteGroupFormat; } bool isWorkspaceIgnored(std::string const& workspace_name); bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } @@ -172,6 +174,8 @@ class Workspaces : public AModule, public EventHandler { util::RegexCollection m_windowRewriteRules; bool m_anyWindowRewriteRuleUsesTitle = false; std::string m_formatWindowSeparator; + int m_windowRewriteGroupThreshold = 0; + std::string m_windowRewriteGroupFormat = "{icon}×{count}"; bool m_withIcon; uint64_t m_monitorId; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 87933ac0..01da287e 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -245,15 +246,53 @@ void Workspace::update(const std::string& workspace_icon) { // need to compute this if enableTaskbar() is true if (!m_workspaceManager.enableTaskbar()) { auto windowSeparator = m_workspaceManager.getWindowSeparator(); + auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold(); - bool isNotFirst = false; + if (groupThreshold > 0) { + // Build ordered counts of each unique icon (including singular ones when threshold set to 1) + std::vector> iconCounts; + for (const auto& window_repr : m_windowMap) { + auto it = std::ranges::find_if(iconCounts, [&](const auto& p) { + return p.first == window_repr.repr_rewrite; + }); + if (it != iconCounts.end()) { + it->second++; + } else { + iconCounts.emplace_back(window_repr.repr_rewrite, 1); + } + } - for (const auto& window_repr : m_windowMap) { - if (isNotFirst) { - windows.append(windowSeparator); + // Format the group string + auto groupFormat = m_workspaceManager.getWindowRewriteGroupFormat(); + bool isNotFirst = false; + for (const auto& [icon, count] : iconCounts) { + if (count >= groupThreshold) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + try { + windows.append(fmt::format(fmt::runtime(groupFormat), + fmt::arg("icon", icon), + fmt::arg("count", count))); + } catch (const fmt::format_error& e) { + spdlog::warn("Formatting window-rewrite-group-format error: {}", e.what()); + windows.append(icon); + } + } else { + for (int i = 0; i < count; ++i) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + windows.append(icon); + } + } + } + } else { + // Not grouping icons + bool isNotFirst = false; + for (const auto& window_repr : m_windowMap) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + windows.append(window_repr.repr_rewrite); } - isNotFirst = true; - windows.append(window_repr.repr_rewrite); } } diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8765d78b..bec503e9 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -649,6 +649,16 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { populateSortByConfig(config); populateIgnoreWorkspacesConfig(config); populateFormatWindowSeparatorConfig(config); + + const auto& groupThreshold = config["window-rewrite-group-threshold"]; + if (groupThreshold.isInt()) { + m_windowRewriteGroupThreshold = groupThreshold.asInt(); + } + const auto& groupFormat = config["window-rewrite-group-format"]; + if (groupFormat.isString()) { + m_windowRewriteGroupFormat = groupFormat.asString(); + } + populateWindowRewriteConfig(config); if (withWindows) { From 55fa8cf185e64041dfaea09048c46a4479acc359 Mon Sep 17 00:00:00 2001 From: BBaoVanC Date: Mon, 2 Feb 2026 14:03:51 -0600 Subject: [PATCH 125/416] Make CssReloadHelper watch CSS new style on appearance change If you have reload_style_on_change enabled, and use dynamic appearance styling (style-dark.css and style-light.css), then changing the appearance doesn't update the files that are being watched for reload. Add a method to CssReloadHelper to change the CSS file being watched, and also provide the CSS file as a parameter to the callback so setupCss can be called on the right file when the file watcher triggers. --- include/util/css_reload_helper.hpp | 6 ++++-- src/client.cpp | 3 ++- src/util/css_reload_helper.cpp | 11 +++++++++-- test/utils/css_reload_helper.cpp | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/include/util/css_reload_helper.hpp b/include/util/css_reload_helper.hpp index 032b2382..e198a5b0 100644 --- a/include/util/css_reload_helper.hpp +++ b/include/util/css_reload_helper.hpp @@ -14,12 +14,14 @@ struct pollfd; namespace waybar { class CssReloadHelper { public: - CssReloadHelper(std::string cssFile, std::function callback); + CssReloadHelper(std::string cssFile, std::function callback); virtual ~CssReloadHelper() = default; virtual void monitorChanges(); + virtual void changeCssFile(const std::string& newCssFile); + protected: std::vector parseImports(const std::string& cssFile); @@ -42,7 +44,7 @@ class CssReloadHelper { private: std::string m_cssFile; - std::function m_callback; + std::function m_callback; std::vector>> m_fileMonitors; }; diff --git a/src/client.cpp b/src/client.cpp index 103b093f..ce178f0a 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -289,9 +289,10 @@ int waybar::Client::main(int argc, char* argv[]) { } m_cssFile = getStyle(style_opt); setupCss(m_cssFile); - m_cssReloadHelper = std::make_unique(m_cssFile, [&]() { setupCss(m_cssFile); }); + m_cssReloadHelper = std::make_unique(m_cssFile, [&](const std::string& css_file) { setupCss(css_file); }); portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) { auto css_file = getStyle(style_opt, appearance); + m_cssReloadHelper->changeCssFile(css_file); setupCss(css_file); }); diff --git a/src/util/css_reload_helper.cpp b/src/util/css_reload_helper.cpp index 06381d60..dc686a4c 100644 --- a/src/util/css_reload_helper.cpp +++ b/src/util/css_reload_helper.cpp @@ -23,7 +23,8 @@ namespace { const std::regex IMPORT_REGEX(R"(@import\s+(?:url\()?(?:"|')([^"')]+)(?:"|')\)?;)"); } -waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, std::function callback) +waybar::CssReloadHelper::CssReloadHelper(std::string cssFile, + std::function callback) : m_cssFile(std::move(cssFile)), m_callback(std::move(callback)) {} std::string waybar::CssReloadHelper::getFileContents(const std::string& filename) { @@ -88,6 +89,12 @@ void waybar::CssReloadHelper::monitorChanges() { } } +void waybar::CssReloadHelper::changeCssFile(const std::string& newCssFile) { + m_fileMonitors.clear(); + m_cssFile = newCssFile; + monitorChanges(); +} + void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr const& file, Glib::RefPtr const& other_type, Gio::FileMonitorEvent event_type) { @@ -95,7 +102,7 @@ void waybar::CssReloadHelper::handleFileChange(Glib::RefPtr const& fi // fire for one if (event_type == Gio::FileMonitorEvent::FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { spdlog::debug("Reloading style, file changed: {}", file->get_path()); - m_callback(); + m_callback(m_cssFile); } } diff --git a/test/utils/css_reload_helper.cpp b/test/utils/css_reload_helper.cpp index f3888a83..0ba0bde4 100644 --- a/test/utils/css_reload_helper.cpp +++ b/test/utils/css_reload_helper.cpp @@ -10,7 +10,7 @@ class CssReloadHelperTest : public waybar::CssReloadHelper { public: - CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this]() { callback(); }) {} + CssReloadHelperTest() : CssReloadHelper("/tmp/waybar_test.css", [this](const std::string&) { callback(); }) {} void callback() { m_callbackCounter++; } From a6a65cb6b79e8871ae82a7451c1cbc5cdb96774e Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Tue, 10 Feb 2026 18:59:56 +0530 Subject: [PATCH 126/416] feat: add support for configurable first day of the week in calendar --- src/modules/clock.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 5fe5407b..b71b2615 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -509,6 +509,18 @@ using deleting_unique_ptr = std::unique_ptr>; // Computations done similarly to Linux cal utility. auto waybar::modules::Clock::first_day_of_week() -> weekday { + const auto firstdow = config_[kCldPlaceholder]["first-day-of-week"]; + if (firstdow.isInt()) { + const int firstDay = firstdow.asInt(); + if (!(firstDay >= 0 && firstDay <= 6)) { + spdlog::warn( + "Clock calender configuration first-day-of-week = {0} must be in range [0, 6]. Default " + "value is used instead", + firstDay); + } else { + return weekday{static_cast(firstDay)}; + } + } if (iso8601Calendar_) { return Monday; } From 15d33f232b9b9707b6c9c4f38e365f9075c19be7 Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Tue, 24 Feb 2026 12:29:57 +0530 Subject: [PATCH 127/416] docs: update man page --- man/waybar-clock.5.scd | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index b4b5d5b7..15c70b2c 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -138,6 +138,11 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ When enabled, the calendar follows the ISO 8601 standard: weeks begin on Monday, and the first week of the year is numbered 1. The default week format is '{:%V}'. +|[ *first-day-of-week* +:[ integer +:[ +:[ The first day of the week, where 0 is Sunday and 6 is Saturday. + When not set, the first day of the week is determined by the locale settings. 3. Addressed by *clock: calendar: format* [- *Option* @@ -207,11 +212,12 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe "format-alt": "{:%A, %B %d, %Y (%R)}  ", "tooltip-format": "{calendar}", "calendar": { - "mode" : "year", - "mode-mon-col" : 3, - "weeks-pos" : "right", - "on-scroll" : 1, - "on-click-right": "mode", + "mode" : "year", + "mode-mon-col" : 3, + "weeks-pos" : "right", + "first-day-of-week": 1, + "on-scroll" : 1, + "on-click-right" : "mode", "format": { "months": "{}", "days": "{}", From f2cfcb85dd4b15cf2e3495387465785b4a92a66d Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Tue, 24 Feb 2026 19:21:13 +0530 Subject: [PATCH 128/416] feat(keyboard-state): add custom icons in format-icons --- include/modules/keyboard_state.hpp | 3 +- src/modules/keyboard_state.cpp | 62 ++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/include/modules/keyboard_state.hpp b/include/modules/keyboard_state.hpp index db7f2550..c9f1afac 100644 --- a/include/modules/keyboard_state.hpp +++ b/include/modules/keyboard_state.hpp @@ -36,8 +36,7 @@ class KeyboardState : public AModule { std::string capslock_format_; std::string scrolllock_format_; const std::chrono::seconds interval_; - std::string icon_locked_; - std::string icon_unlocked_; + std::unordered_map> key_icon_states_; std::string devices_path_; struct libinput* libinput_; diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index a2207fdd..577f52bd 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -78,6 +78,44 @@ auto supportsLockStates(const libevdev* dev) -> bool { libevdev_has_event_code(dev, EV_LED, LED_SCROLLL); } +auto isCommonFormatIcons(const Json::Value& config) -> bool { + return config["format-icons"].isObject() && (config["format-icons"]["locked"].isString() || config["format-icons"]["unlocked"].isString()); +} + +auto keyStateToIcons(const Json::Value& config) -> std::unordered_map> { + std::unordered_map> key_icon_states; + std::vector default_icons = {"unlocked", "locked"}; + + if (isCommonFormatIcons(config)) { + std::vector icons = { + config["format-icons"]["unlocked"].isString() ? config["format-icons"]["unlocked"].asString() : "unlocked", + config["format-icons"]["locked"].isString() ? config["format-icons"]["locked"].asString() : "locked", + }; + key_icon_states["Lock"] = icons; + return key_icon_states; + } + + bool found_any = false; + for (const auto& key : std::vector{"numlock", "capslock", "scrolllock"}) { + std::string map_key = key.substr(0, key.length() - 4); + map_key[0] = std::toupper(map_key[0]); + if (config["format-icons"].isObject() && config["format-icons"][key].isObject()) { + std::string unlocked = config["format-icons"][key]["unlocked"].isString() ? config["format-icons"][key]["unlocked"].asString() : "unlocked"; + std::string locked = config["format-icons"][key]["locked"].isString() ? config["format-icons"][key]["locked"].asString() : "locked"; + key_icon_states[map_key] = {unlocked, locked}; + found_any = true; + } + } + + 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; +} + waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& bar, const Json::Value& config) : AModule(config, "keyboard-state", id, false, !config["disable-scroll"].asBool()), @@ -98,12 +136,7 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& : "{name} {icon}"), interval_( std::chrono::seconds(config_["interval"].isUInt() ? config_["interval"].asUInt() : 1)), - icon_locked_(config_["format-icons"]["locked"].isString() - ? config_["format-icons"]["locked"].asString() - : "locked"), - icon_unlocked_(config_["format-icons"]["unlocked"].isString() - ? config_["format-icons"]["unlocked"].asString() - : "unlocked"), + key_icon_states_(keyStateToIcons(config_)), devices_path_("/dev/input/"), libinput_(nullptr), libinput_devices_({}) { @@ -290,7 +323,7 @@ auto waybar::modules::KeyboardState::update() -> void { bool state; Gtk::Label& label; const std::string& format; - const char* name; + const std::string name; } label_states[] = { {(bool)numl, numlock_label_, numlock_format_, "Num"}, {(bool)capsl, capslock_label_, capslock_format_, "Caps"}, @@ -298,8 +331,21 @@ auto waybar::modules::KeyboardState::update() -> void { }; for (auto& label_state : label_states) { std::string text; + std::string map_key = isCommonFormatIcons(config_) ? "Lock" : label_state.name; + + if (key_icon_states_.find(map_key) == key_icon_states_.end()) { + spdlog::warn("keyboard-state: Missing icon configuration for '{}'", map_key); + continue; + } + + auto& icons = key_icon_states_[map_key]; + if (icons.size() < 2) { + spdlog::warn("keyboard-state: Invalid icon vector size for '{}'", map_key); + continue; + } + text = fmt::format(fmt::runtime(label_state.format), - fmt::arg("icon", label_state.state ? icon_locked_ : icon_unlocked_), + fmt::arg("icon", label_state.state ? icons[1] : icons[0]), fmt::arg("name", label_state.name)); label_state.label.set_markup(text); if (label_state.state) { From a64876b928b86377c61529113e2a95e6b7c1f60d Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Tue, 24 Feb 2026 19:37:01 +0530 Subject: [PATCH 129/416] docs: update man page --- man/waybar-keyboard-state.5.scd | 37 +++++++++++++++++++++++++++++++-- src/modules/keyboard_state.cpp | 21 +++++++++++++------ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/man/waybar-keyboard-state.5.scd b/man/waybar-keyboard-state.5.scd index 79498414..7babe336 100644 --- a/man/waybar-keyboard-state.5.scd +++ b/man/waybar-keyboard-state.5.scd @@ -26,7 +26,10 @@ You must be a member of the input group to use this module. *format-icons*: ++ typeof: object ++ default: {"locked": "locked", "unlocked": "unlocked"} ++ - Based on the keyboard state, the corresponding icon gets selected. The same set of icons is used for number, caps, and scroll lock, but the icon is selected from the set independently for each. See *icons*. + Based on the keyboard state, the corresponding icon gets selected. Supports two syntaxes: + - Common format-icons: "locked" and "unlocked" keys apply to all lock types. + - Per-lock-type format-icons: per-lock-type objects with "numlock", "capslock", "scrolllock" keys, each containing "locked" and "unlocked" icons. + See *icons*. *numlock*: ++ typeof: bool ++ @@ -68,15 +71,21 @@ You must be a member of the input group to use this module. The following *format-icons* can be set. +## Common format-icons for all lock types: - *locked*: Will be shown when the keyboard state is locked. Default "locked". -- *unlocked*: Will be shown when the keyboard state is not locked. Default "unlocked" +- *unlocked*: Will be shown when the keyboard state is not locked. Default "unlocked". + +## Per-lock-type format-icons: +- *numlock*, *capslock*, *scrolllock*: Object containing "locked" and "unlocked" keys for lock-type-specific icons. Defaults to {"locked": "locked", "unlocked": "unlocked"} for each lock type. # EXAMPLE: +## Common format-icons for all lock types: ``` "keyboard-state": { "numlock": true, "capslock": true, + "scrolllock": true, "format": "{name} {icon}", "format-icons": { "locked": "", @@ -85,6 +94,30 @@ The following *format-icons* can be set. } ``` +## Per-lock-type format-icons: +``` +"keyboard-state": { + "numlock": true, + "capslock": true, + "scrolllock": true, + "format": "{name} {icon}", + "format-icons": { + "numlock": { + "locked": "1", + "unlocked": "0" + }, + "capslock": { + "locked": "A", + "unlocked": "a" + }, + "scrolllock": { + "locked": "S", + "unlocked": "s" + } + } +} +``` + # STYLE - *#keyboard-state* diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 577f52bd..49cf863f 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -79,17 +79,22 @@ auto supportsLockStates(const libevdev* dev) -> bool { } auto isCommonFormatIcons(const Json::Value& config) -> bool { - return config["format-icons"].isObject() && (config["format-icons"]["locked"].isString() || config["format-icons"]["unlocked"].isString()); + return config["format-icons"].isObject() && (config["format-icons"]["locked"].isString() || + config["format-icons"]["unlocked"].isString()); } -auto keyStateToIcons(const Json::Value& config) -> std::unordered_map> { +auto keyStateToIcons(const Json::Value& config) + -> std::unordered_map> { std::unordered_map> key_icon_states; std::vector default_icons = {"unlocked", "locked"}; if (isCommonFormatIcons(config)) { std::vector icons = { - config["format-icons"]["unlocked"].isString() ? config["format-icons"]["unlocked"].asString() : "unlocked", - config["format-icons"]["locked"].isString() ? config["format-icons"]["locked"].asString() : "locked", + config["format-icons"]["unlocked"].isString() + ? config["format-icons"]["unlocked"].asString() + : "unlocked", + config["format-icons"]["locked"].isString() ? config["format-icons"]["locked"].asString() + : "locked", }; key_icon_states["Lock"] = icons; return key_icon_states; @@ -100,8 +105,12 @@ auto keyStateToIcons(const Json::Value& config) -> std::unordered_map Date: Mon, 2 Mar 2026 19:10:55 -0500 Subject: [PATCH 130/416] Add support for optional CSS classes --- src/modules/clock.cpp | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 5fe5407b..88f2ec69 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -187,6 +187,52 @@ auto waybar::modules::Clock::update() -> void { } m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(now)); + + // Pango doesn't support CSS classes but to continue using it while staying + // backwards compatible this approach uses post-posting to replace fake + // classes with attributes Pango does understand. + // + // The benefit of this approach is anyone using the original styling choices + // can continue doing that and folks can optionally opt into using classes. + // + // It's also forwards compatible to where if this implemention ever changes + // to support proper classes anyone using them will continue to work. + auto context = label_.get_style_context(); + + const std::vector> calendar_class_map = { + {"calendar-today", "today"}, + {"calendar-days", "days"}, + {"calendar-weeks", "weeks"}, + {"calendar-weekdays", "weekdays"}, + {"calendar-months", "months"} + }; + + for (const auto& [css_class, json_key] : calendar_class_map) { + try { + context->add_class(css_class); + Gdk::RGBA color = context->get_color(); + context->remove_class(css_class); + + std::string hex = fmt::format("#{:02x}{:02x}{:02x}", + static_cast(color.get_red() * 255), + static_cast(color.get_green() * 255), + static_cast(color.get_blue() * 255)); + + std::string search = "class='" + json_key + "'"; + std::string replace = "color='" + hex + "'"; + + for (size_t pos = 0; (pos = m_tlpText_.find(search, pos)) != std::string::npos; pos += replace.length()) { + m_tlpText_.replace(pos, search.length(), replace); + } + } catch (const Glib::Error& e) { + spdlog::warn("Clock: Failed to fetch CSS color for {}: {}", css_class, e.what().raw()); + continue; + } catch (...) { + // Catch-all for any other weirdness. + continue; + } + } + m_tooltip_->set_markup(m_tlpText_); label_.trigger_tooltip_query(); } From c85f2a23de79198fd67b46b09d0fe9a90cfb4797 Mon Sep 17 00:00:00 2001 From: Nick Janetakis Date: Mon, 2 Mar 2026 21:29:15 -0500 Subject: [PATCH 131/416] Add readability improvements and optimizations Replace the inner loop with regex_replace. We're already using this elsewhere and the performance difference isn't worth the loss of human readability. Use static const to pre-calc the search string. --- src/modules/clock.cpp | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 88f2ec69..ec8b71e7 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -199,31 +199,26 @@ auto waybar::modules::Clock::update() -> void { // to support proper classes anyone using them will continue to work. auto context = label_.get_style_context(); - const std::vector> calendar_class_map = { - {"calendar-today", "today"}, - {"calendar-days", "days"}, - {"calendar-weeks", "weeks"}, - {"calendar-weekdays", "weekdays"}, - {"calendar-months", "months"} + static const std::vector> calendar_class_map = { + {"calendar-today", "class='today'"}, + {"calendar-days", "class='days'"}, + {"calendar-weeks", "class='weeks'"}, + {"calendar-weekdays", "class='weekdays'"}, + {"calendar-months", "class='months'"} }; - for (const auto& [css_class, json_key] : calendar_class_map) { + for (const auto& [css_class, search_str] : calendar_class_map) { try { context->add_class(css_class); - Gdk::RGBA color = context->get_color(); + const Gdk::RGBA color = context->get_color(); context->remove_class(css_class); - std::string hex = fmt::format("#{:02x}{:02x}{:02x}", - static_cast(color.get_red() * 255), + const std::string replace_str = fmt::format("color='#{:02x}{:02x}{:02x}'", + static_cast(color.get_red() * 255, static_cast(color.get_green() * 255), static_cast(color.get_blue() * 255)); - std::string search = "class='" + json_key + "'"; - std::string replace = "color='" + hex + "'"; - - for (size_t pos = 0; (pos = m_tlpText_.find(search, pos)) != std::string::npos; pos += replace.length()) { - m_tlpText_.replace(pos, search.length(), replace); - } + m_tlpText_ = std::regex_replace(m_tlpText_, std::regex(search_str), replace_str); } catch (const Glib::Error& e) { spdlog::warn("Clock: Failed to fetch CSS color for {}: {}", css_class, e.what().raw()); continue; From 5de21f4a078ebc251605b35ab430bb59bb1d356f Mon Sep 17 00:00:00 2001 From: jpk Date: Thu, 5 Mar 2026 09:04:21 +0100 Subject: [PATCH 132/416] Added support for hostname based module addition --- include/util/hosts_check.hpp | 8 ++++++++ meson.build | 1 + src/bar.cpp | 14 ++++++++++++++ src/util/hosts_check.cpp | 19 +++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 include/util/hosts_check.hpp create mode 100644 src/util/hosts_check.cpp diff --git a/include/util/hosts_check.hpp b/include/util/hosts_check.hpp new file mode 100644 index 00000000..a927f535 --- /dev/null +++ b/include/util/hosts_check.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +namespace waybar::util { +bool valid_host(const Json::Value& config); +} // namespace waybar::util diff --git a/meson.build b/meson.build index db9407eb..fdab7932 100644 --- a/meson.build +++ b/meson.build @@ -181,6 +181,7 @@ src_files = files( 'src/util/prepare_for_sleep.cpp', 'src/util/ustring_clen.cpp', 'src/util/sanitize_str.cpp', + 'src/util/hosts_check.cpp', 'src/util/rewrite_string.cpp', 'src/util/gtk_icon.cpp', 'src/util/icon_loader.cpp', diff --git a/src/bar.cpp b/src/bar.cpp index 6a78707e..6f2f36a6 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -10,6 +10,7 @@ #include "group.hpp" #include "util/enum.hpp" #include "util/kill_signal.hpp" +#include "util/hosts_check.hpp" #ifdef HAVE_SWAY #include "modules/sway/bar.hpp" @@ -531,6 +532,19 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, for (const auto& name : module_list) { try { auto ref = name.asString(); + const Json::Value* module_config = nullptr; + if (config.isMember(ref)) { + module_config = &config[ref]; + } else { + auto hash_pos = ref.find('#'); + if (hash_pos != std::string::npos) { + std::string ref_base = ref.substr(0, hash_pos); + if (config.isMember(ref_base)) { + module_config = &config[ref_base]; + } + } + } + if (module_config && !waybar::util::valid_host(*module_config)) continue; AModule* module; if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) { diff --git a/src/util/hosts_check.cpp b/src/util/hosts_check.cpp new file mode 100644 index 00000000..27260cd3 --- /dev/null +++ b/src/util/hosts_check.cpp @@ -0,0 +1,19 @@ +#include + +#include "util/hosts_check.hpp" + +namespace waybar::util { +// replaces ``<>&"'`` with their encoded counterparts +bool valid_host(const Json::Value& config) { + if (config.isMember("hosts") && config["hosts"].isArray()) { + auto hostname = Glib::get_host_name(); + for (const auto& host : config["hosts"]) { + if (host.asString() == hostname) { + return true; + } + } + return false; + } + return true; +} +} // namespace waybar::util From 49db7adc0c782a59a65ec99212590472ade16587 Mon Sep 17 00:00:00 2001 From: jpk Date: Thu, 5 Mar 2026 12:28:48 +0100 Subject: [PATCH 133/416] Per host checks --- meson.build | 1 + src/bar.cpp | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/meson.build b/meson.build index fdab7932..58b49b34 100644 --- a/meson.build +++ b/meson.build @@ -183,6 +183,7 @@ src_files = files( 'src/util/sanitize_str.cpp', 'src/util/hosts_check.cpp', 'src/util/rewrite_string.cpp', + 'src/util/hosts_check.cpp', 'src/util/gtk_icon.cpp', 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', diff --git a/src/bar.cpp b/src/bar.cpp index 6f2f36a6..04ad8362 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -9,6 +9,7 @@ #include "factory.hpp" #include "group.hpp" #include "util/enum.hpp" +#include "util/hosts_check.hpp" #include "util/kill_signal.hpp" #include "util/hosts_check.hpp" @@ -532,6 +533,7 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, for (const auto& name : module_list) { try { auto ref = name.asString(); + const Json::Value* module_config = nullptr; if (config.isMember(ref)) { module_config = &config[ref]; @@ -544,7 +546,8 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, } } } - if (module_config && !waybar::util::valid_host(*module_config)) continue; + if ((module_config != nullptr) && !waybar::util::valid_host(*module_config)) continue; + AModule* module; if (ref.compare(0, 6, "group/") == 0 && ref.size() > 6) { From 88e1f85341f2a5c26ab963ab0fad0ed44076e859 Mon Sep 17 00:00:00 2001 From: jpk Date: Thu, 5 Mar 2026 13:29:14 +0100 Subject: [PATCH 134/416] Simplified logic --- include/util/hosts_check.hpp | 1 - src/bar.cpp | 16 +++------------- src/util/hosts_check.cpp | 18 ++++++++---------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/include/util/hosts_check.hpp b/include/util/hosts_check.hpp index a927f535..4179f633 100644 --- a/include/util/hosts_check.hpp +++ b/include/util/hosts_check.hpp @@ -1,7 +1,6 @@ #pragma once #include -#include namespace waybar::util { bool valid_host(const Json::Value& config); diff --git a/src/bar.cpp b/src/bar.cpp index 04ad8362..4b619de2 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "client.hpp" @@ -11,7 +12,6 @@ #include "util/enum.hpp" #include "util/hosts_check.hpp" #include "util/kill_signal.hpp" -#include "util/hosts_check.hpp" #ifdef HAVE_SWAY #include "modules/sway/bar.hpp" @@ -534,19 +534,9 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, try { auto ref = name.asString(); - const Json::Value* module_config = nullptr; - if (config.isMember(ref)) { - module_config = &config[ref]; - } else { - auto hash_pos = ref.find('#'); - if (hash_pos != std::string::npos) { - std::string ref_base = ref.substr(0, hash_pos); - if (config.isMember(ref_base)) { - module_config = &config[ref_base]; - } - } + if (config[ref].isMember("hosts") && !waybar::util::valid_host(config[ref])) { + continue; } - if ((module_config != nullptr) && !waybar::util::valid_host(*module_config)) continue; AModule* module; diff --git a/src/util/hosts_check.cpp b/src/util/hosts_check.cpp index 27260cd3..1769dd78 100644 --- a/src/util/hosts_check.cpp +++ b/src/util/hosts_check.cpp @@ -1,18 +1,16 @@ -#include - #include "util/hosts_check.hpp" +#include +#include + namespace waybar::util { -// replaces ``<>&"'`` with their encoded counterparts bool valid_host(const Json::Value& config) { if (config.isMember("hosts") && config["hosts"].isArray()) { - auto hostname = Glib::get_host_name(); - for (const auto& host : config["hosts"]) { - if (host.asString() == hostname) { - return true; - } - } - return false; + const auto hostname = Glib::get_host_name(); + + if (!std::ranges::any_of(config["hosts"].begin(), config["hosts"].end(), + [&](const auto& h) { return h.asString() == hostname; })) + return false; } return true; } From 78bd6c8b87cd5c6bd987d1fa7563be48bfbc2b7a Mon Sep 17 00:00:00 2001 From: jpk Date: Thu, 5 Mar 2026 14:15:29 +0100 Subject: [PATCH 135/416] Remove duplicate from source include --- meson.build | 1 - 1 file changed, 1 deletion(-) diff --git a/meson.build b/meson.build index 58b49b34..9f533aa8 100644 --- a/meson.build +++ b/meson.build @@ -181,7 +181,6 @@ src_files = files( 'src/util/prepare_for_sleep.cpp', 'src/util/ustring_clen.cpp', 'src/util/sanitize_str.cpp', - 'src/util/hosts_check.cpp', 'src/util/rewrite_string.cpp', 'src/util/hosts_check.cpp', 'src/util/gtk_icon.cpp', From ab5b1a6cd41d10da238e6ab5da32d798aa6ba04c Mon Sep 17 00:00:00 2001 From: 7FM <41307817+7FM@users.noreply.github.com> Date: Fri, 6 Mar 2026 00:37:42 +0100 Subject: [PATCH 136/416] pulseaudio: add target option to control source (microphone) volume The pulseaudio/slider module had partial support for a "target" config option but changeVolume() in the audio backend was hardcoded to only modify sink volume. This adds full source volume control: - Add AudioTarget enum (Sink/Source) to audio_backend - Extend changeVolume() overloads with AudioTarget parameter - Store and use pa_cvolume for source (with mono channel default) - Add sourceVolumeModifyCb for source volume change confirmation - Add "target" config option to the pulseaudio module for scroll control - Document "target" in both pulseaudio and pulseaudio-slider man pages Usage: "pulseaudio/slider#in": { "target": "source" } "pulseaudio#in": { "target": "source" } --- include/modules/pulseaudio.hpp | 1 + include/modules/pulseaudio_slider.hpp | 7 +- include/util/audio_backend.hpp | 13 +++- man/waybar-pulseaudio-slider.5.scd | 16 ++++- man/waybar-pulseaudio.5.scd | 5 ++ src/modules/pulseaudio.cpp | 6 +- src/modules/pulseaudio_slider.cpp | 18 +++--- src/util/audio_backend.cpp | 93 ++++++++++++++++++++------- 8 files changed, 116 insertions(+), 43 deletions(-) diff --git a/include/modules/pulseaudio.hpp b/include/modules/pulseaudio.hpp index eead664f..80b2059b 100644 --- a/include/modules/pulseaudio.hpp +++ b/include/modules/pulseaudio.hpp @@ -22,6 +22,7 @@ class Pulseaudio : public ALabel { const std::vector getPulseIcon() const; std::shared_ptr backend = nullptr; + util::AudioTarget target = util::AudioTarget::Sink; }; } // namespace waybar::modules diff --git a/include/modules/pulseaudio_slider.hpp b/include/modules/pulseaudio_slider.hpp index 3ef44684..c6c06567 100644 --- a/include/modules/pulseaudio_slider.hpp +++ b/include/modules/pulseaudio_slider.hpp @@ -6,11 +6,6 @@ #include "util/audio_backend.hpp" namespace waybar::modules { -enum class PulseaudioSliderTarget { - Sink, - Source, -}; - class PulseaudioSlider : public ASlider { public: PulseaudioSlider(const std::string&, const Json::Value&); @@ -21,7 +16,7 @@ class PulseaudioSlider : public ASlider { private: std::shared_ptr backend = nullptr; - PulseaudioSliderTarget target = PulseaudioSliderTarget::Sink; + util::AudioTarget target = util::AudioTarget::Sink; }; } // namespace waybar::modules \ No newline at end of file diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp index 3737ae26..3e8e0403 100644 --- a/include/util/audio_backend.hpp +++ b/include/util/audio_backend.hpp @@ -14,6 +14,11 @@ namespace waybar::util { +enum class AudioTarget { + Sink, + Source, +}; + class AudioBackend { private: static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); @@ -22,12 +27,14 @@ class AudioBackend { static void sourceInfoCb(pa_context*, const pa_source_info* i, int, void* data); static void serverInfoCb(pa_context*, const pa_server_info*, void*); static void volumeModifyCb(pa_context*, int, void*); + static void sourceVolumeModifyCb(pa_context*, int, void*); void connectContext(); pa_threaded_mainloop* mainloop_; pa_mainloop_api* mainloop_api_; pa_context* context_; pa_cvolume pa_volume_; + pa_cvolume pa_source_volume_; // SINK uint32_t sink_idx_{0}; @@ -67,8 +74,10 @@ class AudioBackend { AudioBackend(std::function on_updated_cb, private_constructor_tag tag); ~AudioBackend(); - void changeVolume(uint16_t volume, uint16_t min_volume = 0, uint16_t max_volume = 100); - void changeVolume(ChangeType change_type, double step = 1, uint16_t max_volume = 100); + void changeVolume(uint16_t volume, uint16_t min_volume = 0, uint16_t max_volume = 100, + AudioTarget target = AudioTarget::Sink); + void changeVolume(ChangeType change_type, double step = 1, uint16_t max_volume = 100, + AudioTarget target = AudioTarget::Sink); void setIgnoredSinks(const Json::Value& config); diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index 0271e7c5..6001d929 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -32,16 +32,28 @@ The volume can be controlled by dragging the slider across the bar or clicking o default: false ++ Enables this module to consume all left over space dynamically. +*target*: ++ + typeof: string ++ + default: sink ++ + The audio target to control. Can be either `sink` (output/speakers) or `source` (input/microphone). + # EXAMPLES ``` "modules-right": [ - "pulseaudio/slider", + "pulseaudio/slider#out", + "pulseaudio/slider#in", ], -"pulseaudio/slider": { +"pulseaudio/slider#out": { "min": 0, "max": 100, "orientation": "horizontal" +}, +"pulseaudio/slider#in": { + "min": 0, + "max": 100, + "orientation": "horizontal", + "target": "source" } ``` diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index f555fd4d..6347291b 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -135,6 +135,11 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c default: false ++ Enables this module to consume all left over space dynamically. +*target*: ++ + typeof: string ++ + default: sink ++ + The audio target to control when scrolling. Can be either `sink` (output/speakers) or `source` (input/microphone). + # FORMAT REPLACEMENTS *{desc}*: Pulseaudio port's description, for bluetooth it'll be the device name. diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index 4d63ff3c..5b4c0be6 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -7,6 +7,10 @@ waybar::modules::Pulseaudio::Pulseaudio(const std::string& id, const Json::Value backend = util::AudioBackend::getInstance([this] { this->dp.emit(); }); backend->setIgnoredSinks(config_["ignored-sinks"]); + + if (config_["target"].isString() && config_["target"].asString() == "source") { + target = util::AudioTarget::Source; + } } bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll* e) { @@ -33,7 +37,7 @@ bool waybar::modules::Pulseaudio::handleScroll(GdkEventScroll* e) { ? util::ChangeType::Increase : util::ChangeType::Decrease; - backend->changeVolume(change_type, step, max_volume); + backend->changeVolume(change_type, step, max_volume, target); return true; } diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp index bf85584e..c789cea0 100644 --- a/src/modules/pulseaudio_slider.cpp +++ b/src/modules/pulseaudio_slider.cpp @@ -10,16 +10,16 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con if (config_["target"].isString()) { std::string target = config_["target"].asString(); if (target == "sink") { - this->target = PulseaudioSliderTarget::Sink; + this->target = util::AudioTarget::Sink; } else if (target == "source") { - this->target = PulseaudioSliderTarget::Source; + this->target = util::AudioTarget::Source; } } } void PulseaudioSlider::update() { switch (target) { - case PulseaudioSliderTarget::Sink: + case util::AudioTarget::Sink: if (backend->getSinkMuted()) { scale_.set_value(min_); } else { @@ -27,7 +27,7 @@ void PulseaudioSlider::update() { } break; - case PulseaudioSliderTarget::Source: + case util::AudioTarget::Source: if (backend->getSourceMuted()) { scale_.set_value(min_); } else { @@ -41,13 +41,13 @@ void PulseaudioSlider::onValueChanged() { bool is_mute = false; switch (target) { - case PulseaudioSliderTarget::Sink: + case util::AudioTarget::Sink: if (backend->getSinkMuted()) { is_mute = true; } break; - case PulseaudioSliderTarget::Source: + case util::AudioTarget::Source: if (backend->getSourceMuted()) { is_mute = true; } @@ -65,18 +65,18 @@ void PulseaudioSlider::onValueChanged() { // If the sink/source is mute, but the user clicked the slider, unmute it! else { switch (target) { - case PulseaudioSliderTarget::Sink: + case util::AudioTarget::Sink: backend->toggleSinkMute(false); break; - case PulseaudioSliderTarget::Source: + case util::AudioTarget::Source: backend->toggleSourceMute(false); break; } } } - backend->changeVolume(volume, min_, max_); + backend->changeVolume(volume, min_, max_, target); } } // namespace waybar::modules \ No newline at end of file diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 4087e096..041e3d5e 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -24,8 +24,9 @@ AudioBackend::AudioBackend(std::function on_updated_cb, private_construc source_volume_(0), source_muted_(false), on_updated_cb_(std::move(on_updated_cb)) { - // Initialize pa_volume_ with safe defaults + // Initialize pa_volume_ and pa_source_volume_ with safe defaults pa_cvolume_init(&pa_volume_); + pa_cvolume_init(&pa_source_volume_); mainloop_ = pa_threaded_mainloop_new(); if (mainloop_ == nullptr) { throw std::runtime_error("pa_mainloop_new() failed."); @@ -155,6 +156,19 @@ void AudioBackend::volumeModifyCb(pa_context* c, int success, void* data) { } } +void AudioBackend::sourceVolumeModifyCb(pa_context* c, int success, void* data) { + auto* backend = static_cast(data); + if (success != 0) { + if ((backend->context_ != nullptr) && + pa_context_get_state(backend->context_) == PA_CONTEXT_READY) { + pa_context_get_source_info_by_index(backend->context_, backend->source_idx_, sourceInfoCb, + data); + } + } else { + spdlog::debug("Source volume modification failed"); + } +} + /* * Called when the requested sink information is ready. */ @@ -234,6 +248,11 @@ void AudioBackend::sourceInfoCb(pa_context* /*context*/, const pa_source_info* i void* data) { auto* backend = static_cast(data); if (i != nullptr && backend->default_source_name_ == i->name) { + if (pa_cvolume_valid(&i->volume) != 0) { + backend->pa_source_volume_ = i->volume; + } else { + pa_cvolume_init(&backend->pa_source_volume_); + } auto source_volume = static_cast(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM}; backend->source_volume_ = std::round(source_volume * 100.0F); backend->source_idx_ = i->index; @@ -259,25 +278,31 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo pa_context_get_source_info_list(context, sourceInfoCb, data); } -void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) { +void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume, + AudioTarget target) { // Early return if context is not ready if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { spdlog::error("PulseAudio context not ready"); return; } + bool is_source = target == AudioTarget::Source; + + // Select the appropriate stored volume structure + auto& ref_volume = is_source ? pa_source_volume_ : pa_volume_; + // Prepare volume structure pa_cvolume pa_volume; pa_cvolume_init(&pa_volume); // Use existing volume structure if valid, otherwise create a safe default - if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) { - pa_volume = pa_volume_; + if ((pa_cvolume_valid(&ref_volume) != 0) && (pa_channels_valid(ref_volume.channels) != 0)) { + pa_volume = ref_volume; } else { - // Set stereo as a safe default - pa_volume.channels = 2; - spdlog::debug("Using default stereo volume structure"); + // Mono for sources (microphones), stereo for sinks + pa_volume.channels = is_source ? 1 : 2; + spdlog::debug("Using default volume structure ({} channels)", pa_volume.channels); } // Set the volume safely @@ -291,39 +316,56 @@ void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t m // Apply the volume change pa_threaded_mainloop_lock(mainloop_); - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (is_source) { + pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb, + this); + } else { + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + } pa_threaded_mainloop_unlock(mainloop_); } -void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume) { +void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume, + AudioTarget target) { // Early return if context is not ready if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { spdlog::error("PulseAudio context not ready"); return; } + bool is_source = target == AudioTarget::Source; + + // Select the appropriate stored volume structure and current volume + auto& ref_volume = is_source ? pa_source_volume_ : pa_volume_; + auto current_volume = is_source ? source_volume_ : volume_; + // Prepare volume structure pa_cvolume pa_volume; pa_cvolume_init(&pa_volume); // Use existing volume structure if valid, otherwise create a safe default - if ((pa_cvolume_valid(&pa_volume_) != 0) && (pa_channels_valid(pa_volume_.channels) != 0)) { - pa_volume = pa_volume_; + if ((pa_cvolume_valid(&ref_volume) != 0) && (pa_channels_valid(ref_volume.channels) != 0)) { + pa_volume = ref_volume; } else { - // Set stereo as a safe default - pa_volume.channels = 2; - spdlog::debug("Using default stereo volume structure"); + // Mono for sources (microphones), stereo for sinks + pa_volume.channels = is_source ? 1 : 2; + spdlog::debug("Using default volume structure ({} channels)", pa_volume.channels); // Initialize all channels to current volume level double volume_tick = static_cast(PA_VOLUME_NORM) / 100; - pa_volume_t vol = volume_ * volume_tick; + pa_volume_t vol = current_volume * volume_tick; for (uint8_t i = 0; i < pa_volume.channels; i++) { pa_volume.values[i] = vol; } // No need to continue with volume change if we had to create a new structure pa_threaded_mainloop_lock(mainloop_); - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (is_source) { + pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, + sourceVolumeModifyCb, this); + } else { + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + } pa_threaded_mainloop_unlock(mainloop_); return; } @@ -333,10 +375,10 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma pa_volume_t change; max_volume = std::min(max_volume, static_cast(PA_VOLUME_UI_MAX)); - if (change_type == ChangeType::Increase && volume_ < max_volume) { + if (change_type == ChangeType::Increase && current_volume < max_volume) { // Calculate how much to increase - if (volume_ + step > max_volume) { - change = round((max_volume - volume_) * volume_tick); + if (current_volume + step > max_volume) { + change = round((max_volume - current_volume) * volume_tick); } else { change = round(step * volume_tick); } @@ -345,10 +387,10 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma for (uint8_t i = 0; i < pa_volume.channels; i++) { pa_volume.values[i] = std::min(pa_volume.values[i] + change, PA_VOLUME_MAX); } - } else if (change_type == ChangeType::Decrease && volume_ > 0) { + } else if (change_type == ChangeType::Decrease && current_volume > 0) { // Calculate how much to decrease - if (volume_ - step < 0) { - change = round(volume_ * volume_tick); + if (current_volume - step < 0) { + change = round(current_volume * volume_tick); } else { change = round(step * volume_tick); } @@ -364,7 +406,12 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma // Apply the volume change pa_threaded_mainloop_lock(mainloop_); - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (is_source) { + pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb, + this); + } else { + pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + } pa_threaded_mainloop_unlock(mainloop_); } From 8a3a78676f9fb46889acc7fe379a4a596b55f529 Mon Sep 17 00:00:00 2001 From: eonphi <259135569+eonphi@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:01:47 +0100 Subject: [PATCH 137/416] docs(hyprland/workspaces): icon grouping man page entries --- man/waybar-hyprland-workspaces.5.scd | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 5284ce99..b495052d 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -41,6 +41,19 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. The separator to be used between windows in a workspace. ++ This setting is ignored if *workspace-taskbar.enable* is set to true. +*window-rewrite-group-threshold*: ++ + typeof: int ++ + default: 0 ++ + When a workspace contains at least this many windows with the same rewrite result, they are collapsed into a single one using *window-rewrite-group-format*. ++ + Set to 0 to disable grouping. ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + +*window-rewrite-group-format*: ++ + typeof: string ++ + default: "{icon}×{count}" ++ + The format used to represent a group of collapsed windows. Available placeholders are {icon} (the icon being grouped) and {count} (how many windows share it). ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + *workspace-taskbar*: ++ typeof: object ++ Contains settings for the workspace taskbar, an alternative mode for the workspaces module which displays the window icons as images instead of text. From c62e987dbd4f8e402f625cdd61cfc2c9dd3c376f Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:22:34 +0530 Subject: [PATCH 138/416] Update waybar.5.scd.in --- man/waybar.5.scd.in | 1 + 1 file changed, 1 insertion(+) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 624798ed..7223daec 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -168,6 +168,7 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( # MODULE FORMAT You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). +Note: Tooltip appearance in Waybar is controlled globally via CSS (e.g., `tooltip { ... }`). It is currently not possible to apply tooltip styles per module. e.g. From ba23ef6487c09aa9b378510a7d2a667259181a6c Mon Sep 17 00:00:00 2001 From: Jakob Sjudin Date: Fri, 20 Mar 2026 11:25:27 +0100 Subject: [PATCH 139/416] Add .empty css class to group if it is empty --- include/group.hpp | 1 + src/group.cpp | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/include/group.hpp b/include/group.hpp index d5cbb289..e263a35a 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -26,6 +26,7 @@ class Group : public AModule { bool is_first_widget = true; bool is_drawer = false; bool click_to_reveal = false; + bool empty_if_drawer_empty = false; std::string add_class_to_drawer_children; bool handleMouseEnter(GdkEventCrossing* const& ev) override; bool handleMouseLeave(GdkEventCrossing* const& ev) override; diff --git a/src/group.cpp b/src/group.cpp index acd7bd42..6b38e220 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -30,6 +30,7 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& box{vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0}, revealer_box{vertical ? Gtk::ORIENTATION_VERTICAL : Gtk::ORIENTATION_HORIZONTAL, 0} { box.set_name(name_); + box.get_style_context()->add_class("empty"); if (!id.empty()) { box.get_style_context()->add_class(id); } @@ -67,6 +68,9 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& const bool start_expanded = (drawer_config["start-expanded"].isBool() ? drawer_config["start-expanded"].asBool() : false); + empty_if_drawer_empty = (drawer_config["empty-if-drawer-empty"].isBool() + ? drawer_config["empty-if-drawer-empty"].asBool() + : false); auto transition_type = getPreferredTransitionType(vertical); @@ -129,7 +133,44 @@ bool Group::handleToggle(GdkEventButton* const& e) { } auto Group::update() -> void { - // noop + bool has_visible_child = false; + bool has_visible_drawer_child = false; + + if (is_drawer) { + for (auto* rev_child : revealer_box.get_children()) { + if (rev_child->get_visible()) { + has_visible_drawer_child = true; + break; + } + } + } + + if (is_drawer && empty_if_drawer_empty) { + has_visible_child = has_visible_drawer_child; + } else { + for (auto* child : box.get_children()) { + if (child == &revealer) { + if (has_visible_drawer_child) { + has_visible_child = true; + break; + } + } else if (child->get_visible()) { + has_visible_child = true; + break; + } + } + } + + auto style = box.get_style_context(); + if (has_visible_child) { + if (style->has_class("empty")) { + style->remove_class("empty"); + } + } else { + if (!style->has_class("empty")) { + style->add_class("empty"); + } + } } bool Group::handleScroll(GdkEventScroll* e) { @@ -147,6 +188,8 @@ void Group::addWidget(Gtk::Widget& widget) { } is_first_widget = false; + widget.property_visible().signal_changed().connect(sigc::mem_fun(*this, &Group::update)); + update(); } Group::operator Gtk::Widget&() { return event_box_; } From e07a2e91976c9265003aae6fa58b8049b221c381 Mon Sep 17 00:00:00 2001 From: Jakob Sjudin Date: Fri, 20 Mar 2026 11:40:36 +0100 Subject: [PATCH 140/416] Update manpages --- man/waybar.5.scd.in | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 8b923eef..e573d7d6 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -374,6 +374,11 @@ A group may hide all but one element, showing them only on mouse hover. In order Defines the direction of the transition animation. If true, the hidden elements will slide from left to right. If false, they will slide from right to left. When the bar is vertical, it reads as top-to-bottom. +*empty-if-drawer-empty*: ++ + typeof: bool ++ + default: false ++ + Defines whether the group should be assigned the "empty" CSS class when all modules inside the drawer are hidden or missing. If false, the group will not receive the "empty" class as long as the leader module remains visible. This allows users to dynamically style or hide the entire group using CSS when its drawer contents are unavailable. + ``` "group/power": { "orientation": "inherit", @@ -390,6 +395,19 @@ A group may hide all but one element, showing them only on mouse hover. In order ] }, ``` +To hide the power group if it is empty, apply to #power and all its children: +``` +#power.empty, +#power.empty * { + min-width: 0px; + min-height: 0px; + padding: 0px; + margin: 0px; + border: none; + font-size: 0px; + opacity: 0; +} +``` # SUPPORTED MODULES From 86d516c74de9ba80960e1ff8d6eadc2cebd85375 Mon Sep 17 00:00:00 2001 From: Thomas Beyer Date: Fri, 20 Mar 2026 11:43:17 +0100 Subject: [PATCH 141/416] clock: add weeks-numbering option for configurable CW calculation The iso8601 flag previously controlled both the calendar grid layout and the week number format as a coupled boolean. This adds a new weeks-numbering option (iso/monday/sunday) that explicitly overrides just the week number calculation method, independent of iso8601 and locale settings. Existing behaviour is fully preserved when the option is not set. Co-Authored-By: Claude Sonnet 4.6 --- include/modules/clock.hpp | 4 +++- man/waybar-clock.5.scd | 12 ++++++++++-- src/modules/clock.cpp | 35 +++++++++++++++++++++++++++++------ 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index c3548063..b04a6e80 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -12,6 +12,7 @@ const std::string kOrdPlaceholder{"ordinal_date"}; enum class CldMode { MONTH, YEAR }; enum class WS { LEFT, RIGHT, HIDDEN }; +enum class WeekNumbering { LOCALE, ISO, MONDAY, SUNDAY }; class Clock final : public ALabel { public: @@ -50,7 +51,8 @@ class Clock final : public ALabel { std::string cldMonCached_; // calendar Month mode. Cached calendar date::day cldBaseDay_{0}; // calendar Cached day. Is used when today is changing(midnight) std::string cldText_{""}; // calendar text to print - bool iso8601Calendar_{false}; // whether the calendar is in ISO8601 + bool iso8601Calendar_{false}; // whether the calendar is in ISO8601 + WeekNumbering weekNumbering_{WeekNumbering::LOCALE}; // week number calculation method CldMode cldMode_{CldMode::MONTH}; auto get_calendar(const date::year_month_day& today, const date::year_month_day& ymd, const date::time_zone* tz) -> const std::string; diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 1b13dcb7..f7d86fa2 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -138,6 +138,13 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ When enabled, the calendar follows the ISO 8601 standard: weeks begin on Monday, and the first week of the year is numbered 1. The default week format is '{:%V}'. +|[ *weeks-numbering* +:[ string +:[ +:[ Override the week number calculation method, independent of *iso8601* and + locale settings. Possible values: *iso* (ISO 8601, {:%V}), *monday* + (Monday-based, {:%W}), *sunday* (Sunday-based, {:%U}). When not set, the + method is derived from *iso8601* or the locale. 3. Addressed by *clock: calendar: format* [- *Option* @@ -155,8 +162,9 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe |[ *weeks* :[ string :[ *{:%U}* -:[ Format is applied to week numbers. When weekday format is not provided then - is used default format: '{:%W}' when week starts with Monday, '{:%U}' otherwise +:[ Format is applied to week numbers. The *{}* placeholder is replaced with the + format determined by *weeks-numbering* (if set), otherwise by *iso8601* or the + locale: '{:%V}' for ISO 8601, '{:%W}' when week starts with Monday, '{:%U}' otherwise |[ *weekdays* :[ string :[ diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 5fe5407b..869c6f5b 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -81,6 +81,20 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) iso8601Calendar_ = config_[kCldPlaceholder]["iso8601"].asBool(); } + if (config_[kCldPlaceholder]["weeks-numbering"].isString()) { + const std::string wn{config_[kCldPlaceholder]["weeks-numbering"].asString()}; + const std::map wnModes{{"iso", WeekNumbering::ISO}, + {"monday", WeekNumbering::MONDAY}, + {"sunday", WeekNumbering::SUNDAY}}; + if (wnModes.find(wn) != wnModes.end()) + weekNumbering_ = wnModes.at(wn); + else + spdlog::warn( + "Clock calendar configuration weeks-numbering \"{}\" is not recognized. " + "Locale default is used instead", + wn); + } + if (config_[kCldPlaceholder]["weeks-pos"].isString()) { if (config_[kCldPlaceholder]["weeks-pos"].asString() == "left") cldWPos_ = WS::LEFT; if (config_[kCldPlaceholder]["weeks-pos"].asString() == "right") cldWPos_ = WS::RIGHT; @@ -104,18 +118,27 @@ waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config) cldBaseDay_ = year_month_day{floor(local_time)}.day(); } else fmtMap_.insert({3, "{}"}); + const auto weekFmt = [this]() -> std::string { + switch (weekNumbering_) { + case WeekNumbering::ISO: + return "{:%V}"; + case WeekNumbering::MONDAY: + return "{:%W}"; + case WeekNumbering::SUNDAY: + return "{:%U}"; + default: + return iso8601Calendar_ ? "{:%V}" + : ((first_day_of_week() == Monday) ? "{:%W}" : "{:%U}"); + } + }(); if (config_[kCldPlaceholder]["format"]["weeks"].isString() && cldWPos_ != WS::HIDDEN) { - const auto defaultFmt = - iso8601Calendar_ ? "{:%V}" : ((first_day_of_week() == Monday) ? "{:%W}" : "{:%U}"); fmtMap_.insert({4, std::regex_replace(config_[kCldPlaceholder]["format"]["weeks"].asString(), - std::regex("\\{\\}"), defaultFmt)}); + std::regex("\\{\\}"), weekFmt)}); Glib::ustring tmp{std::regex_replace(fmtMap_[4], std::regex("]+>|\\{.*\\}"), "")}; cldWnLen_ += tmp.size(); } else { if (cldWPos_ != WS::HIDDEN) { - const auto defaultFmt = - iso8601Calendar_ ? "{:%V}" : ((first_day_of_week() == Monday) ? "{:%W}" : "{:%U}"); - fmtMap_.insert({4, defaultFmt}); + fmtMap_.insert({4, weekFmt}); } else { cldWnLen_ = 0; } From d2aad467a7b888e3b0e65be6d44a258433822407 Mon Sep 17 00:00:00 2001 From: Visal Vijay <150381094+B2krobbery@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:12:36 +0530 Subject: [PATCH 142/416] Update waybar.5.scd.in --- man/waybar.5.scd.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 7223daec..80628285 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -168,7 +168,7 @@ The visual display elements for waybar use a CSS stylesheet, see *waybar-styles( # MODULE FORMAT You can use PangoMarkupFormat (See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html#PangoMarkupFormat). -Note: Tooltip appearance in Waybar is controlled globally via CSS (e.g., `tooltip { ... }`). It is currently not possible to apply tooltip styles per module. +Tooltip appearance is generally controlled globally via CSS and cannot be truly scoped per module. Some modules (such as the clock) provide limited workarounds for customization. e.g. From fff1495efb1ea4360e88ef89a70ffd47111d3f69 Mon Sep 17 00:00:00 2001 From: Skyfrei Date: Tue, 24 Mar 2026 20:12:30 +0100 Subject: [PATCH 143/416] adding a new keyword 'on-click-copy' that accepts a bool and copies to clipboard on true' --- include/ALabel.hpp | 1 + include/AModule.hpp | 3 ++- src/ALabel.cpp | 13 ++++++++++--- src/AModule.cpp | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/include/ALabel.hpp b/include/ALabel.hpp index b3852877..4b5ade12 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -26,6 +26,7 @@ class ALabel : public AModule { std::string default_format_; bool handleToggle(GdkEventButton* const& e) override; + void copyToClipboard(const std::string&); virtual std::string getState(uint8_t value, bool lesser = false); std::map submenus_; diff --git a/include/AModule.hpp b/include/AModule.hpp index a338ffe3..2961d512 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -78,7 +78,8 @@ class AModule : public IModule { {std::make_pair(9, GdkEventType::GDK_BUTTON_PRESS), "on-click-forward"}, {std::make_pair(9, GdkEventType::GDK_BUTTON_RELEASE), "on-click-forward-release"}, {std::make_pair(9, GdkEventType::GDK_2BUTTON_PRESS), "on-double-click-forward"}, - {std::make_pair(9, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click-forward"}}; + {std::make_pair(9, GdkEventType::GDK_3BUTTON_PRESS), "on-triple-click-forward"}, + {std::make_pair(10, GdkEventType::GDK_BUTTON_PRESS), "on-click-copy"}}; }; } // namespace waybar diff --git a/src/ALabel.cpp b/src/ALabel.cpp index b23926d8..f71526f9 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -116,9 +116,8 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st } submenus_[key] = GTK_MENU_ITEM(item); menuActionsMap_[key] = it->asString(); - g_signal_connect(submenus_[key], "activate", - G_CALLBACK(handleGtkMenuEvent), - (gpointer)g_strdup(menuActionsMap_[key].c_str())); + g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), + (gpointer)g_strdup(menuActionsMap_[key].c_str())); } g_object_unref(builder); } catch (std::runtime_error& e) { @@ -190,6 +189,10 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& return ""; } +void ALabel::copyToClipboard(const std::string& literal) { + Gtk::Clipboard::get()->set_text(literal); +} + bool waybar::ALabel::handleToggle(GdkEventButton* const& e) { if (config_["format-alt-click"].isUInt() && e->button == config_["format-alt-click"].asUInt()) { alt_ = !alt_; @@ -199,6 +202,10 @@ bool waybar::ALabel::handleToggle(GdkEventButton* const& e) { format_ = default_format_; } } + + if (config_["on-click-copy"].isBool() && config_["on-click-copy"].asBool()) { + copyToClipboard(label_.get_text()); + } return AModule::handleToggle(e); } diff --git a/src/AModule.cpp b/src/AModule.cpp index a5ba69d3..862aa38e 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -42,7 +42,7 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: std::find_if(eventMap_.cbegin(), eventMap_.cend(), [&config](const auto& eventEntry) { // True if there is any non-release type event return eventEntry.first.second != GdkEventType::GDK_BUTTON_RELEASE && - config[eventEntry.second].isString(); + (config[eventEntry.second].isString() || config[eventEntry.second].isBool()); }) != eventMap_.cend(); if (enable_click || hasUserEvents) { From 0332da0cef5080f31ca6a901a9ca3ea8e11da59f Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Wed, 25 Mar 2026 15:18:00 +0530 Subject: [PATCH 144/416] bar: warn on invalid height type in configuration --- src/bar.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/bar.cpp b/src/bar.cpp index 6a78707e..fe66b806 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -172,6 +172,10 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) right_.set_spacing(spacing); } + if (config.isMember("height") && !config["height"].isUInt()) { + spdlog::warn("Invalid type for 'height', expected unsigned integer"); + } + height_ = config["height"].isUInt() ? config["height"].asUInt() : 0; width_ = config["width"].isUInt() ? config["width"].asUInt() : 0; From a49990c2667a3d6ba7c1fa4c4607b47d4ce0491a Mon Sep 17 00:00:00 2001 From: Jolan Oubrier Date: Fri, 27 Mar 2026 18:31:05 +0100 Subject: [PATCH 145/416] fix: issue #4955 wrong behaviour of regex for sway/window 'rewrite' --- src/util/rewrite_string.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/rewrite_string.cpp b/src/util/rewrite_string.cpp index 3f6ae4ca..8c377d3f 100644 --- a/src/util/rewrite_string.cpp +++ b/src/util/rewrite_string.cpp @@ -18,7 +18,7 @@ std::string rewriteString(const std::string& value, const Json::Value& rules) { // malformated regexes will cause an exception. // in this case, log error and try the next rule. const std::regex rule{it.key().asString(), std::regex_constants::icase}; - if (std::regex_match(value, rule)) { + if (std::regex_match(res, rule)) { res = std::regex_replace(res, rule, it->asString()); } } catch (const std::regex_error& e) { From 39656deab3ebbdd6ddcc630a4a0ded71458138f1 Mon Sep 17 00:00:00 2001 From: g_goessel Date: Wed, 2 Jul 2025 20:36:14 +0200 Subject: [PATCH 146/416] feat: Add wifi network link bitrate value In order to obtain the RX and TX bitrates from the kernel netlink we need to open a second info socket to get the station data. The values are formatted like "1.9Gb/s" and then exposed as `rx_bitrate` and `tx_bitrate`. This code is closely following the `iw` implementation. --- include/modules/network.hpp | 6 +++ man/waybar-network.5.scd | 4 ++ src/modules/network.cpp | 102 ++++++++++++++++++++++++++++++++++-- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 3bc43b23..0d4fc0ca 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -17,6 +17,8 @@ #include "util/rfkill.hpp" #endif +#define ETH_ALEN 6 + enum ip_addr_pref : uint8_t { IPV4, IPV6, IPV4_6 }; namespace waybar::modules { @@ -34,6 +36,7 @@ class Network : public ALabel { static int handleEvents(struct nl_msg*, void*); static int handleEventsDone(struct nl_msg*, void*); static int handleScan(struct nl_msg*, void*); + static int handleStationGet(struct nl_msg *msg, void *data); void askForStateDump(void); @@ -57,6 +60,7 @@ class Network : public ALabel { struct sockaddr_nl nladdr_{0}; struct nl_sock* sock_{nullptr}; struct nl_sock* ev_sock_{nullptr}; + struct nl_sock* station_sock_{nullptr}; int efd_{-1}; int ev_fd_{-1}; int nl80211_id_{-1}; @@ -95,6 +99,8 @@ class Network : public ALabel { util::Rfkill rfkill_{RFKILL_TYPE_WLAN}; #endif float frequency_{0}; + uint32_t tx_bitrate_{0}; + uint32_t rx_bitrate_{0}; }; } // namespace waybar::modules diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index 9e146ac0..d089adff 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -196,6 +196,10 @@ Addressed by *network* *{bandwidthTotalBytes}*: Instant total speed in bytes/seconds. +*{tx_bitrate}*: Link transmit bitrate (e.g., 866.7 Mb/s). + +*{rx_bitrate}*: Link receive bitrate (e.g., 866.7 Mb/s). + *{icon}*: Icon, as defined in *format-icons*. # EXAMPLES diff --git a/src/modules/network.cpp b/src/modules/network.cpp index a39a5ed3..0ffaf2dd 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -145,6 +145,10 @@ waybar::modules::Network::~Network() { nl_close(sock_); nl_socket_free(sock_); } + if (station_sock_ != nullptr) { + nl_close(station_sock_); + nl_socket_free(station_sock_); + } } void waybar::modules::Network::createEventSocket() { @@ -203,6 +207,16 @@ void waybar::modules::Network::createInfoSocket() { if (nl80211_id_ < 0) { spdlog::warn("Can't resolve nl80211 interface"); } + + // Create and configure the station_sock_ for NL80211_CMD_GET_STATION + station_sock_ = nl_socket_alloc(); + if (genl_connect(station_sock_) != 0) { + throw std::runtime_error("Can't connect to station netlink socket"); + } + if (nl_socket_modify_cb(station_sock_, NL_CB_VALID, NL_CB_CUSTOM, handleStationGet, this) < 0) { + throw std::runtime_error("Can't set station callback"); + } + // nl80211_id_ is already resolved from the sock_ setup } void waybar::modules::Network::worker() { @@ -353,7 +367,9 @@ auto waybar::modules::Network::update() -> void { fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s")), + fmt::arg("rxBitrate", pow_format(rx_bitrate_, "b/s")), + fmt::arg("txBitrate", pow_format(tx_bitrate_, "b/s"))); if (text.compare(label_.get_label()) != 0) { label_.set_markup(text); if (text.empty()) { @@ -386,7 +402,9 @@ auto waybar::modules::Network::update() -> void { fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s")), + fmt::arg("rxBitrate", pow_format(rx_bitrate_, "b/s")), + fmt::arg("txBitrate", pow_format(tx_bitrate_, "b/s"))); if (label_.get_tooltip_text() != tooltip_text) { label_.set_tooltip_markup(tooltip_text); } @@ -892,6 +910,51 @@ int waybar::modules::Network::handleScan(struct nl_msg* msg, void* data) { return NL_OK; } +int waybar::modules::Network::handleStationGet(struct nl_msg* msg, void* data) { + auto net = static_cast(data); + auto gnlh = static_cast(nlmsg_data(nlmsg_hdr(msg))); + struct nlattr* tb[NL80211_ATTR_MAX + 1]; + + if (nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0), genlmsg_attrlen(gnlh, 0), + nullptr) < 0) { + return NL_SKIP; + } + + if (tb[NL80211_ATTR_STA_INFO] == nullptr) { + return NL_SKIP; + } + + struct nlattr* sinfo[NL80211_STA_INFO_MAX + 1]; + if (nla_parse_nested(sinfo, NL80211_STA_INFO_MAX, tb[NL80211_ATTR_STA_INFO], nullptr) != 0) { + return NL_SKIP; + } + + if (sinfo[NL80211_STA_INFO_TX_BITRATE] != nullptr) { + struct nlattr* tx_br_info[NL80211_RATE_INFO_MAX + 1]; + if (nla_parse_nested(tx_br_info, NL80211_RATE_INFO_MAX, sinfo[NL80211_STA_INFO_TX_BITRATE], + nullptr) == 0) { + if (tx_br_info[NL80211_RATE_INFO_BITRATE32] != nullptr) { + net->tx_bitrate_ = nla_get_u32(tx_br_info[NL80211_RATE_INFO_BITRATE32]) * pow(10, 5); + } else if (tx_br_info[NL80211_RATE_INFO_BITRATE] != nullptr) { + net->tx_bitrate_ = nla_get_u16(tx_br_info[NL80211_RATE_INFO_BITRATE]) * pow(10, 5); + } + } + } + + if (sinfo[NL80211_STA_INFO_RX_BITRATE] != nullptr) { + struct nlattr* rx_br_info[NL80211_RATE_INFO_MAX + 1]; + if (nla_parse_nested(rx_br_info, NL80211_RATE_INFO_MAX, sinfo[NL80211_STA_INFO_RX_BITRATE], + nullptr) == 0) { + if (rx_br_info[NL80211_RATE_INFO_BITRATE32] != nullptr) { + net->rx_bitrate_ = nla_get_u32(rx_br_info[NL80211_RATE_INFO_BITRATE32]) * pow(10, 5); + } else if (rx_br_info[NL80211_RATE_INFO_BITRATE] != nullptr) { + net->rx_bitrate_ = nla_get_u16(rx_br_info[NL80211_RATE_INFO_BITRATE]) * pow(10, 5); + } + } + } + return NL_OK; +} + void waybar::modules::Network::parseEssid(struct nlattr** bss) { if (bss[NL80211_BSS_INFORMATION_ELEMENTS] != nullptr) { auto ies = static_cast(nla_data(bss[NL80211_BSS_INFORMATION_ELEMENTS])); @@ -991,5 +1054,38 @@ auto waybar::modules::Network::getInfo() -> void { nlmsg_free(nl_msg); return; } - nl_send_sync(sock_, nl_msg); + int err = nl_send_sync(sock_, nl_msg); + if (err < 0) { + spdlog::warn("nl80211: nl_send_sync get_scan error {}", err); + // Proceeding here as get_scan might not be essential if we already have ifid_ + } + + // If connected to an AP, try to get station data for bitrate + if (ifid_ > 0 && !bssid_.empty() && nl80211_id_ >= 0) { + nl_msg = nlmsg_alloc(); + if (nl_msg == nullptr) { + return; + } + + // Convert BSSID string to uint8_t array + uint8_t bssid_mac[ETH_ALEN]; + if (sscanf(bssid_.c_str(), "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", &bssid_mac[0], &bssid_mac[1], + &bssid_mac[2], &bssid_mac[3], &bssid_mac[4], &bssid_mac[5]) == ETH_ALEN) { + if (genlmsg_put(nl_msg, NL_AUTO_PORT, NL_AUTO_SEQ, nl80211_id_, 0, 0, /* No DUMP flag */ + NL80211_CMD_GET_STATION, 0) == nullptr || + nla_put_u32(nl_msg, NL80211_ATTR_IFINDEX, ifid_) < 0 || + nla_put(nl_msg, NL80211_ATTR_MAC, ETH_ALEN, bssid_mac) < 0) { + nlmsg_free(nl_msg); + return; + } + // Use station_sock_ for NL80211_CMD_GET_STATION + err = nl_send_sync(station_sock_, nl_msg); + if (err < 0) { + spdlog::warn("nl80211: nl_send_sync get_station error {}", err); + } + } else { + spdlog::warn("nl80211: Failed to parse BSSID string: {}", bssid_); + nlmsg_free(nl_msg); + } + } } From aa3d82a1c05f1f17c32dbb5ea5e68f9d081a022d Mon Sep 17 00:00:00 2001 From: g_goessel Date: Wed, 2 Jul 2025 20:49:02 +0200 Subject: [PATCH 147/416] Add missing rx/tx_bitrate reset --- src/modules/network.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 0ffaf2dd..4504e499 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -497,6 +497,8 @@ void waybar::modules::Network::clearIface() { signal_strength_ = 0; signal_strength_app_.clear(); frequency_ = 0.0; + rx_bitrate_ = 0; + tx_bitrate_ = 0; } int waybar::modules::Network::handleEvents(struct nl_msg* msg, void* data) { From 34e9f2580b2612b86669923c89aa8612317c03b4 Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Thu, 2 Oct 2025 08:55:40 +0800 Subject: [PATCH 148/416] fix: Correct the error in converting network speed units --- subprojects/.wraplock | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 subprojects/.wraplock diff --git a/subprojects/.wraplock b/subprojects/.wraplock new file mode 100644 index 00000000..e69de29b From 2c2901e8f70a8391399c2f5b4d35aaaddb1566a7 Mon Sep 17 00:00:00 2001 From: vdermichev Date: Fri, 27 Mar 2026 23:34:34 +0100 Subject: [PATCH 149/416] hyprland/workspaces: expose workspace name as CSS class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable per-workspace styling by adding the workspace name as a CSS class to each workspace button. The name is sanitized to a valid CSS identifier: lowercased, non-alphanumeric characters collapsed to hyphens, digit-leading names prefixed with "ws-". Examples: "IDE" → .ide, "special:telegram" → .telegram, "1" → .ws-1 Allows users to style individual workspaces: #workspaces button.telegram { color: #54a0e0; } --- include/modules/hyprland/workspace.hpp | 1 + src/modules/hyprland/workspace.cpp | 40 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 1d80d331..fb06fc72 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -71,6 +71,7 @@ class Workspace { int m_id; std::string m_name; + std::string m_prevNameClass; std::string m_output; uint m_windows; bool m_isActive = false; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 21e7ef9b..02f03008 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -9,6 +10,33 @@ #include "util/command.hpp" #include "util/icon_loader.hpp" +namespace { +constexpr std::string_view kCssClassPrefix = "ws-"; + +// Convert a workspace name to a valid CSS class name. +// Lowercases, replaces non-alphanumeric runs with single hyphens, +// and prefixes digit-leading names (CSS classes can't start with a digit). +std::string sanitizeCssClass(const std::string& name) { + std::string result; + result.reserve(name.size() + kCssClassPrefix.size()); + for (auto c : name) { + auto uc = static_cast(c); + if (std::isalnum(uc)) { + result += static_cast(std::tolower(uc)); + } else if (!result.empty() && result.back() != '-') { + result += '-'; + } + } + if (!result.empty() && result.back() == '-') { + result.pop_back(); + } + if (!result.empty() && std::isdigit(static_cast(result.front()))) { + result.insert(0, kCssClassPrefix); + } + return result; +} +} // namespace + namespace waybar::modules::hyprland { Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, @@ -209,6 +237,7 @@ std::string& Workspace::selectIcon(std::map& icons_map return m_name; } + void Workspace::update(const std::string& workspace_icon) { if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) { m_button.hide(); @@ -240,6 +269,17 @@ void Workspace::update(const std::string& workspace_icon) { addOrRemoveClass(styleContext, isVisible(), "visible"); addOrRemoveClass(styleContext, m_workspaceManager.getBarOutput() == output(), "hosting-monitor"); + // Add workspace name as CSS class for per-workspace styling + if (!m_prevNameClass.empty()) { + styleContext->remove_class(m_prevNameClass); + m_prevNameClass.clear(); + } + auto nameClass = sanitizeCssClass(name()); + if (!nameClass.empty()) { + styleContext->add_class(nameClass); + m_prevNameClass = nameClass; + } + std::string windows; // Optimization: The {windows} substitution string is only possible if the taskbar is disabled, no // need to compute this if enableTaskbar() is true From 66cb828bf8379c52ca17a1ece7ac472197575bab Mon Sep 17 00:00:00 2001 From: OliMoli Date: Tue, 7 Apr 2026 14:02:10 +0200 Subject: [PATCH 150/416] Add CSS selector for expanded drawers --- src/group.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/group.cpp b/src/group.cpp index acd7bd42..be0fd0f1 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -95,11 +95,13 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& void Group::show_group() { box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); revealer.set_reveal_child(true); + box.get_style_context()->add_class("expanded"); } void Group::hide_group() { box.unset_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); revealer.set_reveal_child(false); + box.get_style_context()->remove_class("expanded"); } bool Group::handleMouseEnter(GdkEventCrossing* const& e) { From 7feb23c3dddbfc91b89b38643c2ab250a973c6bb Mon Sep 17 00:00:00 2001 From: OliMoli Date: Tue, 7 Apr 2026 16:29:59 +0200 Subject: [PATCH 151/416] Update manpage to mention the .expanded CSS class --- man/waybar.5.scd.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 8b923eef..c76755f4 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -374,6 +374,8 @@ A group may hide all but one element, showing them only on mouse hover. In order Defines the direction of the transition animation. If true, the hidden elements will slide from left to right. If false, they will slide from right to left. When the bar is vertical, it reads as top-to-bottom. +Group drawers are also given the `.expanded` CSS class when they are expanded. + ``` "group/power": { "orientation": "inherit", From efadb82c56b16699c01b029913af7474b61464f3 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Sat, 13 Sep 2025 00:00:20 +0200 Subject: [PATCH 152/416] feat: basic line graph component and cpu graph module --- include/AGraph.hpp | 50 +++++++++ include/modules/cpu_graph.hpp | 32 ++++++ man/waybar-cpu-graph.5.scd | 85 +++++++++++++++ meson.build | 3 + src/AGraph.cpp | 197 ++++++++++++++++++++++++++++++++++ src/factory.cpp | 4 + src/modules/cpu_graph.cpp | 47 ++++++++ 7 files changed, 418 insertions(+) create mode 100644 include/AGraph.hpp create mode 100644 include/modules/cpu_graph.hpp create mode 100644 man/waybar-cpu-graph.5.scd create mode 100644 src/AGraph.cpp create mode 100644 src/modules/cpu_graph.cpp diff --git a/include/AGraph.hpp b/include/AGraph.hpp new file mode 100644 index 00000000..3466e577 --- /dev/null +++ b/include/AGraph.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include "AModule.hpp" + +namespace waybar { + +class AGraph : public AModule { + public: + AGraph(const Json::Value &, const std::string &, const std::string &, + uint16_t interval = 0, bool enable_click = false, + bool enable_scroll = false); + virtual ~AGraph() = default; + auto update() -> void override; + + protected: + Gtk::DrawingArea graph_; + std::deque values_; + uint16_t datapoints_ = 20; + uint16_t y_offset_ = 0; + + void addValue(const int n); + + const std::chrono::seconds interval_; + + bool onDraw(const Cairo::RefPtr &cr); + + std::map submenus_; + std::map menuActionsMap_; + static void handleGtkMenuEvent(GtkMenuItem *menuitem, gpointer data); + + private: + void drawFilledArea(const Cairo::RefPtr &cr, + const std::vector> &points, + double height, const Gdk::RGBA &bg_color); + + void drawLine(const Cairo::RefPtr &cr, + const std::vector> &points, const Gdk::RGBA &fg_color); + + void drawPath(const Cairo::RefPtr &cr, + const std::vector> &points); +}; + +} // namespace waybar diff --git a/include/modules/cpu_graph.hpp b/include/modules/cpu_graph.hpp new file mode 100644 index 00000000..df5f6ea3 --- /dev/null +++ b/include/modules/cpu_graph.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "AGraph.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CpuGraph : public AGraph { + public: + CpuGraph(const std::string&, const Json::Value&); + virtual ~CpuGraph() = default; + auto update() -> void override; + + private: + static constexpr const char *MODERATE_CLASS = "cpu-moderate"; + static constexpr const char *HIGH_CLASS = "cpu-high"; + static constexpr const char *INTENSIVE_CLASS = "cpu-intensive"; + + std::vector> prev_times_; + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/man/waybar-cpu-graph.5.scd b/man/waybar-cpu-graph.5.scd new file mode 100644 index 00000000..bfe9de42 --- /dev/null +++ b/man/waybar-cpu-graph.5.scd @@ -0,0 +1,85 @@ +waybar-cpu(5) + +# NAME + +waybar - cpu graph module + +# DESCRIPTION + +The *cpu graph* module displays a line graph with the CPU utilization. + +# CONFIGURATION + +*interval*: ++ + typeof: integer ++ + default: 10 ++ + The interval in which the information gets polled. + +*width*: ++ + typeof: integer ++ + The length in pixels the module should display. + +*y_offset*: ++ + typeof: integer ++ + The margin in pixels at the bottom of the module. + +*datapoints*: ++ + typeof: integer ++ + How many data points to show. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# EXAMPLES + +Basic configuration: + +``` +"cpu_graph": { + "interval": 2, + "width": 10, + "y_offset": 4 +} +``` + +# STYLE + +- *#cpu_graph* +- *.cpu-intensive* +- *.cpu-high* +- *.cpu-moderate* diff --git a/meson.build b/meson.build index 0c494eb2..713a723f 100644 --- a/meson.build +++ b/meson.build @@ -159,6 +159,7 @@ endif src_files = files( 'src/factory.cpp', + 'src/AGraph.cpp', 'src/AModule.cpp', 'src/ALabel.cpp', 'src/AIconLabel.cpp', @@ -210,6 +211,7 @@ if is_linux 'src/modules/bluetooth.cpp', 'src/modules/cffi.cpp', 'src/modules/cpu.cpp', + 'src/modules/cpu_graph.cpp', 'src/modules/cpu_frequency/common.cpp', 'src/modules/cpu_frequency/linux.cpp', 'src/modules/cpu_usage/common.cpp', @@ -234,6 +236,7 @@ elif is_dragonfly or is_freebsd or is_netbsd or is_openbsd src_files += files( 'src/modules/cffi.cpp', 'src/modules/cpu.cpp', + 'src/modules/cpu_graph.cpp', 'src/modules/cpu_frequency/bsd.cpp', 'src/modules/cpu_frequency/common.cpp', 'src/modules/cpu_usage/bsd.cpp', diff --git a/src/AGraph.cpp b/src/AGraph.cpp new file mode 100644 index 00000000..529cdb66 --- /dev/null +++ b/src/AGraph.cpp @@ -0,0 +1,197 @@ +#include "AGraph.hpp" + +#include +#include + +#include +#include +#include + +#include "config.hpp" + +namespace waybar { + +AGraph::AGraph(const Json::Value& config, const std::string& name, const std::string& id, + uint16_t interval, bool enable_click, bool enable_scroll) + : 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)) { + graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw)); + graph_.set_name(name); + if (!id.empty()) { + graph_.get_style_context()->add_class(id); + } + graph_.get_style_context()->add_class(MODULE_CLASS); + if (config_["width"].isUInt()) { + graph_.set_size_request(config_["width"].asUInt(), -1); + } else { + graph_.set_size_request(100, -1); + } + + event_box_.add(graph_); + + if (config_["datapoints"].isUInt()) { + datapoints_ = config_["datapoints_"].asUInt(); + } + + if (config_["y_offset"].isUInt()) { + y_offset_ = config_["y_offset"].asUInt(); + } + + // If a GTKMenu is requested in the config + if (config_["menu"].isString()) { + // Create the GTKMenu widget + try { + // Check that the file exists + std::string menuFile = config_["menu-file"].asString(); + + // there might be "~" or "$HOME" in original path, try to expand it. + auto result = Config::tryExpandPath(menuFile, ""); + if (result.empty()) { + throw std::runtime_error("Failed to expand file: " + menuFile); + } + + menuFile = result.front(); + // Read the menu descriptor file + std::ifstream file(menuFile); + if (!file.is_open()) { + throw std::runtime_error("Failed to open file: " + menuFile); + } + std::stringstream fileContent; + fileContent << file.rdbuf(); + GtkBuilder* builder = gtk_builder_new(); + + // Make the GtkBuilder and check for errors in his parsing + if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) { + throw std::runtime_error("Error found in the file " + menuFile); + } + + menu_ = gtk_builder_get_object(builder, "menu"); + if (menu_ == nullptr) { + throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); + } + submenus_ = std::map(); + menuActionsMap_ = std::map(); + + // Linking actions to the GTKMenu based on + for (Json::Value::const_iterator it = config_["menu-actions"].begin(); + it != config_["menu-actions"].end(); ++it) { + std::string key = it.key().asString(); + submenus_[key] = GTK_MENU_ITEM(gtk_builder_get_object(builder, key.c_str())); + menuActionsMap_[key] = it->asString(); + g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), + (gpointer)menuActionsMap_[key].c_str()); + } + } catch (std::runtime_error& e) { + spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what()); + } + } +} + +auto AGraph::update() -> void { + graph_.queue_draw(); + AModule::update(); +} + +void AGraph::handleGtkMenuEvent(GtkMenuItem* /*menuitem*/, gpointer data) { + waybar::util::command::res res = waybar::util::command::exec((char*)data, "GtkMenu"); +} + +void AGraph::addValue(const int n) { + if (values_.size() >= datapoints_) { + values_.pop_front(); + } + values_.push_back(n); +} + +bool AGraph::onDraw(const Cairo::RefPtr& cr) { + const int width = graph_.get_allocated_width(); + const int height = graph_.get_allocated_height() - 1 - y_offset_; + + if (values_.empty() || width <= 0 || height <= 0) { + return false; + } + + auto style_context = graph_.get_style_context(); + Gdk::RGBA fg_color = style_context->get_color(Gtk::STATE_FLAG_NORMAL); + Gdk::RGBA bg_color = fg_color; + bg_color.set_alpha(0.3); + + cr->set_line_width(1.0); + + const double step_width = static_cast(width) / datapoints_; + const int values_count = values_.size(); + const int empty_space = datapoints_ - values_count; + + std::vector> points; + points.reserve(values_count); + + for (int i = empty_space; i < datapoints_; ++i) { + double x = i * step_width; + int value_index = i - empty_space; + int value = values_[value_index]; + double y = height - (static_cast(value) / 100.0 * height); + points.emplace_back(x, y); + } + + if (!points.empty()) { + + drawFilledArea(cr, points, height, bg_color); + + drawLine(cr, points, fg_color); + } + + return false; +} +void AGraph::drawFilledArea(const Cairo::RefPtr& cr, + const std::vector>& points, + double height, const Gdk::RGBA& bg_color) { + if (points.empty()) return; + + double first_x = points.front().first; + double last_x = points.back().first; + + drawPath(cr, points); + + cr->line_to(last_x, height); + cr->line_to(first_x, height); + cr->close_path(); + + cr->set_source_rgba(bg_color.get_red(), bg_color.get_green(), bg_color.get_blue(), + bg_color.get_alpha()); + cr->fill(); +} + +void AGraph::drawLine(const Cairo::RefPtr& cr, + const std::vector>& points, + const Gdk::RGBA& fg_color) { + if (points.empty()) return; + + cr->begin_new_path(); + drawPath(cr, points); + + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + fg_color.get_alpha()); + cr->stroke(); +} + +void AGraph::drawPath(const Cairo::RefPtr& cr, + const std::vector>& points) { + if (points.empty()) return; + + bool first_point = true; + for (const auto& point : points) { + if (first_point) { + cr->move_to(point.first, point.second); + first_point = false; + } else { + cr->line_to(point.first, point.second); + } + } +} + +} // namespace waybar diff --git a/src/factory.cpp b/src/factory.cpp index 2fd3e3b8..e82711b5 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -51,6 +51,7 @@ #endif #if defined(HAVE_CPU_LINUX) || defined(HAVE_CPU_BSD) #include "modules/cpu.hpp" +#include "modules/cpu_graph.hpp" #include "modules/cpu_frequency.hpp" #include "modules/cpu_usage.hpp" #include "modules/load.hpp" @@ -251,6 +252,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "cpu") { return new waybar::modules::Cpu(id, config_[name]); } + if (ref == "cpu_graph") { + return new waybar::modules::CpuGraph(id, config_[name]); + } #if defined(HAVE_CPU_LINUX) if (ref == "cpu_frequency") { return new waybar::modules::CpuFrequency(id, config_[name]); diff --git a/src/modules/cpu_graph.cpp b/src/modules/cpu_graph.cpp new file mode 100644 index 00000000..a9a29dd2 --- /dev/null +++ b/src/modules/cpu_graph.cpp @@ -0,0 +1,47 @@ +#include "modules/cpu_graph.hpp" + +#include "modules/cpu_frequency.hpp" +#include "modules/cpu_usage.hpp" +#include "modules/load.hpp" + +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + +waybar::modules::CpuGraph::CpuGraph(const std::string& id, const Json::Value& config) + : AGraph(config, "cpu_graph", id, 5) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; +} + +auto waybar::modules::CpuGraph::update() -> void { + // TODO: as creating dynamic fmt::arg arrays is buggy we have to calc both + auto [cpu_usage, tooltip] = CpuUsage::getCpuUsage(prev_times_); + if (tooltipEnabled()) { + graph_.set_tooltip_text(tooltip); + } + auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; + addValue(total_usage); + + graph_.get_style_context()->remove_class(MODERATE_CLASS); + graph_.get_style_context()->remove_class(HIGH_CLASS); + graph_.get_style_context()->remove_class(INTENSIVE_CLASS); + + if (total_usage > 90) { + graph_.get_style_context()->add_class(INTENSIVE_CLASS); + } else if (total_usage > 70) { + graph_.get_style_context()->add_class(HIGH_CLASS); + } else if (total_usage > 30) { + graph_.get_style_context()->add_class(MODERATE_CLASS); + } + + // Call parent update + AGraph::update(); +} From 4d6354af48e5fba70d522ce75d8a0eb20cd661c6 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Sat, 11 Oct 2025 01:52:10 +0200 Subject: [PATCH 153/416] feat: add gauge and stacked bar graph type --- include/AGraph.hpp | 19 ++- include/modules/custom_graph.hpp | 49 ++++++ meson.build | 1 + src/AGraph.cpp | 122 ++++++++++++- src/factory.cpp | 4 + src/modules/custom_graph.cpp | 283 +++++++++++++++++++++++++++++++ 6 files changed, 466 insertions(+), 12 deletions(-) create mode 100644 include/modules/custom_graph.hpp create mode 100644 src/modules/custom_graph.cpp diff --git a/include/AGraph.hpp b/include/AGraph.hpp index 3466e577..a49b215d 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -11,11 +11,12 @@ namespace waybar { +enum class GraphType { LINE, BAR, GAUGE }; + class AGraph : public AModule { public: - AGraph(const Json::Value &, const std::string &, const std::string &, - uint16_t interval = 0, bool enable_click = false, - bool enable_scroll = false); + AGraph(const Json::Value &, const std::string &, const std::string &, uint16_t interval = 0, + bool enable_click = false, bool enable_scroll = false); virtual ~AGraph() = default; auto update() -> void override; @@ -24,6 +25,7 @@ class AGraph : public AModule { std::deque values_; uint16_t datapoints_ = 20; uint16_t y_offset_ = 0; + GraphType graph_type_ = GraphType::LINE; void addValue(const int n); @@ -37,14 +39,21 @@ class AGraph : public AModule { private: void drawFilledArea(const Cairo::RefPtr &cr, - const std::vector> &points, - double height, const Gdk::RGBA &bg_color); + const std::vector> &points, double height, + const Gdk::RGBA &bg_color); void drawLine(const Cairo::RefPtr &cr, const std::vector> &points, const Gdk::RGBA &fg_color); void drawPath(const Cairo::RefPtr &cr, const std::vector> &points); + + void drawBars(const Cairo::RefPtr &cr, + double width, double height, int current_value, + const Gdk::RGBA &fg_color); + + void drawGauge(const Cairo::RefPtr &cr, double width, double height, + int current_value, const Gdk::RGBA &fg_color); }; } // namespace waybar diff --git a/include/modules/custom_graph.hpp b/include/modules/custom_graph.hpp new file mode 100644 index 00000000..a081f376 --- /dev/null +++ b/include/modules/custom_graph.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include + +#include "AGraph.hpp" +#include "util/command.hpp" +#include "util/json.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + +class CustomGraph : public AGraph { + public: + CustomGraph(const std::string&, const std::string&, const Json::Value&, const std::string&); + virtual ~CustomGraph(); + auto update() -> void override; + void refresh(int /*signal*/) override; + + private: + void delayWorker(); + void continuousWorker(); + void waitingWorker(); + void parseOutputRaw(); + void parseOutputJson(); + void handleEvent(); + bool handleScroll(GdkEventScroll* e) override; + bool handleToggle(GdkEventButton* const& e) override; + + const std::string name_; + const std::string output_name_; + std::string text_; + std::string id_; + std::string alt_; + std::string tooltip_; + const bool tooltip_format_enabled_; + std::vector class_; + int percentage_; + FILE* fp_; + int pid_; + util::command::res output_; + util::JsonParser parser_; + + util::SleeperThread thread_; +}; + +} // namespace waybar::modules diff --git a/meson.build b/meson.build index 713a723f..8413dbe1 100644 --- a/meson.build +++ b/meson.build @@ -165,6 +165,7 @@ src_files = files( 'src/AIconLabel.cpp', 'src/AAppIconLabel.cpp', 'src/modules/custom.cpp', + 'src/modules/custom_graph.cpp', 'src/modules/disk.cpp', 'src/modules/idle_inhibitor.cpp', 'src/modules/image.cpp', diff --git a/src/AGraph.cpp b/src/AGraph.cpp index 529cdb66..b8076ecc 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,17 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st y_offset_ = config_["y_offset"].asUInt(); } + if (config_["graph_type"].isString()) { + std::string type = config_["graph_type"].asString(); + if (type == "line") { + graph_type_ = GraphType::LINE; + } else if (type == "bar") { + graph_type_ = GraphType::BAR; + } else if (type == "gauge") { + graph_type_ = GraphType::GAUGE; + } + } + // If a GTKMenu is requested in the config if (config_["menu"].isString()) { // Create the GTKMenu widget @@ -137,19 +149,26 @@ bool AGraph::onDraw(const Cairo::RefPtr& cr) { double y = height - (static_cast(value) / 100.0 * height); points.emplace_back(x, y); } - if (!points.empty()) { - - drawFilledArea(cr, points, height, bg_color); - - drawLine(cr, points, fg_color); + switch (graph_type_) { + case GraphType::LINE: + drawFilledArea(cr, points, height, bg_color); + drawLine(cr, points, fg_color); + break; + case GraphType::BAR: + drawBars(cr, width, height, values_.empty() ? 0 : values_.back(), fg_color); + break; + case GraphType::GAUGE: + drawGauge(cr, width, height, values_.empty() ? 0 : values_.back(), fg_color); + break; + } } return false; } void AGraph::drawFilledArea(const Cairo::RefPtr& cr, - const std::vector>& points, - double height, const Gdk::RGBA& bg_color) { + const std::vector>& points, double height, + const Gdk::RGBA& bg_color) { if (points.empty()) return; double first_x = points.front().first; @@ -194,4 +213,93 @@ void AGraph::drawPath(const Cairo::RefPtr& cr, } } +void AGraph::drawBars(const Cairo::RefPtr& cr, + double width, double height, int current_value, + const Gdk::RGBA& fg_color) { + + current_value = std::min(100, std::max(0, current_value)); + + double green_height = height * (std::min(current_value, 40) / 100.0); + cr->set_source_rgba(0.0, 1.0, 0.0, 1.0); + cr->rectangle(0, height - green_height, width, green_height); + cr->fill(); + + if (current_value > 40) { + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + cr->set_source_rgba(1.0, 1.0, 0.0, 1.0); + cr->rectangle(0, height - green_height - yellow_height, width, yellow_height); + cr->fill(); + } + + if (current_value > 75) { + double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; + cr->set_source_rgba(1.0, 0.5, 0.0, 1.0); + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + cr->rectangle(0, height - green_height - yellow_height - orange_height, width, + orange_height); + cr->fill(); + } + + if (current_value > 85) { + double red_height = height * (current_value - 85) / 100.0; + cr->set_source_rgba(1.0, 0.0, 0.0, 1.0); + double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; + double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; + cr->rectangle(0, height - green_height - yellow_height - orange_height - red_height, width, + red_height); + cr->fill(); + } + + double value_height = height * (current_value / 100.0); + cr->set_source_rgba(0.2, 0.2, 0.2, 0.8); + cr->rectangle(0, height - value_height, width, 2); + cr->fill(); +} + +void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color) { + double center_x = width / 2.0; + double center_y = height; + double radius = height / 2.0; + + cr->set_line_width(10.0); + + double angle1 = M_PI; + double angle2 = angle1 + 0.3 * angle1; + + // Green section (0-33%) + cr->set_source_rgba(0.0, 1.0, 0.0, 1.0); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Yellow section (33-66%) + angle1 = angle2; + angle2 = angle1 + 0.3 * angle1; + cr->set_source_rgba(1.0, 1.0, 0.0, 1.0); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Red section (66-100%) + angle1 = angle2; + angle2 = 0.0; + cr->set_source_rgba(1.0, 0.0, 0.0, 0.8); + cr->arc(center_x, center_y, radius, angle1, angle2); + cr->stroke(); + + // Draw needle + double percentage = std::min(100, std::max(0, current_value)) / 100.0; + double needle_angle = M_PI * percentage; + double needle_length = radius; + + double needle_x = center_x - needle_length * cos(needle_angle); + double needle_y = center_y - needle_length * sin(needle_angle); + + cr->set_source_rgba(1.0, 1.0, 1.0, 1.0); + cr->set_line_width(2.0); + cr->begin_new_path(); + cr->move_to(center_x, center_y); + cr->line_to(needle_x, needle_y); + cr->stroke(); +} + } // namespace waybar diff --git a/src/factory.cpp b/src/factory.cpp index e82711b5..dfb5caf4 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -118,6 +118,7 @@ #include "modules/cava/cava_frontend.hpp" #include "modules/cffi.hpp" #include "modules/custom.hpp" +#include "modules/custom_graph.hpp" #include "modules/image.hpp" #include "modules/temperature.hpp" #include "modules/user.hpp" @@ -362,6 +363,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name], bar_.output->name); } + if (ref.compare(0, 13, "custom-graph/") == 0 && ref.size() > 7) { + return new waybar::modules::CustomGraph(ref.substr(7), id, config_[name], bar_.output->name); + } if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) { return new waybar::modules::CFFI(ref.substr(5), id, config_[name]); } diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp new file mode 100644 index 00000000..288d97f2 --- /dev/null +++ b/src/modules/custom_graph.cpp @@ -0,0 +1,283 @@ +#include "modules/custom_graph.hpp" + +#include + +#include "util/scope_guard.hpp" + +waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, + const Json::Value& config, const std::string& output_name) + : AGraph(config, "custom-graph" + name, id), + name_(name), + output_name_(output_name), + id_(id), + tooltip_format_enabled_{config_["tooltip-format"].isString()}, + percentage_(0), + fp_(nullptr), + pid_(-1) { + if (config.isNull()) { + spdlog::warn("There is no configuration for 'custom-graph/{}', element will be hidden", name); + } + dp.emit(); + if (!config_["signal"].empty() && config_["interval"].empty() && + config_["restart-interval"].empty()) { + waitingWorker(); + } else if (interval_.count() > 0) { + delayWorker(); + } else if (config_["exec"].isString()) { + continuousWorker(); + } +} + +waybar::modules::CustomGraph::~CustomGraph() { + if (pid_ != -1) { + killpg(pid_, SIGTERM); + waitpid(pid_, NULL, 0); + pid_ = -1; + } +} + +void waybar::modules::CustomGraph::delayWorker() { + thread_ = [this] { + for (int i : this->pid_children_) { + int status; + waitpid(i, &status, 0); + } + + this->pid_children_.clear(); + + bool can_update = true; + if (config_["exec-if"].isString()) { + output_ = util::command::execNoRead(config_["exec-if"].asString()); + if (output_.exit_code != 0) { + can_update = false; + dp.emit(); + } + } + if (can_update) { + if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString(), output_name_); + } + dp.emit(); + } + thread_.sleep_for(interval_); + }; +} + +void waybar::modules::CustomGraph::continuousWorker() { + auto cmd = config_["exec"].asString(); + pid_ = -1; + fp_ = util::command::open(cmd, pid_, output_name_); + if (!fp_) { + throw std::runtime_error("Unable to open " + cmd); + } + thread_ = [this, cmd] { + char* buff = nullptr; + waybar::util::ScopeGuard buff_deleter([&buff]() { + if (buff) { + free(buff); + } + }); + size_t len = 0; + if (getline(&buff, &len, fp_) == -1) { + int exit_code = 1; + if (fp_) { + exit_code = WEXITSTATUS(util::command::close(fp_, pid_)); + fp_ = nullptr; + } + if (exit_code != 0) { + output_ = {exit_code, ""}; + dp.emit(); + spdlog::error("{} stopped unexpectedly, is it endless?", name_); + } + if (config_["restart-interval"].isUInt()) { + pid_ = -1; + thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt())); + fp_ = util::command::open(cmd, pid_, output_name_); + if (!fp_) { + throw std::runtime_error("Unable to open " + cmd); + } + } else { + thread_.stop(); + return; + } + } else { + std::string output = buff; + + // Remove last newline + if (!output.empty() && output[output.length() - 1] == '\n') { + output.erase(output.length() - 1); + } + output_ = {0, output}; + dp.emit(); + } + }; +} + +void waybar::modules::CustomGraph::waitingWorker() { + thread_ = [this] { + bool can_update = true; + if (config_["exec-if"].isString()) { + output_ = util::command::execNoRead(config_["exec-if"].asString()); + if (output_.exit_code != 0) { + can_update = false; + dp.emit(); + } + } + if (can_update) { + if (config_["exec"].isString()) { + output_ = util::command::exec(config_["exec"].asString(), output_name_); + } + dp.emit(); + } + thread_.sleep(); + }; +} + +void waybar::modules::CustomGraph::refresh(int sig) { + if (sig == SIGRTMIN + config_["signal"].asInt()) { + thread_.wake_up(); + } +} + +void waybar::modules::CustomGraph::handleEvent() { + if (!config_["exec-on-event"].isBool() || config_["exec-on-event"].asBool()) { + thread_.wake_up(); + } +} + +bool waybar::modules::CustomGraph::handleScroll(GdkEventScroll* e) { + auto ret = AGraph::handleScroll(e); + handleEvent(); + return ret; +} + +bool waybar::modules::CustomGraph::handleToggle(GdkEventButton* const& e) { + auto ret = AGraph::handleToggle(e); + handleEvent(); + return ret; +} + +auto waybar::modules::CustomGraph::update() -> void { + // Hide label if output is empty + if ((config_["exec"].isString() || config_["exec-if"].isString()) && + (output_.out.empty() || output_.exit_code != 0)) { + event_box_.hide(); + } else { + if (config_["return-type"].asString() == "json") { + parseOutputJson(); + } else { + parseOutputRaw(); + } + + try { + addValue(percentage_); + + if (tooltipEnabled()) { + if (tooltip_format_enabled_) { + auto tooltip = config_["tooltip-format"].asString(); + tooltip = fmt::format( + fmt::runtime(tooltip), fmt::arg("text", text_), fmt::arg("alt", alt_), + fmt::arg("percentage", percentage_)); + graph_.set_tooltip_markup(tooltip); + } else { + if (graph_.get_tooltip_markup() != tooltip_) { + graph_.set_tooltip_markup(tooltip_); + } + } + } + auto style = graph_.get_style_context(); + auto classes = style->list_classes(); + for (auto const& c : classes) { + if (c == id_) continue; + style->remove_class(c); + } + for (auto const& c : class_) { + style->add_class(c); + } + style->add_class("flat"); + style->add_class(MODULE_CLASS); + event_box_.show(); + } catch (const fmt::format_error& e) { + if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) + throw; + + throw fmt::format_error( + "mixing manual and automatic argument indexing is no longer supported; " + "try replacing \"{}\" with \"{text}\" in your format specifier"); + } + } + // Call parent update + AGraph::update(); +} + +void waybar::modules::CustomGraph::parseOutputRaw() { + std::istringstream output(output_.out); + std::string line; + int i = 0; + while (getline(output, line)) { + Glib::ustring validated_line = line; + if (!validated_line.validate()) { + validated_line = validated_line.make_valid(); + } + + if (i == 0) { + if (config_["escape"].isBool() && config_["escape"].asBool()) { + text_ = Glib::Markup::escape_text(validated_line); + tooltip_ = Glib::Markup::escape_text(validated_line); + } else { + text_ = validated_line; + tooltip_ = validated_line; + } + tooltip_ = validated_line; + class_.clear(); + } else if (i == 1) { + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(validated_line); + } else { + tooltip_ = validated_line; + } + } else if (i == 2) { + class_.push_back(validated_line); + } else { + break; + } + i++; + } +} + +void waybar::modules::CustomGraph::parseOutputJson() { + std::istringstream output(output_.out); + std::string line; + class_.clear(); + while (getline(output, line)) { + auto parsed = parser_.parse(line); + if (config_["escape"].isBool() && config_["escape"].asBool()) { + text_ = Glib::Markup::escape_text(parsed["text"].asString()); + } else { + text_ = parsed["text"].asString(); + } + if (config_["escape"].isBool() && config_["escape"].asBool()) { + alt_ = Glib::Markup::escape_text(parsed["alt"].asString()); + } else { + alt_ = parsed["alt"].asString(); + } + if (config_["escape"].isBool() && config_["escape"].asBool()) { + tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString()); + } else { + tooltip_ = parsed["tooltip"].asString(); + } + if (parsed["class"].isString()) { + class_.push_back(parsed["class"].asString()); + } else if (parsed["class"].isArray()) { + for (auto const& c : parsed["class"]) { + class_.push_back(c.asString()); + } + } + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { + percentage_ = (int)lround(parsed["percentage"].asFloat()); + } else { + percentage_ = 0; + } + break; + } +} From dfc26364e15dfe202990e598426e9f5535f52fde Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Fri, 24 Oct 2025 19:47:43 +0200 Subject: [PATCH 154/416] feat: use the foreground color for cleaner look --- include/AGraph.hpp | 1 - man/waybar-cpu-graph.5.scd | 7 +------ src/AGraph.cpp | 26 +++++++++++++++----------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/include/AGraph.hpp b/include/AGraph.hpp index a49b215d..69045473 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -24,7 +24,6 @@ class AGraph : public AModule { Gtk::DrawingArea graph_; std::deque values_; uint16_t datapoints_ = 20; - uint16_t y_offset_ = 0; GraphType graph_type_ = GraphType::LINE; void addValue(const int n); diff --git a/man/waybar-cpu-graph.5.scd b/man/waybar-cpu-graph.5.scd index bfe9de42..c93aaf06 100644 --- a/man/waybar-cpu-graph.5.scd +++ b/man/waybar-cpu-graph.5.scd @@ -19,10 +19,6 @@ The *cpu graph* module displays a line graph with the CPU utilization. typeof: integer ++ The length in pixels the module should display. -*y_offset*: ++ - typeof: integer ++ - The margin in pixels at the bottom of the module. - *datapoints*: ++ typeof: integer ++ How many data points to show. @@ -72,8 +68,7 @@ Basic configuration: ``` "cpu_graph": { "interval": 2, - "width": 10, - "y_offset": 4 + "width": 10 } ``` diff --git a/src/AGraph.cpp b/src/AGraph.cpp index b8076ecc..1899b1b2 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -39,10 +39,6 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st datapoints_ = config_["datapoints_"].asUInt(); } - if (config_["y_offset"].isUInt()) { - y_offset_ = config_["y_offset"].asUInt(); - } - if (config_["graph_type"].isString()) { std::string type = config_["graph_type"].asString(); if (type == "line") { @@ -122,7 +118,7 @@ void AGraph::addValue(const int n) { bool AGraph::onDraw(const Cairo::RefPtr& cr) { const int width = graph_.get_allocated_width(); - const int height = graph_.get_allocated_height() - 1 - y_offset_; + const int height = graph_.get_allocated_height() - 1; if (values_.empty() || width <= 0 || height <= 0) { return false; @@ -220,20 +216,23 @@ void AGraph::drawBars(const Cairo::RefPtr& cr, current_value = std::min(100, std::max(0, current_value)); double green_height = height * (std::min(current_value, 40) / 100.0); - cr->set_source_rgba(0.0, 1.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.5); cr->rectangle(0, height - green_height, width, green_height); cr->fill(); if (current_value > 40) { double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; - cr->set_source_rgba(1.0, 1.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.7); cr->rectangle(0, height - green_height - yellow_height, width, yellow_height); cr->fill(); } if (current_value > 75) { double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; - cr->set_source_rgba(1.0, 0.5, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.85); double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; cr->rectangle(0, height - green_height - yellow_height - orange_height, width, orange_height); @@ -242,7 +241,8 @@ void AGraph::drawBars(const Cairo::RefPtr& cr, if (current_value > 85) { double red_height = height * (current_value - 85) / 100.0; - cr->set_source_rgba(1.0, 0.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 1.0); double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; cr->rectangle(0, height - green_height - yellow_height - orange_height - red_height, width, @@ -268,14 +268,16 @@ void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, do double angle2 = angle1 + 0.3 * angle1; // Green section (0-33%) - cr->set_source_rgba(0.0, 1.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.5); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); // Yellow section (33-66%) angle1 = angle2; angle2 = angle1 + 0.3 * angle1; - cr->set_source_rgba(1.0, 1.0, 0.0, 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 0.75); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); @@ -283,6 +285,8 @@ void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, do angle1 = angle2; angle2 = 0.0; cr->set_source_rgba(1.0, 0.0, 0.0, 0.8); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), + 1.0); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); From 01c6ebdf9e56b943821dbbccbe8599976589dd04 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Sat, 25 Oct 2025 00:13:15 +0200 Subject: [PATCH 155/416] feat: update man pages --- man/waybar-cpu-graph.5.scd | 2 +- man/waybar-custom-graph.5.scd | 189 ++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 man/waybar-custom-graph.5.scd diff --git a/man/waybar-cpu-graph.5.scd b/man/waybar-cpu-graph.5.scd index c93aaf06..1877aeeb 100644 --- a/man/waybar-cpu-graph.5.scd +++ b/man/waybar-cpu-graph.5.scd @@ -1,4 +1,4 @@ -waybar-cpu(5) +waybar-cpu-graph(5) # NAME diff --git a/man/waybar-custom-graph.5.scd b/man/waybar-custom-graph.5.scd new file mode 100644 index 00000000..4f020a6c --- /dev/null +++ b/man/waybar-custom-graph.5.scd @@ -0,0 +1,189 @@ +waybar-custom-graph(5) +# NAME + +waybar - custom graph module + +# DESCRIPTION + +The *custom-graph* module displays a graph with the percentage output of a script. + +# CONFIGURATION + +Addressed by *custom-graph/* + +*exec*: ++ + typeof: string ++ + The path to the script, which should be executed. + +*exec-if*: ++ + typeof: string ++ + The path to a script, which determines if the script in *exec* should be executed. ++ + *exec* will be executed if the exit code of *exec-if* equals 0. + +*exec-on-event*: ++ + typeof: bool ++ + default: true ++ + If an event command is set (e.g. *on-click* or *on-scroll-up*) then re-execute the script after executing the event command. + +*return-type*: ++ + typeof: string ++ + See *return-type* + +*interval*: ++ + typeof: integer or float ++ + The interval (in seconds) in which the information gets polled. ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + Use *once* if you want to execute the module only on startup. ++ + You can update it manually with a signal. If no *interval* or *signal* is defined, it is assumed that the out script loops itself. ++ + If a *signal* is defined then the script will run once on startup and will only update with a signal. + +*restart-interval*: ++ + typeof: integer or float ++ + The restart interval (in seconds). ++ + Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + Can't be used with the *interval* option, so only with continuous scripts. ++ + Once the script exits, it'll be re-executed after the *restart-interval*. + +*signal*: ++ + typeof: integer ++ + The signal number used to update the module. ++ + The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ + If no interval is defined then a signal will be the only way to update the module. + +*format*: ++ + typeof: string ++ + default: {text} ++ + The format, how information should be displayed. On {text} data gets inserted. + +*format-icons*: ++ + typeof: array ++ + Based on the set percentage, the corresponding icon gets selected. The order is *low* to *high*. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label (in 90 degree increments). + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + The tooltip format. If specified, overrides any tooltip output from the script in *exec*. ++ + Uses the same format replacements as *format*. + +*escape*: ++ + typeof: bool ++ + default: false ++ + Option to enable escaping of script output. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# RETURN-TYPE + +When *return-type* is set to *json*, Waybar expects the *exec*-script to output its data in JSON format. +This should look like this: + +``` +{"text": "$text", "tooltip": "$tooltip", "class": "$class", "percentage": $percentage } +``` + +The *class* parameter also accepts an array of strings. + +If nothing or an invalid option is specified, Waybar expects i3blocks style output. Values are *newline* separated. +This should look like this: + +``` +$text\\n$tooltip\\n$class* +``` + +*class* is a CSS class, to apply different styles in *style.css* + +# FORMAT REPLACEMENTS + +*{text}*: Output of the script. + +*{percentage}* Percentage which can be set via a json return type. + +*{icon}*: An icon from 'format-icons' according to percentage. + +# EXAMPLES + +## Memory: + +``` +"custom-graph/memory": { + "interval": 60, + "graph_type": "gauge", + "width": 52, + "exec": "/path/mem.sh", + "signal": 8, + "return-type": "json" +}, +``` + +mem.sh: + +``` +#!/bin/bash + +mem_info=$(cat /proc/meminfo) +mem_total=$(echo "$mem_info" | grep '^MemTotal:' | awk '{print $2}') +mem_available=$(echo "$mem_info" | grep '^MemAvailable:' | awk '{print $2}') + +mem_used=$((mem_total - mem_available)) +mem_percent=$((mem_used * 100 / mem_total)) + +echo "{\"text\": \"${mem_percent}%\", \"percentage\": ${mem_percent},\"tooltip\": \"Memory: ${mem_used}KB used / ${mem_total}KB total\"}'" +``` + +# STYLE + +- *#custom-graph-* +- *#custom-graph-.* +- ** can be set by the script. For more information see *return-type* From 283515901e1b48a11d1e6af58a1776ea6e95fb64 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Sat, 8 Nov 2025 01:46:31 +0100 Subject: [PATCH 156/416] fix: id read and name set --- src/factory.cpp | 4 ++-- src/modules/custom_graph.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/factory.cpp b/src/factory.cpp index dfb5caf4..b0ac2e8c 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -363,8 +363,8 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref.compare(0, 7, "custom/") == 0 && ref.size() > 7) { return new waybar::modules::Custom(ref.substr(7), id, config_[name], bar_.output->name); } - if (ref.compare(0, 13, "custom-graph/") == 0 && ref.size() > 7) { - return new waybar::modules::CustomGraph(ref.substr(7), id, config_[name], bar_.output->name); + if (ref.compare(0, 13, "custom-graph/") == 0 && ref.size() > 13) { + return new waybar::modules::CustomGraph(ref.substr(13), id, config_[name], bar_.output->name); } if (ref.compare(0, 5, "cffi/") == 0 && ref.size() > 5) { return new waybar::modules::CFFI(ref.substr(5), id, config_[name]); diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index 288d97f2..f8660c5f 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -6,7 +6,7 @@ waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, const Json::Value& config, const std::string& output_name) - : AGraph(config, "custom-graph" + name, id), + : AGraph(config, "custom-graph-" + name, id), name_(name), output_name_(output_name), id_(id), From dbf1cfb0f11b687cbaafa65f73acaf5ce2eeb282 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Mon, 23 Feb 2026 16:20:58 +0100 Subject: [PATCH 157/416] fix: typo reading datapoints --- src/AGraph.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AGraph.cpp b/src/AGraph.cpp index 1899b1b2..139bddb2 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -36,7 +36,7 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st event_box_.add(graph_); if (config_["datapoints"].isUInt()) { - datapoints_ = config_["datapoints_"].asUInt(); + datapoints_ = config_["datapoints"].asUInt(); } if (config_["graph_type"].isString()) { @@ -110,7 +110,7 @@ void AGraph::handleGtkMenuEvent(GtkMenuItem* /*menuitem*/, gpointer data) { } void AGraph::addValue(const int n) { - if (values_.size() >= datapoints_) { + if (datapoints_ > 0 && values_.size() >= datapoints_) { values_.pop_front(); } values_.push_back(n); From 132ca3ac456c374afba1a2d913d6bcf5aae2d418 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Fri, 6 Mar 2026 23:48:12 +0100 Subject: [PATCH 158/416] fix: datapoints should be positive --- src/AGraph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AGraph.cpp b/src/AGraph.cpp index 139bddb2..bd64410f 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -35,7 +35,7 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st event_box_.add(graph_); - if (config_["datapoints"].isUInt()) { + if (config_["datapoints"].isUInt() && config_["datapoints"].asUInt() > 0) { datapoints_ = config_["datapoints"].asUInt(); } From 7d965a874f736854516f18320787ede9c6c47ef4 Mon Sep 17 00:00:00 2001 From: Ricardo Markiewicz Date: Fri, 6 Mar 2026 23:57:22 +0100 Subject: [PATCH 159/416] pr fixes --- src/AGraph.cpp | 31 ++++++++++--------------------- src/modules/custom_graph.cpp | 1 - 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/AGraph.cpp b/src/AGraph.cpp index bd64410f..f79e7218 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -209,40 +209,33 @@ void AGraph::drawPath(const Cairo::RefPtr& cr, } } -void AGraph::drawBars(const Cairo::RefPtr& cr, - double width, double height, int current_value, - const Gdk::RGBA& fg_color) { - +void AGraph::drawBars(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color) { current_value = std::min(100, std::max(0, current_value)); double green_height = height * (std::min(current_value, 40) / 100.0); - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.5); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.5); cr->rectangle(0, height - green_height, width, green_height); cr->fill(); if (current_value > 40) { double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.7); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.7); cr->rectangle(0, height - green_height - yellow_height, width, yellow_height); cr->fill(); } if (current_value > 75) { double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.85); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.85); double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; - cr->rectangle(0, height - green_height - yellow_height - orange_height, width, - orange_height); + cr->rectangle(0, height - green_height - yellow_height - orange_height, width, orange_height); cr->fill(); } if (current_value > 85) { double red_height = height * (current_value - 85) / 100.0; - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 1.0); double yellow_height = height * (std::min(current_value, 75) - 40) / 100.0; double orange_height = height * (std::min(current_value, 85) - 75) / 100.0; cr->rectangle(0, height - green_height - yellow_height - orange_height - red_height, width, @@ -268,25 +261,21 @@ void AGraph::drawGauge(const Cairo::RefPtr& cr, double width, do double angle2 = angle1 + 0.3 * angle1; // Green section (0-33%) - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.5); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.5); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); // Yellow section (33-66%) angle1 = angle2; angle2 = angle1 + 0.3 * angle1; - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 0.75); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 0.75); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); // Red section (66-100%) angle1 = angle2; angle2 = 0.0; - cr->set_source_rgba(1.0, 0.0, 0.0, 0.8); - cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), - 1.0); + cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), 1.0); cr->arc(center_x, center_y, radius, angle1, angle2); cr->stroke(); diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index f8660c5f..c23ed0d6 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -228,7 +228,6 @@ void waybar::modules::CustomGraph::parseOutputRaw() { text_ = validated_line; tooltip_ = validated_line; } - tooltip_ = validated_line; class_.clear(); } else if (i == 1) { if (config_["escape"].isBool() && config_["escape"].asBool()) { From 503d99b1288bd848758369cd51344add6092af10 Mon Sep 17 00:00:00 2001 From: xiyori Date: Thu, 9 Apr 2026 14:15:56 +0300 Subject: [PATCH 160/416] Add max-windows option to hyprland/workspaces --- include/modules/hyprland/workspaces.hpp | 3 +++ man/waybar-hyprland-workspaces.5.scd | 5 +++++ src/modules/hyprland/workspace.cpp | 15 ++++++++++----- src/modules/hyprland/workspaces.cpp | 10 ++++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 03548ccb..94c2ee8a 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -56,6 +56,7 @@ class Workspaces : public AModule, public EventHandler { auto taskbarReverseDirection() const -> bool { return m_taskbarReverseDirection; } auto onClickWindow() const -> std::string { return m_onClickWindow; } auto getIgnoredWindows() const -> std::vector { return m_ignoreWindows; } + auto maxWindows() const -> int { return m_maxWindows; } enum class ActiveWindowPosition { NONE, FIRST, LAST }; auto activeWindowPosition() const -> ActiveWindowPosition { return m_activeWindowPosition; } @@ -89,6 +90,7 @@ class Workspaces : public AModule, public EventHandler { auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; auto populateFormatWindowSeparatorConfig(const Json::Value& config) -> void; auto populateWindowRewriteConfig(const Json::Value& config) -> void; + auto populateMaxWindowsConfig(const Json::Value& config) -> void; auto populateWorkspaceTaskbarConfig(const Json::Value& config) -> void; void registerIpc(); @@ -202,6 +204,7 @@ class Workspaces : public AModule, public EventHandler { }; std::string m_onClickWindow; std::string m_currentActiveWindowAddress; + int m_maxWindows = 0; std::vector m_ignoreWorkspaces; std::vector m_ignoreWindows; diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 5284ce99..d163e688 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -98,6 +98,11 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. - {button} Pressed button number, see https://api.gtkd.org/gdk.c.types.GdkEventButton.button.html. ++ See https://github.com/Alexays/Waybar/wiki/Module:-Hyprland#workspace-taskbars-example for a full example. +*max-windows*: ++ + typeof: int ++ + default: 0 (unlimited) ++ + Maximum number of windows to show per workspace. When set, newest windows beyond the limit are not shown. Set to 0 for unlimited windows. + *show-special*: ++ typeof: bool ++ default: false ++ diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 21e7ef9b..5bd53d72 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -247,13 +247,14 @@ void Workspace::update(const std::string& workspace_icon) { auto windowSeparator = m_workspaceManager.getWindowSeparator(); bool isNotFirst = false; + auto end_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.end() : m_windowMap.begin() + m_workspaceManager.maxWindows(); - for (const auto& window_repr : m_windowMap) { + for (auto it = m_windowMap.begin(); it != end_it; ++it) { if (isNotFirst) { windows.append(windowSeparator); } isNotFirst = true; - windows.append(window_repr.repr_rewrite); + windows.append(it->repr_rewrite); } } @@ -339,12 +340,16 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { }; if (m_workspaceManager.taskbarReverseDirection()) { - for (auto it = m_windowMap.rbegin(); it != m_windowMap.rend(); ++it) { + auto rend_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.rend() : m_windowMap.rbegin() + m_workspaceManager.maxWindows(); + + for (auto it = m_windowMap.rbegin(); it != rend_it; ++it) { processWindow(*it); } } else { - for (const auto& window_repr : m_windowMap) { - processWindow(window_repr); + auto end_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.end() : m_windowMap.begin() + m_workspaceManager.maxWindows(); + + for (auto it = m_windowMap.begin(); it != end_it; ++it) { + processWindow(*it); } } diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index f794249b..94d401de 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -665,6 +665,7 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { populateIgnoreWorkspacesConfig(config); populateFormatWindowSeparatorConfig(config); populateWindowRewriteConfig(config); + populateMaxWindowsConfig(config); if (withWindows) { populateWorkspaceTaskbarConfig(config); @@ -746,6 +747,15 @@ auto Workspaces::populateWindowRewriteConfig(const Json::Value& config) -> void [this](std::string& window_rule) { return windowRewritePriorityFunction(window_rule); }); } +auto Workspaces::populateMaxWindowsConfig(const Json::Value& config) -> void { + if (config["max-windows"].isInt()) { + m_maxWindows = config["max-windows"].asInt(); + if (m_maxWindows < 0) { + m_maxWindows = 0; + } + } +} + auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value& config) -> void { const auto& workspaceTaskbar = config["workspace-taskbar"]; if (!workspaceTaskbar.isObject()) { From a8a46d1f1e1834e652bdeff9640e98b5ab36eb25 Mon Sep 17 00:00:00 2001 From: Visal Vijay Date: Thu, 9 Apr 2026 19:40:05 +0530 Subject: [PATCH 161/416] Refactor: simplify complex conditional expressions in Hyprland workspace module --- src/modules/hyprland/workspace.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 21e7ef9b..da383aa5 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -115,7 +115,11 @@ void Workspace::setActiveWindow(WindowAddress const& addr) { } auto activeWindowPos = m_workspaceManager.activeWindowPosition(); - if (activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE) { + const bool has_active_window = + activeIdx.has_value() && + activeWindowPos != Workspaces::ActiveWindowPosition::NONE; + + if (has_active_window) { auto window = std::move(m_windowMap[*activeIdx]); m_windowMap.erase(m_windowMap.begin() + *activeIdx); if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) { @@ -130,7 +134,10 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) { if (!create_window_payload.isEmpty(m_workspaceManager)) { auto repr = create_window_payload.repr(m_workspaceManager); - if (!repr.empty() || m_workspaceManager.enableTaskbar()) { + const bool should_display = + !repr.empty() || m_workspaceManager.enableTaskbar(); + + if (should_display) { auto addr = create_window_payload.getAddress(); auto it = std::ranges::find_if( m_windowMap, [&addr](const auto& window) { return window.address == addr; }); @@ -142,7 +149,7 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) { } } } -}; +} bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) { if (create_window_payload.getWorkspaceName() == name()) { @@ -270,7 +277,9 @@ void Workspace::update(const std::string& workspace_icon) { bool Workspace::isEmpty() const { auto ignore_list = m_workspaceManager.getIgnoredWindows(); - if (ignore_list.empty()) { + const bool no_ignore_rules = ignore_list.empty(); + + if (no_ignore_rules) { return m_windows == 0; } // If there are windows but they are all ignored, consider the workspace empty @@ -349,7 +358,9 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { } auto formatAfter = m_workspaceManager.formatAfter(); - if (!formatAfter.empty()) { + const bool has_format_after = !formatAfter.empty(); + + if (has_format_after) { m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()), fmt::arg("name", name()), fmt::arg("icon", workspace_icon))); From efd245b552ee279c5eb88cff3bc64c486fd88a60 Mon Sep 17 00:00:00 2001 From: Mervius Date: Fri, 10 Apr 2026 11:42:23 -0500 Subject: [PATCH 162/416] Fix timeout with multiple monitors --- include/client.hpp | 3 +++ src/client.cpp | 31 ++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/include/client.hpp b/include/client.hpp index c3df3f21..8598e356 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -44,6 +44,7 @@ class Client { const char* interface, uint32_t version); static void handleGlobalRemove(void* data, struct wl_registry* registry, uint32_t name); static void handleOutputDone(void*, struct zxdg_output_v1*); + void createBarsBatch(); static void handleOutputName(void*, struct zxdg_output_v1*, const char*); static void handleOutputDescription(void*, struct zxdg_output_v1*, const char*); void handleMonitorAdded(Glib::RefPtr monitor); @@ -58,6 +59,8 @@ class Client { std::string m_cssFile; sigc::connection monitor_added_connection_; sigc::connection monitor_removed_connection_; + std::vector pending_outputs_; + bool bars_scheduled_ = false; }; } // namespace waybar diff --git a/src/client.cpp b/src/client.cpp index d1489186..ff354e18 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -91,11 +91,14 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o output.xdg_output.reset(); spdlog::debug("Output detection done: {} ({})", output.name, output.identifier); - auto configs = client->getOutputConfigs(output); - if (!configs.empty()) { - for (const auto& config : configs) { - client->bars.emplace_back(std::make_unique(&output, config)); - } + client->pending_outputs_.push_back(&output); + + if (!client->bars_scheduled_) { + client->bars_scheduled_ = true; + + Glib::signal_idle().connect_once([client]() { + client->createBarsBatch(); + }, Glib::PRIORITY_HIGH_IDLE); } } } catch (const std::exception& e) { @@ -103,6 +106,24 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } } +void waybar::Client::createBarsBatch() { + for (auto* output : pending_outputs_) { + try { + auto configs = getOutputConfigs(*output); + if (!configs.empty()) { + for (const auto& config : configs) { + bars.emplace_back(std::make_unique(output, config)); + } + } + } catch (const std::exception& e) { + spdlog::warn("Error creating bar: {}", e.what()); + } + } + + pending_outputs_.clear(); + bars_scheduled_ = false; +} + void waybar::Client::handleOutputName(void* data, struct zxdg_output_v1* /*xdg_output*/, const char* name) { auto* client = waybar::Client::inst(); From 450b6a785f4436862c49f0dca48a559153f143ae Mon Sep 17 00:00:00 2001 From: Arnaud Gaudard <24233760+maltherd@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:22:19 +0200 Subject: [PATCH 163/416] Add "ignore-workspaces" option to sway/workspaces module --- include/modules/sway/workspaces.hpp | 3 +++ src/modules/sway/workspaces.cpp | 36 +++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index 14528841..5270faee 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -30,6 +30,8 @@ class Workspaces : public AModule, public sigc::trackable { static int convertWorkspaceNameToNum(const std::string& name); static int windowRewritePriorityFunction(std::string const& window_rule); + auto populateIgnoreWorkspacesConfig(const Json::Value& config) -> void; + bool isWorkspaceIgnored(std::string const& name); void onCmd(const struct Ipc::ipc_response&); void onEvent(const struct Ipc::ipc_response&); bool filterButtons(); @@ -49,6 +51,7 @@ class Workspaces : public AModule, public sigc::trackable { std::vector workspaces_order_; Gtk::Box box_; std::string m_formatWindowSeparator; + std::vector m_ignoreWorkspaces; util::RegexCollection m_windowRewriteRules; util::JsonParser parser_; std::unordered_map buttons_; diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 6abe5383..59b7be69 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -69,6 +69,7 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& m_windowRewriteRules = waybar::util::RegexCollection( windowRewrite, std::move(windowRewriteDefault), windowRewritePriorityFunction); } + populateIgnoreWorkspacesConfig(config); ipc_.subscribe(R"(["workspace"])"); ipc_.subscribe(R"(["window"])"); ipc_.signal_event.connect(sigc::mem_fun(*this, &Workspaces::onEvent)); @@ -97,6 +98,36 @@ void Workspaces::onEvent(const struct Ipc::ipc_response& res) { } } +auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value& config) -> void { + auto ignoreWorkspaces = config["ignore-workspaces"]; + if (ignoreWorkspaces.isArray()) { + for (const auto& workspaceRegex : ignoreWorkspaces) { + if (workspaceRegex.isString()) { + std::string ruleString = workspaceRegex.asString(); + try { + const std::regex rule{ruleString, std::regex_constants::icase}; + m_ignoreWorkspaces.emplace_back(rule); + } catch (const std::regex_error& e) { + spdlog::error("Invalid rule {}: {}", ruleString, e.what()); + } + } else { + spdlog::error("Not a string: '{}'", workspaceRegex); + } + } + } +} + +bool Workspaces::isWorkspaceIgnored(std::string const& name) { + for (auto& rule : m_ignoreWorkspaces) { + if (std::regex_match(name, rule)) { + return true; + break; + } + } + + return false; +} + void Workspaces::onCmd(const struct Ipc::ipc_response& res) { if (res.type == IPC_GET_TREE) { try { @@ -118,8 +149,9 @@ void Workspaces::onCmd(const struct Ipc::ipc_response& res) { }); for (auto& output : outputs) { - std::copy(output["nodes"].begin(), output["nodes"].end(), - std::back_inserter(workspaces_)); + std::copy_if( + output["nodes"].begin(), output["nodes"].end(), std::back_inserter(workspaces_), + [&](const auto& node) { return !(isWorkspaceIgnored(node["name"].asString())); }); std::copy(output["floating_nodes"].begin(), output["floating_nodes"].end(), std::back_inserter(workspaces_)); } From 1ab1d41e4dd5e2c2aced0354b5c19c4348fbe2e4 Mon Sep 17 00:00:00 2001 From: lilly-lizard Date: Tue, 14 Apr 2026 08:25:04 +1200 Subject: [PATCH 164/416] removed residual debug logging --- src/modules/river/tags.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index 5712e9a0..a9f14faf 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -201,8 +201,6 @@ Tags::~Tags() { } void Tags::handle_show() { - spdlog::info("bruh1"); - 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); @@ -211,8 +209,6 @@ void Tags::handle_show() { zriver_status_manager_v1_destroy(status_manager_); status_manager_ = nullptr; - - spdlog::info("bruh2"); } void Tags::handle_primary_clicked(uint32_t tag) { From 0ee4fd4aa25948483e1704d5c4a35bd8d9a32292 Mon Sep 17 00:00:00 2001 From: lilly-lizard Date: Tue, 14 Apr 2026 13:47:53 +1200 Subject: [PATCH 165/416] added output class to dwl/mangowm allowing tags to display differently when the output is focused (for multi-monitor setups) --- include/modules/dwl/tags.hpp | 2 ++ man/waybar-dwl-tags.5.scd | 3 ++- src/modules/dwl/tags.cpp | 16 ++++++++++++++-- src/modules/river/tags.cpp | 5 ++--- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/include/modules/dwl/tags.hpp b/include/modules/dwl/tags.hpp index 856a35dd..45a6f01f 100644 --- a/include/modules/dwl/tags.hpp +++ b/include/modules/dwl/tags.hpp @@ -21,6 +21,8 @@ class Tags : public waybar::AModule { void handle_primary_clicked(uint32_t tag); bool handle_button_press(GdkEventButton* event_button, uint32_t tag); + void handle_active_output(zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active); + struct zdwl_ipc_manager_v2* status_manager_; struct wl_seat* seat_; diff --git a/man/waybar-dwl-tags.5.scd b/man/waybar-dwl-tags.5.scd index 39d15b7a..f775b742 100644 --- a/man/waybar-dwl-tags.5.scd +++ b/man/waybar-dwl-tags.5.scd @@ -46,8 +46,9 @@ Addressed by *dwl/tags* - *#tags button.empty* - *#tags button.focused* - *#tags button.urgent* +- *#tags button.output* -Note that occupied/focused/urgent status may overlap. That is, a tag may be +Note that occupied/focused/urgent/output status may overlap. That is, a tag may be both occupied and focused at the same time. # SEE ALSO diff --git a/src/modules/dwl/tags.cpp b/src/modules/dwl/tags.cpp index 243aaa92..52554707 100644 --- a/src/modules/dwl/tags.cpp +++ b/src/modules/dwl/tags.cpp @@ -26,7 +26,7 @@ static void toggle_visibility(void* data, zdwl_ipc_output_v2* zdwl_output_v2) { } static void active(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) { - // Intentionally empty + static_cast(data)->handle_active_output(zdwl_output_v2, active); } static void set_tag(void* data, zdwl_ipc_output_v2* zdwl_output_v2, uint32_t tag, uint32_t state, @@ -163,7 +163,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con i <<= 1; } - struct wl_output* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + struct wl_output *output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); output_status_ = zdwl_ipc_manager_v2_get_output(status_manager_, output); zdwl_ipc_output_v2_add_listener(output_status_, &output_status_listener_impl, this); @@ -223,4 +223,16 @@ void Tags::handle_view_tags(uint32_t tag, uint32_t state, uint32_t clients, uint } } +void Tags::handle_active_output(zdwl_ipc_output_v2* zdwl_output_v2, uint32_t active) { + if (output_status_ == zdwl_output_v2) { + for (size_t i = 0; i < buttons_.size(); ++i) { + if (active == 0) { + buttons_[i].get_style_context()->remove_class("output"); + } else { + buttons_[i].get_style_context()->add_class("output"); + } + } + } +} + } /* namespace waybar::modules::dwl */ diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index f8faf60c..a864daea 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -114,8 +114,8 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con control_{nullptr}, seat_{nullptr}, bar_(bar), - box_{bar.orientation, 0}, output_{nullptr}, + box_{bar.orientation, 0}, output_status_{nullptr}, seat_status_{nullptr} { struct wl_display* display = Client::inst()->wl_display; @@ -204,8 +204,7 @@ Tags::~Tags() { void Tags::handle_show() { if (!status_manager_) return; - struct wl_output* output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - 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); seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); From 8b5b1eebbfa40325b77629b03a62f82d4a048158 Mon Sep 17 00:00:00 2001 From: Mervius Date: Wed, 15 Apr 2026 10:35:26 -0500 Subject: [PATCH 166/416] Check if output still is in outputs before doing anything with it --- src/client.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client.cpp b/src/client.cpp index ff354e18..99a74123 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -95,7 +95,7 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o if (!client->bars_scheduled_) { client->bars_scheduled_ = true; - + Glib::signal_idle().connect_once([client]() { client->createBarsBatch(); }, Glib::PRIORITY_HIGH_IDLE); @@ -107,6 +107,8 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } void waybar::Client::createBarsBatch() { + pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), + [output](const auto& o) { return &o == output; }); }); for (auto* output : pending_outputs_) { try { auto configs = getOutputConfigs(*output); From 09b6df37caa2d05bfc783e5729120ecc81f74576 Mon Sep 17 00:00:00 2001 From: Mervius Date: Wed, 15 Apr 2026 10:39:21 -0500 Subject: [PATCH 167/416] Check if output still is in outputs before doing anything with it --- include/client.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/client.hpp b/include/client.hpp index 8598e356..10a634fc 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -59,7 +59,7 @@ class Client { std::string m_cssFile; sigc::connection monitor_added_connection_; sigc::connection monitor_removed_connection_; - std::vector pending_outputs_; + std::list pending_outputs_; bool bars_scheduled_ = false; }; From d9b5d24284f2e91f53f496ad15d31843bb879497 Mon Sep 17 00:00:00 2001 From: Mervius Date: Wed, 15 Apr 2026 10:55:49 -0500 Subject: [PATCH 168/416] Check if output still is in outputs before doing anything with it --- src/client.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 99a74123..4d8bcb65 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -107,8 +107,7 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } void waybar::Client::createBarsBatch() { - pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), - [output](const auto& o) { return &o == output; }); }); + pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [output](const auto& o) { return &o == output; }); }); for (auto* output : pending_outputs_) { try { auto configs = getOutputConfigs(*output); From e2d608e7fdc4e69aff491a6561ef43212fd4a5fa Mon Sep 17 00:00:00 2001 From: Mervius Date: Wed, 15 Apr 2026 10:59:03 -0500 Subject: [PATCH 169/416] Check if output still is in outputs before doing anything with it --- src/client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.cpp b/src/client.cpp index 4d8bcb65..94fcfe41 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -107,7 +107,7 @@ void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_o } void waybar::Client::createBarsBatch() { - pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [output](const auto& o) { return &o == output; }); }); + pending_outputs_.remove_if([this](auto* output) { return std::none_of(outputs_.begin(), outputs_.end(), [&output](const auto& o) { return &o == output; }); }); for (auto* output : pending_outputs_) { try { auto configs = getOutputConfigs(*output); From 0241d0eac6febbfbcffd498535cad680263c0391 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:27:39 +0200 Subject: [PATCH 170/416] Remove redefinition or SIGRTMIN/SIGRTMAX for OpenBSD --- include/util/SafeSignal.hpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/include/util/SafeSignal.hpp b/include/util/SafeSignal.hpp index afb0893e..b1e3a91a 100644 --- a/include/util/SafeSignal.hpp +++ b/include/util/SafeSignal.hpp @@ -12,11 +12,6 @@ #include #include -#ifdef __OpenBSD__ -#define SIGRTMIN SIGUSR1 - 1 -#define SIGRTMAX SIGUSR1 + 1 -#endif - namespace waybar { /** From bd39795f806048a2d480c0aa5522fe3cde4047b1 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:31:59 +0200 Subject: [PATCH 171/416] Fix build for systems without SIGRTMIN/SIGRTMAX --- src/main.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 9d7d0ba2..0866a9b7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,9 +55,11 @@ static void catchSignals(waybar::SafeSignal& signal_handler) { std::signal(SIGINT, writeSignalToPipe); std::signal(SIGCHLD, writeSignalToPipe); +#ifdef SIGRTMIN for (int sig = SIGRTMIN + 1; sig <= SIGRTMAX; ++sig) { std::signal(sig, writeSignalToPipe); } +#endif while (true) { int signum; @@ -119,13 +121,15 @@ void handleUserSignal(int signal, bool& reload) { // If this signal should restart or close the bar, this function will write // `true` or `false`, respectively, into `reload`. static void handleSignalMainThread(int signum, bool& reload) { +#ifdef SIGRTMIN if (signum >= SIGRTMIN + 1 && signum <= SIGRTMAX) { for (auto& bar : waybar::Client::inst()->bars) { bar->handleSignal(signum); } return; } - +#endif + switch (signum) { case SIGUSR1: handleUserSignal(SIGUSR1, reload); From 729dbbb82e81b5feb095c1312ccd04a5e3795289 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:33:27 +0200 Subject: [PATCH 172/416] Fix build for systems without SIGRTMIN/SIGRTMAX --- src/modules/image.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 98bf3c46..48729cc4 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -46,9 +46,11 @@ void waybar::modules::Image::delayWorker() { } void waybar::modules::Image::refresh(int sig) { +#ifdef SIGRTMIN if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); } +#endif } auto waybar::modules::Image::update() -> void { From 29979e9227e06db41bde47e89a088ecfc169c756 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:35:31 +0200 Subject: [PATCH 173/416] Fix build for systems without SIGRTMIN/SIGRTMAX --- src/modules/custom.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 28def8c9..99d072de 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -138,9 +138,11 @@ void waybar::modules::Custom::waitingWorker() { } void waybar::modules::Custom::refresh(int sig) { +#ifdef SIGRTMIN if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); } +#endif } void waybar::modules::Custom::handleEvent() { From ab7bc8de1c7319fa7398fcc448616a2bee075034 Mon Sep 17 00:00:00 2001 From: cebem1nt Date: Sat, 18 Apr 2026 13:34:29 -0300 Subject: [PATCH 174/416] feature: group - add reveal-delay config option for drawer --- include/group.hpp | 3 +++ src/group.cpp | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/include/group.hpp b/include/group.hpp index d5cbb289..c5faf7c3 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -10,6 +10,8 @@ namespace waybar { class Group : public AModule { + sigc::connection reveal_timeout_; + public: Group(const std::string&, const std::string&, const Json::Value&, bool); ~Group() override = default; @@ -26,6 +28,7 @@ class Group : public AModule { bool is_first_widget = true; bool is_drawer = false; bool click_to_reveal = false; + int reveal_delay = 0; std::string add_class_to_drawer_children; bool handleMouseEnter(GdkEventCrossing* const& ev) override; bool handleMouseLeave(GdkEventCrossing* const& ev) override; diff --git a/src/group.cpp b/src/group.cpp index acd7bd42..92734fc9 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -63,6 +63,7 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& ? drawer_config["transition-left-to-right"].asBool() : true); click_to_reveal = drawer_config["click-to-reveal"].asBool(); + reveal_delay = drawer_config["reveal-delay"].asInt(); const bool start_expanded = (drawer_config["start-expanded"].isBool() ? drawer_config["start-expanded"].asBool() @@ -104,13 +105,27 @@ void Group::hide_group() { bool Group::handleMouseEnter(GdkEventCrossing* const& e) { if (!click_to_reveal) { - show_group(); + if (reveal_delay > 0) { + if (reveal_timeout_.connected()) { + reveal_timeout_.disconnect(); + } + + reveal_timeout_ = Glib::signal_timeout().connect( + [this]() { show_group(); return false; }, + reveal_delay); + } else { + show_group(); + } } return false; } bool Group::handleMouseLeave(GdkEventCrossing* const& e) { if (!click_to_reveal && e->detail != GDK_NOTIFY_INFERIOR) { + if (reveal_delay > 0 && reveal_timeout_.connected()) { + reveal_timeout_.disconnect(); + } + hide_group(); } return false; From 7e0e99b5aa8423ada72a3018588104550d671315 Mon Sep 17 00:00:00 2001 From: GlassyBridge <188959481+GlassyBridge@users.noreply.github.com> Date: Sun, 19 Apr 2026 02:21:17 +0300 Subject: [PATCH 175/416] feat (hyprland/workspaces): add support for state specific icon overrides --- src/modules/hyprland/workspace.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 21e7ef9b..263e50e0 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -155,6 +155,11 @@ bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payloa std::string& Workspace::selectIcon(std::map& icons_map) { spdlog::trace("Selecting icon for workspace {}", name()); if (isUrgent()) { + auto urgentNamedIconIt = icons_map.find("urgent:" + name()); + if (urgentNamedIconIt != icons_map.end()) { + return urgentNamedIconIt->second; + } + auto urgentIconIt = icons_map.find("urgent"); if (urgentIconIt != icons_map.end()) { return urgentIconIt->second; @@ -162,6 +167,11 @@ std::string& Workspace::selectIcon(std::map& icons_map } if (isActive()) { + auto activeNamedIconIt = icons_map.find("active:" + name()); + if (activeNamedIconIt != icons_map.end()) { + return activeNamedIconIt->second; + } + auto activeIconIt = icons_map.find("active"); if (activeIconIt != icons_map.end()) { return activeIconIt->second; @@ -169,6 +179,11 @@ std::string& Workspace::selectIcon(std::map& icons_map } if (isSpecial()) { + auto specialNamedIconIt = icons_map.find("special:" + name()); + if (specialNamedIconIt != icons_map.end()) { + return specialNamedIconIt->second; + } + auto specialIconIt = icons_map.find("special"); if (specialIconIt != icons_map.end()) { return specialIconIt->second; From 255f17d13cf2d920f9459c3762d159b5dbcd9a91 Mon Sep 17 00:00:00 2001 From: Evans Date: Tue, 28 Apr 2026 03:24:55 +0000 Subject: [PATCH 176/416] battery: guard _exists = true assignment inside if condition --- src/modules/battery.cpp | 77 ++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 30be3857..537c62b0 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -296,63 +296,73 @@ waybar::modules::Battery::getInfos() { int32_t _current_now_int = 0; bool current_now_exists = false; if (fs::exists(bat / "current_now")) { - current_now_exists = true; - std::ifstream(bat / "current_now") >> _current_now_int; + if (std::ifstream(bat / "current_now") >> _current_now_int) { + current_now_exists = true; + } } else if (fs::exists(bat / "current_avg")) { - current_now_exists = true; - std::ifstream(bat / "current_avg") >> _current_now_int; + if (std::ifstream(bat / "current_avg") >> _current_now_int) { + current_now_exists = true; + } } // Documentation ABI allows a negative value when discharging, positive // value when charging. current_now = std::abs(_current_now_int); if (fs::exists(bat / "time_to_empty_now")) { - time_to_empty_now_exists = true; - std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now; + if (std::ifstream(bat / "time_to_empty_now") >> time_to_empty_now) { + time_to_empty_now_exists = true; + } } if (fs::exists(bat / "time_to_full_now")) { - time_to_full_now_exists = true; - std::ifstream(bat / "time_to_full_now") >> time_to_full_now; + if (std::ifstream(bat / "time_to_full_now") >> time_to_full_now) { + time_to_full_now_exists = true; + } } uint32_t voltage_now = 0; bool voltage_now_exists = false; if (fs::exists(bat / "voltage_now")) { - voltage_now_exists = true; - std::ifstream(bat / "voltage_now") >> voltage_now; + if (std::ifstream(bat / "voltage_now") >> voltage_now) { + voltage_now_exists = true; + } } else if (fs::exists(bat / "voltage_avg")) { - voltage_now_exists = true; - std::ifstream(bat / "voltage_avg") >> voltage_now; + if (std::ifstream(bat / "voltage_avg") >> voltage_now) { + voltage_now_exists = true; + } } uint32_t charge_full = 0; bool charge_full_exists = false; if (fs::exists(bat / "charge_full")) { - charge_full_exists = true; - std::ifstream(bat / "charge_full") >> charge_full; + if (std::ifstream(bat / "charge_full") >> charge_full) { + charge_full_exists = true; + } } uint32_t charge_full_design = 0; bool charge_full_design_exists = false; if (fs::exists(bat / "charge_full_design")) { - charge_full_design_exists = true; - std::ifstream(bat / "charge_full_design") >> charge_full_design; + if (std::ifstream(bat / "charge_full_design") >> charge_full_design) { + charge_full_design_exists = true; + } } uint32_t charge_now = 0; bool charge_now_exists = false; if (fs::exists(bat / "charge_now")) { - charge_now_exists = true; - std::ifstream(bat / "charge_now") >> charge_now; + if (std::ifstream(bat / "charge_now") >> charge_now) { + charge_now_exists = true; + } } uint32_t power_now = 0; int32_t _power_now_int = 0; bool power_now_exists = false; if (fs::exists(bat / "power_now")) { - power_now_exists = true; - std::ifstream(bat / "power_now") >> _power_now_int; + if (std::ifstream(bat / "power_now") >> _power_now_int) { + power_now_exists = true; + } } // Some drivers (example: Qualcomm) exposes use a negative value when // discharging, positive value when charging. @@ -361,22 +371,25 @@ waybar::modules::Battery::getInfos() { uint32_t energy_now = 0; bool energy_now_exists = false; if (fs::exists(bat / "energy_now")) { - energy_now_exists = true; - std::ifstream(bat / "energy_now") >> energy_now; + if (std::ifstream(bat / "energy_now") >> energy_now) { + energy_now_exists = true; + } } uint32_t energy_full = 0; bool energy_full_exists = false; if (fs::exists(bat / "energy_full")) { - energy_full_exists = true; - std::ifstream(bat / "energy_full") >> energy_full; + if (std::ifstream(bat / "energy_full") >> energy_full) { + energy_full_exists = true; + } } uint32_t energy_full_design = 0; bool energy_full_design_exists = false; if (fs::exists(bat / "energy_full_design")) { - energy_full_design_exists = true; - std::ifstream(bat / "energy_full_design") >> energy_full_design; + if (std::ifstream(bat / "energy_full_design") >> energy_full_design) { + energy_full_design_exists = true; + } } uint16_t cycleCount = 0; @@ -412,8 +425,9 @@ waybar::modules::Battery::getInfos() { capacity_exists = true; capacity = 100 * (uint64_t)energy_now / (uint64_t)energy_full; } else if (fs::exists(bat / "capacity")) { - capacity_exists = true; - std::ifstream(bat / "capacity") >> capacity; + if (std::ifstream(bat / "capacity") >> capacity) { + capacity_exists = true; + } } if (!voltage_now_exists) { @@ -593,11 +607,12 @@ waybar::modules::Battery::getInfos() { float calculated_capacity{0.0f}; if (total_capacity_exists) { - if (total_capacity > 0.0f) + if (total_capacity > 0.0f) { calculated_capacity = (float)total_capacity / batteries_.size(); - else if (total_energy_full_exists && total_energy_exists) { - if (total_energy_full > 0.0f) + } else if (total_energy_full_exists && total_energy_exists) { + if (total_energy_full > 0.0f) { calculated_capacity = ((float)total_energy * 100.0f / (float)total_energy_full); + } } } From a66d6b1596a6c32697776f5d1b4a7ed09da70d52 Mon Sep 17 00:00:00 2001 From: Neltharion Date: Wed, 29 Apr 2026 03:40:06 +0300 Subject: [PATCH 177/416] Do not reconnect if connection was refused Otherwise, it will try to reconnect recursively --- src/util/audio_backend.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 4087e096..fb368ca5 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -99,15 +99,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { nullptr, nullptr); break; case PA_CONTEXT_FAILED: - // When pulseaudio server restarts, the connection is "failed". Try to reconnect. - // pa_threaded_mainloop_lock is already acquired in callback threads. - // So there is no need to lock it again. - if (backend->context_ != nullptr) { - pa_context_disconnect(backend->context_); - pa_context_unref(backend->context_); - backend->context_ = nullptr; + if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) { + // When pulseaudio server restarts, the connection is "failed". Try to reconnect. + // pa_threaded_mainloop_lock is already acquired in callback threads. + // So there is no need to lock it again. + if (backend->context_ != nullptr) { + pa_context_disconnect(backend->context_); + pa_context_unref(backend->context_); + backend->context_ = nullptr; + } + backend->connectContext(); } - backend->connectContext(); break; case PA_CONTEXT_CONNECTING: case PA_CONTEXT_AUTHORIZING: From b28af75e62f8c6739990e94c383447e30cd74411 Mon Sep 17 00:00:00 2001 From: shiyi Date: Wed, 29 Apr 2026 23:34:09 +0800 Subject: [PATCH 178/416] feat(language): add tooltip support with configurable tooltip-format --- src/modules/hyprland/language.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 6e0fe23d..ed608e10 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -58,6 +58,25 @@ 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(); } From c81a039a55271394e4aebd0e928ff1c83dd091a0 Mon Sep 17 00:00:00 2001 From: Yui Date: Fri, 1 May 2026 22:29:48 +0300 Subject: [PATCH 179/416] feat: add compact versions of bandwidthBytes to network module Adds the following: - bandwidthDownByesCompact - bandwidthUpByesCompact Differences from normal version: - drop the "/s" suffix - drop the deciaml part if less than 1MB/s --- include/util/format.hpp | 8 +++++--- src/modules/network.cpp | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/include/util/format.hpp b/include/util/format.hpp index c8ed837a..2eb148c4 100644 --- a/include/util/format.hpp +++ b/include/util/format.hpp @@ -5,12 +5,13 @@ class pow_format { public: - pow_format(long long val, std::string&& unit, bool binary = false) - : val_(val), unit_(unit), binary_(binary) {}; + pow_format(long long val, std::string&& unit, bool binary = false, int min_pow_for_decimal = 0) + : val_(val), unit_(unit), binary_(binary), min_pow_for_decimal_(min_pow_for_decimal) {}; long long val_; std::string unit_; bool binary_; + int min_pow_for_decimal_; }; namespace fmt { @@ -74,7 +75,8 @@ struct formatter { break; case 0: default: - format = "{coefficient:.1f}{prefix}{unit}"; + format = pow < s.min_pow_for_decimal_ ? "{coefficient:.0f}{prefix}{unit}" + : "{coefficient:.1f}{prefix}{unit}"; break; } return fmt::format_to( diff --git a/src/modules/network.cpp b/src/modules/network.cpp index f43893c5..8bb54fce 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -368,6 +368,10 @@ auto waybar::modules::Network::update() -> void { pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), + fmt::arg("bandwidthDownBytesCompact", + pow_format(bandwidth_down / elapsed_seconds, "B", false, 2)), + fmt::arg("bandwidthUpBytesCompact", + pow_format(bandwidth_up / elapsed_seconds, "B", false, 2)), fmt::arg("bandwidthTotalBytes", pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (text.compare(label_.get_label()) != 0) { @@ -401,6 +405,10 @@ auto waybar::modules::Network::update() -> void { pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), + fmt::arg("bandwidthDownBytesCompact", + pow_format(bandwidth_down / elapsed_seconds, "B", false, 2)), + fmt::arg("bandwidthUpBytesCompact", + pow_format(bandwidth_up / elapsed_seconds, "B", false, 2)), fmt::arg("bandwidthTotalBytes", pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); if (label_.get_tooltip_text() != tooltip_text) { From d1d501450d27eeb31504ff5379537f27ff7373dc Mon Sep 17 00:00:00 2001 From: nonamescm Date: Mon, 4 May 2026 03:38:17 -0300 Subject: [PATCH 180/416] Add static river tags --- man/waybar-river-tags.5.scd | 1 + src/modules/river/tags.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index 64621229..7481b974 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -50,6 +50,7 @@ Addressed by *river/tags* - *#tags button.occupied* - *#tags button.focused* - *#tags button.urgent* +- *#tags button.tag-N* Note that occupied/focused/urgent status may overlap. That is, a tag may be both occupied and focused at the same time. diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index f06565f6..5381bf9f 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -149,6 +149,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con button.signal_button_press_event().connect( sigc::bind(sigc::mem_fun(*this, &Tags::handle_button_press), (1 << tag))); } + button.get_style_context()->add_class("tag-" + std::to_string(tag + 1)); button.show(); } From 5c3ec1fd634731ccc61aca7c41211643af0295c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Rodon?= Date: Mon, 11 May 2026 22:47:24 +0200 Subject: [PATCH 181/416] fix(tray): hide module when no items are visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tray::update() already had logic to hide the module when all child items were passive, but it was only re-run on item add/remove — never on a status change. When the last visible item transitioned to Passive, the item hid itself but the now-empty module remained visible. Connect Tray to each item's Gtk::EventBox signal_show/signal_hide so update() runs on every visibility transition, and simplify update() to check child->get_visible() directly instead of inspecting the `passive` CSS class. The Tray-level `show-passive-items` read becomes redundant since Item already honours it when deciding its own visibility; remove it along with the now-unused Tray::show_passive_ member. Fixes: #3721 --- include/modules/sni/tray.hpp | 1 - src/modules/sni/tray.cpp | 17 ++++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 5f12d7f2..6cda35d7 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -21,7 +21,6 @@ class Tray : public AModule { void onRemove(std::unique_ptr& item); static inline std::size_t nb_hosts_ = 0; - bool show_passive_ = false; Gtk::Box box_; SNI::Watcher::singleton watcher_; SNI::Host host_; diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 34a3c05f..126ee03d 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -23,9 +23,6 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) if (config_["spacing"].isUInt()) { box_.set_spacing(config_["spacing"].asUInt()); } - if (config["show-passive-items"].isBool()) { - show_passive_ = config["show-passive-items"].asBool(); - } nb_hosts_ += 1; if (config_["icons"].isObject()) { IconManager::instance().setIconsConfig(config_["icons"]); @@ -39,6 +36,8 @@ void Tray::onAdd(std::unique_ptr& item) { } else { box_.pack_start(item->event_box); } + item->event_box.signal_show().connect([this] { dp.emit(); }); + item->event_box.signal_hide().connect([this] { dp.emit(); }); dp.emit(); } @@ -48,17 +47,9 @@ void Tray::onRemove(std::unique_ptr& item) { } auto Tray::update() -> void { - // Show tray only when items are available std::vector children = box_.get_children(); - if (show_passive_) { - event_box_.set_visible(!children.empty()); - } else { - event_box_.set_visible(!std::all_of(children.begin(), children.end(), [](Gtk::Widget* child) { - return child->get_style_context()->has_class("passive"); - })); - } - - // Call parent update + event_box_.set_visible(std::any_of(children.begin(), children.end(), + [](Gtk::Widget* child) { return child->get_visible(); })); AModule::update(); } From 2a05edaf6aea1509555d5b3f3bc57549287ae98e Mon Sep 17 00:00:00 2001 From: yyyumeniku Date: Tue, 12 May 2026 20:09:13 -0400 Subject: [PATCH 182/416] hyprland/workspaces: add debounce timer to prevent workspace button flicker When switching between workspaces rapidly (especially empty ones), Hyprland sends createworkspace and destroyworkspace events slightly out of order. This causes workspace buttons to render in wrong positions for a split second before snapping to their correct spots. Add a 7ms debounce timer that batches workspace events before calling dp.emit(), preventing the visual glitch. Fixes #4376 --- include/modules/hyprland/workspaces.hpp | 3 + src/modules/hyprland/workspaces.cpp | 90 +++++++++++++++---------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 03548ccb..be5bc66d 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -211,6 +211,9 @@ class Workspaces : public AModule, public EventHandler { Gtk::Box m_box; sigc::connection m_scrollEventConnection_; IPC& m_ipc; + + sigc::connection m_debounceTimer; + bool m_updatePending = false; }; } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 2496117f..762060e2 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -338,46 +338,62 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& c } void Workspaces::onEvent(const std::string& ev) { - std::lock_guard lock(m_mutex); - const auto separator = ev.find(">>"); - if (separator == std::string::npos) { - spdlog::warn("Malformed Hyprland workspace event: {}", ev); - return; - } - std::string eventName = ev.substr(0, separator); - std::string payload = ev.substr(separator + 2); + { + std::lock_guard lock(m_mutex); + const auto separator = ev.find(">>"); + if (separator == std::string::npos) { + spdlog::warn("Malformed Hyprland workspace event: {}", ev); + return; + } + std::string eventName = ev.substr(0, separator); + std::string payload = ev.substr(separator + 2); - if (eventName == "workspacev2") { - onWorkspaceActivated(payload); - } else if (eventName == "activespecial") { - onSpecialWorkspaceActivated(payload); - } else if (eventName == "destroyworkspacev2") { - onWorkspaceDestroyed(payload); - } else if (eventName == "createworkspacev2") { - onWorkspaceCreated(payload); - } else if (eventName == "focusedmonv2") { - onMonitorFocused(payload); - } else if (eventName == "moveworkspacev2") { - onWorkspaceMoved(payload); - } else if (eventName == "openwindow") { - onWindowOpened(payload); - } else if (eventName == "closewindow") { - onWindowClosed(payload); - } else if (eventName == "movewindowv2") { - onWindowMoved(payload); - } else if (eventName == "urgent") { - setUrgentWorkspace(payload); - } else if (eventName == "renameworkspace") { - onWorkspaceRenamed(payload); - } else if (eventName == "windowtitlev2") { - onWindowTitleEvent(payload); - } else if (eventName == "activewindowv2") { - onActiveWindowChanged(payload); - } else if (eventName == "configreloaded") { - onConfigReloaded(); + if (eventName == "workspacev2") { + onWorkspaceActivated(payload); + } else if (eventName == "activespecial") { + onSpecialWorkspaceActivated(payload); + } else if (eventName == "destroyworkspacev2") { + onWorkspaceDestroyed(payload); + } else if (eventName == "createworkspacev2") { + onWorkspaceCreated(payload); + } else if (eventName == "focusedmonv2") { + onMonitorFocused(payload); + } else if (eventName == "moveworkspacev2") { + onWorkspaceMoved(payload); + } else if (eventName == "openwindow") { + onWindowOpened(payload); + } else if (eventName == "closewindow") { + onWindowClosed(payload); + } else if (eventName == "movewindowv2") { + onWindowMoved(payload); + } else if (eventName == "urgent") { + setUrgentWorkspace(payload); + } else if (eventName == "renameworkspace") { + onWorkspaceRenamed(payload); + } else if (eventName == "windowtitlev2") { + onWindowTitleEvent(payload); + } else if (eventName == "activewindowv2") { + onActiveWindowChanged(payload); + } else if (eventName == "configreloaded") { + onConfigReloaded(); + } } - dp.emit(); + if (m_debounceTimer.connected()) { + m_debounceTimer.disconnect(); + m_updatePending = false; + } + + m_updatePending = true; + m_debounceTimer = Glib::signal_timeout().connect([this]() { + if (!m_updatePending) return false; + std::lock_guard lock(m_mutex); + if (m_updatePending) { + dp.emit(); + m_updatePending = false; + } + return false; + }, 7); } void Workspaces::onWorkspaceActivated(std::string const& payload) { From 3971bb6c00cc437dab6fc92c86ea4b8be63d42e7 Mon Sep 17 00:00:00 2001 From: Antoine Gaudreau Simard Date: Sat, 20 Dec 2025 18:07:52 -0500 Subject: [PATCH 183/416] feat: add support for multiple batteries in a single bluetooth device --- include/modules/bluetooth.hpp | 6 ++ man/waybar-bluetooth.5.scd | 14 ++++ src/modules/bluetooth.cpp | 151 +++++++++++++++++++++++++++++++++- 3 files changed, 168 insertions(+), 3 deletions(-) diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index b89383a0..a06d4645 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -41,6 +41,7 @@ class Bluetooth : public ALabel { bool services_resolved; // NOTE: experimental feature in bluez std::optional battery_percentage; + std::optional battery_percentage_peripheral; }; public: @@ -59,6 +60,11 @@ class Bluetooth : public ALabel { gpointer) -> void; auto getDeviceBatteryPercentage(GDBusObject*) -> std::optional; + auto getDeviceGattBatteryLevels(GDBusObject*, std::optional&, + std::optional&) -> void; + static auto processBatteryServiceCharacteristics(GList*, const std::string&, const std::string&, + const std::string&, std::optional&, + std::optional&) -> void; auto getDeviceProperties(GDBusObject*, DeviceInfo&) -> bool; auto getControllerProperties(GDBusObject*, ControllerInfo&) -> bool; diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index fd7d5fb5..36c6443a 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -178,6 +178,9 @@ At the time of writing, the experimental features of BlueZ need to be turned on, *{device_battery_percentage}*: Battery percentage of the displayed device if available. Use only in the config options defined below. +*{device_battery_percentage_peripheral}*: Battery percentage of the peripheral half of a split keyboard (e.g., ZMK keyboards with separate central and peripheral batteries). ++ +This is read from GATT Battery Service characteristics that have a User Description descriptor. Use only in the config options defined below. + ## CONFIGURATION *format-connected-battery*: ++ @@ -220,6 +223,17 @@ At the time of writing, the experimental features of BlueZ need to be turned on, } ``` +Split keyboard with separate central/peripheral batteries (e.g., ZMK): + +``` +"bluetooth": { + "format-device-preference": [ "Keyball44" ], + "format": "", + "format-connected-battery": " {device_battery_percentage}%|{device_battery_percentage_peripheral}%", + "tooltip-format-connected": "{device_alias}\\nCentral: {device_battery_percentage}%\\nPeripheral: {device_battery_percentage_peripheral}%" +} +``` + # STYLE - *#bluetooth* diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index c59af3b5..97aad8b2 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -83,6 +83,66 @@ auto getUcharProperty(GDBusProxy* proxy, const char* property_name) -> unsigned return 0; } +auto isChildPath(const std::string& child, const std::string& parent) -> bool { + return child.starts_with(parent); +} + +auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional { + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + + GError* error = nullptr; + GVariant* gvar = g_dbus_proxy_call_sync(proxy_char, "ReadValue", g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + if (error != nullptr) { + g_error_free(error); + return std::nullopt; + } + if (gvar == nullptr) { + return std::nullopt; + } + + GVariant* value_array = g_variant_get_child_value(gvar, 0); + gsize n_elements; + const auto* data = + static_cast(g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar))); + + std::optional result; + if (data != nullptr && n_elements > 0) { + result = data[0]; + } + + g_variant_unref(value_array); + g_variant_unref(gvar); + return result; +} + +auto hasUserDescriptionDescriptor(GList* objects, const std::string& char_path, + const std::string& user_description_uuid) -> bool { + for (GList* n = objects; n != nullptr; n = n->next) { + GDBusObject* desc_object = G_DBUS_OBJECT(n->data); + std::string desc_path = g_dbus_object_get_object_path(desc_object); + + if (!isChildPath(desc_path, char_path)) { + continue; + } + + GDBusProxy* proxy_desc = + G_DBUS_PROXY(g_dbus_object_get_interface(desc_object, "org.bluez.GattDescriptor1")); + if (proxy_desc == nullptr) { + continue; + } + + auto desc_uuid = getOptionalStringProperty(proxy_desc, "UUID"); + g_object_unref(proxy_desc); + + if (desc_uuid.has_value() && desc_uuid.value().find(user_description_uuid) != std::string::npos) { + return true; + } + } + return false; +} + } // namespace waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& config) @@ -232,8 +292,9 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::arg("device_address", cur_focussed_device_.address), fmt::arg("device_address_type", cur_focussed_device_.address_type), fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_label), - fmt::arg("device_battery_percentage", - cur_focussed_device_.battery_percentage.value_or(0)))); + fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + cur_focussed_device_.battery_percentage_peripheral.value_or(0)))); } if (tooltipEnabled()) { @@ -258,7 +319,9 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::runtime(enumerate_format), fmt::arg("device_address", dev.address), fmt::arg("device_address_type", dev.address_type), fmt::arg("device_alias", dev.alias), fmt::arg("icon", enumerate_icon), - fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0))); + fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + dev.battery_percentage_peripheral.value_or(0))); } } device_enumerate_ = ss.str(); @@ -278,6 +341,8 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::arg("device_address_type", cur_focussed_device_.address_type), fmt::arg("device_alias", cur_focussed_device_.alias), fmt::arg("icon", icon_tooltip), fmt::arg("device_battery_percentage", cur_focussed_device_.battery_percentage.value_or(0)), + fmt::arg("device_battery_percentage_peripheral", + cur_focussed_device_.battery_percentage_peripheral.value_or(0)), fmt::arg("device_enumerate", device_enumerate_))); } @@ -398,6 +463,84 @@ auto waybar::modules::Bluetooth::getDeviceBatteryPercentage(GDBusObject* object) return std::nullopt; } +auto waybar::modules::Bluetooth::getDeviceGattBatteryLevels( + GDBusObject* device_object, std::optional& central_battery, + std::optional& peripheral_battery) -> void { + const std::string BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb"; + const std::string BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb"; + const std::string USER_DESCRIPTION_UUID = "00002901-0000-1000-8000-00805f9b34fb"; + + GList* objects = g_dbus_object_manager_get_objects(manager_.get()); + std::string device_path = g_dbus_object_get_object_path(device_object); + + for (GList* l = objects; l != nullptr; l = l->next) { + GDBusObject* service_object = G_DBUS_OBJECT(l->data); + std::string service_path = g_dbus_object_get_object_path(service_object); + + if (!isChildPath(service_path, device_path)) { + continue; + } + + GDBusProxy* proxy_service = + G_DBUS_PROXY(g_dbus_object_get_interface(service_object, "org.bluez.GattService1")); + if (proxy_service == nullptr) { + continue; + } + + auto service_uuid = getOptionalStringProperty(proxy_service, "UUID"); + g_object_unref(proxy_service); + + if (!service_uuid.has_value() || + service_uuid.value().find(BATTERY_SERVICE_UUID) == std::string::npos) { + continue; + } + + processBatteryServiceCharacteristics(objects, service_path, BATTERY_LEVEL_UUID, + USER_DESCRIPTION_UUID, central_battery, peripheral_battery); + } + + g_list_free_full(objects, g_object_unref); +} + +auto waybar::modules::Bluetooth::processBatteryServiceCharacteristics( + GList* objects, const std::string& service_path, const std::string& battery_level_uuid, + const std::string& user_description_uuid, std::optional& central_battery, + std::optional& peripheral_battery) -> void { + for (GList* m = objects; m != nullptr; m = m->next) { + GDBusObject* char_object = G_DBUS_OBJECT(m->data); + std::string char_path = g_dbus_object_get_object_path(char_object); + + if (!isChildPath(char_path, service_path)) { + continue; + } + + GDBusProxy* proxy_char = + G_DBUS_PROXY(g_dbus_object_get_interface(char_object, "org.bluez.GattCharacteristic1")); + if (proxy_char == nullptr) { + continue; + } + + auto char_uuid = getOptionalStringProperty(proxy_char, "UUID"); + if (!char_uuid.has_value() || char_uuid.value().find(battery_level_uuid) == std::string::npos) { + g_object_unref(proxy_char); + continue; + } + + auto battery_value = readBatteryCharacteristicValue(proxy_char); + g_object_unref(proxy_char); + + if (!battery_value.has_value()) { + continue; + } + + if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) { + peripheral_battery = battery_value.value(); + } else { + central_battery = battery_value.value(); + } + } +} + auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, DeviceInfo& device_info) -> bool { GDBusProxy* proxy_device = G_DBUS_PROXY(g_dbus_object_get_interface(object, "org.bluez.Device1")); @@ -418,6 +561,8 @@ 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); return true; } From 7105ad9bbacc94ace219985ef909431fc250e83c Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Thu, 14 May 2026 01:22:25 +0200 Subject: [PATCH 184/416] waybar-styles.5: add semicolon to css import statement --- man/waybar-styles.5.scd.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in index b11e15bd..e5a70517 100644 --- a/man/waybar-styles.5.scd.in +++ b/man/waybar-styles.5.scd.in @@ -21,7 +21,7 @@ It uses the first file found in this search order: An example user-controlled stylesheet that just changes the color of the clock to be green on black, while keeping the rest of the system config the same would be: ``` -@import url("file:///etc/xdg/waybar/style.css") +@import url("file:///etc/xdg/waybar/style.css"); #clock { background: #000000; From 3229c2c9fc2a7dd97eae1cd4b7d4937e591f7c96 Mon Sep 17 00:00:00 2001 From: Agnibha Mukherjee Date: Thu, 14 May 2026 11:01:58 +0530 Subject: [PATCH 185/416] feat: support Pango markup in remaining tooltips --- src/modules/systemd_failed_units.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 8d7e866f..1a3fdf6e 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -292,7 +292,7 @@ auto SystemdFailedUnits::update() -> void { fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_), fmt::arg("failed_units_list", failed_list))); } else { - label_.set_tooltip_text(""); + label_.set_tooltip_markup(""); } } ALabel::update(); From 0ec6d06472cb42a061d9503fc426b47bf6cf3301 Mon Sep 17 00:00:00 2001 From: Milad Alizadeh Date: Fri, 15 May 2026 17:29:21 +0100 Subject: [PATCH 186/416] Guard set_current_layout against unknown layout names (wtype, hot-plugged devices) so transient events stop blanking layout_. Refresh layouts_map_ on "added"/"xkb_keymap" input events and union layouts across all keyboards so new devices contribute their layouts. Release mutex_ before the refresh sendCmd to avoid self-deadlock from the synchronous signal_cmd emit. --- src/modules/sway/language.cpp | 57 ++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index 2e1103d3..28f1ef24 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -54,21 +54,28 @@ void Language::onCmd(const struct Ipc::ipc_response& res) { std::lock_guard lock(mutex_); auto payload = parser_.parse(res.payload); std::vector used_layouts; - // Display current layout of a device with a maximum count of layouts, expecting that all will - // be OK + // Union layout names across every keyboard input so hot-plugged devices contribute their + // layouts to the map. Track the device with the most layouts to seed the initially displayed + // layout, matching the previous behaviour at startup. Json::ArrayIndex max_id = 0, max = 0; for (Json::ArrayIndex i = 0; i < payload.size(); i++) { - auto size = payload[i][XKB_LAYOUT_NAMES_KEY].size(); - if (size > max) { - max = size; + if (payload[i]["type"].asString() != "keyboard") continue; + const auto& names = payload[i][XKB_LAYOUT_NAMES_KEY]; + if (names.size() > max) { + max = names.size(); max_id = i; } + for (const auto& layout : names) { + const auto name = layout.asString(); + if (std::find(used_layouts.begin(), used_layouts.end(), name) == used_layouts.end()) { + used_layouts.push_back(name); + } + } } - for (const auto& layout : payload[max_id][XKB_LAYOUT_NAMES_KEY]) { - used_layouts.push_back(layout.asString()); - } - + // Rebuild from scratch so init_layouts_map's duplicate-suffix pass doesn't compound across + // refreshes (e.g. "us" -> "us1" -> "us11"). + layouts_map_.clear(); init_layouts_map(used_layouts); set_current_layout(payload[max_id][XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); dp.emit(); @@ -82,13 +89,26 @@ void Language::onEvent(const struct Ipc::ipc_response& res) { return; } + bool refresh_inputs = false; try { - std::lock_guard lock(mutex_); - auto payload = parser_.parse(res.payload)["input"]; - if (payload["type"].asString() == "keyboard") { - set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + { + std::lock_guard lock(mutex_); + auto root = parser_.parse(res.payload); + auto change = root["change"].asString(); + auto payload = root["input"]; + if (payload["type"].asString() == "keyboard") { + // A device was added or its keymap changed - the layout set may have grown, so refresh + // layouts_map_ via IPC_GET_INPUTS once we've released mutex_. + refresh_inputs = (change == "added" || change == "xkb_keymap"); + set_current_layout(payload[XKB_ACTIVE_LAYOUT_NAME_KEY].asString()); + } + dp.emit(); + } + // sendCmd is synchronous: it blocks on the IPC reply and then emits signal_cmd on this same + // thread, which lands in onCmd and re-locks mutex_. Must call it with mutex_ released. + if (refresh_inputs) { + ipc_.sendCmd(IPC_GET_INPUTS); } - dp.emit(); } catch (const std::exception& e) { spdlog::error("Language: {}", e.what()); } @@ -125,8 +145,15 @@ auto Language::update() -> void { } auto Language::set_current_layout(const std::string& current_layout) -> void { + // Guard against unknown / empty layout names: transient virtual keyboards (e.g. wtype) and + // hot-plugged devices whose layouts haven't made it into the map yet would otherwise blank out + // layout_ via map::operator[]'s default-construct-on-miss. + auto it = layouts_map_.find(current_layout); + if (it == layouts_map_.end()) { + return; + } label_.get_style_context()->remove_class(layout_.short_name); - layout_ = layouts_map_[current_layout]; + layout_ = it->second; label_.get_style_context()->add_class(layout_.short_name); } From 51becf2fbcf19bca2e7079657ca1d1a40647a43e Mon Sep 17 00:00:00 2001 From: Jack Barnes Date: Sat, 16 May 2026 13:03:53 +0800 Subject: [PATCH 187/416] feat(niri/workspaces): Add `enable-bar-scroll` option --- include/modules/niri/workspaces.hpp | 1 + man/waybar-niri-workspaces.5.scd | 5 ++++ src/modules/niri/workspaces.cpp | 45 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index 08986412..01840b33 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -20,6 +20,7 @@ class Workspaces : public AModule, public EventHandler { void doUpdate(); Gtk::Button& addButton(const Json::Value& ws); std::string getIcon(const std::string& value, const Json::Value& ws); + bool handleScroll(GdkEventScroll* /*unused*/) override; const Bar& bar_; Gtk::Box box_; diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 4be85eb3..ec329404 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -31,6 +31,11 @@ Addressed by *niri/workspaces* default: false ++ If set to false, you can click to change workspace. If set to true this behaviour is disabled. +*enable-bar-scroll*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can't scroll to cycle throughout workspaces from the entire bar. If set to true this behaviour is enabled. + *disable-markup*: ++ typeof: bool ++ default: false ++ diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 97d15215..97472260 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -22,6 +22,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); gIPC->registerForIPC("WorkspaceUrgencyChanged", this); + if (config["enable-bar-scroll"].asBool()) { + auto& window = const_cast(bar_).window; + window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); + } + dp.emit(); } @@ -198,4 +204,43 @@ std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) return value; } +bool Workspaces::handleScroll(GdkEventScroll* e) { + if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) { + /** + * Ignore emulated scroll events on window + */ + return false; + } + + auto dir = AModule::getScrollDir(e); + if (dir == SCROLL_DIR::NONE) { + return true; + } + + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + + std::string action_name; + + if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) { + action_name = "FocusWorkspaceDown"; + } else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) { + action_name = "FocusWorkspaceUp"; + } else { + return true; + } + + action[action_name] = Json::Value(Json::objectValue); + + IPC::send(request); + + } catch (const std::exception& e) { + spdlog::error("Workspaces: {}", e.what()); + return false; + } + + return true; +} + } // namespace waybar::modules::niri From cdb792af41e78d01263c2152d8b5050c40705ea9 Mon Sep 17 00:00:00 2001 From: Tom Benham Date: Sat, 16 May 2026 18:03:09 +0200 Subject: [PATCH 188/416] fix(hyprland/workspaces): fix IPC call when using `"move-to-monitor": true` --- src/modules/hyprland/backend.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index 08cf97c1..f38f8216 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -320,7 +320,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri // New format: /dispatch hl.dsp.focus({ workspace = "1" }) // // Old format: dispatch focusworkspaceoncurrentmonitor 2 - // New format: /dispatch hl.dsp.focus({ workspace = "2", monitor = "current" }) + // New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true }) // // Old format: dispatch togglespecialworkspace name // New format: /dispatch hl.dsp.workspace.toggle_special("name") @@ -329,7 +329,7 @@ std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::stri return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })"; } if (dispatcher == "focusworkspaceoncurrentmonitor") { - return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", monitor = \"current\" })"; + return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })"; } if (dispatcher == "togglespecialworkspace") { if (arg.empty()) { From 40c9d92e3de1ee287da1ac4901264d1571fe803d Mon Sep 17 00:00:00 2001 From: Tom Benham Date: Sat, 16 May 2026 21:29:23 +0200 Subject: [PATCH 189/416] test(hyprland): fix dispatch test --- test/hyprland/backend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/hyprland/backend.cpp b/test/hyprland/backend.cpp index 62d23ae4..f2e10dae 100644 --- a/test/hyprland/backend.cpp +++ b/test/hyprland/backend.cpp @@ -160,7 +160,7 @@ TEST_CASE("buildLuaDispatch focusworkspaceoncurrentmonitor", "[buildLuaDispatch] IPCTestHelper::buildLuaDispatch("focusworkspaceoncurrentmonitor", "3"); REQUIRE( result == - "/dispatch hl.dsp.focus({ workspace = \"3\", monitor = \"current\" })"); + "/dispatch hl.dsp.focus({ workspace = \"3\", on_current_monitor = true })"); } TEST_CASE("buildLuaDispatch togglespecialworkspace", "[buildLuaDispatch]") { From 7e2afaa5b3d78ae5c526e4d95708c9e9f52bb0e5 Mon Sep 17 00:00:00 2001 From: Desuwa Date: Sat, 16 May 2026 14:05:17 -0700 Subject: [PATCH 190/416] fix: mpd specify timer interval in seconds --- include/modules/mpd/state.inl.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp index 895970e6..a55a7948 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -1,13 +1,14 @@ #pragma once namespace detail { +using namespace std::literals::chrono_literals; inline bool Context::is_connected() const { return mpd_module_->connection_ != nullptr; } inline bool Context::is_playing() const { return mpd_module_->playing(); } inline bool Context::is_paused() const { return mpd_module_->paused(); } inline bool Context::is_stopped() const { return mpd_module_->stopped(); } -constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_.count(); } +constexpr inline std::size_t Context::interval() const { return mpd_module_->interval_ / 1s; } inline void Context::tryConnect() const { mpd_module_->tryConnect(); } inline unique_connection& Context::connection() { return mpd_module_->connection_; } constexpr inline mpd_state Context::state() const { return mpd_module_->state_; } From 546b53d6759c2b7cc54051889d99fccaa6c57295 Mon Sep 17 00:00:00 2001 From: Desuwa Date: Sat, 16 May 2026 15:41:09 -0700 Subject: [PATCH 191/416] mpd: Implement idle events for Playing state --- include/modules/mpd/state.hpp | 4 ++ src/modules/mpd/state.cpp | 114 +++++++++++++++++++++++++++++----- 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 2c9071b4..35e8dd39 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -82,6 +82,7 @@ class Idle : public State { class Playing : public State { Context* const ctx_; sigc::connection timer_connection_; + sigc::connection idle_connection_; public: Playing(Context* const ctx) : ctx_{ctx} {} @@ -98,7 +99,10 @@ class Playing : public State { Playing(Playing const&) = delete; Playing& operator=(Playing const&) = delete; + void timer() noexcept; + void idle() noexcept; bool on_timer(); + bool on_io(Glib::IOCondition const&); }; class Paused : public State { diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 3d7c8561..80b4933d 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -19,13 +19,13 @@ auto format_as(enum mpd_idle val) { namespace waybar::modules::detail { -#define IDLE_RUN_NOIDLE_AND_CMD(...) \ +#define RUN_NOIDLE_AND_CMD(STATE, ...) \ if (idle_connection_.connected()) { \ idle_connection_.disconnect(); \ auto conn = ctx_->connection().get(); \ if (!mpd_run_noidle(conn)) { \ if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \ - spdlog::error("mpd: Idle: failed to unregister for IDLE events"); \ + spdlog::error("mpd: STATE: failed to unregister for IDLE events"); \ ctx_->checkErrors(conn); \ } \ } \ @@ -33,24 +33,23 @@ namespace waybar::modules::detail { } void Idle::play() { - IDLE_RUN_NOIDLE_AND_CMD(mpd_run_play(conn)); + RUN_NOIDLE_AND_CMD(Idle, mpd_run_play(conn)); ctx_->setState(std::make_unique(ctx_)); } void Idle::pause() { - IDLE_RUN_NOIDLE_AND_CMD(mpd_run_pause(conn, true)); + RUN_NOIDLE_AND_CMD(Idle, mpd_run_pause(conn, true)); ctx_->setState(std::make_unique(ctx_)); } void Idle::stop() { - IDLE_RUN_NOIDLE_AND_CMD(mpd_run_stop(conn)); + RUN_NOIDLE_AND_CMD(Idle, mpd_run_stop(conn)); ctx_->setState(std::make_unique(ctx_)); } -#undef IDLE_RUN_NOIDLE_AND_CMD void Idle::update() noexcept { // This is intentionally blank. @@ -118,18 +117,50 @@ bool Idle::on_io(Glib::IOCondition const&) { } void Playing::entry() noexcept { - sigc::slot timer_slot = sigc::mem_fun(*this, &Playing::on_timer); - timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1); + timer(); + idle(); spdlog::debug("mpd: Playing: enabled 1 second periodic timer."); } void Playing::exit() noexcept { + if (idle_connection_.connected()) { + idle_connection_.disconnect(); + spdlog::debug("mpd: Playing: unwatching FD"); + } + if (timer_connection_.connected()) { timer_connection_.disconnect(); spdlog::debug("mpd: Playing: disabled 1 second periodic timer."); } } +void Playing::timer() noexcept { + if (timer_connection_.connected()) { + timer_connection_.disconnect(); + } + + sigc::slot timer_slot = sigc::mem_fun(*this, &Playing::on_timer); + timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1); +} + + +void Playing::idle() noexcept { + auto conn = ctx_->connection().get(); + assert(conn != nullptr); + + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Playing: failed to register for IDLE events"); + } else if (!idle_connection_.connected()) { + spdlog::trace("mpd: Playing: watching FD"); + sigc::slot idle_slot = sigc::mem_fun(*this, &Playing::on_io); + idle_connection_ = + Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), + Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + } +} + bool Playing::on_timer() { // Attempt to connect with MPD. try { @@ -141,6 +172,8 @@ bool Playing::on_timer() { return false; } + RUN_NOIDLE_AND_CMD(Playing); + ctx_->fetchState(); if (!ctx_->is_playing()) { @@ -154,6 +187,50 @@ bool Playing::on_timer() { ctx_->queryMPD(); ctx_->emit(); + + idle(); + } catch (std::exception const& e) { + spdlog::warn("mpd: Playing: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); + return false; + } + + return true; +} + +bool Playing::on_io(Glib::IOCondition const&) { + auto conn = ctx_->connection().get(); + + // callback should do this: + enum mpd_idle events = mpd_recv_idle(conn, /* ignore_timeout?= */ false); + spdlog::debug("mpd: Playing: recv_idle events -> {}", events); + + mpd_response_finish(conn); + try { + ctx_->checkErrors(conn); + + ctx_->fetchState(); + + if (!ctx_->is_playing()) { + if (ctx_->is_paused()) { + ctx_->setState(std::make_unique(ctx_)); + } else { + ctx_->setState(std::make_unique(ctx_)); + } + return false; + } + + ctx_->queryMPD(); + ctx_->emit(); + + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Playing: failed to register for IDLE events"); + } + + // Defer the next timer + timer(); } catch (std::exception const& e) { spdlog::warn("mpd: Playing: error: {}", e.what()); ctx_->setState(std::make_unique(ctx_)); @@ -164,21 +241,25 @@ bool Playing::on_timer() { } void Playing::stop() { - if (timer_connection_.connected()) { - timer_connection_.disconnect(); + RUN_NOIDLE_AND_CMD(Playing, + if (timer_connection_.connected()) { + timer_connection_.disconnect(); - mpd_run_stop(ctx_->connection().get()); - } + mpd_run_stop(ctx_->connection().get()); + } + ); ctx_->setState(std::make_unique(ctx_)); } void Playing::pause() { - if (timer_connection_.connected()) { - timer_connection_.disconnect(); + RUN_NOIDLE_AND_CMD(Playing, + if (timer_connection_.connected()) { + timer_connection_.disconnect(); - mpd_run_pause(ctx_->connection().get(), true); - } + mpd_run_pause(ctx_->connection().get(), true); + } + ); ctx_->setState(std::make_unique(ctx_)); } @@ -387,4 +468,5 @@ bool Disconnected::on_timer() { void Disconnected::update() noexcept { ctx_->do_update(); } +#undef RUN_NOIDLE_AND_CMD } // namespace waybar::modules::detail From f7001410d2fa25792390a2fa4d48b6fa6a23881b Mon Sep 17 00:00:00 2001 From: Desuwa Date: Sat, 16 May 2026 16:32:42 -0700 Subject: [PATCH 192/416] mpd: ensure emit is called after state changes --- src/modules/mpd/state.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 80b4933d..9b22eb4e 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -96,19 +96,16 @@ bool Idle::on_io(Glib::IOCondition const&) { } ctx_->fetchState(); + ctx_->emit(); mpd_state state = ctx_->state(); if (state == MPD_STATE_STOP) { - ctx_->emit(); ctx_->setState(std::make_unique(ctx_)); } else if (state == MPD_STATE_PLAY) { - ctx_->emit(); ctx_->setState(std::make_unique(ctx_)); } else if (state == MPD_STATE_PAUSE) { - ctx_->emit(); ctx_->setState(std::make_unique(ctx_)); } else { - ctx_->emit(); // self transition ctx_->setState(std::make_unique(ctx_)); } @@ -177,6 +174,7 @@ bool Playing::on_timer() { ctx_->fetchState(); if (!ctx_->is_playing()) { + ctx_->emit(); if (ctx_->is_paused()) { ctx_->setState(std::make_unique(ctx_)); } else { @@ -212,6 +210,7 @@ bool Playing::on_io(Glib::IOCondition const&) { ctx_->fetchState(); if (!ctx_->is_playing()) { + ctx_->emit(); if (ctx_->is_paused()) { ctx_->setState(std::make_unique(ctx_)); } else { @@ -293,7 +292,6 @@ bool Paused::on_timer() { } ctx_->fetchState(); - ctx_->emit(); if (ctx_->is_paused()) { @@ -364,7 +362,6 @@ bool Stopped::on_timer() { } ctx_->fetchState(); - ctx_->emit(); if (ctx_->is_stopped()) { From ff76e96786cbdd0f586bc9c97e88a414635f2365 Mon Sep 17 00:00:00 2001 From: Adam Druzd Date: Sun, 17 May 2026 09:12:08 +0200 Subject: [PATCH 193/416] fix(hyprland): keep workspace hover over taskbar icons --- include/modules/hyprland/workspace.hpp | 11 +++ src/modules/hyprland/workspace.cpp | 102 +++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 1d80d331..5730dda9 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -45,6 +45,15 @@ class Workspace { bool isUrgent() const { return m_isUrgent; }; bool handleClicked(GdkEventButton* bt) const; + + bool handleEnter(GdkEventCrossing* event); + bool handleLeave(GdkEventCrossing* event); + + void startHoverCheck(); + void stopHoverCheck(); + bool syncHoverClass(); + bool pointerInsideButton(); + void setActive(bool value = true) { m_isActive = value; }; void setPersistentRule(bool value = true) { m_isPersistentRule = value; }; void setPersistentConfig(bool value = true) { m_isPersistentConfig = value; }; @@ -80,6 +89,8 @@ class Workspace { bool m_isUrgent = false; bool m_isVisible = false; + sigc::connection m_hoverCheckConnection; + std::vector m_windowMap; Gtk::Button m_button; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 753893f2..67ef4506 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -30,6 +31,11 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma } m_button.add_events(Gdk::BUTTON_PRESS_MASK); + m_button.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK); + + m_button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleEnter)); + m_button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleLeave)); + m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked), false); @@ -66,6 +72,102 @@ std::optional Workspace::closeWindow(WindowAddress const& addr) { return std::nullopt; } +bool Workspace::pointerInsideButton() { + auto display = Gdk::Display::get_default(); + if (!display) { + return false; + } + + auto seat = display->get_default_seat(); + if (!seat) { + return false; + } + + auto pointer = seat->get_pointer(); + if (!pointer) { + return false; + } + + Glib::RefPtr screen; + int pointerRootX = 0; + int pointerRootY = 0; + + pointer->get_position(screen, pointerRootX, pointerRootY); + + Gtk::Widget* toplevel = m_button.get_toplevel(); + if (toplevel == nullptr || !toplevel->get_window()) { + return false; + } + + int buttonX = 0; + int buttonY = 0; + + if (!m_button.translate_coordinates(*toplevel, 0, 0, buttonX, buttonY)) { + return false; + } + + int windowRootX = 0; + int windowRootY = 0; + toplevel->get_window()->get_root_origin(windowRootX, windowRootY); + + const auto allocation = m_button.get_allocation(); + + const int buttonRootX = windowRootX + buttonX; + const int buttonRootY = windowRootY + buttonY; + const int buttonWidth = allocation.get_width(); + const int buttonHeight = allocation.get_height(); + + return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY && + pointerRootX < buttonRootX + buttonWidth && + pointerRootY < buttonRootY + buttonHeight; +} + +bool Workspace::syncHoverClass() { + auto styleContext = m_button.get_style_context(); + + if (pointerInsideButton()) { + styleContext->add_class("workspace-hover"); + return true; + } + + styleContext->remove_class("workspace-hover"); + stopHoverCheck(); + return false; +} + +void Workspace::startHoverCheck() { + if (m_hoverCheckConnection.connected()) { + return; + } + + m_hoverCheckConnection = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &Workspace::syncHoverClass), + 50); +} + +void Workspace::stopHoverCheck() { + if (m_hoverCheckConnection.connected()) { + m_hoverCheckConnection.disconnect(); + } +} + +bool Workspace::handleEnter(GdkEventCrossing* /*event*/) { + m_button.get_style_context()->add_class("workspace-hover"); + startHoverCheck(); + return false; +} + +bool Workspace::handleLeave(GdkEventCrossing* /*event*/) { + /* + * Do not remove immediately. + * Workspace taskbar children can fire misleading leave events while the + * pointer is still visually inside the workspace button. + * + * The polling check will remove the class once the pointer really leaves. + */ + startHoverCheck(); + return false; +} bool Workspace::handleClicked(GdkEventButton* bt) const { if (bt->type == GDK_BUTTON_PRESS) { try { From 0351ac8fb2d0f204f67f9b2ddbe7a1c8d54ba413 Mon Sep 17 00:00:00 2001 From: finitemonkey <56678540+finitemonkey@users.noreply.github.com> Date: Sat, 16 May 2026 20:40:01 +0100 Subject: [PATCH 194/416] Additional options when muted for the PulseAudio-Slider module: - Add "muted" class for styling the muted state. - Add "zero-on-mute" boolean option to control slider position when muted. - Add "unmute-on-volume-change" boolean option to control whether to automatically unmute when the volume changes. --- include/modules/pulseaudio_slider.hpp | 16 +++-- include/util/audio_backend.hpp | 11 +++- man/waybar-pulseaudio-slider.5.scd | 28 +++++++-- src/modules/pulseaudio_slider.cpp | 84 ++++++++++----------------- src/util/audio_backend.cpp | 24 ++++++++ 5 files changed, 98 insertions(+), 65 deletions(-) diff --git a/include/modules/pulseaudio_slider.hpp b/include/modules/pulseaudio_slider.hpp index 3ef44684..c9f82341 100644 --- a/include/modules/pulseaudio_slider.hpp +++ b/include/modules/pulseaudio_slider.hpp @@ -6,10 +6,6 @@ #include "util/audio_backend.hpp" namespace waybar::modules { -enum class PulseaudioSliderTarget { - Sink, - Source, -}; class PulseaudioSlider : public ASlider { public: @@ -21,7 +17,15 @@ class PulseaudioSlider : public ASlider { private: std::shared_ptr backend = nullptr; - PulseaudioSliderTarget target = PulseaudioSliderTarget::Sink; + util::PulseaudioTarget target = util::PulseaudioTarget::Sink; + + bool zero_on_mute = true; + bool unmute_on_volume_change = true; + // zero_on_mute and unmute_on_volume_change default to true + // in order to maintain previous behaviour when using a + // config in which these values are undefined + + bool previously_muted = false; }; -} // namespace waybar::modules \ No newline at end of file +} // namespace waybar::modules diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp index 3737ae26..f5904b43 100644 --- a/include/util/audio_backend.hpp +++ b/include/util/audio_backend.hpp @@ -14,6 +14,11 @@ namespace waybar::util { +enum class PulseaudioTarget { + Sink, + Source, +}; + class AudioBackend { private: static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); @@ -92,7 +97,11 @@ class AudioBackend { void toggleSourceMute(); void toggleSourceMute(bool); + uint16_t getVolume(PulseaudioTarget) const; + bool getMuted(PulseaudioTarget) const; + void unmute(PulseaudioTarget); + bool isBluetooth(); }; -} // namespace waybar::util \ No newline at end of file +} // namespace waybar::util diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index 0271e7c5..3f56a80c 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -28,9 +28,20 @@ The volume can be controlled by dragging the slider across the bar or clicking o The orientation of the slider. Can be either `horizontal` or `vertical`. *expand*: ++ - typeof: bool ++ - default: false ++ - Enables this module to consume all left over space dynamically. + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +*zero-on-mute*: ++ + typeof: bool ++ + default: true ++ + `true` = The slider will be set to `min` when the source/sink is muted. ++ + `false` = The slider will continue to show the unmuted volume level when the source/sink is muted. + +*unmute-on-volume-change*: ++ + typeof: bool ++ + default: true ++ + Specifies whether to unmute a muted souce/sink when its volume is changed by the user moving the slider. # EXAMPLES @@ -41,7 +52,9 @@ The volume can be controlled by dragging the slider across the bar or clicking o "pulseaudio/slider": { "min": 0, "max": 100, - "orientation": "horizontal" + "orientation": "horizontal", + "zero-on-mute": false, + "unmute-on-volume-change": false } ``` @@ -52,6 +65,9 @@ The slider is a component with multiple CSS Nodes, of which the following are ex *#pulseaudio-slider*: ++ Controls the style of the box *around* the slider and bar. +*#pulseaudio-slider.muted*: ++ + Controls the style when the audio source/sink is muted. + *#pulseaudio-slider slider*: ++ Controls the style of the slider handle. @@ -85,4 +101,8 @@ The slider is a component with multiple CSS Nodes, of which the following are ex border-radius: 5px; background: green; } + +#pulseaudio-slider.muted highlight { + background-color: orange; +} ``` diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp index bf85584e..aeddeb24 100644 --- a/src/modules/pulseaudio_slider.cpp +++ b/src/modules/pulseaudio_slider.cpp @@ -10,73 +10,49 @@ PulseaudioSlider::PulseaudioSlider(const std::string& id, const Json::Value& con if (config_["target"].isString()) { std::string target = config_["target"].asString(); if (target == "sink") { - this->target = PulseaudioSliderTarget::Sink; + this->target = util::PulseaudioTarget::Sink; } else if (target == "source") { - this->target = PulseaudioSliderTarget::Source; + this->target = util::PulseaudioTarget::Source; } } + + if (config_["zero-on-mute"].isBool()) { + zero_on_mute = config_["zero-on-mute"].asBool(); + } + + if (config_["unmute-on-volume-change"].isBool()) { + unmute_on_volume_change = config_["unmute-on-volume-change"].asBool(); + } } void PulseaudioSlider::update() { - switch (target) { - case PulseaudioSliderTarget::Sink: - if (backend->getSinkMuted()) { - scale_.set_value(min_); - } else { - scale_.set_value(backend->getSinkVolume()); - } - break; + uint16_t display_value = backend->getVolume(target); + bool is_muted = backend->getMuted(target); - case PulseaudioSliderTarget::Source: - if (backend->getSourceMuted()) { - scale_.set_value(min_); - } else { - scale_.set_value(backend->getSourceVolume()); - } - break; + if (is_muted && !previously_muted) { + if (zero_on_mute) { + display_value = min_; + } + scale_.get_style_context()->add_class("muted"); + } else if (previously_muted && !is_muted) { + scale_.get_style_context()->remove_class("muted"); } + + scale_.set_value(display_value); + + previously_muted = is_muted; } void PulseaudioSlider::onValueChanged() { - bool is_mute = false; + uint16_t slider_value = scale_.get_value(); - switch (target) { - case PulseaudioSliderTarget::Sink: - if (backend->getSinkMuted()) { - is_mute = true; - } - break; - - case PulseaudioSliderTarget::Source: - if (backend->getSourceMuted()) { - is_mute = true; - } - break; - } - - uint16_t volume = scale_.get_value(); - - if (is_mute) { - // Avoid setting sink/source to volume 0 if the user muted if via another mean. - if (volume == 0) { - return; - } - - // If the sink/source is mute, but the user clicked the slider, unmute it! - else { - switch (target) { - case PulseaudioSliderTarget::Sink: - backend->toggleSinkMute(false); - break; - - case PulseaudioSliderTarget::Source: - backend->toggleSourceMute(false); - break; - } + // Avoid setting sink/source to volume 0 if the user muted it via other means. + if (!backend->getMuted(target) || slider_value != 0) { + if (unmute_on_volume_change) { + backend->unmute(target); } + backend->changeVolume(slider_value, min_, max_); } - - backend->changeVolume(volume, min_, max_); } -} // namespace waybar::modules \ No newline at end of file +} // namespace waybar::modules diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 4087e096..d617813d 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -259,6 +259,14 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo pa_context_get_source_info_list(context, sourceInfoCb, data); } +uint16_t AudioBackend::getVolume(PulseaudioTarget target) const { + if (target == PulseaudioTarget::Source) { + return source_volume_; + } else { + return volume_; + } +} + void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) { // Early return if context is not ready if ((context_ == nullptr) || pa_context_get_state(context_) != PA_CONTEXT_READY) { @@ -368,6 +376,14 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma pa_threaded_mainloop_unlock(mainloop_); } +bool AudioBackend::getMuted(PulseaudioTarget target) const { + if (target == PulseaudioTarget::Source) { + return source_muted_; + } else { + return muted_; + } +} + void AudioBackend::toggleSinkMute() { if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = !muted_; @@ -404,6 +420,14 @@ void AudioBackend::toggleSourceMute(bool mute) { pa_threaded_mainloop_unlock(mainloop_); } +void AudioBackend::unmute(PulseaudioTarget target) { + if (target == PulseaudioTarget::Source) { + toggleSourceMute(false); + } else { + toggleSinkMute(false); + } +} + bool AudioBackend::isBluetooth() { return monitor_.find("a2dp_sink") != std::string::npos || // PulseAudio monitor_.find("a2dp-sink") != std::string::npos || // PipeWire From 23e635acf8f1cd2e87674357aae319d5078820ea Mon Sep 17 00:00:00 2001 From: finitemonkey <56678540+finitemonkey@users.noreply.github.com> Date: Sat, 16 May 2026 21:49:36 +0100 Subject: [PATCH 195/416] Add missing documentation for the "target" option of the PulseAudio-Slider module. --- man/waybar-pulseaudio-slider.5.scd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index 0271e7c5..729fe910 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -22,6 +22,12 @@ The volume can be controlled by dragging the slider across the bar or clicking o default: 100 ++ The maximum volume value the slider should display and set. +*target*: ++ + typeof: string ++ + default: sink ++ + Whether the slider will control the default audio source or default audio sink. ++ + Valid values are either "sink" or "source". + *orientation*: ++ typeof: string ++ default: horizontal ++ From 6bdafede071c2e18711bfb3804f852208debd48b Mon Sep 17 00:00:00 2001 From: Ivan Ribeiro Date: Tue, 19 May 2026 21:21:56 +0100 Subject: [PATCH 196/416] feat(cpu): add {icons} format replacement for all per-core icons Concatenates all per-core icons into a single {icons} placeholder, so configs work across machines with different core counts without manually specifying {icon0}{icon1}...{iconN}. Closes #4240 --- man/waybar-cpu.5.scd | 12 ++++++++++++ src/modules/cpu.cpp | 6 +++++- src/modules/cpu_usage/common.cpp | 6 +++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index 40682372..5dd4a893 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -106,6 +106,8 @@ The *cpu* module displays the current CPU utilization. *{icon*{n}*}*: Icon for CPU core n usage. Use like {icon0}. +*{icon0}{icon1}{icon2}{icon3}*: All per-core icons concatenated. Equivalent to {icon0}{icon1}...{icon*N*} but adapts to the number of cores automatically. + # EXAMPLES Basic configuration: @@ -128,6 +130,16 @@ CPU usage per core rendered as icons: }, ``` +Automatically determine number of icons according to number of logical cores: + +``` +"cpu": { + "interval": 1, + "format": "{icons} {usage:>2}% ", + "format-icons": ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"], +}, +``` + # STYLE - *#cpu* diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 8cfda2c1..9f3245f0 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -48,13 +48,17 @@ auto waybar::modules::Cpu::update() -> void { store.push_back(fmt::arg("avg_frequency", avg_frequency)); std::vector arg_names; arg_names.reserve(cpu_usage.size() * 2); + std::string all_icons; for (size_t i = 1; i < cpu_usage.size(); ++i) { auto core_i = i - 1; arg_names.push_back(fmt::format("usage{}", core_i)); store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i])); + auto core_icon = getIcon(cpu_usage[i], icons); + all_icons += core_icon; arg_names.push_back(fmt::format("icon{}", core_i)); - store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons))); + store.push_back(fmt::arg(arg_names.back().c_str(), core_icon)); } + store.push_back(fmt::arg("icons", all_icons)); label_.set_markup(fmt::vformat(format, store)); if (tooltipEnabled()) { diff --git a/src/modules/cpu_usage/common.cpp b/src/modules/cpu_usage/common.cpp index 1571cb74..79ce6941 100644 --- a/src/modules/cpu_usage/common.cpp +++ b/src/modules/cpu_usage/common.cpp @@ -38,13 +38,17 @@ auto waybar::modules::CpuUsage::update() -> void { store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); std::vector arg_names; arg_names.reserve(cpu_usage.size() * 2); + std::string all_icons; for (size_t i = 1; i < cpu_usage.size(); ++i) { auto core_i = i - 1; arg_names.push_back(fmt::format("usage{}", core_i)); store.push_back(fmt::arg(arg_names.back().c_str(), cpu_usage[i])); + auto core_icon = getIcon(cpu_usage[i], icons); + all_icons += core_icon; arg_names.push_back(fmt::format("icon{}", core_i)); - store.push_back(fmt::arg(arg_names.back().c_str(), getIcon(cpu_usage[i], icons))); + store.push_back(fmt::arg(arg_names.back().c_str(), core_icon)); } + store.push_back(fmt::arg("icons", all_icons)); label_.set_markup(fmt::vformat(format, store)); if (tooltipEnabled()) { From 876eaaf70f9ec9d42992849e0cb44533637a7f8c Mon Sep 17 00:00:00 2001 From: Francois Lachese Date: Wed, 20 May 2026 15:57:38 +0200 Subject: [PATCH 197/416] fix(amodule): prevent crash when cursor timeout fires after destruction --- include/AModule.hpp | 1 + src/AModule.cpp | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index a338ffe3..7119b2f6 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -57,6 +57,7 @@ class AModule : public IModule { bool hasUserEvents_; gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; + sigc::connection cursor_timeout_conn_; std::map eventActionMap_; static const inline std::map, std::string> eventMap_{ {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, diff --git a/src/AModule.cpp b/src/AModule.cpp index a5ba69d3..ea9fd2b4 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -17,7 +17,8 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: isTooltip{config_["tooltip"].isBool() ? config_["tooltip"].asBool() : true}, isExpand{config_["expand"].isBool() ? config_["expand"].asBool() : false}, distance_scrolled_y_(0.0), - distance_scrolled_x_(0.0) { + distance_scrolled_x_(0.0), + cursor_timeout_conn_() { // Configure module action Map const Json::Value actions{config_["actions"]}; @@ -83,6 +84,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } AModule::~AModule() { + if (cursor_timeout_conn_.connected()) { + cursor_timeout_conn_.disconnect(); + } for (const auto& pid : pid_children_) { if (pid != -1) { killpg(pid, SIGTERM); @@ -118,7 +122,7 @@ void AModule::setCursor(Gdk::CursorType const& c) { } else { // window may not be accessible yet, in this case, // schedule another call for setting the cursor in 1 sec - Glib::signal_timeout().connect_seconds( + cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds( [this, c]() { setCursor(c); return false; From fcf1fb798cf9674761a4d83a5c11ff15213e2006 Mon Sep 17 00:00:00 2001 From: Ezequiel Colunga Acha Date: Wed, 20 May 2026 15:06:41 -0300 Subject: [PATCH 198/416] fix(wlr/taskbar): let task titles shrink within available space The taskbar currently requests enough width for every task title, which can push right-side modules off-screen when many windows are open. Allow task buttons to share the taskbar allocation and ellipsize title labels instead of forcing the whole module to grow. This keeps neighboring modules visible while preserving the existing taskbar click actions and formatting behavior. Co-authored-by: Cursor --- src/modules/wlr/taskbar.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index bde90910..6ba41ce3 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -93,10 +93,20 @@ Task::Task(const waybar::Bar& bar, const Json::Value& config, Taskbar* tbar, zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); button.set_relief(Gtk::RELIEF_NONE); + button.set_hexpand(true); + content_.set_hexpand(true); + text_before_.set_ellipsize(Pango::ELLIPSIZE_END); + text_before_.set_single_line_mode(true); + text_before_.set_width_chars(1); + text_before_.set_xalign(0.0); + text_after_.set_ellipsize(Pango::ELLIPSIZE_END); + text_after_.set_single_line_mode(true); + text_after_.set_width_chars(1); + text_after_.set_xalign(0.0); - content_.add(text_before_); - content_.add(icon_); - content_.add(text_after_); + content_.pack_start(text_before_, true, true, 0); + content_.pack_start(icon_, false, false, 0); + content_.pack_start(text_after_, true, true, 0); content_.show(); button.add(content_); @@ -718,7 +728,7 @@ void Taskbar::handle_finished() { } void Taskbar::add_button(Gtk::Button& bt) { - box_.pack_start(bt, false, false); + box_.pack_start(bt, true, true); box_.get_style_context()->remove_class("empty"); } From eb3b86c4d83a8e2e54eeca77a36861b5ccc8d432 Mon Sep 17 00:00:00 2001 From: buzz Date: Thu, 21 May 2026 17:02:39 +0200 Subject: [PATCH 199/416] fix(hyprland/workspaces): support on-scroll-up and on-scroll-down config options The handleScroll method unconditionally dispatched workspace cycling commands and never checked for custom on-scroll-up/on-scroll-down config values. Register the scroll handler when custom scroll commands are configured (even without enable-bar-scroll), and delegate to AModule::handleScroll so user commands are executed, matching the pattern used by other modules. --- src/modules/hyprland/workspaces.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 2496117f..c9fffc97 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -50,7 +50,8 @@ void Workspaces::init() { if (m_scrollEventConnection_.connected()) { m_scrollEventConnection_.disconnect(); } - if (barScroll()) { + bool hasScrollConfig = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString(); + if (barScroll() || hasScrollConfig) { auto& window = const_cast(m_bar).window; window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); m_scrollEventConnection_ = @@ -1188,6 +1189,12 @@ bool Workspaces::handleScroll(GdkEventScroll* e) { if (gdk_event_get_pointer_emulated((GdkEvent*)e)) { return false; } + + // Check for custom scroll commands first; delegate to base class + if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) { + return AModule::handleScroll(e); + } + auto dir = AModule::getScrollDir(e); if (dir == SCROLL_DIR::NONE) { return true; From ea36aab3babe79f646e87b8cd3a49d041dc32443 Mon Sep 17 00:00:00 2001 From: buzz Date: Thu, 21 May 2026 17:33:43 +0200 Subject: [PATCH 200/416] docs(hyprland/workspaces): add on-scroll-up and on-scroll-down to man page --- man/waybar-hyprland-workspaces.5.scd | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 5284ce99..c852f2ba 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -135,6 +135,14 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. default: false ++ If set to false, you can't scroll to cycle throughout workspaces from the entire bar. If set to true this behaviour is enabled. +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. This replaces the default behaviour of workspace cycling. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. This replaces the default behaviour of workspace cycling. + *ignore-workspaces*: ++ typeof: array ++ default: [] ++ From 85163c684746fe3f08180598bcb05768bbf1016d Mon Sep 17 00:00:00 2001 From: buzz Date: Thu, 21 May 2026 18:50:43 +0200 Subject: [PATCH 201/416] fix(hyprland/workspaces): replace EventBox with Button for taskbar windows --- src/modules/hyprland/workspace.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 753893f2..ca427dc9 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -301,15 +301,18 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { auto window_box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL); window_box->set_tooltip_markup(window_repr.window_title); - window_box->get_style_context()->add_class("taskbar-window"); + + auto button = Gtk::manage(new Gtk::Button()); + button->set_relief(Gtk::RELIEF_NONE); + button->add(*window_box); + button->get_style_context()->add_class("taskbar-window"); if (window_repr.isActive) { - window_box->get_style_context()->add_class("active"); + button->get_style_context()->add_class("active"); } - auto event_box = Gtk::manage(new Gtk::EventBox()); - event_box->add(*window_box); if (m_workspaceManager.onClickWindow() != "") { - event_box->signal_button_press_event().connect( - sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address)); + button->signal_button_press_event().connect( + sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), + false); } auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), @@ -334,8 +337,8 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { window_box->pack_start(*window_label_after, true, true); } - m_content.pack_start(*event_box, true, false); - event_box->show_all(); + m_content.pack_start(*button, true, false); + button->show_all(); }; if (m_workspaceManager.taskbarReverseDirection()) { From f8cc7173cae8e2913ca03d33a638f83ddfb84edd Mon Sep 17 00:00:00 2001 From: Siriusuna Date: Fri, 22 May 2026 02:44:50 +0800 Subject: [PATCH 202/416] refactor: extract utf8 truncation logic to utility Move the UTF-8 visual-width measurement and truncation helper functions from the mpris module to a common utility (include/util/utf8_string.hpp and src/util/utf8_string.cpp). This decouples string truncation and width measurement from the mpris module, allowing other modules (like mpd) to reuse. The helper function `utf8_truncate` (formerly `truncate` in mpris) and `utf8_width` is exported under the `waybar::util` namespace, while the low-level `measure_and_truncate` (formerly `utf8_truncate`) is encapsulated in an anonymous namespace in src/util/utf8_string.cpp to avoid unnecessary API exposure. No functional changes were made to the mpris module's behavior. --- include/util/utf8_string.hpp | 7 ++++ meson.build | 3 +- src/modules/mpris/mpris.cpp | 62 +++++------------------------------- src/util/utf8_string.cpp | 62 ++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 55 deletions(-) create mode 100644 include/util/utf8_string.hpp create mode 100644 src/util/utf8_string.cpp diff --git a/include/util/utf8_string.hpp b/include/util/utf8_string.hpp new file mode 100644 index 00000000..07a04466 --- /dev/null +++ b/include/util/utf8_string.hpp @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace waybar::util { +size_t utf8_width(const std::string& str); +void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len); +} // namespace waybar::util diff --git a/meson.build b/meson.build index 0c494eb2..e528a6da 100644 --- a/meson.build +++ b/meson.build @@ -186,7 +186,8 @@ src_files = files( 'src/util/icon_loader.cpp', 'src/util/regex_collection.cpp', 'src/util/css_reload_helper.cpp', - 'src/util/transform_8bit_to_rgba.cpp' + 'src/util/transform_8bit_to_rgba.cpp', + 'src/util/utf8_string.cpp' ) man_files = files( diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index e0a27c92..6ebf34f7 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -7,6 +7,7 @@ #include #include "util/scope_guard.hpp" +#include "util/utf8_string.hpp" extern "C" { #include @@ -202,69 +203,22 @@ auto Mpris::getIconFromJson(const Json::Value& icons, const std::string& key) -> // Wide characters count as two, zero-width characters count as zero // Modifies str in-place (unless width = std::string::npos) // Returns the total width of the string pre-truncating -size_t utf8_truncate(std::string& str, size_t width = std::string::npos) { - if (str.length() == 0) return 0; - - const gchar* trunc_end = nullptr; - - size_t total_width = 0; - - for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) { - gunichar c = g_utf8_get_char_validated(data, end - data); - if (c == -1U || c == -2U) { - // invalid unicode, treat string as ascii - if (width != std::string::npos && str.length() > width) str.resize(width); - return str.length(); - } else if (g_unichar_iswide(c)) { - total_width += 2; - } else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen - total_width += 1; - } - - data = g_utf8_find_next_char(data, end); - if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c)) - trunc_end = data; - } - - if (trunc_end) str.resize(trunc_end - str.data()); - - return total_width; -} - -size_t utf8_width(const std::string& str) { return utf8_truncate(const_cast(str)); } - -void truncate(std::string& s, const std::string& ellipsis, size_t max_len) { - if (max_len == 0) { - s.resize(0); - return; - } - size_t len = utf8_truncate(s, max_len); - if (len > max_len) { - size_t ellipsis_len = utf8_width(ellipsis); - if (max_len >= ellipsis_len) { - if (ellipsis_len) utf8_truncate(s, max_len - ellipsis_len); - s += ellipsis; - } else { - s.resize(0); - } - } -} auto Mpris::getArtistStr(const PlayerInfo& info, bool truncated) -> std::string { auto artist = info.artist.value_or(std::string()); - if (truncated && artist_len_ >= 0) truncate(artist, ellipsis_, artist_len_); + if (truncated && artist_len_ >= 0) waybar::util::utf8_truncate(artist, ellipsis_, artist_len_); return artist; } auto Mpris::getAlbumStr(const PlayerInfo& info, bool truncated) -> std::string { auto album = info.album.value_or(std::string()); - if (truncated && album_len_ >= 0) truncate(album, ellipsis_, album_len_); + if (truncated && album_len_ >= 0) waybar::util::utf8_truncate(album, ellipsis_, album_len_); return album; } auto Mpris::getTitleStr(const PlayerInfo& info, bool truncated) -> std::string { auto title = info.title.value_or(std::string()); - if (truncated && title_len_ >= 0) truncate(title, ellipsis_, title_len_); + if (truncated && title_len_ >= 0) waybar::util::utf8_truncate(title, ellipsis_, title_len_); return title; } @@ -292,9 +246,9 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> // keep position format same as length format auto position = getPositionStr(info, truncated && truncate_hours_ && length.length() < 6); - size_t artistLen = utf8_width(artist); - size_t albumLen = utf8_width(album); - size_t titleLen = utf8_width(title); + size_t artistLen = waybar::util::utf8_width(artist); + size_t albumLen = waybar::util::utf8_width(album); + size_t titleLen = waybar::util::utf8_width(title); size_t lengthLen = length.length(); size_t posLen = position.length(); @@ -313,7 +267,7 @@ auto Mpris::getDynamicStr(const PlayerInfo& info, bool truncated, bool html) -> // Since the first element doesn't present a separator and we don't know a priori which one // it will be, we add a "virtual separatorLen" to the dynamicLen, since we are adding the // separatorLen to all the other lengths. - size_t separatorLen = utf8_width(dynamic_separator_); + size_t separatorLen = waybar::util::utf8_width(dynamic_separator_); size_t dynamicLen = dynamic_len_ + separatorLen; if (showArtist) artistLen += separatorLen; if (showAlbum) albumLen += separatorLen; diff --git a/src/util/utf8_string.cpp b/src/util/utf8_string.cpp new file mode 100644 index 00000000..f96f2bd1 --- /dev/null +++ b/src/util/utf8_string.cpp @@ -0,0 +1,62 @@ +#include + +#include +#include + +namespace waybar::util { + +namespace { +// Wide characters count as two, zero-width characters count as zero +// Modifies str in-place (unless width = std::string::npos) +// Returns the total width of the string pre-truncating +size_t measure_and_truncate(std::string& str, size_t width = std::string::npos) { + if (str.length() == 0) return 0; + + const gchar* trunc_end = nullptr; + + size_t total_width = 0; + + for (gchar *data = str.data(), *end = data + str.size(); data != nullptr;) { + gunichar c = g_utf8_get_char_validated(data, end - data); + if (c == -1U || c == -2U) { + // invalid unicode, treat string as ascii + if (width != std::string::npos && str.length() > width) str.resize(width); + return str.length(); + } else if (g_unichar_iswide(c)) { + total_width += 2; + } else if (!g_unichar_iszerowidth(c) && c != 0xAD) { // neither zero-width nor soft hyphen + total_width += 1; + } + + data = g_utf8_find_next_char(data, end); + if (width != std::string::npos && total_width <= width && !g_unichar_isspace(c)) + trunc_end = data; + } + + if (trunc_end) str.resize(trunc_end - str.data()); + + return total_width; +} +} // namespace + +size_t utf8_width(const std::string& str) { + return measure_and_truncate(const_cast(str)); +} + +void utf8_truncate(std::string& s, const std::string& ellipsis, size_t max_len) { + if (max_len == 0) { + s.resize(0); + return; + } + size_t len = measure_and_truncate(s, max_len); + if (len > max_len) { + size_t ellipsis_len = utf8_width(ellipsis); + if (max_len >= ellipsis_len) { + if (ellipsis_len) measure_and_truncate(s, max_len - ellipsis_len); + s += ellipsis; + } else { + s.resize(0); + } + } +} +} // namespace waybar::util From 435c212be10125cf0bde4c823ad2f66c10d8c229 Mon Sep 17 00:00:00 2001 From: Siriusuna Date: Fri, 22 May 2026 02:59:09 +0800 Subject: [PATCH 203/416] fix(mpris): escape Pango markup in tooltip Fix an issue where the mpris module's tooltip failed to render when track metadata contained unescaped XML/HTML markup characters. This occurred because the mpris module formatted raw strings into the tooltip template before passing the final string directly to `set_tooltip_markup()`, triggering GTK/Pango parsing warnings and rendering failures. Resolve this by wrapping tooltip metadata fields in `Glib::Markup::escape_text` before formatting, ensuring Pango-compliant strings are always delivered to the GTK tooltip markup. --- src/modules/mpris/mpris.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 6ebf34f7..13ba836d 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -729,9 +729,12 @@ auto Mpris::update() -> void { auto tooltip_text = fmt::format( fmt::runtime(tooltipstr), fmt::arg("player", info.name), fmt::arg("status", info.status_string), - fmt::arg("artist", getArtistStr(info, tooltip_len_limits_)), - fmt::arg("title", getTitleStr(info, tooltip_len_limits_)), - fmt::arg("album", getAlbumStr(info, tooltip_len_limits_)), + fmt::arg("artist", + std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))), + fmt::arg("title", + std::string(Glib::Markup::escape_text(getTitleStr(info, tooltip_len_limits_)))), + 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("player_icon", getIconFromJson(config_["player-icons"], info.name)), From 81fd9ee4ab39b90b496c34aabe9abf6bc253b0eb Mon Sep 17 00:00:00 2001 From: Siriusuna Date: Fri, 22 May 2026 03:04:35 +0800 Subject: [PATCH 204/416] fix(mpd): truncate before sanitization and improve tooltip UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix an issue in the MPD module where text failed to render when raw tags containing markup characters were truncated mid-sequence after sanitization, resulting in fragmented XML entities (such as `&` cut into `&am`). Resolve this by shifting the order of operations to truncate the raw strings *before* running `sanitize_string`. To keep `setLabel` decoupled and clean, we introduce helper methods (`getArtistStr`, `getAlbumArtistStr`, `getAlbumStr`, `getTitleStr`) inside the `MPD` class, matching the architectural style of MPRIS. Additionally: - Introduce support for the customizable `ellipsis` config option (defaulting to `…`). - Switch the truncation logic of the MPD module to be visual-width-aware using the newly extracted `waybar::util::utf8_truncate`. - Decouple the tooltip text from the main label's truncated text. The tooltip now displays un-truncated, complete metadata (properly escaped), providing a significantly better user experience. --- include/modules/mpd/mpd.hpp | 5 ++++ src/modules/mpd/mpd.cpp | 58 ++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 9d81638e..609f69ed 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -33,6 +33,7 @@ class MPD : public ALabel { detail::unique_status status_; mpd_state state_; detail::unique_song song_; + std::string ellipsis_; public: MPD(const std::string&, const Json::Value&); @@ -45,6 +46,10 @@ class MPD : public ALabel { void setLabel(); std::string getStateIcon() const; std::string getOptionIcon(const std::string& optionName, bool activated) const; + std::string getArtistStr(bool truncated) const; + std::string getAlbumArtistStr(bool truncated) const; + std::string getAlbumStr(bool truncated) const; + std::string getTitleStr(bool truncated) const; // GUI-side methods bool handlePlayPause(GdkEventButton* const&); diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..a3e7c23d 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -6,6 +6,7 @@ #include #include +#include using namespace waybar::util; #include "modules/mpd/state.hpp" @@ -24,7 +25,8 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) timeout_(config_["timeout"].isUInt() ? config_["timeout"].asUInt() * 1'000 : 30'000), connection_(nullptr, &mpd_connection_free), status_(nullptr, &mpd_status_free), - song_(nullptr, &mpd_song_free) { + song_(nullptr, &mpd_song_free), + ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "\u2026") { if (!config_["port"].isNull() && !config_["port"].isUInt()) { spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_); } @@ -117,7 +119,6 @@ void waybar::modules::MPD::setLabel() { label_.get_style_context()->remove_class("disconnected"); auto format = format_; - Glib::ustring artist, album_artist, album, title; std::string date, filename, uri; int song_pos = 0, queue_length = 0, volume = 0; std::chrono::seconds elapsedTime, totalTime; @@ -145,10 +146,6 @@ void waybar::modules::MPD::setLabel() { stateIcon = getStateIcon(); - artist = sanitize_string(getTag(MPD_TAG_ARTIST)); - album_artist = sanitize_string(getTag(MPD_TAG_ALBUM_ARTIST)); - album = sanitize_string(getTag(MPD_TAG_ALBUM)); - title = sanitize_string(getTag(MPD_TAG_TITLE)); date = sanitize_string(getTag(MPD_TAG_DATE)); filename = sanitize_string(getFilename()); uri = mpd_song_get_uri(song_.get()); @@ -170,17 +167,12 @@ void waybar::modules::MPD::setLabel() { std::string repeatIcon = getOptionIcon("repeat", repeatActivated); bool singleActivated = mpd_status_get_single(status_.get()); std::string singleIcon = getOptionIcon("single", singleActivated); - if (config_["artist-len"].isInt()) artist = artist.substr(0, config_["artist-len"].asInt()); - if (config_["album-artist-len"].isInt()) - album_artist = album_artist.substr(0, config_["album-artist-len"].asInt()); - if (config_["album-len"].isInt()) album = album.substr(0, config_["album-len"].asInt()); - if (config_["title-len"].isInt()) title = title.substr(0, config_["title-len"].asInt()); try { auto text = fmt::format( - fmt::runtime(format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::runtime(format), fmt::arg("artist", getArtistStr(true)), + fmt::arg("albumArtist", getAlbumArtistStr(true)), fmt::arg("album", getAlbumStr(true)), + fmt::arg("title", getTitleStr(true)), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), @@ -202,9 +194,9 @@ void waybar::modules::MPD::setLabel() { : "MPD (connected)"; try { auto tooltip_text = fmt::format( - fmt::runtime(tooltip_format), fmt::arg("artist", artist.raw()), - fmt::arg("albumArtist", album_artist.raw()), fmt::arg("album", album.raw()), - fmt::arg("title", title.raw()), fmt::arg("date", date), fmt::arg("volume", volume), + fmt::runtime(tooltip_format), fmt::arg("artist", getArtistStr(false)), + fmt::arg("albumArtist", getAlbumArtistStr(false)), fmt::arg("album", getAlbumStr(false)), + fmt::arg("title", getTitleStr(false)), fmt::arg("date", date), fmt::arg("volume", volume), fmt::arg("elapsedTime", elapsedTime), fmt::arg("totalTime", totalTime), fmt::arg("songPosition", song_pos), fmt::arg("queueLength", queue_length), fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), @@ -257,6 +249,38 @@ std::string waybar::modules::MPD::getOptionIcon(const std::string& optionName, } } +std::string waybar::modules::MPD::getArtistStr(bool truncated) const { + std::string artist = getTag(MPD_TAG_ARTIST); + if (truncated && config_["artist-len"].isInt()) { + waybar::util::utf8_truncate(artist, ellipsis_, config_["artist-len"].asInt()); + } + return sanitize_string(artist); +} + +std::string waybar::modules::MPD::getAlbumArtistStr(bool truncated) const { + std::string album_artist = getTag(MPD_TAG_ALBUM_ARTIST); + if (truncated && config_["album-artist-len"].isInt()) { + waybar::util::utf8_truncate(album_artist, ellipsis_, config_["album-artist-len"].asInt()); + } + return sanitize_string(album_artist); +} + +std::string waybar::modules::MPD::getAlbumStr(bool truncated) const { + std::string album = getTag(MPD_TAG_ALBUM); + if (truncated && config_["album-len"].isInt()) { + waybar::util::utf8_truncate(album, ellipsis_, config_["album-len"].asInt()); + } + return sanitize_string(album); +} + +std::string waybar::modules::MPD::getTitleStr(bool truncated) const { + std::string title = getTag(MPD_TAG_TITLE); + if (truncated && config_["title-len"].isInt()) { + waybar::util::utf8_truncate(title, ellipsis_, config_["title-len"].asInt()); + } + return sanitize_string(title); +} + static bool isServerUnavailable(const std::error_code& ec) { if (ec.category() == std::system_category()) { switch (ec.value()) { From 89b5456cd3930d3bfa5a4782869732b3fb39385c Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sun, 24 May 2026 06:41:51 +0100 Subject: [PATCH 205/416] Sway IPC: fix socket path discovery Ensure to only read actual "sway --get-socketpath" output, not subsequent garbage in buffer. --- src/modules/sway/ipc/client.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index 3ebccccd..53ad64a1 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -45,7 +45,7 @@ const std::string Ipc::getSocketPath() const { throw std::runtime_error("Failed to get socket path"); } while (fgets(buf, sizeof(buf), in) != nullptr) { - str_buf.append(buf, sizeof(buf)); + str_buf.append(buf); } pclose(in); str = str_buf; From 1bc633fc0ea454f1891d1de6f0c1aa857e9b4c34 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sun, 24 May 2026 06:45:30 +0100 Subject: [PATCH 206/416] Sway IPC: more robust socket send/receive logic Handle EINTR/EAGAIN, partial sends, etc. --- src/modules/sway/ipc/client.cpp | 47 ++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index 53ad64a1..fa6b31fd 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -1,11 +1,33 @@ #include "modules/sway/ipc/client.hpp" +#include #include #include #include +#include namespace waybar::modules::sway { +namespace { + +void sendAll(int fd, const char* data, size_t size, const char* what) { + size_t total = 0; + while (total < size) { + const auto res = ::send(fd, data + total, size - total, 0); + if (res < 0) { + if (errno == EINTR || errno == EAGAIN) { + continue; + } + throw std::runtime_error(what); + } + if (res == 0) { + throw std::runtime_error(what); + } + total += static_cast(res); + } +} + +} // namespace Ipc::Ipc() { const std::string& socketPath = getSocketPath(); @@ -89,10 +111,16 @@ struct Ipc::ipc_response Ipc::recv(int fd) { // IPC is closed so just return an empty response return {0, 0, ""}; } - if (res <= 0) { + if (res < 0) { + if (errno == EINTR || errno == EAGAIN) { + continue; + } throw std::runtime_error("Unable to receive IPC header"); } - total += res; + if (res == 0) { + throw std::runtime_error("Unable to receive IPC header"); + } + total += static_cast(res); } auto magic = std::string(header.data(), header.data() + ipc_magic_.size()); if (magic != ipc_magic_) { @@ -110,9 +138,12 @@ struct Ipc::ipc_response Ipc::recv(int fd) { } throw std::runtime_error("Unable to receive IPC payload"); } - total += res; + if (res == 0) { + throw std::runtime_error("Unable to receive IPC payload"); + } + total += static_cast(res); } - return {data32[0], data32[1], &payload.front()}; + return {data32[0], data32[1], std::move(payload)}; } struct Ipc::ipc_response Ipc::send(int fd, uint32_t type, const std::string& payload) { @@ -123,12 +154,8 @@ struct Ipc::ipc_response Ipc::send(int fd, uint32_t type, const std::string& pay data32[0] = payload.size(); data32[1] = type; - if (::send(fd, header.data(), ipc_header_size_, 0) == -1) { - throw std::runtime_error("Unable to send IPC header"); - } - if (::send(fd, payload.c_str(), payload.size(), 0) == -1) { - throw std::runtime_error("Unable to send IPC payload"); - } + sendAll(fd, header.data(), ipc_header_size_, "Unable to send IPC header"); + sendAll(fd, payload.data(), payload.size(), "Unable to send IPC payload"); return Ipc::recv(fd); } From d8848a478bdf2412c905f145d7311e27265c8841 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sun, 24 May 2026 08:09:43 +0100 Subject: [PATCH 207/416] Sway IPC: code cleanups --- include/modules/sway/ipc/client.hpp | 13 ++--- src/modules/sway/ipc/client.cpp | 91 ++++++++++++++++------------- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/include/modules/sway/ipc/client.hpp b/include/modules/sway/ipc/client.hpp index f6eb7c40..2665caf9 100644 --- a/include/modules/sway/ipc/client.hpp +++ b/include/modules/sway/ipc/client.hpp @@ -1,14 +1,10 @@ #pragma once #include -#include -#include -#include -#include -#include +#include +#include #include -#include #include #include "ipc.hpp" @@ -41,8 +37,9 @@ class Ipc { static inline const std::string ipc_magic_ = "i3-ipc"; static inline const size_t ipc_header_size_ = ipc_magic_.size() + 8; - const std::string getSocketPath() const; - int open(const std::string&) const; + static std::string getSocketPath(); + static int open(const std::string&); + struct ipc_response send(int fd, uint32_t type, const std::string& payload = ""); struct ipc_response recv(int fd); diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index fa6b31fd..c833fe64 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -1,12 +1,22 @@ #include "modules/sway/ipc/client.hpp" -#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include +#include "modules/sway/ipc/ipc.hpp" + namespace waybar::modules::sway { namespace { @@ -30,7 +40,7 @@ void sendAll(int fd, const char* data, size_t size, const char* what) { } // namespace Ipc::Ipc() { - const std::string& socketPath = getSocketPath(); + const std::string socketPath = getSocketPath(); fd_ = util::ScopedFd(open(socketPath)); fd_event_ = util::ScopedFd(open(socketPath)); } @@ -51,49 +61,49 @@ Ipc::~Ipc() { } } -void Ipc::setWorker(std::function&& func) { thread_ = func; } +void Ipc::setWorker(std::function&& func) { thread_ = std::move(func); } -const std::string Ipc::getSocketPath() const { +std::string Ipc::getSocketPath() { const char* env = getenv("SWAYSOCK"); - if (env != nullptr) { - return std::string(env); + if (env != nullptr && env[0] != '\0') { + return {env}; } + + FILE* in = popen("sway --get-socketpath 2>/dev/null", "r"); + if (in == nullptr) { + throw std::runtime_error("Failed to get socket path"); + } + std::string str; - { - std::string str_buf; - FILE* in; - char buf[512] = {0}; - if ((in = popen("sway --get-socketpath 2>/dev/null", "r")) == nullptr) { - throw std::runtime_error("Failed to get socket path"); - } - while (fgets(buf, sizeof(buf), in) != nullptr) { - str_buf.append(buf); - } - pclose(in); - str = str_buf; - if (str.empty()) { - throw std::runtime_error("Socket path is empty"); - } + char buf[512] = {0}; + while (fgets(buf, sizeof(buf), in) != nullptr) { + str.append(buf); } - if (str.back() == '\n') { + + if (pclose(in) == -1) { + throw std::runtime_error("Failed to get socket path"); + } + + if (str.ends_with('\n')) { str.pop_back(); } + if (str.empty()) { + throw std::runtime_error("Socket path is empty"); + } + return str; } -int Ipc::open(const std::string& socketPath) const { +int Ipc::open(const std::string& socketPath) { util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0)); if (fd == -1) { throw std::runtime_error("Unable to open Unix socket"); } (void)fcntl(fd, F_SETFD, FD_CLOEXEC); - struct sockaddr_un addr; - memset(&addr, 0, sizeof(struct sockaddr_un)); - addr.sun_family = AF_UNIX; + struct sockaddr_un addr{.sun_family = AF_UNIX}; strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); addr.sun_path[sizeof(addr.sun_path) - 1] = 0; - int l = sizeof(struct sockaddr_un); - if (::connect(fd, reinterpret_cast(&addr), l) == -1) { + if (::connect(fd, reinterpret_cast(&addr), sizeof addr) == -1) { throw std::runtime_error("Unable to connect to Sway"); } return fd.release(); @@ -102,14 +112,13 @@ int Ipc::open(const std::string& socketPath) const { struct Ipc::ipc_response Ipc::recv(int fd) { std::string header; header.resize(ipc_header_size_); - auto data32 = reinterpret_cast(header.data() + ipc_magic_.size()); - size_t total = 0; + size_t total = 0; while (total < ipc_header_size_) { - auto res = ::recv(fd, header.data() + total, ipc_header_size_ - total, 0); + const ssize_t res = ::recv(fd, header.data() + total, ipc_header_size_ - total, 0); if (fd_event_ == -1 || fd_ == -1) { // IPC is closed so just return an empty response - return {0, 0, ""}; + return {.size = 0, .type = 0, .payload = ""}; } if (res < 0) { if (errno == EINTR || errno == EAGAIN) { @@ -122,16 +131,18 @@ struct Ipc::ipc_response Ipc::recv(int fd) { } total += static_cast(res); } - auto magic = std::string(header.data(), header.data() + ipc_magic_.size()); - if (magic != ipc_magic_) { + if (std::string_view(header.data(), ipc_magic_.size()) != ipc_magic_) { throw std::runtime_error("Invalid IPC magic"); } - total = 0; + const auto* data32 = reinterpret_cast(header.data() + ipc_magic_.size()); + std::string payload; payload.resize(data32[0]); + + total = 0; while (total < data32[0]) { - auto res = ::recv(fd, payload.data() + total, data32[0] - total, 0); + const ssize_t res = ::recv(fd, payload.data() + total, data32[0] - total, 0); if (res < 0) { if (errno == EINTR || errno == EAGAIN) { continue; @@ -143,19 +154,21 @@ struct Ipc::ipc_response Ipc::recv(int fd) { } total += static_cast(res); } - return {data32[0], data32[1], std::move(payload)}; + + return {.size = data32[0], .type = data32[1], .payload = std::move(payload)}; } struct Ipc::ipc_response Ipc::send(int fd, uint32_t type, const std::string& payload) { std::string header; header.resize(ipc_header_size_); - auto data32 = reinterpret_cast(header.data() + ipc_magic_.size()); - memcpy(header.data(), ipc_magic_.c_str(), ipc_magic_.size()); + memcpy(header.data(), ipc_magic_.data(), ipc_magic_.size()); + auto* data32 = reinterpret_cast(header.data() + ipc_magic_.size()); data32[0] = payload.size(); data32[1] = type; sendAll(fd, header.data(), ipc_header_size_, "Unable to send IPC header"); sendAll(fd, payload.data(), payload.size(), "Unable to send IPC payload"); + return Ipc::recv(fd); } From 3056bad95a5911b528b1dbc29cfb6ab4367f0696 Mon Sep 17 00:00:00 2001 From: Carlo Teubner Date: Sun, 24 May 2026 08:13:05 +0100 Subject: [PATCH 208/416] Sway IPC: avoid reinterpret_cast --- src/modules/sway/ipc/client.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index c833fe64..dcbe7fa3 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -135,14 +136,18 @@ struct Ipc::ipc_response Ipc::recv(int fd) { throw std::runtime_error("Invalid IPC magic"); } - const auto* data32 = reinterpret_cast(header.data() + ipc_magic_.size()); + uint32_t payload_size = 0; + uint32_t payload_type = 0; + memcpy(&payload_size, header.data() + ipc_magic_.size(), sizeof payload_size); + memcpy(&payload_type, header.data() + ipc_magic_.size() + sizeof payload_size, + sizeof payload_type); std::string payload; - payload.resize(data32[0]); + payload.resize(payload_size); total = 0; - while (total < data32[0]) { - const ssize_t res = ::recv(fd, payload.data() + total, data32[0] - total, 0); + while (total < payload_size) { + const ssize_t res = ::recv(fd, payload.data() + total, payload_size - total, 0); if (res < 0) { if (errno == EINTR || errno == EAGAIN) { continue; @@ -155,16 +160,19 @@ struct Ipc::ipc_response Ipc::recv(int fd) { total += static_cast(res); } - return {.size = data32[0], .type = data32[1], .payload = std::move(payload)}; + return {.size = payload_size, .type = payload_type, .payload = std::move(payload)}; } struct Ipc::ipc_response Ipc::send(int fd, uint32_t type, const std::string& payload) { std::string header; header.resize(ipc_header_size_); memcpy(header.data(), ipc_magic_.data(), ipc_magic_.size()); - auto* data32 = reinterpret_cast(header.data() + ipc_magic_.size()); - data32[0] = payload.size(); - data32[1] = type; + if (payload.size() > std::numeric_limits::max()) { + throw std::runtime_error("IPC payload is too large"); + } + const auto payload_size = static_cast(payload.size()); + memcpy(header.data() + ipc_magic_.size(), &payload_size, sizeof payload_size); + memcpy(header.data() + ipc_magic_.size() + sizeof payload_size, &type, sizeof type); sendAll(fd, header.data(), ipc_header_size_, "Unable to send IPC header"); sendAll(fd, payload.data(), payload.size(), "Unable to send IPC payload"); From 822071f36efbf828781e99ea3ab36a97d9a55cb7 Mon Sep 17 00:00:00 2001 From: stalker Date: Mon, 25 May 2026 08:18:12 +0400 Subject: [PATCH 209/416] feat(custom): add image --- include/modules/custom.hpp | 7 +++++-- src/modules/custom.cpp | 32 +++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index a345a33b..442b1c37 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -5,14 +5,14 @@ #include #include -#include "ALabel.hpp" +#include "AIconLabel.hpp" #include "util/command.hpp" #include "util/json.hpp" #include "util/sleeper_thread.hpp" namespace waybar::modules { -class Custom : public ALabel { +class Custom : public AIconLabel { public: Custom(const std::string&, const std::string&, const Json::Value&, const std::string&); virtual ~Custom(); @@ -36,6 +36,9 @@ class Custom : public ALabel { std::string alt_; std::string tooltip_; std::string last_tooltip_markup_; + std::string image_path_; + std::string image_name_; + unsigned app_icon_size_{24}; const bool tooltip_format_enabled_; std::vector class_; int percentage_; diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 28def8c9..22465802 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -8,7 +8,7 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, const Json::Value& config, const std::string& output_name) - : ALabel(config, "custom-" + name, id, "{}"), + : AIconLabel(config, "custom-" + name, id, "{}"), name_(name), output_name_(output_name), id_(id), @@ -28,6 +28,15 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, } else if (config_["exec"].isString()) { continuousWorker(); } + if (config_["image-path"].isString()) { + image_path_ = config_["image-path"].asString(); + } + if (config_["image-name"].isString()) { + image_name_ = config_["image-name"].asString(); + } + if (config["icon-size"].isUInt()) { + app_icon_size_ = config["icon-size"].asUInt(); + } } waybar::modules::Custom::~Custom() { @@ -177,7 +186,7 @@ auto waybar::modules::Custom::update() -> void { auto str = fmt::format(fmt::runtime(format_), fmt::arg("text", text_), fmt::arg("alt", alt_), fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); - if ((config_["hide-empty-text"].asBool() && text_.empty()) || str.empty()) { + if ((config_["hide-empty-text"].asBool() && text_.empty()) || (str.empty() && image_path_.empty() && image_name_.empty())) { event_box_.hide(); } else { label_.set_markup(str); @@ -212,7 +221,20 @@ auto waybar::modules::Custom::update() -> void { style->add_class("flat"); style->add_class("text-button"); style->add_class(MODULE_CLASS); + auto image_style = image_.get_style_context(); + 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); + } 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) { if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) @@ -222,9 +244,12 @@ auto waybar::modules::Custom::update() -> void { "mixing manual and automatic argument indexing is no longer supported; " "try replacing \"{}\" with \"{text}\" in your format specifier"); } + + + } // Call parent update - ALabel::update(); + AIconLabel::update(); } void waybar::modules::Custom::parseOutputRaw() { @@ -290,6 +315,7 @@ void waybar::modules::Custom::parseOutputJson() { class_.push_back(c.asString()); } } + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { percentage_ = (int)lround(parsed["percentage"].asFloat()); } else { From ec2651370e68e171e408ab2c932ca6631f7a50cf Mon Sep 17 00:00:00 2001 From: jacobs Date: Tue, 26 May 2026 11:09:45 +0100 Subject: [PATCH 210/416] (Fix) (sway workspace module) persistent workspaces always removed --- src/modules/sway/workspaces.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 6abe5383..b7d63b58 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -229,7 +229,10 @@ bool Workspaces::filterButtons() { auto ws = std::find_if(workspaces_.begin(), workspaces_.end(), [it](const auto& node) { return node["name"].asString() == it->first; }); if (ws == workspaces_.end() || - (!config_["all-outputs"].asBool() && (*ws)["output"].asString() != bar_.output->name)) { + ((*ws).isMember("target_output") ? (*ws)["target_output"].asString() != bar_.output->name && + (*ws)["target_output"].asString() != "" + : !config_["all-outputs"].asBool() && + (*ws)["output"].asString() != bar_.output->name)) { it = buttons_.erase(it); needReorder = true; } else { From 415da7aef4d7be5f9e004389aceb135e245cfb50 Mon Sep 17 00:00:00 2001 From: Artur Ciesielski Date: Tue, 26 May 2026 19:39:03 +0200 Subject: [PATCH 211/416] sway/workspaces: refer to outputs by their monitor identifier (make and model) --- src/modules/sway/workspaces.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 6abe5383..512e4cb6 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -143,7 +143,8 @@ void Workspaces::onCmd(const struct Ipc::ipc_response& res) { if (p_w.isArray() && !p_w.empty()) { // Adding to target outputs for (const Json::Value& output : p_w) { - if (output.asString() == bar_.output->name) { + auto output_name = output.asString(); + if (output_name == bar_.output->name || output_name == bar_.output->identifier) { Json::Value v; v["name"] = p_w_name; v["target_output"] = bar_.output->name; From 97acbf30d0f0466990c68de41275b879a0c18dee Mon Sep 17 00:00:00 2001 From: Artur Ciesielski Date: Tue, 26 May 2026 20:32:38 +0200 Subject: [PATCH 212/416] Update man pages for persistent-workspaces in sway/workspaces --- man/waybar-sway-workspaces.5.scd | 1 + 1 file changed, 1 insertion(+) diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index 3d4e3ad0..f51c4155 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -147,6 +147,7 @@ an empty list denoting all outputs. "3": [], // Always show a workspace with name '3', on all outputs if it does not exist "4": ["eDP-1"], // Always show a workspace with name '4', on output 'eDP-1' if it does not exist "5": ["eDP-1", "DP-2"] // Always show a workspace with name '5', on outputs 'eDP-1' and 'DP-2' if it does not exist + "6": ["MonitorMaker 3000 ABC0123"], // Always show a workspace with name '6' on outputs with an identifier 'MonitorMaker 3000 ABC0123' (usually a triple of vendor/model/serial) } } ``` From 31a161eb06d82ff792c5339b1f9650f06d6b8b53 Mon Sep 17 00:00:00 2001 From: muratyalcin-ce Date: Wed, 27 May 2026 23:39:39 +0300 Subject: [PATCH 213/416] Fix (wayfire/backend): Fixed unhandled exception (out_of_range) after no valid outputs in unordered_map --- src/modules/wayfire/backend.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 42976d20..037a61e1 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -347,10 +347,13 @@ auto IPC::update_state_handler(const std::string& event, const Json::Value& data if (event == "output-wset-changed") { // data: { event, new-wset: wset.name, output: id, new-wset-data: wset, output-data: output } - auto& output = state.outputs.at(data["output-data"]["name"].asString()); - auto wset_idx = data["new-wset-data"]["index"].asUInt(); - state.wsets.at(wset_idx).output = output; - output.wset_idx = wset_idx; + try { + auto& output = state.outputs.at(data["output-data"]["name"].asString()); + auto wset_idx = data["new-wset-data"]["index"].asUInt(); + state.wsets.at(wset_idx).output = output; + output.wset_idx = wset_idx; + } catch (const std::exception&) { + } return; } From 5ea824c24cfbf503fd13200db875fbc489d2027d Mon Sep 17 00:00:00 2001 From: muratyalcin-ce Date: Thu, 28 May 2026 23:26:40 +0300 Subject: [PATCH 214/416] Use unit in config for memory module tooltip --- include/modules/memory.hpp | 2 -- src/modules/memory/common.cpp | 44 ++++++++++++++++------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/include/modules/memory.hpp b/include/modules/memory.hpp index c73ece23..0e405187 100644 --- a/include/modules/memory.hpp +++ b/include/modules/memory.hpp @@ -19,8 +19,6 @@ class Memory : public ALabel { private: void parseMeminfo(); - static float calc_divisor(const std::string& divisor); - std::unordered_map meminfo_; util::SleeperThread thread_; diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 731f0b08..d41b4fc2 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -1,5 +1,18 @@ #include "modules/memory.hpp" +namespace { +const std::unordered_map kUnits = { + {"kB", 1.000}, + {"kiB", 1.024}, + {"MB", 1.000 * 1000.0}, + {"MiB", 1.024 * 1024.0}, + {"GB", 1.000 * 1000.0 * 1000.0}, + {"GiB", 1.024 * 1024.0 * 1024.0}, + {"TB", 1.000 * 1000.0 * 1000.0 * 1000.0}, + {"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0} +}; +} + waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) : ALabel(config, "memory", id, "{}%", 30) { thread_ = [this] { @@ -8,6 +21,11 @@ waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config }; if (config["unit"].isString()) { unit_ = config["unit"].asString(); + if (!kUnits.contains(unit_)) { + unit_ = "GiB"; + } + } else { + unit_ = "GiB"; } } @@ -40,7 +58,7 @@ auto waybar::modules::Memory::update() -> void { used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; } - float divisor = calc_divisor(unit_); + float divisor = kUnits.at(unit_); float total_ram = memtotal / divisor; float total_swap = swaptotal / divisor; float used_ram = (memtotal - memfree) / divisor; @@ -82,7 +100,7 @@ auto waybar::modules::Memory::update() -> void { fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), fmt::arg("swapAvail", available_swap))); } else { - label_.set_tooltip_markup(fmt::format("{:.{}f}GiB used", used_ram, 1)); + label_.set_tooltip_markup(fmt::format("{:.{}f}{} used", used_ram, 1, unit_)); } } } else { @@ -91,25 +109,3 @@ auto waybar::modules::Memory::update() -> void { // Call parent update ALabel::update(); } - -float waybar::modules::Memory::calc_divisor(const std::string& divisor) { - if (divisor == "kB") { - return 1.0; - } else if (divisor == "kiB") { - return 1.024; - } else if (divisor == "MB") { - return 1.000 * 1000.0; - } else if (divisor == "MiB") { - return 1.024 * 1024.0; - } else if (divisor == "GB") { - return 1.000 * 1000.0 * 1000.0; - } else if (divisor == "GiB") { - return 1.024 * 1024.0 * 1024.0; - } else if (divisor == "TB") { - return 1.000 * 1000.0 * 1000.0 * 1000.0; - } else if (divisor == "TiB") { - return 1.024 * 1024.0 * 1024.0 * 1024.0; - } else { // default to GiB if it is anything that we don't recongnise - return 1.024 * 1024.0 * 1024.0; - } -} From 3eeffbe3891f517f977d474decc8d3b794e63d83 Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Thu, 28 May 2026 15:25:35 +0800 Subject: [PATCH 215/416] feat: add mango modules: workspace,language,keymode,window,layout --- README.md | 1 + include/modules/mango/backend.hpp | 67 ++++++ include/modules/mango/keymode.hpp | 27 +++ include/modules/mango/language.hpp | 43 ++++ include/modules/mango/layout.hpp | 27 +++ include/modules/mango/window.hpp | 30 +++ include/modules/mango/workspaces.hpp | 40 ++++ man/waybar-mango-keymode.5.scd | 63 ++++++ man/waybar-mango-language.5.scd | 74 +++++++ man/waybar-mango-layout.5.scd | 75 +++++++ man/waybar-mango-window.5.scd | 70 ++++++ man/waybar-mango-workspaces.5.scd | 112 ++++++++++ meson.build | 19 ++ meson_options.txt | 1 + src/factory.cpp | 24 ++ src/modules/mango/backend.cpp | 317 +++++++++++++++++++++++++++ src/modules/mango/keymode.cpp | 58 +++++ src/modules/mango/language.cpp | 148 +++++++++++++ src/modules/mango/layout.cpp | 56 +++++ src/modules/mango/window.cpp | 93 ++++++++ src/modules/mango/workspaces.cpp | 252 +++++++++++++++++++++ 21 files changed, 1597 insertions(+) create mode 100644 include/modules/mango/backend.hpp create mode 100644 include/modules/mango/keymode.hpp create mode 100644 include/modules/mango/language.hpp create mode 100644 include/modules/mango/layout.hpp create mode 100644 include/modules/mango/window.hpp create mode 100644 include/modules/mango/workspaces.hpp create mode 100644 man/waybar-mango-keymode.5.scd create mode 100644 man/waybar-mango-language.5.scd create mode 100644 man/waybar-mango-layout.5.scd create mode 100644 man/waybar-mango-window.5.scd create mode 100644 man/waybar-mango-workspaces.5.scd create mode 100644 src/modules/mango/backend.cpp create mode 100644 src/modules/mango/keymode.cpp create mode 100644 src/modules/mango/language.cpp create mode 100644 src/modules/mango/layout.cpp create mode 100644 src/modules/mango/window.cpp create mode 100644 src/modules/mango/workspaces.cpp diff --git a/README.md b/README.md index 9ca041d1..687f8234 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - River (Mapping mode, Tags, Focused window name) - Hyprland (Window Icons, Workspaces, Focused window name) - Niri (Workspaces, Focused window name, Language) +- Mango (Workspaces, Focused window name, Language, Keymode) - DWL (Tags, Focused window name) [requires dwl ipc patch](https://codeberg.org/dwl/dwl-patches/src/branch/main/patches/ipc) - Tray [#21](https://github.com/Alexays/Waybar/issues/21) - Local time diff --git a/include/modules/mango/backend.hpp b/include/modules/mango/backend.hpp new file mode 100644 index 00000000..a1b0392e --- /dev/null +++ b/include/modules/mango/backend.hpp @@ -0,0 +1,67 @@ +// include/modules/mango/backend.hpp +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "util/json.hpp" + +namespace waybar::modules::mango { + +class EventHandler { + public: + virtual void onEvent(const Json::Value& ev) = 0; + virtual ~EventHandler() = default; +}; + +class IPC { + public: + static IPC& getInstance(); + IPC(const IPC&) = delete; + IPC& operator=(const IPC&) = delete; + + void registerForIPC(const std::string& ev, EventHandler* handler); + void unregisterForIPC(EventHandler* handler); + + static Json::Value send(const Json::Value& request); + static void sendAsync(const Json::Value& request); + + std::unique_lock lockData() { return std::unique_lock(data_mutex_); } + + std::unordered_map getMonitors() const; + Json::Value getMonitor(const std::string& name); + Json::Value getActiveClientForMonitor(const std::string& name) const; + std::string getKeyboardLayout() const; + std::string getKeymode() const; + std::string getLayoutSymbolForMonitor(const std::string& name) const; + + private: + IPC(); + ~IPC(); + void startIPC(); + static int connectToSocket(); + void parseIPC(const std::string& line); + + void handleMonitorUpdate(const Json::Value& mon); + void updateFocusingClient(const Json::Value& client); + void updateKeyboardLayout(const std::string& layout); + + static Json::Value sendCommand(const std::string& cmd); + + int sockfd_ = -1; + std::thread ipc_thread_; + mutable std::mutex data_mutex_; + std::unordered_map monitors_; + std::unordered_map clients_; + uint64_t focusing_client_id_ = 0; + std::string keyboard_layout_; + std::string keymode_; + Json::Value active_client_; + std::mutex callback_mutex_; + std::list> callbacks_; +}; +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/keymode.hpp b/include/modules/mango/keymode.hpp new file mode 100644 index 00000000..5e3ef685 --- /dev/null +++ b/include/modules/mango/keymode.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Keymode : public ALabel, public EventHandler { + public: + Keymode(const std::string&, const Bar&, const Json::Value&); + ~Keymode() override; + void update() override; + + private: + void onEvent(const Json::Value& ev) override; + void doUpdate(); + + std::mutex mutex_; + const Bar& bar_; + std::string last_keymode_; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/language.hpp b/include/modules/mango/language.hpp new file mode 100644 index 00000000..3d91515b --- /dev/null +++ b/include/modules/mango/language.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Language : public ALabel, public EventHandler { + public: + Language(const std::string&, const Bar&, const Json::Value&); + ~Language() override; + void update() override; + + private: + void updateFromIPC(); + void onEvent(const Json::Value& ev) override; + void doUpdate(); + + struct Layout { + std::string full_name; + std::string short_name; + std::string variant; + std::string short_description; + }; + + Layout getLayout(const std::string& fullName); + + std::mutex mutex_; + const Bar& bar_; + + std::vector layouts_; + unsigned current_idx_; + std::string last_short_name_; + + struct rxkb_context* rxkb_ctx_ = nullptr; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/layout.hpp b/include/modules/mango/layout.hpp new file mode 100644 index 00000000..4a349876 --- /dev/null +++ b/include/modules/mango/layout.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include "ALabel.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Layout : public ALabel, public EventHandler { + public: + Layout(const std::string&, const Bar&, const Json::Value&); + ~Layout() override; + void update() override; + + private: + void onEvent(const Json::Value& ev) override; + void doUpdate(); + + std::mutex mutex_; + const Bar& bar_; + std::string last_symbol_; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/window.hpp b/include/modules/mango/window.hpp new file mode 100644 index 00000000..987da34b --- /dev/null +++ b/include/modules/mango/window.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include + +#include "AAppIconLabel.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Window : public AAppIconLabel, public EventHandler { + public: + Window(const std::string&, const Bar&, const Json::Value&); + ~Window() override; + void update() override; + + private: + void onEvent(const Json::Value& ev) override; + void doUpdate(); + void setClass(const std::string& className, bool enable); + + const Bar& bar_; + std::string oldAppId_; + std::mutex mutex_; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/include/modules/mango/workspaces.hpp b/include/modules/mango/workspaces.hpp new file mode 100644 index 00000000..f5e52022 --- /dev/null +++ b/include/modules/mango/workspaces.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include + +#include "AModule.hpp" +#include "bar.hpp" +#include "modules/mango/backend.hpp" + +namespace waybar::modules::mango { + +class Workspaces : public AModule, public EventHandler { + public: + Workspaces(const std::string&, const Bar&, const Json::Value&); + ~Workspaces() override; + void update() override; + + private: + void onEvent(const Json::Value& ev) override; + void doUpdate(); + + Gtk::Button& addButton(uint64_t idx); + void updateButtonState(Gtk::Button& button, const Json::Value& tag, const Json::Value& monitor); + std::string getIcon(const std::string& value, const Json::Value& tag); + bool handleButtonClick(GdkEventButton* event, uint64_t idx, bool isOverview); + + const Bar& bar_; + Gtk::Box box_; + + std::unordered_map buttons_; + Gtk::Button* overview_button_ = nullptr; + + std::string on_click_left_; + std::string on_click_middle_; + std::string on_click_right_; +}; + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/man/waybar-mango-keymode.5.scd b/man/waybar-mango-keymode.5.scd new file mode 100644 index 00000000..b96c1903 --- /dev/null +++ b/man/waybar-mango-keymode.5.scd @@ -0,0 +1,63 @@ +waybar-mango-keymode(5) + +# NAME + +waybar - mango keymode module + +# DESCRIPTION + +The *keymode* module displays the current keyboard mode (e.g. "resize", "default") in the Mango compositor. It is hidden when no mode is active. + +# CONFIGURATION + +Addressed by *mango/keymode* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format, how the mode should be displayed. *{mode}* is replaced by the current mode name. + +*format-*: ++ + typeof: string ++ + Provide a custom format for a specific keymode. ** is the mode name as reported by Mango (e.g. "resize"). The value can contain *{mode}* as a placeholder. + If this option is set, it overrides the main *format* for that mode. + +*menu*: ++ + typeof: string ++ + Action that pops up a menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. + +*menu-actions*: ++ + typeof: array ++ + Actions for the menu buttons. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables the module to consume all leftover space. + +# FORMAT REPLACEMENTS + +*{mode}*: The name of the current keymode. + +# EXAMPLES + +``` +"mango/keymode": { + "format": "[{mode}]", + "format-resize": " Resizing" +} +``` + +# STYLE + +- *#keymode* + +A CSS class with the current mode name (e.g. *.resize*) is added to the widget, allowing per‑mode styling: + +``` +#keymode.resize { background: #ff0000; } +``` diff --git a/man/waybar-mango-language.5.scd b/man/waybar-mango-language.5.scd new file mode 100644 index 00000000..53fd9569 --- /dev/null +++ b/man/waybar-mango-language.5.scd @@ -0,0 +1,74 @@ +waybar-mango-language(5) + +# NAME + +waybar - mango language module + +# DESCRIPTION + +The *language* module displays the currently active keyboard layout in the Mango compositor. + +# CONFIGURATION + +Addressed by *mango/language* + +*format*: ++ + typeof: string ++ + default: {} ++ + The format, how the layout should be displayed. See *FORMAT REPLACEMENTS*. + +*format-*: ++ + typeof: string ++ + Provide an alternative format string for a given language. + is the short description of the layout (e.g. "us", "de"). + The value is used as the replacement for *{}* in the main *format*. + This option can be repeated for multiple languages. + +*menu*: ++ + typeof: string ++ + Action that pops up a menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. It must contain an element of type GtkMenu with id *menu*. + +*menu-actions*: ++ + typeof: array ++ + Actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all leftover space dynamically. + +# FORMAT REPLACEMENTS + +*{short}*: Short name of the layout (e.g. "us"). This is also the default when no format is specified. + +*{shortDescription}*: Short description of the layout (same as *{short}* in most cases). + +*{long}*: Full name of the layout as reported by Mango (e.g. "English (US)"). + +*{variant}*: Variant of the layout, if any. + +# EXAMPLES + +``` +"mango/language": { + "format": " {long} ", + "format-us": "US", + "format-de": "DE" +} +``` + +# STYLE + +- *#language* + +A CSS class matching the current layout's short name is added to the widget. +This allows per‑layout styling: + +``` +#language.us { color: #00ff00; } +#language.de { color: #ff0000; } +``` diff --git a/man/waybar-mango-layout.5.scd b/man/waybar-mango-layout.5.scd new file mode 100644 index 00000000..6cf3290b --- /dev/null +++ b/man/waybar-mango-layout.5.scd @@ -0,0 +1,75 @@ +waybar-mango-layout(5) + +# NAME + +waybar - mango layout module + +# DESCRIPTION + +The *layout* module displays the current layout symbol of the monitor (e.g. "S", "M") in the Mango compositor. +It supports dynamic CSS classes and custom formats based on the active layout symbol. + +# CONFIGURATION + +Addressed by *mango/layout* + +*format*: ++ + typeof: string ++ + default: {symbol} ++ + The default format, how the layout symbol should be displayed. *{symbol}* is replaced by the current layout symbol. + +*format-*: ++ + typeof: string ++ + default: *none* ++ + The custom format to use when a specific layout symbol is active (e.g., *format-S*, *format-M*). Note that the symbol string is strictly case-sensitive. If no match is found, it falls back to *format*. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables the module to consume all leftover space. + +# FORMAT REPLACEMENTS + +*{symbol}*: The layout symbol reported by Mango (e.g., "S", "M", "Dwindle"). + +# CUSTOM FORMATS + +You can define specific formats for different layouts by appending the exact layout symbol to the *format-* key in your configuration. + +For example, if your Mango compositor reports the symbol "S" for a spiral layout and "M" for a master layout, you can use *format-S* and *format-M* to define unique icons or text for each. Keep in mind that JSON keys are case-sensitive, so if the compositor sends "S", the key must be exactly *format-S*. + +# STYLE + +The layout module provides dynamic CSS classes based on the current layout symbol, allowing you to style each layout differently. + +* *#mango-layout* +* *.* - The current layout symbol is dynamically added as a CSS class name (e.g., *.S*, *.M*). + +# EXAMPLES + +``` +"mango/layout": { + "format": "[] {symbol}", + "format-S": "󰌌 {symbol}", + "format-M": "󰕰 {symbol}" +} +``` + +## CSS Example + +``` +#mango-layout { + color: #ffffff; + padding: 0 5px; +} + +/* Specific color for the "S" layout */ +#mango-layout.S { + color: #a6e3a1; +} + +/* Specific color for the "M" layout */ +#mango-layout.M { + color: #f38ba8; +} +``` diff --git a/man/waybar-mango-window.5.scd b/man/waybar-mango-window.5.scd new file mode 100644 index 00000000..7ee9b02c --- /dev/null +++ b/man/waybar-mango-window.5.scd @@ -0,0 +1,70 @@ +waybar-mango-window(5) + +# NAME + +waybar - mango window module + +# DESCRIPTION + +The *window* module displays the title and app ID of the currently focused window in the Mango compositor. + +# CONFIGURATION + +Addressed by *mango/window* + +*format*: ++ + typeof: string ++ + default: {title} ++ + The format string. See *FORMAT REPLACEMENTS*. + +*rewrite*: ++ + typeof: object ++ + Rules to rewrite the window title. Each key is a regular expression and its value is the replacement string. Captures can be used with *$1*, *$2*, etc. + +*icon*: ++ + typeof: bool ++ + default: false ++ + Whether to show the application icon. + +*icon-size*: ++ + typeof: integer ++ + default: 24 ++ + Size of the application icon in pixels. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables the module to consume all leftover space. + +# FORMAT REPLACEMENTS + +*{title}*: The current window title. + +*{app_id}*: The app ID of the focused window. + +# REWRITE RULES + +If the title matches a regular expression from the *rewrite* object, it is replaced by the corresponding value. Regular expression syntax follows ECMAScript rules. Unmatched titles are left unchanged. + +# EXAMPLES + +``` +"mango/window": { + "format": "{title}", + "rewrite": { + "(.*) - Mozilla Firefox": "🌎 $1", + "(.*) - zsh": "> [$1]" + }, + "icon": true, + "icon-size": 20 +} +``` + +# STYLE + +- *#window* +- *#window.empty* – applied when no window is focused (module hidden by default) +- *#window.solo* – applied when only one window is present on the active workspace +- *#window.* – applied when a single window with the given app ID is on the workspace + +The classes *.empty*, *.solo*, and the app‑ID class are set on the module’s event box. \ No newline at end of file diff --git a/man/waybar-mango-workspaces.5.scd b/man/waybar-mango-workspaces.5.scd new file mode 100644 index 00000000..985398e7 --- /dev/null +++ b/man/waybar-mango-workspaces.5.scd @@ -0,0 +1,112 @@ +waybar-mango-workspaces(5) + +# NAME + +waybar - mango workspaces module + +# DESCRIPTION + +The *workspaces* module displays the tags (workspaces) of the Mango compositor. It shows an overview button when the overview mode is active (active tag is 0), and individual tag buttons otherwise. + +# CONFIGURATION + +Addressed by *mango/workspaces* + +*format*: ++ + typeof: string ++ + default: {value} ++ + The format for each tag button. See *FORMAT REPLACEMENTS*. + +*format-icons*: ++ + typeof: object ++ + Icons to be used instead of the workspace index or name. Keys can be a workspace index (as a string), or one of the following special state keys: *default*, *active*, *urgent*, *empty*. + +*disable-markup*: ++ + typeof: bool ++ + default: false ++ + If true, the button label will not be interpreted as Pango markup. + +*current-only*: ++ + typeof: bool ++ + default: false ++ + If true, only the currently active workspace button is shown. + +*hide-empty*: ++ + typeof: bool ++ + default: false ++ + If true, buttons for empty (client_count == 0) workspaces are hidden, unless the workspace is active. + +*on-click*: ++ + typeof: string ++ + Command to execute on left click. Typically set to *activate* or *toggle*. + +*on-click-middle*: ++ + typeof: string ++ + Command for middle click. Same actions as *on-click*. + +*on-click-right*: ++ + typeof: string ++ + Command for right click. Same actions as *on-click*. + +*overview-label*: ++ + typeof: string ++ + default: "OVERVIEW" ++ + Label shown on the overview button when the overview is active. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables the module to consume leftover space. + +# FORMAT REPLACEMENTS + +*{value}*: Workspace index (same as *{index}* for unnamed workspaces). + +*{name}*: Workspace name (equal to the index for unnamed workspaces in Mango). + +*{icon}*: Icon selected from *format-icons* based on workspace index and state. + +*{index}*: Numeric index of the workspace. + +*{output}*: Name of the output where the workspace is located. + +# CLICK ACTIONS + +When a tag button is clicked, the action from *on-click* (or its middle/right variants) is evaluated. +Supported actions: + +- *activate*: dispatch view, +- *toggle*: dispatch toggleview, + +When the overview button is clicked, the actions change to: + +- *activate*: dispatch overview +- *toggle*: dispatch toggleoverview + +# EXAMPLES + +``` +"mango/workspaces": { + "format": "{icon}", + "format-icons": { + "1": "一", + "2": "二", + "active": "", + "default": "", + "urgent": "", + "empty": "" + }, + "on-click": "activate", + "on-click-right": "toggle", + "overview-label": "" +} +``` + +# STYLE + +- *#workspaces button* +- *#workspaces button.active* – the workspace is active (visible) on its output. +- *#workspaces button.urgent* – the workspace has at least one urgent window. +- *#workspaces button.empty* – the workspace contains no clients. +- *#workspaces button.current_output* – the workspace belongs to the output where the bar is shown. +- *#workspaces button.overview* – the overview button (visible in overview mode). \ No newline at end of file diff --git a/meson.build b/meson.build index 0c494eb2..7a957559 100644 --- a/meson.build +++ b/meson.build @@ -348,6 +348,25 @@ if get_option('niri') ) endif +if get_option('mango') + add_project_arguments('-DHAVE_MANGO', language: 'cpp') + src_files += files( + 'src/modules/mango/backend.cpp', + 'src/modules/mango/language.cpp', + 'src/modules/mango/keymode.cpp', + 'src/modules/mango/window.cpp', + 'src/modules/mango/workspaces.cpp', + 'src/modules/mango/layout.cpp' + ) + man_files += files( + 'man/waybar-mango-language.5.scd', + 'man/waybar-mango-keymode.5.scd', + 'man/waybar-mango-window.5.scd', + 'man/waybar-mango-workspaces.5.scd', + 'man/waybar-mango-layout.5.scd' + ) +endif + if true add_project_arguments('-DHAVE_WAYFIRE', language: 'cpp') src_files += files( diff --git a/meson_options.txt b/meson_options.txt index c0f63f28..65ff2a64 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -20,5 +20,6 @@ option('jack', type: 'feature', value: 'auto', description: 'Enable support for option('wireplumber', type: 'feature', value: 'auto', description: 'Enable support for WirePlumber') option('cava', type: 'feature', value: 'auto', description: 'Enable support for Cava') option('niri', type: 'boolean', description: 'Enable support for niri') +option('mango', type: 'boolean', description: 'Enable support for mango') option('login-proxy', type: 'boolean', description: 'Enable interfacing with dbus login interface') option('gps', type: 'feature', value: 'auto', description: 'Enable support for gps') diff --git a/src/factory.cpp b/src/factory.cpp index 2fd3e3b8..47166f1a 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -42,6 +42,13 @@ #include "modules/niri/window.hpp" #include "modules/niri/workspaces.hpp" #endif +#ifdef HAVE_MANGO +#include "modules/mango/language.hpp" +#include "modules/mango/keymode.hpp" +#include "modules/mango/window.hpp" +#include "modules/mango/workspaces.hpp" +#include "modules/mango/layout.hpp" +#endif #ifdef HAVE_WAYFIRE #include "modules/wayfire/window.hpp" #include "modules/wayfire/workspaces.hpp" @@ -231,6 +238,23 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, return new waybar::modules::niri::Workspaces(id, bar_, config_[name]); } #endif +#ifdef HAVE_MANGO + if (ref == "mango/window") { + return new waybar::modules::mango::Window(id, bar_, config_[name]); + } + if (ref == "mango/workspaces") { + return new waybar::modules::mango::Workspaces(id, bar_, config_[name]); + } + if (ref == "mango/language") { + return new waybar::modules::mango::Language(id, bar_, config_[name]); + } + if (ref == "mango/keymode") { + return new waybar::modules::mango::Keymode(id, bar_, config_[name]); + } + if (ref == "mango/layout") { + return new waybar::modules::mango::Layout(id, bar_, config_[name]); + } +#endif #ifdef HAVE_WAYFIRE if (ref == "wayfire/window") { return new waybar::modules::wayfire::Window(id, bar_, config_[name]); diff --git a/src/modules/mango/backend.cpp b/src/modules/mango/backend.cpp new file mode 100644 index 00000000..a8976f21 --- /dev/null +++ b/src/modules/mango/backend.cpp @@ -0,0 +1,317 @@ +#include "modules/mango/backend.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "util/scoped_fd.hpp" + +namespace waybar::modules::mango { + +int IPC::connectToSocket() { + const char* socket_path = getenv("MANGO_INSTANCE_SIGNATURE"); + if (!socket_path) { + throw std::runtime_error("Mango IPC: MANGO_INSTANCE_SIGNATURE not set"); + } + + struct sockaddr_un addr; + util::ScopedFd fd(socket(AF_UNIX, SOCK_STREAM, 0)); + if (fd == -1) throw std::runtime_error("socket() failed"); + + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + + if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { + throw std::runtime_error("connect() failed"); + } + return fd.release(); +} + +Json::Value IPC::sendCommand(const std::string& cmd) { + util::ScopedFd fd(IPC::connectToSocket()); + std::string full_cmd = cmd + "\n"; + + ssize_t total_written = 0; + while (total_written < (ssize_t)full_cmd.size()) { + ssize_t res = write(fd, full_cmd.c_str() + total_written, full_cmd.size() - total_written); + if (res < 0) { + if (errno == EINTR) continue; + throw std::runtime_error("Failed to write command"); + } + total_written += res; + } + + char buf[4096]; + std::string response; + while (true) { + ssize_t n = read(fd, buf, sizeof(buf) - 1); + if (n <= 0) { + if (n == 0) break; + if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) continue; + throw std::runtime_error("Read error"); + } + buf[n] = '\0'; + response += buf; + if (response.find('\n') != std::string::npos) break; + } + + Json::Value root; + std::istringstream iss(response); + Json::CharReaderBuilder builder; + std::string errors; + if (!Json::parseFromStream(builder, iss, &root, &errors)) { + throw std::runtime_error("JSON parse error: " + errors); + } + return root; +} + +Json::Value IPC::send(const Json::Value& request) { + if (!request.isMember("command")) { + throw std::runtime_error("Mango IPC: request must have 'command' field"); + } + return sendCommand(request["command"].asString()); +} + +void IPC::sendAsync(const Json::Value& request) { + if (!request.isMember("command")) { + spdlog::error("Mango IPC: request must have 'command' field"); + return; + } + std::string cmd = request["command"].asString(); + + std::thread([cmd]() { + try { + IPC::sendCommand(cmd); + } catch (const std::exception& e) { + spdlog::error("IPC async send failed: {}", e.what()); + } + }).detach(); +} + +IPC::IPC() : sockfd_(-1), active_client_(Json::nullValue) { startIPC(); } + +IPC::~IPC() { + if (sockfd_ != -1) close(sockfd_); + if (ipc_thread_.joinable()) ipc_thread_.join(); +} + +void IPC::startIPC() { + sockfd_ = IPC::connectToSocket(); + + ipc_thread_ = std::thread([this]() { + spdlog::info("Mango IPC thread started"); + + struct pollfd pfd; + pfd.fd = sockfd_; + pfd.events = POLLIN; + + const std::vector subs = {"watch all-monitors"}; + for (const auto& cmd : subs) { + if (write(sockfd_, cmd.c_str(), cmd.size()) != (ssize_t)cmd.size() || + write(sockfd_, "\n", 1) != 1) { + spdlog::error("Failed to subscribe to {}", cmd); + return; + } + } + + char buf[4096]; + std::string buffer; + while (true) { + int ret = poll(&pfd, 1, 1000); + if (ret == 0) continue; + if (ret < 0) { + if (errno == EINTR) continue; + spdlog::error("IPC poll error: {}", strerror(errno)); + break; + } + + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + spdlog::info("Mango IPC socket closed or invalid"); + break; + } + + if (pfd.revents & POLLIN) { + ssize_t n = read(sockfd_, buf, sizeof(buf)); + if (n == 0) { + spdlog::info("Mango IPC connection closed"); + break; + } + if (n < 0) { + if (errno == EINTR) continue; + spdlog::error("IPC read error: {}", strerror(errno)); + break; + } + buffer.append(buf, n); + + size_t pos; + while ((pos = buffer.find('\n')) != std::string::npos) { + std::string line = buffer.substr(0, pos); + buffer.erase(0, pos + 1); + if (line.empty()) continue; + try { + parseIPC(line); + } catch (const std::exception& e) { + spdlog::warn("Failed to parse IPC line: {} - {}", line, e.what()); + } + } + } + } + }); +} + +void IPC::parseIPC(const std::string& line) { + Json::Value root; + Json::CharReaderBuilder builder; + std::string errors; + std::istringstream iss(line); + if (!Json::parseFromStream(builder, iss, &root, &errors)) { + throw std::runtime_error("JSON parse error: " + errors); + } + + if (root.isMember("monitors") && root["monitors"].isArray()) { + for (const auto& mon : root["monitors"]) { + handleMonitorUpdate(mon); + } + + Json::Value active_monitor; + for (const auto& mon : root["monitors"]) { + if (mon["active"].asBool()) { + active_monitor = mon; + break; + } + } + + if (!active_monitor.isNull()) { + const auto& active_client = active_monitor["active_client"]; + updateFocusingClient(active_client); + + if (active_monitor.isMember("keyboardlayout")) { + updateKeyboardLayout(active_monitor["keyboardlayout"].asString()); + } + + if (active_monitor.isMember("keymode")) { + std::lock_guard lock(data_mutex_); + keymode_ = active_monitor["keymode"].asString(); + } + } + + std::vector handlers_to_notify; + { + std::lock_guard lock(callback_mutex_); + for (auto& [ev, handler] : callbacks_) { + if (ev == "monitor") { + handlers_to_notify.push_back(handler); + } + } + } + + for (auto* handler : handlers_to_notify) { + handler->onEvent(root); + } + + return; + } + + spdlog::debug("Unhandled IPC message: {}", line); +} + +std::unordered_map IPC::getMonitors() const { + std::lock_guard lock(data_mutex_); + return monitors_; +} + +IPC& IPC::getInstance() { + static IPC instance; + return instance; +} + +Json::Value IPC::getMonitor(const std::string& name) { + std::lock_guard lock(data_mutex_); + auto it = monitors_.find(name); + if (it != monitors_.end()) { + return it->second; + } + return Json::nullValue; +} + +std::string IPC::getKeyboardLayout() const { + std::lock_guard lock(data_mutex_); + return keyboard_layout_; +} + +std::string IPC::getKeymode() const { + std::lock_guard lock(data_mutex_); + return keymode_; +} + +Json::Value IPC::getActiveClientForMonitor(const std::string& name) const { + std::lock_guard lock(data_mutex_); + auto it = monitors_.find(name); + if (it != monitors_.end() && it->second.isMember("active_client")) { + return it->second["active_client"]; + } + return Json::nullValue; +} + +std::string IPC::getLayoutSymbolForMonitor(const std::string& name) const { + std::lock_guard lock(data_mutex_); + auto it = monitors_.find(name); + if (it != monitors_.end() && it->second.isMember("layout_symbol")) { + return it->second["layout_symbol"].asString(); + } + return {}; +} + +void IPC::handleMonitorUpdate(const Json::Value& mon) { + std::lock_guard lock(data_mutex_); + monitors_[mon["name"].asString()] = mon; +} + +void IPC::updateFocusingClient(const Json::Value& client) { + { + std::lock_guard lock(data_mutex_); + active_client_ = client; + + if (client.isNull() || !client.isObject() || client["id"].isNull()) { + focusing_client_id_ = 0; + } else { + focusing_client_id_ = client["id"].asUInt64(); + clients_[focusing_client_id_] = client; + } + } +} + +void IPC::updateKeyboardLayout(const std::string& layout) { + { + std::lock_guard lock(data_mutex_); + keyboard_layout_ = layout; + } +} + +void IPC::registerForIPC(const std::string& ev, EventHandler* handler) { + if (!handler) return; + std::lock_guard lock(callback_mutex_); + callbacks_.emplace_back(ev, handler); +} + +void IPC::unregisterForIPC(EventHandler* handler) { + if (!handler) return; + std::lock_guard lock(callback_mutex_); + for (auto it = callbacks_.begin(); it != callbacks_.end();) { + if (it->second == handler) + it = callbacks_.erase(it); + else + ++it; + } +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/keymode.cpp b/src/modules/mango/keymode.cpp new file mode 100644 index 00000000..16009712 --- /dev/null +++ b/src/modules/mango/keymode.cpp @@ -0,0 +1,58 @@ +#include "modules/mango/keymode.hpp" + +#include + +namespace waybar::modules::mango { + +Keymode::Keymode(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "keymode", id, "{}", 0, false), bar_(bar) { + IPC::getInstance().registerForIPC("monitor", this); + dp.emit(); +} + +Keymode::~Keymode() { IPC::getInstance().unregisterForIPC(this); } + +void Keymode::onEvent(const Json::Value& ev) { dp.emit(); } + +void Keymode::doUpdate() { + std::lock_guard lock(mutex_); + + std::string current = IPC::getInstance().getKeymode(); + + // if keymode is empty, hide the label + if (current.empty()) { + label_.hide(); + last_keymode_.clear(); + return; + } + + // if keymode is the same as last time, skip style changes + if (current != last_keymode_) { + if (!last_keymode_.empty()) label_.get_style_context()->remove_class(last_keymode_); + label_.get_style_context()->add_class(current); + last_keymode_ = current; + } + + // support config's format-keymode custom format (such as format-default, format-resize, etc.) + std::string text; + std::string format_key = "format-" + current; + if (config_.isMember(format_key)) { + text = fmt::format(fmt::runtime(config_[format_key].asString()), fmt::arg("mode", current)); + } else { + text = fmt::format(fmt::runtime(format_), fmt::arg("mode", current)); + } + + if (!text.empty()) { + label_.show(); + label_.set_markup(text); + } else { + label_.hide(); + } +} + +void Keymode::update() { + doUpdate(); + ALabel::update(); +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/language.cpp b/src/modules/mango/language.cpp new file mode 100644 index 00000000..9415002a --- /dev/null +++ b/src/modules/mango/language.cpp @@ -0,0 +1,148 @@ +#include "modules/mango/language.hpp" + +#include +#include +#include + +#include "util/string.hpp" + +namespace waybar::modules::mango { + +Language::Language(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "language", id, "{}", 0, false), bar_(bar), rxkb_ctx_(nullptr) { + rxkb_ctx_ = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); + if (rxkb_ctx_) { + rxkb_context_parse_default_ruleset(rxkb_ctx_); + } + + IPC::getInstance().registerForIPC("monitor", this); + updateFromIPC(); + dp.emit(); +} + +Language::~Language() { + IPC::getInstance().unregisterForIPC(this); + if (rxkb_ctx_) rxkb_context_unref(rxkb_ctx_); +} + +void Language::updateFromIPC() { + std::lock_guard lock(mutex_); + std::string layout = IPC::getInstance().getKeyboardLayout(); + + layouts_.clear(); + if (!layout.empty()) { + Layout l = getLayout(layout); + layouts_.push_back(l); + current_idx_ = 0; + } else { + current_idx_ = 0; + } +} + +void Language::doUpdate() { + std::lock_guard lock(mutex_); + if (layouts_.empty() || current_idx_ >= layouts_.size()) { + label_.hide(); + return; + } + const auto& layout = layouts_[current_idx_]; + + if (!last_short_name_.empty()) label_.get_style_context()->remove_class(last_short_name_); + if (!layout.short_name.empty()) { + label_.get_style_context()->add_class(layout.short_name); + last_short_name_ = layout.short_name; + } + + std::string layoutName; + std::string variant_key = "format-" + layout.short_description + "-" + layout.variant; + if (!layout.variant.empty() && config_.isMember(variant_key)) { + layoutName = + fmt::format(fmt::runtime(config_[variant_key].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("format-" + layout.short_description)) { + std::string key = "format-" + layout.short_description; + layoutName = + fmt::format(fmt::runtime(config_[key].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 { + layoutName = fmt::format(fmt::runtime(format_), fmt::arg("long", layout.full_name), + fmt::arg("short", layout.short_name), + fmt::arg("shortDescription", layout.short_description), + fmt::arg("variant", layout.variant)); + } + + if (!layoutName.empty()) { + label_.show(); + label_.set_markup(layoutName); + } else { + label_.hide(); + } +} + +void Language::update() { + updateFromIPC(); + doUpdate(); + ALabel::update(); +} + +void Language::onEvent(const Json::Value& ev) { + updateFromIPC(); + dp.emit(); +} + +Language::Layout Language::getLayout(const std::string& fullName) { + if (rxkb_ctx_) { + rxkb_layout* layout = rxkb_layout_first(rxkb_ctx_); + while (layout != nullptr) { + std::string desc = rxkb_layout_get_description(layout); + if (desc == fullName) { + std::string short_name = rxkb_layout_get_name(layout); + const char* variant_ptr = rxkb_layout_get_variant(layout); + std::string variant = variant_ptr ? variant_ptr : ""; + const char* brief_ptr = rxkb_layout_get_brief(layout); + std::string short_description = brief_ptr ? brief_ptr : ""; + + if (short_description.empty()) { + short_description = short_name; + } + + short_description = short_name; + + Layout info{desc, short_name, variant, short_description}; + return info; + } + layout = rxkb_layout_next(layout); + } + } + + spdlog::warn("mango language: rxkb failed to find layout '{}', using string parsing fallback", + fullName); + + Layout l; + l.full_name = fullName; + l.variant = ""; + + size_t paren_start = fullName.find('('); + size_t paren_end = fullName.find(')'); + if (paren_start != std::string::npos && paren_end != std::string::npos && + paren_end > paren_start) { + l.short_name = fullName.substr(paren_start + 1, paren_end - paren_start - 1); + } else if (fullName.length() >= 2) { + l.short_name = fullName.substr(0, 2); + } else { + l.short_name = fullName; + } + + std::transform(l.short_name.begin(), l.short_name.end(), l.short_name.begin(), + [](unsigned char c) { return std::tolower(c); }); + + l.short_description = l.short_name; + + return l; +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/layout.cpp b/src/modules/mango/layout.cpp new file mode 100644 index 00000000..124d26cb --- /dev/null +++ b/src/modules/mango/layout.cpp @@ -0,0 +1,56 @@ +#include "modules/mango/layout.hpp" + +#include + +namespace waybar::modules::mango { + +Layout::Layout(const std::string& id, const Bar& bar, const Json::Value& config) + : ALabel(config, "layout", id, "{}", 0, false), bar_(bar) { + IPC::getInstance().registerForIPC("monitor", this); + dp.emit(); +} + +Layout::~Layout() { IPC::getInstance().unregisterForIPC(this); } + +void Layout::onEvent(const Json::Value& ev) { dp.emit(); } + +void Layout::doUpdate() { + std::lock_guard lock(mutex_); + + std::string symbol = IPC::getInstance().getLayoutSymbolForMonitor(bar_.output->name); + + if (symbol.empty()) { + label_.hide(); + last_symbol_.clear(); + return; + } + + if (symbol != last_symbol_) { + if (!last_symbol_.empty()) label_.get_style_context()->remove_class(last_symbol_); + label_.get_style_context()->add_class(symbol); + last_symbol_ = symbol; + } + + std::string text; + std::string format_key = "format-" + symbol; + + if (config_.isMember(format_key)) { + text = fmt::format(fmt::runtime(config_[format_key].asString()), fmt::arg("symbol", symbol)); + } else { + text = fmt::format(fmt::runtime(format_), fmt::arg("symbol", symbol)); + } + + if (!text.empty()) { + label_.show(); + label_.set_markup(text); + } else { + label_.hide(); + } +} + +void Layout::update() { + doUpdate(); + ALabel::update(); +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/window.cpp b/src/modules/mango/window.cpp new file mode 100644 index 00000000..54f677bb --- /dev/null +++ b/src/modules/mango/window.cpp @@ -0,0 +1,93 @@ +#include "modules/mango/window.hpp" + +#include + +#include "util/rewrite_string.hpp" +#include "util/sanitize_str.hpp" + +namespace waybar::modules::mango { + +Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) + : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) { + IPC::getInstance().registerForIPC("monitor", this); + dp.emit(); +} + +Window::~Window() { IPC::getInstance().unregisterForIPC(this); } + +void Window::onEvent(const Json::Value& ev) { dp.emit(); } + +void Window::doUpdate() { + std::lock_guard lock(mutex_); + + const Json::Value& client = IPC::getInstance().getActiveClientForMonitor(bar_.output->name); + + // judge whether to hide: active_client is null or title field is null + if (client.isNull() || !client.isObject() || client["title"].isNull()) { + event_box_.hide(); + label_.hide(); + updateAppIconName("", ""); + setClass("empty", true); + if (!oldAppId_.empty()) setClass(oldAppId_, false); + oldAppId_.clear(); + return; + } + + // if we have a valid client, show the label and update content + event_box_.show(); + label_.show(); + setClass("empty", false); + + std::string title = client["title"].asString(); + std::string appid = client["appid"].asString(); + std::string sanitized_title = waybar::util::sanitize_string(title); + std::string sanitized_appid = waybar::util::sanitize_string(appid); + + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", sanitized_title), + fmt::arg("app_id", sanitized_appid)), + config_["rewrite"])); + + updateAppIconName(appid, ""); + if (tooltipEnabled()) label_.set_tooltip_markup(title); + + // Solo judgment + bool solo = false; + if (client.isMember("tags") && client["tags"].isArray() && client["tags"].size() == 1) { + int tag_idx = client["tags"][0].asInt(); + const auto& monitors = IPC::getInstance().getMonitors(); + auto mon_it = monitors.find(client["monitor"].asString()); + if (mon_it != monitors.end()) { + const auto& tags = mon_it->second["tags"]; + for (const auto& tag : tags) { + if (tag["index"].asInt() == tag_idx) { + solo = (tag["client_count"].asInt() == 1); + break; + } + } + } + } + setClass("solo", solo); + if (!appid.empty()) setClass(appid, solo); + + if (oldAppId_ != appid) { + if (!oldAppId_.empty()) setClass(oldAppId_, false); + oldAppId_ = appid; + } +} + +void Window::update() { + doUpdate(); + AAppIconLabel::update(); +} + +void Window::setClass(const std::string& className, bool enable) { + auto style_context = event_box_.get_style_context(); + if (enable) { + if (!style_context->has_class(className)) style_context->add_class(className); + } else { + style_context->remove_class(className); + } +} + +} // namespace waybar::modules::mango \ No newline at end of file diff --git a/src/modules/mango/workspaces.cpp b/src/modules/mango/workspaces.cpp new file mode 100644 index 00000000..f8849228 --- /dev/null +++ b/src/modules/mango/workspaces.cpp @@ -0,0 +1,252 @@ +#include "modules/mango/workspaces.hpp" + +#include + +#include + +namespace waybar::modules::mango { + +Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config) + : AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) { + box_.set_name("workspaces"); + if (!id.empty()) box_.get_style_context()->add_class(id); + box_.get_style_context()->add_class(MODULE_CLASS); + event_box_.add(box_); + + if (config_["on-click"].isString()) on_click_left_ = config_["on-click"].asString(); + if (config_["on-click-middle"].isString()) + on_click_middle_ = config_["on-click-middle"].asString(); + if (config_["on-click-right"].isString()) on_click_right_ = config_["on-click-right"].asString(); + + overview_button_ = new Gtk::Button("OVERVIEW"); + overview_button_->set_relief(Gtk::RELIEF_NONE); + box_.pack_start(*overview_button_, false, false, 0); + + if (!on_click_left_.empty() || !on_click_middle_.empty() || !on_click_right_.empty()) { + overview_button_->add_events(Gdk::BUTTON_PRESS_MASK); + overview_button_->signal_button_press_event().connect( + [this](GdkEventButton* event) -> bool { return handleButtonClick(event, 0, true); }, false); + } + + IPC::getInstance().registerForIPC("monitor", this); + dp.emit(); +} + +Workspaces::~Workspaces() { + IPC::getInstance().unregisterForIPC(this); + + if (overview_button_) { + box_.remove(*overview_button_); + delete overview_button_; + overview_button_ = nullptr; + } + + for (auto& [idx, btn] : buttons_) { + box_.remove(btn); + } + buttons_.clear(); +} + +void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); } + +void Workspaces::doUpdate() { + Json::Value monitor = IPC::getInstance().getMonitor(bar_.output->name); + if (monitor.isNull()) return; + + const auto& tags = monitor["tags"]; + + bool overview_mode = false; + if (monitor.isMember("active_tags") && monitor["active_tags"].isArray()) { + const auto& active_tags = monitor["active_tags"]; + if (active_tags.size() == 1 && active_tags[0].asInt() == 0) { + overview_mode = true; + } + } + + for (auto& [idx, btn] : buttons_) { + btn.hide(); + } + + if (overview_mode) { + overview_button_->show(); + auto style = overview_button_->get_style_context(); + style->add_class("overview"); + if (monitor["active"].asBool()) + style->add_class("current_output"); + else + style->remove_class("current_output"); + + std::string label = + config_["overview-label"].isString() ? config_["overview-label"].asString() : "OVERVIEW"; + + if (!config_["disable-markup"].asBool()) { + if (auto gtk_label = dynamic_cast(overview_button_->get_child())) { + gtk_label->set_markup(label); + } + } else { + overview_button_->set_label(label); + } + } else { + overview_button_->hide(); + + for (auto btn_it = buttons_.begin(); btn_it != buttons_.end();) { + uint64_t id = btn_it->first; + bool found = std::any_of(tags.begin(), tags.end(), [id](const Json::Value& tag) { + return tag["index"].asUInt64() == id; + }); + + if (!found) { + box_.remove(btn_it->second); + btn_it = buttons_.erase(btn_it); + } else { + ++btn_it; + } + } + + for (const auto& tag : tags) { + uint64_t idx = tag["index"].asUInt64(); + auto btn_it = buttons_.find(idx); + Gtk::Button& button = (btn_it == buttons_.end()) ? addButton(idx) : btn_it->second; + updateButtonState(button, tag, monitor); + } + + std::vector indices; + for (const auto& tag : tags) indices.push_back(tag["index"].asUInt64()); + std::sort(indices.begin(), indices.end()); + int pos = 0; + for (uint64_t idx : indices) { + box_.reorder_child(buttons_[idx], pos + 1); + pos++; + } + } +} + +void Workspaces::update() { + doUpdate(); + AModule::update(); +} + +Gtk::Button& Workspaces::addButton(uint64_t idx) { + auto [it, _] = buttons_.emplace(idx, std::to_string(idx)); + auto& button = it->second; + box_.pack_start(button, false, false, 0); + button.set_relief(Gtk::RELIEF_NONE); + + if (!on_click_left_.empty() || !on_click_middle_.empty() || !on_click_right_.empty()) { + button.add_events(Gdk::BUTTON_PRESS_MASK); + button.signal_button_press_event().connect( + [this, idx](GdkEventButton* event) -> bool { return handleButtonClick(event, idx, false); }, + false); + } + + button.show_all(); + return button; +} + +void Workspaces::updateButtonState(Gtk::Button& button, const Json::Value& tag, + const Json::Value& monitor) { + auto style = button.get_style_context(); + bool active = tag["is_active"].asBool(); + bool urgent = tag["is_urgent"].asBool(); + bool empty = (tag["client_count"].asInt() == 0); + + if (active) + style->add_class("active"); + else + style->remove_class("active"); + + if (urgent) + style->add_class("urgent"); + else + style->remove_class("urgent"); + + if (empty) + style->add_class("empty"); + else + style->remove_class("empty"); + + if (monitor["active"].asBool()) + style->add_class("current_output"); + else + style->remove_class("current_output"); + + uint64_t idx = tag["index"].asUInt64(); + std::string name = std::to_string(idx); + if (config_["format"].isString()) { + name = fmt::format(fmt::runtime(config_["format"].asString()), + fmt::arg("icon", getIcon(name, tag)), fmt::arg("value", name), + fmt::arg("index", idx), fmt::arg("output", monitor["name"].asString())); + } + + if (!config_["disable-markup"].asBool()) { + if (auto gtk_label = dynamic_cast(button.get_child())) { + gtk_label->set_markup(name); + } + } else { + button.set_label(name); + } + + if (config_["current-only"].asBool()) { + if (active) + button.show(); + else + button.hide(); + } else if (config_["hide-empty"].asBool() && empty && !active) { + button.hide(); + } else { + button.show(); + } +} + +std::string Workspaces::getIcon(const std::string& value, const Json::Value& tag) { + const auto& icons = config_["format-icons"]; + if (!icons) return value; + + if (tag["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString(); + if (tag["is_active"].asBool() && icons["active"]) return icons["active"].asString(); + if (tag["client_count"].asInt() == 0 && icons["empty"]) return icons["empty"].asString(); + + std::string idx = std::to_string(tag["index"].asUInt()); + if (icons[idx]) return icons[idx].asString(); + if (icons["default"]) return icons["default"].asString(); + return value; +} + +bool Workspaces::handleButtonClick(GdkEventButton* event, uint64_t idx, bool isOverview) { + std::string action; + if (event->button == 1) + action = on_click_left_; + else if (event->button == 2) + action = on_click_middle_; + else if (event->button == 3) + action = on_click_right_; + + if (action.empty()) return true; + + try { + std::string cmd; + if (isOverview) { + if (action == "activate") + cmd = "dispatch overview"; + else if (action == "toggle") + cmd = "dispatch toggleoverview"; + } else { + if (action == "activate") + cmd = "dispatch view," + std::to_string(idx); + else if (action == "toggle") + cmd = "dispatch toggleview," + std::to_string(idx); + } + + if (!cmd.empty()) { + Json::Value req; + req["command"] = cmd; + IPC::sendAsync(req); + } + } catch (const std::exception& e) { + spdlog::error("Error sending IPC command: {}", e.what()); + } + + return true; +} + +} // namespace waybar::modules::mango \ No newline at end of file From 0af3ca1a4ed399d53d853ebe7dd6c7f8c00ea321 Mon Sep 17 00:00:00 2001 From: Matthew Orlando Date: Sun, 31 May 2026 09:58:09 -0700 Subject: [PATCH 216/416] custom_modules: Fix mediaplayer.py handling untitled track * Fix crash calling method on None * Show artist if title is None --- resources/custom_modules/mediaplayer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/resources/custom_modules/mediaplayer.py b/resources/custom_modules/mediaplayer.py index 524d4d2a..4de1edf8 100755 --- a/resources/custom_modules/mediaplayer.py +++ b/resources/custom_modules/mediaplayer.py @@ -105,24 +105,26 @@ class PlayerManager: current_player = self.get_first_playing_player() if current_player is not None: self.on_metadata_changed(current_player, current_player.props.metadata) - else: + else: self.clear_output() def on_metadata_changed(self, player, metadata, _=None): logger.debug(f"Metadata changed for player {player.props.player_name}") player_name = player.props.player_name artist = player.get_artist() - artist = artist.replace("&", "&") + artist = artist and artist.replace("&", "&") title = player.get_title() - title = title.replace("&", "&") + title = title and title.replace("&", "&") track_info = "" if player_name == "spotify" and "mpris:trackid" in metadata.keys() and ":ad:" in player.props.metadata["mpris:trackid"]: track_info = "Advertisement" elif artist is not None and title is not None: track_info = f"{artist} - {title}" - else: + elif title is not None: track_info = title + elif artist is not None: + track_info = artist if track_info: if player.props.status == "Playing": From 3315631012befb1bd33f7e231272b54a86b79488 Mon Sep 17 00:00:00 2001 From: oqlatulesba3 Date: Tue, 2 Jun 2026 10:51:05 +0300 Subject: [PATCH 217/416] fix(sni): retry Host proxy creation on transient errors When Host::proxyReady fails to create the SnWatcher proxy (e.g. because the Watcher has not finished exporting /StatusNotifierWatcher yet), the cancellable is left set, causing nameAppeared to early-return on every subsequent event (see the // TODO marker). The Host is then stuck without a watcher, and the tray module reports 'No such object path /StatusNotifierWatcher' until the bar is fully restarted. Clear the cancellable on non-CANCELLED errors and schedule a single delayed retry of nameAppeared. The guard `watcher_ != nullptr` skips the retry if a parallel call already succeeded. Closes #3468 --- src/modules/sni/host.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 567fbf9f..091c45d5 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -6,6 +6,8 @@ namespace waybar::modules::SNI { +static const unsigned RETRY_DELAY_MS = 200; + Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, const std::function&)>& on_add, const std::function&)>& on_remove, @@ -72,11 +74,21 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { return; } auto host = static_cast(data); - host->watcher_ = watcher; if (error != nullptr) { spdlog::error("Host: {}", error->message); + g_clear_object(&host->cancellable_); + Glib::signal_timeout().connect_once( + [host]() { + if (host->watcher_ != nullptr) { + return; + } + auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); + host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + }, + RETRY_DELAY_MS); return; } + host->watcher_ = watcher; sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_, &Host::registerHost, data); } From 5cce616d3d72b402dabc64aabb872e0cfb25165c Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 14:41:31 -0700 Subject: [PATCH 218/416] perf(json): parse directly from string buffers --- include/util/json.hpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/include/util/json.hpp b/include/util/json.hpp index 5f756f35..5f45ae92 100644 --- a/include/util/json.hpp +++ b/include/util/json.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #if (FMT_VERSION >= 90000) @@ -26,14 +27,19 @@ class JsonParser { Json::Value root; // replace all occurrences of "\x" with "\u00", because JSON doesn't allow "\x" escape sequences - std::string modifiedJsonStr = replaceHexadecimalEscape(jsonStr); + std::string modifiedJsonStr; + const std::string* json = &jsonStr; + if (jsonStr.find("\\x") != std::string::npos) { + modifiedJsonStr = replaceHexadecimalEscape(jsonStr); + json = &modifiedJsonStr; + } - std::istringstream jsonStream(modifiedJsonStr); std::string errs; // Use local CharReaderBuilder for thread safety - the IPC singleton's // parser can be called concurrently from multiple module threads Json::CharReaderBuilder readerBuilder; - if (!Json::parseFromStream(readerBuilder, jsonStream, &root, &errs)) { + auto reader = std::unique_ptr(readerBuilder.newCharReader()); + if (!reader->parse(json->data(), json->data() + json->size(), &root, &errs)) { throw std::runtime_error("Error parsing JSON: " + errs); } return root; From 2190871a68730d52cd0e0ce7e2ed959e11c23ead Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 15:18:43 -0700 Subject: [PATCH 219/416] perf(label): skip redundant markup updates --- include/ALabel.hpp | 9 +++++++++ src/ALabel.cpp | 20 ++++++++++++++++++++ src/modules/mpd/mpd.cpp | 8 ++++---- src/modules/sway/window.cpp | 4 ++-- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/include/ALabel.hpp b/include/ALabel.hpp index b3852877..7c5150a0 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -4,6 +4,8 @@ #include #include +#include + #include "AModule.hpp" namespace waybar { @@ -25,12 +27,19 @@ class ALabel : public AModule { bool alt_ = false; std::string default_format_; + bool setLabelMarkup(const Glib::ustring& markup); + bool setTooltipMarkup(const Glib::ustring& markup); + bool handleToggle(GdkEventButton* const& e) override; virtual std::string getState(uint8_t value, bool lesser = false); std::map submenus_; std::map menuActionsMap_; static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data); + + private: + std::optional last_label_markup_; + std::optional last_tooltip_markup_; }; } // namespace waybar diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 795f87f1..f2a585fe 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -139,6 +139,26 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st auto ALabel::update() -> void { AModule::update(); } +bool ALabel::setLabelMarkup(const Glib::ustring& markup) { + if (last_label_markup_ == markup) { + return false; + } + + label_.set_markup(markup); + last_label_markup_ = markup; + return true; +} + +bool ALabel::setTooltipMarkup(const Glib::ustring& markup) { + if (last_tooltip_markup_ == markup) { + return false; + } + + label_.set_tooltip_markup(markup); + last_tooltip_markup_ = markup; + return true; +} + std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_t max) { auto format_icons = config_["format-icons"]; if (format_icons.isObject()) { diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..873b6251 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -98,7 +98,7 @@ void waybar::modules::MPD::setLabel() { ? config_["format-disconnected"].asString() : "disconnected"; if (format.empty()) { - label_.set_markup(format); + setLabelMarkup(format); label_.show(); } else { label_.hide(); @@ -110,7 +110,7 @@ void waybar::modules::MPD::setLabel() { ? config_["tooltip-format-disconnected"].asString() : "MPD (disconnected)"; // Nothing to format - label_.set_tooltip_markup(tooltip_format); + setTooltipMarkup(tooltip_format); } return; } @@ -190,7 +190,7 @@ void waybar::modules::MPD::setLabel() { label_.hide(); } else { label_.show(); - label_.set_markup(text); + setLabelMarkup(text); } } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error: {}", e.what()); @@ -210,7 +210,7 @@ void waybar::modules::MPD::setLabel() { fmt::arg("stateIcon", stateIcon), fmt::arg("consumeIcon", consumeIcon), fmt::arg("randomIcon", randomIcon), fmt::arg("repeatIcon", repeatIcon), fmt::arg("singleIcon", singleIcon), fmt::arg("filename", filename), fmt::arg("uri", uri)); - label_.set_tooltip_markup(tooltip_text); + setTooltipMarkup(tooltip_text); } catch (fmt::format_error const& e) { spdlog::warn("mpd: format error (tooltip): {}", e.what()); } diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index 2908b85c..2b62da14 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -94,12 +94,12 @@ auto Window::update() -> void { old_app_id_ = app_id_; } - label_.set_markup(waybar::util::rewriteString( + setLabelMarkup(waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", window_), fmt::arg("app_id", app_id_), fmt::arg("shell", shell_), fmt::arg("marks", marks_)), config_["rewrite"])); if (tooltipEnabled()) { - label_.set_tooltip_markup(window_); + setTooltipMarkup(window_); } updateAppIcon(); From 0cccf0d8b50dd7576f2295b24065616ef4fe4d0a Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 15:23:54 -0700 Subject: [PATCH 220/416] perf(mpd): avoid duplicate playing-state updates --- include/modules/mpd/mpd.hpp | 1 - include/modules/mpd/state.hpp | 1 - include/modules/mpd/state.inl.hpp | 1 - src/modules/mpd/mpd.cpp | 15 --------------- src/modules/mpd/state.cpp | 1 - 5 files changed, 19 deletions(-) diff --git a/include/modules/mpd/mpd.hpp b/include/modules/mpd/mpd.hpp index 9d81638e..27c56153 100644 --- a/include/modules/mpd/mpd.hpp +++ b/include/modules/mpd/mpd.hpp @@ -54,7 +54,6 @@ class MPD : public ALabel { void tryConnect(); void checkErrors(mpd_connection* conn); void fetchState(); - void queryMPD(); inline bool stopped() const { return connection_ && state_ == MPD_STATE_STOP; } inline bool playing() const { return connection_ && state_ == MPD_STATE_PLAY; } diff --git a/include/modules/mpd/state.hpp b/include/modules/mpd/state.hpp index 2c9071b4..0624c196 100644 --- a/include/modules/mpd/state.hpp +++ b/include/modules/mpd/state.hpp @@ -197,7 +197,6 @@ class Context { void tryConnect() const; void checkErrors(mpd_connection*) const; void do_update(); - void queryMPD() const; void fetchState() const; constexpr mpd_state state() const; void emit() const; diff --git a/include/modules/mpd/state.inl.hpp b/include/modules/mpd/state.inl.hpp index 895970e6..06232b95 100644 --- a/include/modules/mpd/state.inl.hpp +++ b/include/modules/mpd/state.inl.hpp @@ -15,7 +15,6 @@ constexpr inline mpd_state Context::state() const { return mpd_module_->state_; inline void Context::do_update() { mpd_module_->setLabel(); } inline void Context::checkErrors(mpd_connection* conn) const { mpd_module_->checkErrors(conn); } -inline void Context::queryMPD() const { mpd_module_->queryMPD(); } inline void Context::fetchState() const { mpd_module_->fetchState(); } inline void Context::emit() const { mpd_module_->emit(); } diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..a84abf5c 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -51,21 +51,6 @@ auto waybar::modules::MPD::update() -> void { ALabel::update(); } -void waybar::modules::MPD::queryMPD() { - if (connection_ != nullptr) { - spdlog::trace("{}: fetching state information", module_name_); - try { - fetchState(); - spdlog::trace("{}: fetch complete", module_name_); - } catch (std::exception const& e) { - spdlog::error("{}: {}", module_name_, e.what()); - state_ = MPD_STATE_UNKNOWN; - } - - dp.emit(); - } -} - std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const { std::string result = config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A"; diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 3d7c8561..eb95665f 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -152,7 +152,6 @@ bool Playing::on_timer() { return false; } - ctx_->queryMPD(); ctx_->emit(); } catch (std::exception const& e) { spdlog::warn("mpd: Playing: error: {}", e.what()); From bef7df8f08b66c1f33910037e84fdfe170d2bbcf Mon Sep 17 00:00:00 2001 From: Alexey Ivanov Date: Thu, 4 Jun 2026 16:08:50 -0700 Subject: [PATCH 221/416] fix(mpd): avoid false no-song warning when stopped --- src/modules/mpd/mpd.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..19a59239 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -124,8 +124,9 @@ void waybar::modules::MPD::setLabel() { std::string stateIcon = ""; bool no_song = song_.get() == nullptr; - if (stopped() || no_song) { - if (no_song) spdlog::warn("Bug in mpd: no current song but state is not stopped."); + bool is_stopped = stopped(); + if (is_stopped || no_song) { + if (no_song && !is_stopped) spdlog::warn("mpd: no current song while state is not stopped"); format = config_["format-stopped"].isString() ? config_["format-stopped"].asString() : "stopped"; label_.get_style_context()->add_class("stopped"); From f7b9d8c1238db93325ed1f50681fbc3f9a98ccc9 Mon Sep 17 00:00:00 2001 From: Zuodong Yu Date: Sun, 7 Jun 2026 17:35:37 +0800 Subject: [PATCH 222/416] modules/niri: add {total} format token to workspaces --- man/waybar-niri-workspaces.5.scd | 2 ++ src/modules/niri/workspaces.cpp | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 4be85eb3..80cc7eb5 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -63,6 +63,8 @@ as defined by niri. *{output}*: Output where the workspace is located. +*{total}*: The total number of workspaces. + # ICONS Additional to workspace name matching, the following *format-icons* can be set. diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 97d15215..ce0d2376 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -100,7 +100,8 @@ void Workspaces::doUpdate() { name = fmt::format(fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name), fmt::arg("name", ws["name"].asString()), fmt::arg("index", ws["idx"].asUInt()), - fmt::arg("output", ws["output"].asString())); + fmt::arg("output", ws["output"].asString()), + fmt::arg("total", my_workspaces.size())); } if (!config_["disable-markup"].asBool()) { static_cast(button.get_children()[0])->set_markup(name); From fb829c8a8e037fd8631af94dc2f888075612d1d0 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 9 Jun 2026 06:20:05 +0000 Subject: [PATCH 223/416] ## fix(niri/workspaces): crash (segfault) when setting workspace button markup ### Problem Waybar segfaults on startup when `niri/workspaces` is the only configured module (default options). Right after the first `WorkspacesChanged` event: Thread 1 "waybar" received signal SIGSEGV #0 gtk_label_set_markup () #1 waybar::modules::niri::Workspaces::doUpdate () at src/modules/niri/workspaces.cpp:106 ### Root cause [doUpdate()](cci:1://file:///home/lj/Downloads/Waybar/src/modules/niri/window.cpp:27:0-90:1) set the label markup via: static_cast(button.get_children()[0])->set_markup(name); The button's child *is* a valid `GtkLabel`, but gtkmm's `get_children()` returns it wrapped as a generic `Gtk::Widget` (confirmed: `dynamic_cast` to `Gtk::Label*` yields `nullptr`). The unchecked `static_cast` then performs an invalid downcast, producing a corrupt pointer whose `gobj()` is garbage (`0x1`), so `gtk_label_set_markup()` dereferences it and crashes. This is reliably triggered when no other module has instantiated a `Gtk::Label` yet (so the `Gtk::Label` wrapper isn't registered), which is exactly the case for a `niri/workspaces`-only bar. The same idiom exists in `sway/workspaces` and `wayfire/workspaces`; it's masked there because typical configs include other label-using modules. ### Testing - Reproduced the crash on 0.15.0 / current `master` with a minimal `"modules-left": ["niri/workspaces"]` config under niri. - After the fix: no crash; three workspace buttons render with correct names/labels (`niri-workspace-1/2/3`). --- src/modules/niri/workspaces.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 97d15215..a817ab9c 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -103,7 +103,9 @@ void Workspaces::doUpdate() { fmt::arg("output", ws["output"].asString())); } if (!config_["disable-markup"].asBool()) { - static_cast(button.get_children()[0])->set_markup(name); + auto* child = gtk_bin_get_child(GTK_BIN(button.gobj())); + if (child != nullptr && GTK_IS_LABEL(child)) + gtk_label_set_markup(GTK_LABEL(child), name.c_str()); } else { button.set_label(name); } From 25061962744c196560cc9853243c2ce986b80d60 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 9 Jun 2026 06:49:41 +0000 Subject: [PATCH 224/416] Update privacy.hpp --- include/modules/privacy/privacy.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index 68d2bf19..729fcc6a 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -28,6 +28,7 @@ class Privacy : public AModule { // Config Gtk::Box box_; + std::vector modules_; uint iconSpacing = 4; uint iconSize = 20; uint transition_duration = 250; From 2d9fee144e7427fb3abfd48921931ade5d08149d Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 9 Jun 2026 06:51:14 +0000 Subject: [PATCH 225/416] Update privacy.cpp --- src/modules/privacy/privacy.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 904c8fd9..4e613971 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -71,6 +71,7 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat auto* item = Gtk::make_managed(module, nodeType, nodePtr, orientation, pos, iconSize, transition_duration); box_.add(*item); + modules_.push_back(item); } } @@ -148,9 +149,7 @@ auto Privacy::update() -> void { bool useAudioOut = false; mutex_.lock(); - for (Gtk::Widget* widget : box_.get_children()) { - auto* module = dynamic_cast(widget); - if (module == nullptr) continue; + for (PrivacyItem* module : modules_) { switch (module->privacy_type) { case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: setScreenshare = true; From 61178f7b0e99e30617cc0a7b51aaecbea5e9da9a Mon Sep 17 00:00:00 2001 From: InetAddress <78892259+InetAddress@users.noreply.github.com> Date: Fri, 12 Jun 2026 04:38:10 +0000 Subject: [PATCH 226/416] Change error handling to warning Fixes a bug where removing a hot-plugged device mid-loop causes a filesystem error that turns into a fatal runtime error that crashes waybar entirely, changed to warning instead of runtime error. --- src/modules/battery.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 25436baf..67d6bb57 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -153,7 +153,7 @@ void waybar::modules::Battery::refreshBatteries() { } } } catch (fs::filesystem_error& e) { - throw std::runtime_error(e.what()); + spdlog::warn("Battery directory tracking failed: {}", e.what()); } if (warnFirstTime_ && batteries_.empty()) { if (config_["bat"].isString()) { From 4ef828f7123a6067a619c409f330e2c21660179c Mon Sep 17 00:00:00 2001 From: martuscellifaria Date: Fri, 12 Jun 2026 09:14:04 +0200 Subject: [PATCH 227/416] Fixing CPU usage spike on Waybar --- src/modules/custom.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 28def8c9..c6104e40 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -61,6 +61,10 @@ void waybar::modules::Custom::delayWorker() { } dp.emit(); } + if (!config_["exec"].isString() && !config_["exec-if"].isString()) { + thread_.stop(); + return; + } thread_.sleep_for(interval_); }; } From eb3669decc8efb1a95d4a29b6846f7e93ed5abeb Mon Sep 17 00:00:00 2001 From: martuscellifaria Date: Fri, 12 Jun 2026 09:48:44 +0200 Subject: [PATCH 228/416] Rearranging code in a non-redundant way. --- src/modules/custom.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index c6104e40..7fa5cf01 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -39,12 +39,16 @@ waybar::modules::Custom::~Custom() { } void waybar::modules::Custom::delayWorker() { + if (!config_["exec"].isString() && !config_["exec-if"].isString()) { + dp.emit(); + return; + } + thread_ = [this] { for (int i : this->pid_children_) { int status; waitpid(i, &status, 0); } - this->pid_children_.clear(); bool can_update = true; @@ -61,10 +65,6 @@ void waybar::modules::Custom::delayWorker() { } dp.emit(); } - if (!config_["exec"].isString() && !config_["exec-if"].isString()) { - thread_.stop(); - return; - } thread_.sleep_for(interval_); }; } From 0bc664a99eb27dab7bc351b7e490d82868e3470d Mon Sep 17 00:00:00 2001 From: martuscellifaria Date: Fri, 12 Jun 2026 09:52:03 +0200 Subject: [PATCH 229/416] Readded the whitespace from the original code --- src/modules/custom.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 7fa5cf01..1d9b320c 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -49,6 +49,7 @@ void waybar::modules::Custom::delayWorker() { int status; waitpid(i, &status, 0); } + this->pid_children_.clear(); bool can_update = true; From 14e59daae4137c88157545397644853f22e4eab6 Mon Sep 17 00:00:00 2001 From: iamcheyan Date: Sat, 13 Jun 2026 11:34:29 +0900 Subject: [PATCH 230/416] feat(wlr/taskbar): expose active toplevel state on the bar window Add opt-in bar-css-states option to wlr/taskbar module. When enabled, the module tracks the active workspace and updates CSS classes on window#waybar to reflect the current window state: - toplevel-active: when a window is focused - toplevel-maximized: when any non-minimized window on active workspace is maximized - toplevel-minimized: when the active window is minimized - toplevel-fullscreen: when any non-minimized window on active workspace is fullscreen This feature requires compositor support for ext-workspace-v1 protocol to correctly track workspace membership. Without it, classes fall back to the active application state only. Also includes active-only option implementation. Changes: - Add ext-workspace-v1 protocol support for workspace tracking - Implement WorkspaceState tracking in Taskbar class - Add workspace management lifecycle (create/remove/done) - Add bar CSS class update logic with aggregation - Add visible() getter to Task class - Implement active-only task visibility control - Update man page with documentation and CSS examples Signed-off-by: iamcheyan --- include/modules/wlr/taskbar.hpp | 27 ++++ man/waybar-wlr-taskbar.5.scd | 45 ++++++ src/modules/wlr/taskbar.cpp | 245 +++++++++++++++++++++++++++++++- 3 files changed, 316 insertions(+), 1 deletion(-) diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index 413eae85..3ad41c6e 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -18,6 +18,7 @@ #include "AModule.hpp" #include "bar.hpp" #include "client.hpp" +#include "ext-workspace-v1-client-protocol.h" #include "giomm/desktopappinfo.h" #include "util/icon_loader.hpp" #include "util/json.hpp" @@ -80,6 +81,7 @@ class Task { std::string title_; std::string app_id_; uint32_t state_ = 0; + struct ext_workspace_handle_v1* workspace_ = nullptr; int32_t drag_start_x; int32_t drag_start_y; @@ -102,6 +104,9 @@ class Task { bool minimized() const { return state_ & MINIMIZED; } bool active() const { return state_ & ACTIVE; } bool fullscreen() const { return state_ & FULLSCREEN; } + bool visible() const { return button_visible_; } + struct ext_workspace_handle_v1* workspace() const { return workspace_; } + void set_workspace(struct ext_workspace_handle_v1* workspace) { workspace_ = workspace; } public: /* Callbacks for the wlr protocol */ @@ -142,6 +147,12 @@ using TaskPtr = std::unique_ptr; class Taskbar : public waybar::AModule { public: + struct WorkspaceState { + Taskbar* taskbar; + struct ext_workspace_handle_v1* handle; + uint32_t state = 0; + }; + Taskbar(const std::string&, const waybar::Bar&, const Json::Value&); ~Taskbar(); void update(); @@ -156,22 +167,35 @@ class Taskbar : public waybar::AModule { std::map app_ids_replace_map_; struct zwlr_foreign_toplevel_manager_v1* manager_; + struct ext_workspace_manager_v1* workspace_manager_; struct wl_seat* seat_; + std::vector workspace_groups_; + std::vector> workspaces_; + struct ext_workspace_handle_v1* current_workspace_ = nullptr; public: /* Callbacks for global registration */ void register_manager(struct wl_registry*, uint32_t name, uint32_t version); + void register_workspace_manager(struct wl_registry*, uint32_t name, uint32_t version); void register_seat(struct wl_registry*, uint32_t name, uint32_t version); /* Callbacks for the wlr protocol */ void handle_toplevel_create(struct zwlr_foreign_toplevel_handle_v1*); void handle_finished(); + void handle_workspace_group_create(struct ext_workspace_group_handle_v1*); + void handle_workspace_group_removed(struct ext_workspace_group_handle_v1*); + void handle_workspace_create(struct ext_workspace_handle_v1*); + void handle_workspace_done(); + void handle_workspace_finished(); + void handle_workspace_removed(struct ext_workspace_handle_v1*); public: void add_button(Gtk::Button&); void move_button(Gtk::Button&, int); void remove_button(Gtk::Button&); void remove_task(uint32_t); + void assign_current_workspace(Task&); + void update_bar_css_classes(); bool show_output(struct wl_output*) const; bool all_outputs() const; @@ -179,6 +203,9 @@ class Taskbar : public waybar::AModule { const IconLoader& icon_loader() const; const std::unordered_set& ignore_list() const; const std::map& app_ids_replace_map() const; + + private: + void set_bar_css_class(const std::string&, bool); }; } /* namespace waybar::modules::wlr */ diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index af1ba97f..77cf91e5 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -18,6 +18,13 @@ Addressed by *wlr/taskbar* default: false ++ If set to false applications on the waybar's current output will be shown. Otherwise, all applications are shown. +*bar-css-states*: ++ + typeof: bool ++ + default: false ++ + If set to true, application state is exposed as CSS classes on the Waybar + window. Maximized and fullscreen state is aggregated across applications + known to belong to the active workspace. See *Bar state style* below. + *format*: ++ typeof: string ++ default: {icon} ++ @@ -52,6 +59,12 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, always reorder the tasks in the taskbar so that the currently active one is first. Otherwise don't reorder. +*active-only*: ++ + typeof: bool ++ + default: false ++ + If set to true, only the currently active application button is shown. + Other applications remain tracked and reappear when activated. + *sort-by-app-id*: ++ typeof: bool ++ default: false ++ @@ -156,3 +169,35 @@ Invalid expressions (e.g., mismatched parentheses) are skipped. - *#taskbar button.minimized* - *#taskbar button.active* - *#taskbar button.fullscreen* + +# Bar state style + +When *bar-css-states* is enabled, the following classes are added to +*window#waybar*: + +- *window#waybar.toplevel-active* +- *window#waybar.toplevel-maximized* +- *window#waybar.toplevel-minimized* +- *window#waybar.toplevel-fullscreen* + +The active, minimized classes describe the active application. The maximized +and fullscreen classes are set if any non-minimized application known to belong +to the active workspace has that state. + +Workspace membership is learned when an application is activated and requires +the compositor to support *ext-workspace-v1*. Before an application has been +activated during the current Waybar session, its workspace may be unknown. On +compositors without *ext-workspace-v1*, these classes fall back to the active +application's state. + +For example: + +``` +window#waybar { + background-color: rgba(0, 0, 0, 0.5); +} + +window#waybar.toplevel-maximized { + background-color: rgba(0, 0, 0, 1); +} +``` diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index bde90910..ab76d89e 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -293,9 +293,12 @@ void Task::handle_output_enter(struct wl_output* output) { button.signal_size_allocate().connect_notify( sigc::mem_fun(this, &Task::on_button_size_allocated)); tbar_->add_button(button); - button.show(); + if (!config_["active-only"].asBool() || active()) { + button.show(); + } button_visible_ = true; spdlog::debug("{} now visible on {}", repr(), bar_.output->name); + tbar_->update_bar_css_classes(); } } @@ -308,6 +311,7 @@ void Task::handle_output_leave(struct wl_output* output) { button.hide(); button_visible_ = false; spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); + tbar_->update_bar_css_classes(); } } @@ -350,6 +354,20 @@ void Task::handle_done() { button.get_style_context()->remove_class("fullscreen"); } + if (button_visible_ && config_["active-only"].asBool()) { + if (active()) { + button.show(); + } else { + button.hide(); + } + } + + if (active()) { + tbar_->assign_current_workspace(*this); + } + + tbar_->update_bar_css_classes(); + if (config_["active-first"].isBool() && config_["active-first"].asBool() && active()) tbar_->move_button(button, 0); @@ -551,6 +569,8 @@ static void handle_global(void* data, struct wl_registry* registry, uint32_t nam const char* interface, uint32_t version) { if (std::strcmp(interface, zwlr_foreign_toplevel_manager_v1_interface.name) == 0) { static_cast(data)->register_manager(registry, name, version); + } else if (std::strcmp(interface, ext_workspace_manager_v1_interface.name) == 0) { + static_cast(data)->register_workspace_manager(registry, name, version); } else if (std::strcmp(interface, wl_seat_interface.name) == 0) { static_cast(data)->register_seat(registry, name, version); } @@ -568,6 +588,7 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu bar_(bar), box_{bar.orientation, 0}, manager_{nullptr}, + workspace_manager_{nullptr}, seat_{nullptr} { box_.set_name("taskbar"); if (!id.empty()) { @@ -623,6 +644,27 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu } Taskbar::~Taskbar() { + for (auto& workspace : workspaces_) { + ext_workspace_handle_v1_destroy(workspace->handle); + } + workspaces_.clear(); + for (auto* group : workspace_groups_) { + ext_workspace_group_handle_v1_destroy(group); + } + workspace_groups_.clear(); + + if (workspace_manager_) { + struct wl_display* display = Client::inst()->wl_display; + ext_workspace_manager_v1_stop(workspace_manager_); + wl_display_roundtrip(display); + + if (workspace_manager_) { + spdlog::warn("Workspace manager destroyed before .finished event"); + ext_workspace_manager_v1_destroy(workspace_manager_); + workspace_manager_ = nullptr; + } + } + if (manager_) { struct wl_display* display = Client::inst()->wl_display; /* @@ -639,6 +681,13 @@ Taskbar::~Taskbar() { manager_ = nullptr; } } + + if (config_["bar-css-states"].asBool()) { + set_bar_css_class("toplevel-active", false); + set_bar_css_class("toplevel-maximized", false); + set_bar_css_class("toplevel-minimized", false); + set_bar_css_class("toplevel-fullscreen", false); + } } void Taskbar::update() { @@ -674,6 +723,80 @@ static const struct zwlr_foreign_toplevel_manager_v1_listener toplevel_manager_i .finished = tm_handle_finished, }; +static void workspace_handle_id(void*, struct ext_workspace_handle_v1*, const char*) {} +static void workspace_handle_name(void*, struct ext_workspace_handle_v1*, const char*) {} +static void workspace_handle_coordinates(void*, struct ext_workspace_handle_v1*, struct wl_array*) { +} + +static void workspace_handle_state(void* data, struct ext_workspace_handle_v1*, uint32_t state) { + static_cast(data)->state = state; +} + +static void workspace_handle_capabilities(void*, struct ext_workspace_handle_v1*, uint32_t) {} + +static void workspace_handle_removed(void* data, struct ext_workspace_handle_v1* handle) { + static_cast(data)->taskbar->handle_workspace_removed(handle); +} + +static const struct ext_workspace_handle_v1_listener workspace_handle_impl = { + .id = workspace_handle_id, + .name = workspace_handle_name, + .coordinates = workspace_handle_coordinates, + .state = workspace_handle_state, + .capabilities = workspace_handle_capabilities, + .removed = workspace_handle_removed, +}; + +static void workspace_group_handle_capabilities(void*, struct ext_workspace_group_handle_v1*, + uint32_t) {} +static void workspace_group_handle_output_enter(void*, struct ext_workspace_group_handle_v1*, + struct wl_output*) {} +static void workspace_group_handle_output_leave(void*, struct ext_workspace_group_handle_v1*, + struct wl_output*) {} +static void workspace_group_handle_workspace_enter(void*, struct ext_workspace_group_handle_v1*, + struct ext_workspace_handle_v1*) {} +static void workspace_group_handle_workspace_leave(void*, struct ext_workspace_group_handle_v1*, + struct ext_workspace_handle_v1*) {} + +static void workspace_group_handle_removed(void* data, + struct ext_workspace_group_handle_v1* group) { + static_cast(data)->handle_workspace_group_removed(group); +} + +static const struct ext_workspace_group_handle_v1_listener workspace_group_impl = { + .capabilities = workspace_group_handle_capabilities, + .output_enter = workspace_group_handle_output_enter, + .output_leave = workspace_group_handle_output_leave, + .workspace_enter = workspace_group_handle_workspace_enter, + .workspace_leave = workspace_group_handle_workspace_leave, + .removed = workspace_group_handle_removed, +}; + +static void workspace_manager_handle_group(void* data, struct ext_workspace_manager_v1*, + struct ext_workspace_group_handle_v1* group) { + static_cast(data)->handle_workspace_group_create(group); +} + +static void workspace_manager_handle_workspace(void* data, struct ext_workspace_manager_v1*, + struct ext_workspace_handle_v1* workspace) { + static_cast(data)->handle_workspace_create(workspace); +} + +static void workspace_manager_handle_done(void* data, struct ext_workspace_manager_v1*) { + static_cast(data)->handle_workspace_done(); +} + +static void workspace_manager_handle_finished(void* data, struct ext_workspace_manager_v1*) { + static_cast(data)->handle_workspace_finished(); +} + +static const struct ext_workspace_manager_v1_listener workspace_manager_impl = { + .workspace_group = workspace_manager_handle_group, + .workspace = workspace_manager_handle_workspace, + .done = workspace_manager_handle_done, + .finished = workspace_manager_handle_finished, +}; + void Taskbar::register_manager(struct wl_registry* registry, uint32_t name, uint32_t version) { if (manager_) { spdlog::warn("Register foreign toplevel manager again although already existing!"); @@ -698,6 +821,18 @@ void Taskbar::register_manager(struct wl_registry* registry, uint32_t name, uint spdlog::debug("Failed to register manager"); } +void Taskbar::register_workspace_manager(struct wl_registry* registry, uint32_t name, + uint32_t version) { + if (workspace_manager_) { + return; + } + + version = std::min(version, ext_workspace_manager_v1_interface.version); + workspace_manager_ = static_cast( + wl_registry_bind(registry, name, &ext_workspace_manager_v1_interface, version)); + ext_workspace_manager_v1_add_listener(workspace_manager_, &workspace_manager_impl, this); +} + void Taskbar::register_seat(struct wl_registry* registry, uint32_t name, uint32_t version) { if (seat_) { spdlog::warn("Register seat again although already existing!"); @@ -717,6 +852,64 @@ void Taskbar::handle_finished() { manager_ = nullptr; } +void Taskbar::handle_workspace_group_create(struct ext_workspace_group_handle_v1* handle) { + ext_workspace_group_handle_v1_add_listener(handle, &workspace_group_impl, this); + workspace_groups_.push_back(handle); +} + +void Taskbar::handle_workspace_group_removed(struct ext_workspace_group_handle_v1* handle) { + const auto group = std::find(workspace_groups_.begin(), workspace_groups_.end(), handle); + if (group != workspace_groups_.end()) { + ext_workspace_group_handle_v1_destroy(*group); + workspace_groups_.erase(group); + } +} + +void Taskbar::handle_workspace_create(struct ext_workspace_handle_v1* handle) { + auto workspace = std::make_unique(WorkspaceState{this, handle}); + ext_workspace_handle_v1_add_listener(handle, &workspace_handle_impl, workspace.get()); + workspaces_.push_back(std::move(workspace)); +} + +void Taskbar::handle_workspace_done() { + const auto active_workspace = + std::find_if(workspaces_.begin(), workspaces_.end(), [](const auto& workspace) { + return workspace->state & EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE; + }); + current_workspace_ = + active_workspace == workspaces_.end() ? nullptr : (*active_workspace)->handle; + if (current_workspace_) { + const auto active_task = + std::find_if(tasks_.begin(), tasks_.end(), [](const auto& task) { return task->active(); }); + if (active_task != tasks_.end()) { + (*active_task)->set_workspace(current_workspace_); + } + } + update_bar_css_classes(); +} + +void Taskbar::handle_workspace_finished() { workspace_manager_ = nullptr; } + +void Taskbar::handle_workspace_removed(struct ext_workspace_handle_v1* handle) { + if (current_workspace_ == handle) { + current_workspace_ = nullptr; + } + for (auto& task : tasks_) { + if (task->workspace() == handle) { + task->set_workspace(nullptr); + } + } + + const auto workspace = + std::find_if(workspaces_.begin(), workspaces_.end(), + [handle](const auto& workspace) { return workspace->handle == handle; }); + if (workspace != workspaces_.end()) { + ext_workspace_handle_v1_destroy((*workspace)->handle); + workspaces_.erase(workspace); + } + update_bar_css_classes(); +} + void Taskbar::add_button(Gtk::Button& bt) { box_.pack_start(bt, false, false); box_.get_style_context()->remove_class("empty"); @@ -741,6 +934,56 @@ void Taskbar::remove_task(uint32_t id) { } tasks_.erase(it); + update_bar_css_classes(); +} + +void Taskbar::assign_current_workspace(Task& task) { + if (current_workspace_) { + task.set_workspace(current_workspace_); + } +} + +void Taskbar::update_bar_css_classes() { + if (!config_["bar-css-states"].asBool()) { + return; + } + + const auto active_task = std::find_if(tasks_.begin(), tasks_.end(), [](const TaskPtr& task) { + return task->visible() && task->active(); + }); + + const bool has_active_task = active_task != tasks_.end(); + const auto on_current_workspace = [this](const TaskPtr& task) { + if (!current_workspace_) { + return task->active(); + } + return task->workspace() == current_workspace_; + }; + const bool has_maximized_task = + std::any_of(tasks_.begin(), tasks_.end(), [&on_current_workspace](const TaskPtr& task) { + return task->visible() && !task->minimized() && on_current_workspace(task) && + task->maximized(); + }); + const bool has_fullscreen_task = + std::any_of(tasks_.begin(), tasks_.end(), [&on_current_workspace](const TaskPtr& task) { + return task->visible() && !task->minimized() && on_current_workspace(task) && + task->fullscreen(); + }); + set_bar_css_class("toplevel-active", has_active_task); + set_bar_css_class("toplevel-maximized", has_maximized_task); + set_bar_css_class("toplevel-minimized", has_active_task && (*active_task)->minimized()); + set_bar_css_class("toplevel-fullscreen", has_fullscreen_task); +} + +void Taskbar::set_bar_css_class(const std::string& class_name, bool enabled) { + const auto style = bar_.window.get_style_context(); + if (enabled && !style->has_class(class_name)) { + spdlog::trace("Adding bar class: {}", class_name); + style->add_class(class_name); + } else if (!enabled && style->has_class(class_name)) { + spdlog::trace("Removing bar class: {}", class_name); + style->remove_class(class_name); + } } bool Taskbar::show_output(struct wl_output* output) const { From 942411d7a102951fd3b38407823c066481521120 Mon Sep 17 00:00:00 2001 From: "Robert M. Thomson" Date: Mon, 15 Jun 2026 09:53:44 +0200 Subject: [PATCH 231/416] Force layer-shell commit after configure Assisted-by: pi:gpt-5.5 --- include/bar.hpp | 1 + src/bar.cpp | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/include/bar.hpp b/include/bar.hpp index 54b332bf..292085ae 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -99,6 +99,7 @@ class Bar : public sigc::trackable { void setMode(const bar_mode&); void setPassThrough(bool passthrough); void setPosition(Gtk::PositionType position); + void forceLayerCommit(); void onConfigure(GdkEventConfigure* ev); void configureGlobalOffset(int width, int height); void onOutputGeometryChanged(); diff --git a/src/bar.cpp b/src/bar.cpp index 7ff73c4f..65017c8b 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -309,6 +309,19 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) setupWidgets(); window.show_all(); + /* + * If gtk-layer-shell's synchronous wait for the initial configure timed out, show_all() can + * return with a configured but not-yet-presented surface. Kick GTK/layer-shell once control has + * returned to the main loop, when any late initial configure has been dispatched and widgets have + * had a chance to allocate/draw. + */ + Glib::signal_idle().connect(sigc::track_obj([this] { + window.queue_resize(); + window.queue_draw(); + forceLayerCommit(); + return false; + }, *this)); + if (spdlog::should_log(spdlog::level::debug)) { // Unfortunately, this function isn't in the C++ bindings, so we have to call the C version. char* gtk_tree = gtk_style_context_to_string( @@ -374,7 +387,14 @@ void waybar::Bar::setMode(const struct bar_mode& mode) { * gtk-layer-shell schedules a commit on the next frame event in GTK, but this could fail in * certain scenarios, such as fully occluded bar. */ - gtk_layer_try_force_commit(gtk_window); + forceLayerCommit(); +} + +void waybar::Bar::forceLayerCommit() { + auto* gtk_window = window.gobj(); + if (gtk_window != nullptr && gtk_widget_get_realized(GTK_WIDGET(gtk_window))) { + gtk_layer_try_force_commit(gtk_window); + } wl_display_flush(Client::inst()->wl_display); } @@ -652,6 +672,17 @@ void waybar::Bar::onConfigure(GdkEventConfigure* ev) { configureGlobalOffset(ev->width, ev->height); spdlog::info(BAR_SIZE_MSG, ev->width, ev->height, output->name); + + /* + * gtk-layer-shell waits for the compositor's initial configure while realizing the window. On a + * busy compositor (common during session startup) that wait can time out even though the initial + * configure arrives shortly afterwards. In that case GTK may not schedule the frame commit that + * presents the first layer-surface buffer, leaving an otherwise configured bar invisible. Force a + * commit after every configure so late initial configures, and later compositor-driven resizes, + * always result in a submitted surface state. + */ + window.queue_draw(); + forceLayerCommit(); } void waybar::Bar::configureGlobalOffset(int width, int height) { From 47b2ff09b71e243b8c836b6f42ec08fedf753e67 Mon Sep 17 00:00:00 2001 From: Johannes Haase Date: Tue, 16 Jun 2026 23:27:27 +0200 Subject: [PATCH 232/416] feat(cursor): use cursor-shape-v1 cursor names instead of Gdk::CursorType --- include/AModule.hpp | 2 +- man/waybar-styles.5.scd.in | 20 +++++++++----------- src/AModule.cpp | 14 +++++++------- src/modules/user.cpp | 1 - 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index a338ffe3..634994da 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -41,7 +41,7 @@ class AModule : public IModule { const Json::Value& config_; Gtk::EventBox event_box_; - virtual void setCursor(Gdk::CursorType const& c); + virtual void setCursor(std::string const& c); virtual bool handleToggle(GdkEventButton* const& ev); virtual bool handleMouseEnter(GdkEventCrossing* const& ev); diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in index b11e15bd..5f31a5bb 100644 --- a/man/waybar-styles.5.scd.in +++ b/man/waybar-styles.5.scd.in @@ -43,18 +43,17 @@ You can apply special styling to any module for when the cursor hovers it. Most, if not all, module types support setting the `cursor` option. This is configured in your `config.jsonc`. If set to `false`, when hovering the module a -"pointer"(as commonly known from web CSS styling `cursor: pointer`) style cursor +"pointer" (as commonly known from web CSS styling `cursor: pointer`) style cursor will not be shown. Default behavior is to indicate an interaction event is available. -There are more cursor types to choose from by setting the `cursor` option to -a number, see Gdk3 official docs for all possible cursor types: -https://docs.gtk.org/gdk3/enum.CursorType.html. -However, note that not all cursor options listed may be available on -your system. If you attempt to use a cursor which is not available, the -application will crash. +If set to a string value, it must be a valid cursor name +(e.g. `"pointer"`, `"default"`, `"grab"`, `"text"`, `"crosshair"`, etc.), +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. -Example of disabling pointer(`Gdk::Hand2`) cursor type on a custom module: +Example of disabling the cursor on a custom module: ``` "custom/my-custom-module": { @@ -63,13 +62,12 @@ Example of disabling pointer(`Gdk::Hand2`) cursor type on a custom module: } ``` -Example of setting cursor type to `Gdk::Boat`(according to -https://docs.gtk.org/gdk3/enum.CursorType.html#boat): +Example of setting the cursor type to `"grab"`: ``` "custom/my-custom-module": { ... - "cursor": 8, + "cursor": "grab", } ``` diff --git a/src/AModule.cpp b/src/AModule.cpp index a5ba69d3..11e78123 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -73,9 +73,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: // Respect user configuration of cursor if (config_.isMember("cursor")) { if (config_["cursor"].isBool() && config_["cursor"].asBool()) { - setCursor(Gdk::HAND2); - } else if (config_["cursor"].isInt()) { - setCursor(Gdk::CursorType(config_["cursor"].asInt())); + setCursor("pointer"); + } else if (config_["cursor"].isString()) { + setCursor(config_["cursor"].asString()); } else { spdlog::warn("unknown cursor option configured on module {}", name_); } @@ -110,10 +110,10 @@ auto AModule::doAction(const std::string& name) -> void { } } -void AModule::setCursor(Gdk::CursorType const& c) { +void AModule::setCursor(std::string const& c) { auto gdk_window = event_box_.get_window(); if (gdk_window) { - auto cursor = Gdk::Cursor::create(c); + 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, @@ -134,7 +134,7 @@ bool AModule::handleMouseEnter(GdkEventCrossing* const& e) { // Default behavior indicating event availability if (hasUserEvents_ && !config_.isMember("cursor")) { - setCursor(Gdk::HAND2); + setCursor("pointer"); } return false; @@ -147,7 +147,7 @@ bool AModule::handleMouseLeave(GdkEventCrossing* const& e) { // Default behavior indicating event availability if (hasUserEvents_ && !config_.isMember("cursor")) { - setCursor(Gdk::ARROW); + setCursor("default"); } return false; diff --git a/src/modules/user.cpp b/src/modules/user.cpp index 3ca5e431..87f5ea0f 100644 --- a/src/modules/user.cpp +++ b/src/modules/user.cpp @@ -7,7 +7,6 @@ #include #include -#include "gdkmm/cursor.h" #include "gdkmm/event.h" #include "gdkmm/types.h" #include "glibmm/fileutils.h" From e37fcee35d3124a98e90c220fa6cb5c0862fd296 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Thu, 18 Jun 2026 14:28:39 +0000 Subject: [PATCH 233/416] fix(sni): ignore name/pixmap updates for custom icons --- include/modules/sni/item.hpp | 1 + src/modules/sni/item.cpp | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 74d54f4c..df87bfb7 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -46,6 +46,7 @@ class Item : public sigc::trackable { std::string title; std::string icon_name; Glib::RefPtr icon_pixmap; + bool has_custom_icon_ = false; Glib::RefPtr icon_theme; std::string overlay_icon_name; Glib::RefPtr overlay_icon_pixmap; diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 2f368083..8e2e2570 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -190,9 +190,17 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } else if (name == "Status") { setStatus(get_variant(value)); } else if (name == "IconName") { - icon_name = get_variant(value); + if (has_custom_icon_) { + spdlog::trace("Item '{}': ignoring IconName update, custom icon is set", id); + } else { + icon_name = get_variant(value); + } } else if (name == "IconPixmap") { - icon_pixmap = this->extractPixBuf(value.gobj()); + if (has_custom_icon_) { + spdlog::trace("Item '{}': ignoring IconPixmap update, custom icon is set", id); + } else { + icon_pixmap = this->extractPixBuf(value.gobj()); + } } else if (name == "OverlayIconName") { overlay_icon_name = get_variant(value); } else if (name == "OverlayIconPixmap") { @@ -270,11 +278,13 @@ void Item::setCustomIcon(const std::string& id) { Glib::RefPtr custom_pixbuf = Gdk::Pixbuf::create_from_file(custom_icon); icon_name = ""; // icon_name has priority over pixmap icon_pixmap = custom_pixbuf; + has_custom_icon_ = true; } catch (const Glib::Error& e) { spdlog::error("Failed to load custom icon {}: {}", custom_icon, e.what()); } } else { // if file doesn't exist it's most likely an icon_name icon_name = custom_icon; + has_custom_icon_ = true; } } } From 42f2832d212aacc8e663212ea095ba78c86d03fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20NICOLAS?= Date: Sat, 20 Jun 2026 17:42:23 +0200 Subject: [PATCH 234/416] docs(man): document light and dark stylesheets --- man/waybar-styles.5.scd.in | 26 ++++++++++++++++++++------ man/waybar.5.scd.in | 2 +- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in index b11e15bd..130e5db3 100644 --- a/man/waybar-styles.5.scd.in +++ b/man/waybar-styles.5.scd.in @@ -8,13 +8,27 @@ waybar-styles - using stylesheets for waybar Waybar uses Cascading Style Sheets (CSS) to configure its appearance. -It uses the first file found in this search order: +When the system appearance is light, Waybar first looks for *style-light.css*. +When the system appearance is dark, Waybar first looks for *style-dark.css*. +If no appearance-specific stylesheet is found, it falls back to *style.css*. -- *$XDG_CONFIG_HOME/waybar/style.css* -- *~/.config/waybar/style.css* -- *~/waybar/style.css* -- */etc/xdg/waybar/style.css* -- *@sysconfdir@/xdg/waybar/style.css* +Waybar uses the first file found in this search order for each stylesheet name: + +- *$XDG_CONFIG_HOME/waybar/* +- *~/.config/waybar/* +- *~/waybar/* +- */etc/xdg/waybar/* +- *@sysconfdir@/xdg/waybar/* + +For example, a light theme is loaded from the first available file among: + +- *$XDG_CONFIG_HOME/waybar/style-light.css* +- *~/.config/waybar/style-light.css* +- *~/waybar/style-light.css* +- */etc/xdg/waybar/style-light.css* +- *@sysconfdir@/xdg/waybar/style-light.css* + +If no file is found there, Waybar repeats the same search using *style.css*. # EXAMPLE diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 8b923eef..4dba6694 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -19,7 +19,7 @@ Valid locations for this file are: A good starting point is the default configuration found at https://github.com/Alexays/Waybar/blob/master/resources/config.jsonc Also, a minimal example configuration can be found at the bottom of this man page. -The visual display elements for waybar use a CSS stylesheet, see *waybar-styles(5)* for details. +The visual display elements for waybar use a CSS stylesheet. Waybar supports *style.css* and the appearance-specific *style-light.css* and *style-dark.css* files; see *waybar-styles(5)* for details. # BAR CONFIGURATION From 22ce5887835b87653ca6cdccdbc317ec69d7a6f0 Mon Sep 17 00:00:00 2001 From: Muhammed Sahin Date: Sat, 27 Jun 2026 17:18:52 +0200 Subject: [PATCH 235/416] Fallback formatting when format-paused is missing uses ALabel With the minimal change the paused state, if format-paused state is not set, will show the Alabel if the Alabel was active. --- src/modules/mpd/mpd.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 9874f458..80cb6559 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -137,8 +137,9 @@ void waybar::modules::MPD::setLabel() { label_.get_style_context()->add_class("playing"); label_.get_style_context()->remove_class("paused"); } else if (paused()) { - format = config_["format-paused"].isString() ? config_["format-paused"].asString() - : config_["format"].asString(); + if (config_["format-paused"].isString()) { + format = config_["format-paused"].asString(); + } label_.get_style_context()->add_class("paused"); label_.get_style_context()->remove_class("playing"); } From 601e84d159e9b2b85e6e32a6366d2f3877b17744 Mon Sep 17 00:00:00 2001 From: Grin Date: Wed, 1 Jul 2026 17:23:51 -0300 Subject: [PATCH 236/416] feat(ALabel): allow custom percentage thresholds in format-icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, format-icons with N icons would divide the 0-100 range into N equal parts. This made it impossible to define custom ranges like 0%, 1-69%, 70-100%. This adds support for an alternative object syntax in format-icons: "format-icons": [ { "icon": "▁", "max": 0}, { "icon": "▅", "max": 69 }, { "icon": "▇", "max": 100 } ] The array is iterated in order and the first icon whose max value is >= the current percentage is returned. If no threshold matches, the last icon is used as fallback. The existing string array syntax remains fully supported and unchanged. Applies to both getIcon overloads, affecting all modules that use format-icons (battery, pulseaudio, network, backlight, etc.). Closes #5150 --- src/ALabel.cpp | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 795f87f1..46eeac2f 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -150,7 +150,26 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ } if (format_icons.isArray()) { auto size = format_icons.size(); - if (size != 0U) { + if (size != 0U && format_icons[0].isObject()) { + std::string last_icon; + for (const auto& threshold : format_icons) { + if (!threshold.isObject() || !threshold["icon"].isString() || !threshold["max"].isUInt()) { + static bool warned = false; + if (!warned) { + spdlog::warn("format-icons: skipping invalid threshold object, expected {\"icon\": \"...\", \"max\": N}"); + warned = true; + } + continue; + } + last_icon = threshold["icon"].asString(); + if (percentage <= threshold["max"].asUInt()) { + return last_icon; + } + } + if (!last_icon.empty()) { + return last_icon; + } + } else if (size != 0U) { auto divisor = std::max(1U, (max == 0 ? 100U : static_cast(max)) / size); auto idx = std::clamp(percentage / divisor, 0U, size - 1); format_icons = format_icons[idx]; @@ -177,7 +196,26 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& } if (format_icons.isArray()) { auto size = format_icons.size(); - if (size != 0U) { + if (size != 0U && format_icons[0].isObject()) { + std::string last_icon; + for (const auto& threshold : format_icons) { + if (!threshold.isObject() || !threshold["icon"].isString() || !threshold["max"].isUInt()) { + static bool warned = false; + if (!warned) { + spdlog::warn("format-icons: skipping invalid threshold object, expected {\"icon\": \"...\", \"max\": N}"); + warned = true; + } + continue; + } + last_icon = threshold["icon"].asString(); + if (percentage <= threshold["max"].asUInt()) { + return last_icon; + } + } + if (!last_icon.empty()) { + return last_icon; + } + } else if (size != 0U) { auto divisor = std::max(1U, (max == 0 ? 100U : static_cast(max)) / size); auto idx = std::clamp(percentage / divisor, 0U, size - 1); format_icons = format_icons[idx]; From 4d1a561cdcbacb9e408a534b49b567821923331a Mon Sep 17 00:00:00 2001 From: Ibrahim Issa Date: Fri, 3 Jul 2026 18:44:03 +0200 Subject: [PATCH 237/416] feat(modules): pause background threads when display is asleep Currently, modules polling hardware nodes continuously wake up kernel drivers even when the monitor is asleep (e.g., DPMS off), flooding dmesg. - Adds 'disable-on-sleep' config flag - Hooks GTK window map/unmap signals to track DPMS state - Propagates suspend/resume calls to AModule worker threads --- include/AModule.hpp | 6 ++++++ include/bar.hpp | 2 ++ include/modules/temperature.hpp | 2 ++ include/util/sleeper_thread.hpp | 24 ++++++++++++++++++++++++ src/AModule.cpp | 3 +++ src/bar.cpp | 33 +++++++++++++++++++++++++++++++-- src/modules/temperature.cpp | 4 ++++ 7 files changed, 72 insertions(+), 2 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index a338ffe3..e4e4a0ca 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -25,6 +25,10 @@ class AModule : public IModule { bool expandEnabled() const; + virtual void suspend() {}; + virtual void resume() {}; + bool shouldSuspend() const { return disable_on_sleep_; } + protected: // Don't need to make an object directly // Derived classes are able to use it @@ -48,6 +52,8 @@ class AModule : public IModule { virtual bool handleMouseLeave(GdkEventCrossing* const& ev); virtual bool handleScroll(GdkEventScroll*); virtual bool handleRelease(GdkEventButton* const& ev); + + bool disable_on_sleep_{false}; GObject* menu_ = nullptr; private: diff --git a/include/bar.hpp b/include/bar.hpp index 54b332bf..8f23d6e8 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -75,6 +75,8 @@ class Bar : public sigc::trackable { util::KillSignalAction getOnSigusr1Action(); util::KillSignalAction getOnSigusr2Action(); + void toggleSuspend(bool suspend); + struct waybar_output* output; Json::Value config; struct wl_surface* surface; diff --git a/include/modules/temperature.hpp b/include/modules/temperature.hpp index 918281be..d65ef84d 100644 --- a/include/modules/temperature.hpp +++ b/include/modules/temperature.hpp @@ -14,6 +14,8 @@ class Temperature : public ALabel { Temperature(const std::string&, const Json::Value&); virtual ~Temperature() = default; auto update() -> void override; + void suspend() override; + void resume() override; private: float getTemperature(); diff --git a/include/util/sleeper_thread.hpp b/include/util/sleeper_thread.hpp index 1f3a372d..9d20f252 100644 --- a/include/util/sleeper_thread.hpp +++ b/include/util/sleeper_thread.hpp @@ -79,6 +79,12 @@ class SleeperThread { auto sleep_for(std::chrono::system_clock::duration dur) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; + + condvar_.wait(lk, [this] { + return !is_paused_ || signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); + constexpr auto max_time_point = std::chrono::steady_clock::time_point::max(); auto wait_end = max_time_point; auto now = std::chrono::steady_clock::now(); @@ -95,6 +101,12 @@ class SleeperThread { time_point) { std::unique_lock lk(mutex_); CancellationGuard cancel_lock; + + condvar_.wait(lk, [this] { + return !is_paused_ || signal_.load(std::memory_order_relaxed) || + !do_run_.load(std::memory_order_relaxed); + }); + return condvar_.wait_until(lk, time_point, [this] { return signal_.load(std::memory_order_relaxed) || !do_run_.load(std::memory_order_relaxed); }); @@ -122,6 +134,17 @@ class SleeperThread { } } + void pause() { + std::lock_guard lock(mutex_); + is_paused_ = true; + } + + void resume() { + std::lock_guard lock(mutex_); + is_paused_ = false; + condvar_.notify_all(); + } + ~SleeperThread() { connection_.disconnect(); stop(); @@ -137,6 +160,7 @@ class SleeperThread { std::atomic do_run_ = true; std::atomic signal_ = false; sigc::connection connection_; + bool is_paused_{false}; }; } // namespace waybar::util diff --git a/src/AModule.cpp b/src/AModule.cpp index a5ba69d3..d36688f8 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -21,6 +21,9 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: // Configure module action Map const Json::Value actions{config_["actions"]}; + disable_on_sleep_ = + config_["disable-on-sleep"].isBool() ? config_["disable-on-sleep"].asBool() : false; + for (Json::Value::const_iterator it = actions.begin(); it != actions.end(); ++it) { if (it.key().isString() && it->isString()) if (!eventActionMap_.contains(it.key().asString())) { diff --git a/src/bar.cpp b/src/bar.cpp index 7ff73c4f..44d02c5d 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -263,6 +263,16 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); + window.signal_unmap().connect([this]() { + spdlog::debug("Output {} unmapped (DPMS off), suspending modules", output->name); + toggleSuspend(true); + }); + + window.signal_map().connect([this]() { + spdlog::debug("Output {} mapped (DPMS on), resuming modules", output->name); + toggleSuspend(false); + }); + #if HAVE_SWAY if (auto ipc = config["ipc"]; ipc.isBool() && ipc.asBool()) { bar_id = Client::inst()->bar_id; @@ -545,8 +555,8 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, if (group_config["modules"].isNull()) { spdlog::warn("Group definition '{}' has not been found, group will be hidden", ref); } - auto group_module = std::make_unique( - id_name, class_name, group_config, vertical); + auto group_module = + std::make_unique(id_name, class_name, group_config, vertical); getModules(factory, ref, group_module.get()); module = group_module.release(); @@ -696,3 +706,22 @@ void waybar::Bar::configureGlobalOffset(int width, int height) { void waybar::Bar::onOutputGeometryChanged() { configureGlobalOffset(window.get_width(), window.get_height()); } + +void waybar::Bar::toggleSuspend(bool suspend) { + auto process_modules = [suspend](Gtk::Box& module_box) { + for (auto* widget : module_box.get_children()) { + auto* module = dynamic_cast(widget); + if (module && module->shouldSuspend()) { + if (suspend) { + module->suspend(); + } else { + module->resume(); + } + } + } + }; + + process_modules(left_); + process_modules(center_); + process_modules(right_); +} diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index bb5ff02c..dcfb0b2f 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -155,3 +155,7 @@ bool waybar::modules::Temperature::isCritical(uint16_t temperature_c) { return config_["critical-threshold"].isInt() && temperature_c >= config_["critical-threshold"].asInt(); } + +void waybar::modules::Temperature::suspend() { thread_.pause(); } + +void waybar::modules::Temperature::resume() { thread_.resume(); } From 6793b97eed90bbe3d97cf4f0ac31ea722b84a394 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:15:35 +0200 Subject: [PATCH 238/416] fix(hyprland/language): handle commas in keyboard-name config The active-layout event payload is `KEYBOARDNAME,LAYOUT`, and kbName was taken as everything before the first comma. Keyboard names reported by Hyprland can themselves contain commas (e.g. some MSI keyboards), so a `keyboard-name` configured with a comma never matched the truncated value and the module stopped updating. Match the configured keyboard-name as a full prefix (followed by the ',' separator) instead. Layout parsing already handles commas via the last-comma-before-parenthesis heuristic, so only the keyboard-name filter needed fixing. Addresses the keyboard-name case originally reported by @tirsek in #3406. --- src/modules/hyprland/language.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 6e0fe23d..890dc86b 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -74,8 +74,6 @@ void Language::onEvent(const std::string& ev) { spdlog::warn("hyprland language received malformed event payload: {}", ev); return; } - std::string kbName = payload.substr(0, kbSeparator); - // Last comma before variants parenthesis, eg: // activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl., // with dead keys) @@ -93,8 +91,15 @@ void Language::onEvent(const std::string& ev) { } auto layoutName = payload.substr(layoutSeparator + 1); - if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString()) - return; // ignore + if (config_.isMember("keyboard-name")) { + const auto keyboardName = config_["keyboard-name"].asString(); + // The keyboard name itself can contain commas, so match it as a full prefix + // (followed by the ',' separator) rather than comparing against the substring + // before the first comma, which would truncate such names and drop the event. + if (payload.size() <= keyboardName.size() || payload[keyboardName.size()] != ',' || + payload.compare(0, keyboardName.size(), keyboardName) != 0) + return; // ignore + } layoutName = waybar::util::sanitize_string(layoutName); From dc017d8fd3e2550fb397627bea9981c6ec707ab7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:32:55 +0200 Subject: [PATCH 239/416] fix(hyprland/workspaces): disconnect hover-check timeout on Workspace destruction The hover-check Glib::signal_timeout re-arms every 50ms and is not owned by the Workspace. Since Workspaces are destroyed at runtime, a workspace removed while its check is armed would let the timeout fire on freed memory (use-after- free, also touching the destroyed m_button). Add a destructor that calls stopHoverCheck() to disconnect the source. --- include/modules/hyprland/workspace.hpp | 1 + src/modules/hyprland/workspace.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 5730dda9..a4cccee6 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -30,6 +30,7 @@ class Workspace { public: explicit Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager, const Json::Value& clients_data = Json::Value::nullRef); + ~Workspace(); std::string& selectIcon(std::map& icons_map); Gtk::Button& button() { return m_button; }; diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index e31132fe..b8868236 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -51,6 +51,12 @@ Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_ma initializeWindowMap(clients_data); } +Workspace::~Workspace() { + // Disconnect the hover-check timeout so it can't fire on this destroyed + // instance (Workspaces are removed at runtime while a check may be armed). + stopHoverCheck(); +} + void addOrRemoveClass(const Glib::RefPtr& context, bool condition, const std::string& class_name) { if (condition) { From 806f6333182c4015e61e20fca7a0bc38e682f629 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:38:46 +0200 Subject: [PATCH 240/416] hyprland/workspaces: refresh locally instead of reloading Hyprland config Replace the getSocket1JsonReply("reload") call in onSpecialWorkspaceActivated with the module's normal local refresh path: the state update to m_activeSpecialWorkspaceName is picked up by the update() render (triggered by the dp.emit() already issued at the end of onEvent), which re-evaluates active state, icons and CSS classes. This avoids reloading the entire Hyprland config on every special workspace activation while keeping the special-active icon and CSS improvements. --- src/modules/hyprland/workspaces.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index a95fd9a5..3fa92920 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -396,7 +396,6 @@ void Workspaces::onWorkspaceActivated(std::string const& payload) { void Workspaces::onSpecialWorkspaceActivated(std::string const& payload) { std::string name(begin(payload), begin(payload) + payload.find_first_of(',')); m_activeSpecialWorkspaceName = (!name.starts_with("special:") ? name : name.substr(8)); - m_ipc.getSocket1JsonReply("reload"); } void Workspaces::onWorkspaceDestroyed(std::string const& payload) { From 5181f84648c20742e2a73146387b887c3ad1a3eb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:39:36 +0200 Subject: [PATCH 241/416] fix(sni): prevent use-after-free and infinite retry in Host proxy retry Store the retry timeout connection in a Host member and disconnect it in ~Host, so a Host destroyed within the retry window no longer invokes the timeout callback on freed memory. Cap the retries (MAX_RETRIES) so a watcher that never reappears no longer spins forever every 200ms; reset the counter on success and when the watcher name vanishes. --- include/modules/sni/host.hpp | 2 ++ src/modules/sni/host.cpp | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index d76ec74a..f18cf414 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -43,6 +43,8 @@ class Host { std::size_t watcher_id_; GCancellable* cancellable_ = nullptr; SnWatcher* watcher_ = nullptr; + sigc::connection retry_connection_; + unsigned retry_count_ = 0; const Json::Value& config_; const Bar& bar_; const std::function&)> on_add_; diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 091c45d5..2c0c42c7 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -7,6 +7,7 @@ namespace waybar::modules::SNI { static const unsigned RETRY_DELAY_MS = 200; +static const unsigned MAX_RETRIES = 10; Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, const std::function&)>& on_add, @@ -24,6 +25,7 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, on_update_(on_update) {} Host::~Host() { + retry_connection_.disconnect(); if (bus_name_id_ > 0) { Gio::DBus::unown_name(bus_name_id_); bus_name_id_ = 0; @@ -55,6 +57,8 @@ void Host::nameAppeared(const Glib::RefPtr& conn, const G } void Host::nameVanished(const Glib::RefPtr& conn, const Glib::ustring name) { + retry_connection_.disconnect(); + retry_count_ = 0; g_cancellable_cancel(cancellable_); g_clear_object(&cancellable_); g_clear_object(&watcher_); @@ -77,17 +81,26 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { if (error != nullptr) { spdlog::error("Host: {}", error->message); g_clear_object(&host->cancellable_); - Glib::signal_timeout().connect_once( + if (host->retry_count_ >= MAX_RETRIES) { + spdlog::warn("Host: giving up on watcher proxy creation after {} retries", host->retry_count_); + return; + } + host->retry_count_ += 1; + // 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]() { if (host->watcher_ != nullptr) { - return; + return false; } auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + return false; }, RETRY_DELAY_MS); return; } + host->retry_count_ = 0; host->watcher_ = watcher; sn_watcher_call_register_host(host->watcher_, host->object_path_.c_str(), host->cancellable_, &Host::registerHost, data); From 488a75fadaa110c4a4fa16ab73fd3795d38829b3 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:39:48 +0200 Subject: [PATCH 242/416] fix(wlr/taskbar): gate title shrink/expand behind 'expand' option Only stretch buttons and ellipsize titles when the new boolean 'expand' option is enabled and the bar is horizontal. Restores the previous content-based sizing as the default and avoids truncating every title to an ellipsis on vertical bars. --- man/waybar-wlr-taskbar.5.scd | 5 ++++ src/modules/wlr/taskbar.cpp | 52 ++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index af1ba97f..2aa26f3f 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -57,6 +57,11 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, group tasks by their app_id. Cannot be used with 'active-first'. +*expand*: ++ + typeof: bool ++ + default: false ++ + If set to true, task buttons stretch to fill the available space in the taskbar and long titles are ellipsized to fit. Only takes effect on a horizontal bar; on a vertical bar the buttons keep their content-based size. If set to false, buttons are sized to their content. + *on-click*: ++ typeof: string ++ The action which should be triggered when clicking on the application button with the left mouse button. diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 6ba41ce3..f382e1fc 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -93,20 +93,36 @@ Task::Task(const waybar::Bar& bar, const Json::Value& config, Taskbar* tbar, zwlr_foreign_toplevel_handle_v1_add_listener(handle_, &toplevel_handle_impl, this); button.set_relief(Gtk::RELIEF_NONE); - button.set_hexpand(true); - content_.set_hexpand(true); - text_before_.set_ellipsize(Pango::ELLIPSIZE_END); - text_before_.set_single_line_mode(true); - text_before_.set_width_chars(1); - text_before_.set_xalign(0.0); - text_after_.set_ellipsize(Pango::ELLIPSIZE_END); - text_after_.set_single_line_mode(true); - text_after_.set_width_chars(1); - text_after_.set_xalign(0.0); - content_.pack_start(text_before_, true, true, 0); - content_.pack_start(icon_, false, false, 0); - content_.pack_start(text_after_, true, true, 0); + /* When "expand" is enabled the buttons stretch to fill the taskbar and the + * titles ellipsize to fit within the available space. This only makes sense + * on a horizontal bar; on a vertical bar the box grows along the vertical + * axis, so ellipsizing/forcing width_chars(1) would truncate every title to + * "…". Keep the historical behavior (buttons sized to their content) as the + * default and when the bar is vertical. */ + bool expand = config_["expand"].isBool() && config_["expand"].asBool(); + bool horizontal = bar.orientation == Gtk::ORIENTATION_HORIZONTAL; + + if (expand && horizontal) { + button.set_hexpand(true); + content_.set_hexpand(true); + text_before_.set_ellipsize(Pango::ELLIPSIZE_END); + text_before_.set_single_line_mode(true); + text_before_.set_width_chars(1); + text_before_.set_xalign(0.0); + text_after_.set_ellipsize(Pango::ELLIPSIZE_END); + text_after_.set_single_line_mode(true); + text_after_.set_width_chars(1); + text_after_.set_xalign(0.0); + + content_.pack_start(text_before_, true, true, 0); + content_.pack_start(icon_, false, false, 0); + content_.pack_start(text_after_, true, true, 0); + } else { + content_.add(text_before_); + content_.add(icon_); + content_.add(text_after_); + } content_.show(); button.add(content_); @@ -728,7 +744,15 @@ void Taskbar::handle_finished() { } void Taskbar::add_button(Gtk::Button& bt) { - box_.pack_start(bt, true, true); + /* Only let the buttons expand to fill the taskbar when "expand" is enabled + * and the bar is horizontal (see the Task constructor for details). */ + bool expand = config_["expand"].isBool() && config_["expand"].asBool(); + bool horizontal = bar_.orientation == Gtk::ORIENTATION_HORIZONTAL; + if (expand && horizontal) { + box_.pack_start(bt, true, true); + } else { + box_.pack_start(bt, false, false); + } box_.get_style_context()->remove_class("empty"); } From 31a3d7e1bb9c047ca3449490cb5d695abbd98e0b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:40:41 +0200 Subject: [PATCH 243/416] fix(battery): gate Full-as-Plugged behind full-at-plugged option, drop debug output Reporting a full battery on AC as 'Plugged' is now opt-in via the 'full-at-plugged' config option (default false), so existing format-full / tooltip-format-full setups keep working. Also documents the option. --- man/waybar-battery.5.scd | 5 +++++ src/modules/battery.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 42b9e046..0c71518c 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -27,6 +27,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: false ++ Option to use the battery design capacity instead of its current maximal capacity. +*full-at-plugged*: ++ + typeof: bool ++ + default: false ++ + When enabled, a battery that is *Full* while the adapter is online is reported with the *Plugged* status instead of *Full* (so you can style/format it separately). Disabled by default to preserve the existing *Full* behaviour. + *interval*: ++ typeof: integer ++ default: 60 ++ diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 53305068..2581ef9a 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -706,7 +706,7 @@ auto waybar::modules::Battery::update() -> void { if (!adapter_.empty()) { std::ifstream(adapter_ / "online") >> adapter_online; } - if (status == "Full" && adapter_online) { + if (config_["full-at-plugged"].asBool() && status == "Full" && adapter_online) { status = "Plugged"; } auto status_pretty = status; From 9ac3f9b95198408f8bb084f7fc900b29d4be900e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:41:10 +0200 Subject: [PATCH 244/416] clock: make calendar padding portable across FormatStream char types std::setfill is templated on the fill char type, which must match the Glib::ustring FormatStream's char type. That type is wchar_t with libstdc++/glibmm but char with libc++, so no fixed literal (' ' or L' ') compiles everywhere. The stream's default fill is already a space, so drop std::setfill entirely; std::setw/std::left/std::right are char-type-agnostic and keep the exact padding behavior. --- src/modules/clock.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 36581e51..97777667 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -402,8 +402,13 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea data = g_utf8_find_next_char(data, end); } } + // Note: the stream's default fill character is already a space (L' ' on + // libstdc++'s wide FormatStream, ' ' on libc++'s narrow one), so no + // std::setfill is needed. Passing std::setfill(' ')/std::setfill(L' ') + // here is not portable because the fill char type must match the + // FormatStream's char type, which differs between standard libraries. os << Glib::ustring::format( - (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setfill(' '), + (cldWPos_ != WS::LEFT || line == 0) ? std::left : std::right, std::setw(cldMonColLen_ + ((line < 2) ? cldWnLen_ - wideCharCount : 0)), calendarLine); From 8f7ed880c413d85fc51c13e0252d8e18fdaf827c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:49:25 +0200 Subject: [PATCH 245/416] bar: fix indentation of height type warning --- src/bar.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bar.cpp b/src/bar.cpp index 626124da..fe44b3ed 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -173,7 +173,7 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) } if (config.isMember("height") && !config["height"].isUInt()) { - spdlog::warn("Invalid type for 'height', expected unsigned integer"); + spdlog::warn("Invalid type for 'height', expected unsigned integer"); } height_ = config["height"].isUInt() ? config["height"].asUInt() : 0; From cbc70d10f81f85ab51f5c18d04fecbce340c7f53 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:49:54 +0200 Subject: [PATCH 246/416] pulseaudio_slider: keep zero-on-mute applied on every update while muted The zero-on-mute display_value = min_ assignment was nested inside the !previously_muted transition branch, so subsequent updates while still muted re-displayed the real volume. Apply it on every update as long as the target is muted, keeping the CSS class add/remove on transitions only. --- src/modules/pulseaudio_slider.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/modules/pulseaudio_slider.cpp b/src/modules/pulseaudio_slider.cpp index aeddeb24..d0e54459 100644 --- a/src/modules/pulseaudio_slider.cpp +++ b/src/modules/pulseaudio_slider.cpp @@ -29,12 +29,14 @@ void PulseaudioSlider::update() { uint16_t display_value = backend->getVolume(target); bool is_muted = backend->getMuted(target); - if (is_muted && !previously_muted) { + if (is_muted) { if (zero_on_mute) { display_value = min_; } - scale_.get_style_context()->add_class("muted"); - } else if (previously_muted && !is_muted) { + if (!previously_muted) { + scale_.get_style_context()->add_class("muted"); + } + } else if (previously_muted) { scale_.get_style_context()->remove_class("muted"); } From 26c834a59b65f56b0e3faa864c57a95c75346d58 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:51:07 +0200 Subject: [PATCH 247/416] cpu: respect tooltip-format- without overwriting it The tooltip selection block redundantly reassigned tooltip_format to config_["tooltip-format"], overwriting the state-specific format (and emptying it when no global tooltip-format was set). A lone tooltip-format- therefore produced an empty tooltip. Consolidate the tooltip handling into a single block that prefers tooltip-format-, falls back to the global tooltip-format, then to the computed default tooltip, reusing the existing format arg store. --- src/modules/cpu.cpp | 47 ++++++++------------------------------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 254c6945..5c461851 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -28,15 +28,11 @@ auto waybar::modules::Cpu::update() -> void { auto [max_frequency, min_frequency, avg_frequency] = CpuFrequency::getCpuFrequency(); auto format = format_; - std::string tooltip_format; auto total_usage = cpu_usage.empty() ? 0 : cpu_usage[0]; auto state = getState(total_usage); if (!state.empty() && config_["format-" + state].isString()) { format = config_["format-" + state].asString(); } - if (config_["tooltip-format-" + state].isString()) { - tooltip_format = config_["tooltip-format-" + state].asString(); - } if (format.empty()) { event_box_.hide(); @@ -62,47 +58,20 @@ auto waybar::modules::Cpu::update() -> void { label_.set_markup(fmt::vformat(format, store)); if (tooltipEnabled()) { - if (config_["tooltip-format"].isString()) { - tooltip = config_["tooltip-format"].asString(); - label_.set_tooltip_markup(fmt::vformat(tooltip, store)); + std::string tooltip_format; + if (!state.empty() && config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } else if (config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + label_.set_tooltip_markup(fmt::vformat(tooltip_format, store)); } else { label_.set_tooltip_markup(tooltip); } } } - if (tooltipEnabled()) { - if (tooltip_format.empty() && config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); - } - if (!tooltip_format.empty()) { - tooltip_format = config_["tooltip-format"].asString(); - - auto icons = std::vector{state}; - fmt::dynamic_format_arg_store store; - store.push_back(fmt::arg("load", load1)); - store.push_back(fmt::arg("usage", total_usage)); - store.push_back(fmt::arg("icon", getIcon(total_usage, icons))); - store.push_back(fmt::arg("max_frequency", max_frequency)); - store.push_back(fmt::arg("min_frequency", min_frequency)); - store.push_back(fmt::arg("avg_frequency", avg_frequency)); - for (size_t i = 1; i < cpu_usage.size(); ++i) { - auto core_i = i - 1; - auto core_format = fmt::format("usage{}", core_i); - store.push_back(fmt::arg(core_format.c_str(), cpu_usage[i])); - auto icon_format = fmt::format("icon{}", core_i); - store.push_back(fmt::arg(icon_format.c_str(), getIcon(cpu_usage[i], icons))); - } - auto tooltip_text = fmt::vformat(tooltip_format, store); - - if (label_.get_tooltip_text() != tooltip_text) { - label_.set_tooltip_markup(tooltip_text); - } - } else { - label_.set_tooltip_text(tooltip); - } - } - // Call parent update ALabel::update(); } From 25e57bc62585793593660420a8a17c82fcb0a64f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 21:51:20 +0200 Subject: [PATCH 248/416] fix(hyprland/window): use 2-space indentation for fallback block (clang-format) --- src/modules/hyprland/window.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 423257db..029f9722 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -1,5 +1,6 @@ #include "modules/hyprland/window.hpp" +#include #include #include #include @@ -13,8 +14,6 @@ #include "util/rewrite_string.hpp" #include "util/sanitize_str.hpp" -#include - namespace waybar::modules::hyprland { std::shared_mutex windowIpcSmtx; @@ -56,11 +55,11 @@ auto Window::update() -> void { if (!format_.empty()) { label_.show(); - // If the focused window name is empty and fallback is configured, use fallback text - std::string displayTitle = windowName; - if (displayTitle.empty() && config_["fallback"].isString()) { - displayTitle = config_["fallback"].asString(); - } + // If the focused window name is empty and fallback is configured, use fallback text + std::string displayTitle = windowName; + if (displayTitle.empty() && config_["fallback"].isString()) { + displayTitle = config_["fallback"].asString(); + } label_text = waybar::util::rewriteString( fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle), From 4bf82b88a06a8ee10f4468616d177087b904eae6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:01:20 +0200 Subject: [PATCH 249/416] style(pulseaudio): clang-format audio_backend helpers --- include/modules/pulseaudio_slider.hpp | 1 - src/util/audio_backend.cpp | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/include/modules/pulseaudio_slider.hpp b/include/modules/pulseaudio_slider.hpp index c9f82341..597b3cc8 100644 --- a/include/modules/pulseaudio_slider.hpp +++ b/include/modules/pulseaudio_slider.hpp @@ -6,7 +6,6 @@ #include "util/audio_backend.hpp" namespace waybar::modules { - class PulseaudioSlider : public ASlider { public: PulseaudioSlider(const std::string&, const Json::Value&); diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 601a9cc0..96a5bdb4 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -263,9 +263,9 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo uint16_t AudioBackend::getVolume(PulseaudioTarget target) const { if (target == PulseaudioTarget::Source) { - return source_volume_; + return source_volume_; } else { - return volume_; + return volume_; } } @@ -380,9 +380,9 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma bool AudioBackend::getMuted(PulseaudioTarget target) const { if (target == PulseaudioTarget::Source) { - return source_muted_; + return source_muted_; } else { - return muted_; + return muted_; } } @@ -424,9 +424,9 @@ void AudioBackend::toggleSourceMute(bool mute) { void AudioBackend::unmute(PulseaudioTarget target) { if (target == PulseaudioTarget::Source) { - toggleSourceMute(false); + toggleSourceMute(false); } else { - toggleSinkMute(false); + toggleSinkMute(false); } } From e736a934ba2f6a5754428e8d61dbf84316eae96a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:10:13 +0200 Subject: [PATCH 250/416] fix(clock): correct parentheses in fmt::format for calendar tooltip color --- src/modules/clock.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 5d09f72f..df3c2cc6 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -214,7 +214,7 @@ auto waybar::modules::Clock::update() -> void { context->remove_class(css_class); const std::string replace_str = fmt::format("color='#{:02x}{:02x}{:02x}'", - static_cast(color.get_red() * 255, + static_cast(color.get_red() * 255), static_cast(color.get_green() * 255), static_cast(color.get_blue() * 255)); From c5cefb5520d249d0c0b1188186a0c7db32be0506 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:10:16 +0200 Subject: [PATCH 251/416] AIconLabel: make regex static and initialize label_contains_icon Compile the icon-extraction std::regex objects once via static locals instead of rebuilding them on every update() of every AIconLabel-derived module (custom, mpris, ...). Also default-initialize label_contains_icon to false at its declaration. --- include/AIconLabel.hpp | 2 +- src/AIconLabel.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/AIconLabel.hpp b/include/AIconLabel.hpp index 1fac814c..80aa431a 100644 --- a/include/AIconLabel.hpp +++ b/include/AIconLabel.hpp @@ -21,7 +21,7 @@ class AIconLabel : public ALabel { Gtk::Box box_; unsigned app_icon_size_{24}; - bool label_contains_icon; + bool label_contains_icon{false}; bool iconEnabled() const; }; diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index 9cadcf39..c17eb8d4 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -65,12 +65,12 @@ std::tuple AIconLabel::extractIcon(const std::string& std::string icon_result = ""; std::string label_result = input; try { - const std::regex icon_search(R"((?=\\0icon\\1f).+?(?=\\n))"); + static const std::regex icon_search(R"((?=\\0icon\\1f).+?(?=\\n))"); std::smatch icon_match; if (std::regex_search(input, icon_match, icon_search)) { icon_result = icon_match[0].str().substr(9); - - const std::regex clean_label_pattern(R"(\\0icon\\1f.+?\\n)"); + + static const std::regex clean_label_pattern(R"(\\0icon\\1f.+?\\n)"); label_result = std::regex_replace(input, clean_label_pattern, ""); } } catch (const std::exception& e) { From 6dc168c4f2635bc62013ea418f39210fc0fae989 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:10:46 +0200 Subject: [PATCH 252/416] river/tags: fix memory leak of output_status_/seat_status_ The constructor created output_status_ and seat_status_ without adding any listener, then handle_show() recreated both (leaking the constructor-created objects) before destroying status_manager_. Additionally seat_status_ was never destroyed in ~Tags(). Create these objects lazily (with listeners) only in handle_show(), and destroy seat_status_ in the destructor. --- src/modules/river/tags.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index a864daea..aac304ed 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -138,9 +138,10 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con return; } + // Store the output this module belongs to; the river_output_status and + // river_seat_status objects (and their listeners) are created lazily in + // handle_show() to avoid leaking objects without listeners here. output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output_); - seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); box_.set_name("tags"); if (!id.empty()) { @@ -193,6 +194,10 @@ Tags::~Tags() { zriver_output_status_v1_destroy(output_status_); } + if (seat_status_) { + zriver_seat_status_v1_destroy(seat_status_); + } + if (control_) { zriver_control_v1_destroy(control_); } From fe0e1c139bc4c6044f4592d708063a63d3725ffa Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:11:51 +0200 Subject: [PATCH 253/416] power-profiles-daemon: preserve backward compatibility for driver Keep the {driver} placeholder working and support older power-profiles-daemon versions that only expose a single Driver DBus property, while still reading the new CpuDriver/PlatformDriver fields. - Read the legacy Driver property and fall back to it when CpuDriver or PlatformDriver are missing (older daemon). - Derive {driver} from CpuDriver when the daemon no longer exposes Driver (recent daemon), so existing configs using {driver} keep working. - Align the default tooltip-format in the constructor with the documented default. --- include/modules/power_profiles_daemon.hpp | 10 ++++++- man/waybar-power-profiles-daemon.5.scd | 2 +- src/modules/power_profiles_daemon.cpp | 35 +++++++++++++++++++---- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/include/modules/power_profiles_daemon.hpp b/include/modules/power_profiles_daemon.hpp index eb636662..1aa087a4 100644 --- a/include/modules/power_profiles_daemon.hpp +++ b/include/modules/power_profiles_daemon.hpp @@ -9,10 +9,18 @@ namespace waybar::modules { struct Profile { std::string name; + // Legacy driver field, kept for backward compatibility with the + // `{driver}` format placeholder and with older power-profiles-daemon + // versions that only expose a single `Driver` DBus property. + std::string driver; std::string cpuDriver; std::string platformDriver; - Profile(std::string n, std::string cd, std::string pd) : name(std::move(n)), cpuDriver(std::move(cd)), platformDriver(std::move(pd)) {} + Profile(std::string n, std::string d, std::string cd, std::string pd) + : name(std::move(n)), + driver(std::move(d)), + cpuDriver(std::move(cd)), + platformDriver(std::move(pd)) {} }; class PowerProfilesDaemon : public ALabel { diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd index c1b2f5b8..da331795 100644 --- a/man/waybar-power-profiles-daemon.5.scd +++ b/man/waybar-power-profiles-daemon.5.scd @@ -26,7 +26,7 @@ $XDG_CONFIG_HOME/waybar/config |[ *tooltip-format* :[ string :[ "Power profile: {profile}\\nCPU driver: {cpu_driver}\\nPlatform driver: {platform_driver}" -:[ Messaged displayed in the module tooltip. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. +:[ Messaged displayed in the module tooltip. {icon} and {profile} are respectively substituted with the icon representing the active profile and its full name. {cpu_driver} and {platform_driver} are substituted with the CPU and platform drivers reported by recent power-profiles-daemon versions. {driver} is kept for backward compatibility: it resolves to the legacy single driver on older daemons and falls back to the CPU driver on recent ones. |[ *tooltip* :[ bool :[ true diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index b272805e..d7eb5bcd 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -12,7 +12,8 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu if (config_["tooltip-format"].isString()) { tooltipFormat_ = config_["tooltip-format"].asString(); } else { - tooltipFormat_ = "Power profile: {profile}\nCpuDriver: {cpu_driver}"; + tooltipFormat_ = + "Power profile: {profile}\nCPU driver: {cpu_driver}\nPlatform driver: {platform_driver}"; } // Fasten your seatbelt, we're up for quite a ride. The rest of the // init is performed asynchronously. There's 2 callbacks involved. @@ -95,11 +96,17 @@ void PowerProfilesDaemon::populateInitState() { powerProfilesProxy_->get_cached_property(profilesVariant, "Profiles"); for (auto& variantDict : profilesVariant.get()) { Glib::ustring name; + // Legacy single `Driver` property, still exposed by older + // power-profiles-daemon versions. + Glib::ustring driver; Glib::ustring cpuDriver; Glib::ustring platformDriver; if (auto p = variantDict.find("Profile"); p != variantDict.end()) { name = p->second.get(); } + if (auto d = variantDict.find("Driver"); d != variantDict.end()) { + driver = d->second.get(); + } if (auto cd = variantDict.find("CpuDriver"); cd != variantDict.end()) { cpuDriver = cd->second.get(); } @@ -107,17 +114,33 @@ void PowerProfilesDaemon::populateInitState() { platformDriver = pd->second.get(); } + // Recent power-profiles-daemon versions split the single `Driver` + // property into `CpuDriver` and `PlatformDriver`. When talking to an + // older daemon that only exposes `Driver`, fall back to it so the new + // {cpu_driver}/{platform_driver} placeholders still resolve. if (cpuDriver.empty()) { - cpuDriver = "Unavailable"; - spdlog::warn("Cannot find power profiles daemon cpu driver."); + cpuDriver = driver; } if (platformDriver.empty()) { + platformDriver = driver; + } + // Conversely, keep the legacy {driver} placeholder working against a + // recent daemon that no longer exposes `Driver` by deriving it from + // the CPU driver. + if (driver.empty()) { + driver = cpuDriver; + } + + if (driver.empty()) { + driver = "Unavailable"; + cpuDriver = "Unavailable"; platformDriver = "Unavailable"; - spdlog::warn("Cannot find power profiles daemon platform driver."); + spdlog::warn("Cannot find power profiles daemon driver."); } if (!name.empty()) { - availableProfiles_.emplace_back(std::move(name), std::move(cpuDriver), std::move(platformDriver)); + availableProfiles_.emplace_back(std::move(name), std::move(driver), std::move(cpuDriver), + std::move(platformDriver)); } else { spdlog::error( "Power profiles daemon: power-profiles-daemon sent us an empty power profile name. " @@ -167,6 +190,8 @@ auto PowerProfilesDaemon::update() -> void { // Set label fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("profile", profile.name)); + // Legacy placeholder, kept for backward compatibility with existing configs. + store.push_back(fmt::arg("driver", profile.driver)); store.push_back(fmt::arg("cpu_driver", profile.cpuDriver)); store.push_back(fmt::arg("platform_driver", profile.platformDriver)); store.push_back(fmt::arg("icon", getIcon(0, profile.name))); From 573ad779bad545269ef52380a5cf5e9788b5c0a6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:21:03 +0200 Subject: [PATCH 254/416] Apply clang-format to clock.cpp --- src/modules/clock.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index df3c2cc6..bdeaf889 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -200,12 +200,11 @@ auto waybar::modules::Clock::update() -> void { auto context = label_.get_style_context(); static const std::vector> calendar_class_map = { - {"calendar-today", "class='today'"}, - {"calendar-days", "class='days'"}, - {"calendar-weeks", "class='weeks'"}, + {"calendar-today", "class='today'"}, + {"calendar-days", "class='days'"}, + {"calendar-weeks", "class='weeks'"}, {"calendar-weekdays", "class='weekdays'"}, - {"calendar-months", "class='months'"} - }; + {"calendar-months", "class='months'"}}; for (const auto& [css_class, search_str] : calendar_class_map) { try { @@ -213,18 +212,17 @@ auto waybar::modules::Clock::update() -> void { const Gdk::RGBA color = context->get_color(); context->remove_class(css_class); - const std::string replace_str = fmt::format("color='#{:02x}{:02x}{:02x}'", - static_cast(color.get_red() * 255), - static_cast(color.get_green() * 255), - static_cast(color.get_blue() * 255)); + const std::string replace_str = fmt::format( + "color='#{:02x}{:02x}{:02x}'", static_cast(color.get_red() * 255), + static_cast(color.get_green() * 255), static_cast(color.get_blue() * 255)); m_tlpText_ = std::regex_replace(m_tlpText_, std::regex(search_str), replace_str); } catch (const Glib::Error& e) { - spdlog::warn("Clock: Failed to fetch CSS color for {}: {}", css_class, e.what().raw()); - continue; + spdlog::warn("Clock: Failed to fetch CSS color for {}: {}", css_class, e.what().raw()); + continue; } catch (...) { - // Catch-all for any other weirdness. - continue; + // Catch-all for any other weirdness. + continue; } } From cd2b1802cbcb9d49803b5742a1b7d11455b6e43b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:21:07 +0200 Subject: [PATCH 255/416] Apply clang-format to wireplumber module --- src/modules/wireplumber.cpp | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 2607d22d..751e885c 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -1,6 +1,7 @@ #include "modules/wireplumber.hpp" #include + #include #include @@ -450,7 +451,6 @@ auto waybar::modules::Wireplumber::update() -> void { label_.get_style_context()->remove_class("source-muted"); } - double vol_cube = pow(volume_, 3); double source_vol_cube = pow(source_volume_, 3); @@ -460,7 +460,6 @@ auto waybar::modules::Wireplumber::update() -> void { double vol_db = 20.0 * log10(volume_); double source_vol_db = 20.0 * log10(source_volume_); - // Get the state and apply state-specific format if available auto state = getState(vol); if (!state.empty()) { @@ -492,8 +491,9 @@ auto waybar::modules::Wireplumber::update() -> void { fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_), fmt::arg("volume_linear", volume_), fmt::arg("volume_cubic", vol_cube), - fmt::arg("volume_db", vol_db),fmt::arg("source_volume_linear", source_volume_), - fmt::arg("source_volume_cubic", source_vol_cube), fmt::arg("source_volume_db", source_vol_db)); + fmt::arg("volume_db", vol_db), fmt::arg("source_volume_linear", source_volume_), + fmt::arg("source_volume_cubic", source_vol_cube), + fmt::arg("source_volume_db", source_vol_db)); label_.set_markup(markup); if (tooltipEnabled()) { @@ -508,7 +508,8 @@ auto waybar::modules::Wireplumber::update() -> void { fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_), fmt::arg("volume_linear", volume_), fmt::arg("volume_cubic", vol_cube), fmt::arg("volume_db", vol_db), fmt::arg("source_volume_linear", source_volume_), - fmt::arg("source_volume_cubic", source_vol_cube), fmt::arg("source_volume_db", source_vol_db))); + fmt::arg("source_volume_cubic", source_vol_cube), + fmt::arg("source_volume_db", source_vol_db))); } else { label_.set_tooltip_markup(node_name_); } @@ -543,15 +544,12 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { if (scale == "cubic") { vol = pow(vol, 3); - } - else if (scale == "db") { + } else if (scale == "db") { vol = log10(vol) * 20.0; - } - else if (scale == "cubic_percent") { + } else if (scale == "cubic_percent") { vol = pow(vol, 3) * 100.0; } - double newVol = vol; if (dir == SCROLL_DIR::UP) { newVol = vol + step; @@ -561,11 +559,9 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { if (scale == "cubic") { newVol = cbrt(newVol); - } - else if (scale == "db") { + } else if (scale == "db") { newVol = exp10(newVol / 20.0); - } - else if (scale == "cubic_percent") { + } else if (scale == "cubic_percent") { newVol = cbrt(newVol / 100.0); } @@ -579,8 +575,10 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { } } - if (newVol < 0) newVol = 0; - else if (newVol > maxVolume) newVol = maxVolume; + if (newVol < 0) + newVol = 0; + else if (newVol > maxVolume) + newVol = maxVolume; if (newVol != volume_) { if (mixer_api_ == nullptr) return true; From 22f6e3c83c4fe6e5efd01271c2c2c9a578a5f78a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:22:35 +0200 Subject: [PATCH 256/416] mpd: adapt Playing::on_io to post-refactor Context API queryMPD() was removed from Context by the master-side dedup refactor (perf(mpd): avoid duplicate playing-state updates). fetchState() is already called earlier in on_io and emit() updates the GUI, so the redundant queryMPD() call is dropped, matching the identical resolution applied to Playing::on_timer on master. --- src/modules/mpd/state.cpp | 45 +++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 5f529983..8888a884 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -19,17 +19,17 @@ auto format_as(enum mpd_idle val) { namespace waybar::modules::detail { -#define RUN_NOIDLE_AND_CMD(STATE, ...) \ - if (idle_connection_.connected()) { \ - idle_connection_.disconnect(); \ - auto conn = ctx_->connection().get(); \ - if (!mpd_run_noidle(conn)) { \ - if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \ +#define RUN_NOIDLE_AND_CMD(STATE, ...) \ + if (idle_connection_.connected()) { \ + idle_connection_.disconnect(); \ + auto conn = ctx_->connection().get(); \ + if (!mpd_run_noidle(conn)) { \ + if (mpd_connection_get_error(conn) != MPD_ERROR_SUCCESS) { \ spdlog::error("mpd: STATE: failed to unregister for IDLE events"); \ - ctx_->checkErrors(conn); \ - } \ - } \ - __VA_ARGS__; \ + ctx_->checkErrors(conn); \ + } \ + } \ + __VA_ARGS__; \ } void Idle::play() { @@ -50,7 +50,6 @@ void Idle::stop() { ctx_->setState(std::make_unique(ctx_)); } - void Idle::update() noexcept { // This is intentionally blank. } @@ -140,7 +139,6 @@ void Playing::timer() noexcept { timer_connection_ = Glib::signal_timeout().connect_seconds(timer_slot, 1); } - void Playing::idle() noexcept { auto conn = ctx_->connection().get(); assert(conn != nullptr); @@ -218,7 +216,6 @@ bool Playing::on_io(Glib::IOCondition const&) { return false; } - ctx_->queryMPD(); ctx_->emit(); if (!mpd_send_idle_mask( @@ -239,25 +236,23 @@ bool Playing::on_io(Glib::IOCondition const&) { } void Playing::stop() { - RUN_NOIDLE_AND_CMD(Playing, - if (timer_connection_.connected()) { - timer_connection_.disconnect(); + RUN_NOIDLE_AND_CMD( + Playing, if (timer_connection_.connected()) { + timer_connection_.disconnect(); - mpd_run_stop(ctx_->connection().get()); - } - ); + mpd_run_stop(ctx_->connection().get()); + }); ctx_->setState(std::make_unique(ctx_)); } void Playing::pause() { - RUN_NOIDLE_AND_CMD(Playing, - if (timer_connection_.connected()) { - timer_connection_.disconnect(); + RUN_NOIDLE_AND_CMD( + Playing, if (timer_connection_.connected()) { + timer_connection_.disconnect(); - mpd_run_pause(ctx_->connection().get(), true); - } - ); + mpd_run_pause(ctx_->connection().get(), true); + }); ctx_->setState(std::make_unique(ctx_)); } From 1afb1a5229798b11f669061306b95dce0e879f7c Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:35:12 +0200 Subject: [PATCH 257/416] docs(man): document options implemented but missing from man pages Sync man pages with the code for options that were read but undocumented: - sway/workspaces: ignore-workspaces - niri/workspaces: sort-by-number, hide-empty - wayfire/workspaces: disable-scroll, enable-bar-scroll - river/tags: set-tags, toggle-tags - hyprland/workspaces: persistent-workspaces (formalized) - hyprland/window: fallback, tooltip-format - cpu: format-, tooltip-format, tooltip-format- - pulseaudio/slider: ignored-sinks - hyprland/niri/mango language: format-- (+ hyprland tooltip variant) --- man/waybar-cpu.5.scd | 12 ++++++++++++ man/waybar-hyprland-language.5.scd | 8 ++++++++ man/waybar-hyprland-window.5.scd | 9 +++++++++ man/waybar-hyprland-workspaces.5.scd | 5 +++++ man/waybar-mango-language.5.scd | 4 ++++ man/waybar-niri-language.5.scd | 4 ++++ man/waybar-niri-workspaces.5.scd | 10 ++++++++++ man/waybar-pulseaudio-slider.5.scd | 5 +++++ man/waybar-river-tags.5.scd | 8 ++++++++ man/waybar-sway-workspaces.5.scd | 5 +++++ man/waybar-wayfire-workspaces.5.scd | 10 ++++++++++ 11 files changed, 80 insertions(+) diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index f6e98a1c..3f68998f 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -83,6 +83,18 @@ The *cpu* module displays the current CPU utilization. default: true ++ Option to disable tooltip on hover. +*tooltip-format*: ++ + typeof: string ++ + The format of the tooltip shown on hover. Supports the same replacements as *format*. + +*format-*: ++ + typeof: string ++ + The format to use when the given *state* (see *states*) is active. Supports the same replacements as *format*. + +*tooltip-format-*: ++ + typeof: string ++ + The tooltip format to use when the given *state* (see *states*) is active. Takes precedence over *tooltip-format*. + *expand*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index b5ce80aa..88dd82d7 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -21,6 +21,10 @@ Addressed by *hyprland/language* typeof: string++ Provide an alternative name to display per language where is the language of your choosing. Can be passed multiple times with multiple languages as shown by the example below. +*format--* ++ + typeof: string ++ + Like *format-* but also matches the layout variant, taking precedence over *format-* when both the language and variant match. + *keyboard-name*: ++ typeof: string ++ Specifies which keyboard to use from hyprctl devices output. Using the option that begins with "at-translated-set..." is recommended. @@ -52,6 +56,10 @@ Addressed by *hyprland/language* typeof: string ++ Allows specifying a different tooltip format for each language. The ** should be replaced with the language code. This can be used to provide a custom tooltip for each language. +*tooltip-format--*: ++ + typeof: string ++ + Like *tooltip-format-* but also matches the layout variant, taking precedence over *tooltip-format-* when both the language and variant match. + *expand*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-hyprland-window.5.scd b/man/waybar-hyprland-window.5.scd index 34ebf89b..ecbacd4b 100644 --- a/man/waybar-hyprland-window.5.scd +++ b/man/waybar-hyprland-window.5.scd @@ -25,6 +25,15 @@ Addressed by *hyprland/window* typeof: bool ++ Show the active window of the monitor the bar belongs to, instead of the focused window. +*fallback*: ++ + typeof: string ++ + Text to display when the focused window title is empty (for example when no window is focused). + +*tooltip-format*: ++ + typeof: string ++ + default: {title} ++ + The format of the tooltip shown on hover. Supports the same replacements as *format*. Requires *tooltip* to be enabled. + *icon*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index d60557fa..85bbf4be 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -118,6 +118,11 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. default: false ++ If set to true, only persistent workspaces will be shown on bar. +*persistent-workspaces*: ++ + typeof: object ++ + default: empty ++ + Lists workspaces that should always be shown, even when they do not exist. Keys are workspace names and values are arrays of output names on which the workspace should be shown (an empty array means all outputs). See the examples below. + *all-outputs*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-mango-language.5.scd b/man/waybar-mango-language.5.scd index 53fd9569..b153f0d3 100644 --- a/man/waybar-mango-language.5.scd +++ b/man/waybar-mango-language.5.scd @@ -24,6 +24,10 @@ Addressed by *mango/language* The value is used as the replacement for *{}* in the main *format*. This option can be repeated for multiple languages. +*format--*: ++ + typeof: string ++ + Like *format-* but also matches the layout variant, taking precedence over *format-* when both the language and variant match. + *menu*: ++ typeof: string ++ Action that pops up a menu. diff --git a/man/waybar-niri-language.5.scd b/man/waybar-niri-language.5.scd index 78cf14f8..d58408d8 100644 --- a/man/waybar-niri-language.5.scd +++ b/man/waybar-niri-language.5.scd @@ -21,6 +21,10 @@ Addressed by *niri/language* typeof: string++ Provide an alternative name to display per language where is the language of your choosing. Can be passed multiple times with multiple languages as shown by the example below. +*format--* ++ + typeof: string ++ + Like *format-* but also matches the layout variant, taking precedence over *format-* when both the language and variant match. + *menu*: ++ typeof: string ++ Action that popups the menu. diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index b8fd15ec..af0a53dc 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -32,6 +32,11 @@ Addressed by *niri/workspaces* default: false ++ Sort workspaces by id, taking precedence over other sort options. +*sort-by-number*: ++ + typeof: bool ++ + default: false ++ + Deprecated alias for *sort-by-id*; prefer *sort-by-id* instead. + If none of the sorting options are enabled, workspaces keep their output/index order. *format*: ++ @@ -58,6 +63,11 @@ If none of the sorting options are enabled, workspaces keep their output/index o default: false ++ If set to true, only the active or focused workspace will be shown. +*hide-empty*: ++ + typeof: bool ++ + default: false ++ + If set to true, empty workspaces will not be shown. + *on-update*: ++ typeof: string ++ Command to execute when the module is updated. diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index 7ed5ba41..ef2988ff 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -48,6 +48,11 @@ The volume can be controlled by dragging the slider across the bar or clicking o default: sink ++ The audio target to control. Can be either `sink` (output/speakers) or `source` (input/microphone). +*ignored-sinks*: ++ + typeof: array ++ + default: empty ++ + A list of sink descriptions to ignore when tracking the default sink, so switching to those sinks does not update the slider. + # EXAMPLES ``` diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index 03281640..1966d335 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -26,6 +26,14 @@ Addressed by *river/tags* default: false ++ If set to false, you can left-click to set focused tag. Right-click to toggle tag focus. If set to true this behaviour is disabled. +*set-tags*: ++ + typeof: array ++ + An array of tag bitmasks, one per tag button. Left-clicking a tag sets the focused tags to the corresponding bitmask instead of the single tag. Requires *disable-click* to be false. + +*toggle-tags*: ++ + typeof: array ++ + An array of tag bitmasks, one per tag button. Right-clicking a tag toggles the corresponding bitmask in the focused tags instead of the single tag. Requires *disable-click* to be false. + *expand*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index ec39982d..b90f860c 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -70,6 +70,11 @@ Addressed by *sway/workspaces* default: false ++ If set to true. Only focused workspaces will be shown. +*ignore-workspaces*: ++ + typeof: array ++ + default: empty ++ + List of regular expressions. Workspaces whose name matches any of the given patterns are not shown. + *persistent-workspaces*: ++ typeof: json (see below) ++ default: empty ++ diff --git a/man/waybar-wayfire-workspaces.5.scd b/man/waybar-wayfire-workspaces.5.scd index 53a179e8..04f3be52 100644 --- a/man/waybar-wayfire-workspaces.5.scd +++ b/man/waybar-wayfire-workspaces.5.scd @@ -26,6 +26,16 @@ Addressed by *wayfire/workspaces* default: false ++ If set to false, you can click to change workspace. If set to true this behaviour is disabled. +*disable-scroll*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can scroll to cycle through workspaces. If set to true this behaviour is disabled. + +*enable-bar-scroll*: ++ + typeof: bool ++ + default: false ++ + If set to false, you can't scroll to cycle through workspaces from the entire bar. If set to true this behaviour is enabled. + *disable-markup*: ++ typeof: bool ++ default: false ++ From 8eb916ac8485b9faf0b4ca1c20254ac2f8364a03 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:46:31 +0200 Subject: [PATCH 258/416] refactor(ALabel): add generic label+tooltip helper to remove duplication Modules duplicated the same boilerplate to format their label and tooltip (read tooltip-format, fmt::format, set_tooltip_markup) across ~29 modules. Add updateLabelAndTooltip(labelFormat, tooltipDefault, args...) and its state-aware variant updateLabelAndTooltipForState(state, ...) to ALabel: they build a single fmt arg store, render the label and the resolved tooltip format (tooltip-format[-state] or default) through the dedup-aware setters, honoring the tooltip toggle. resolveTooltipFormat() centralizes the format resolution. Migrate temperature and disk as the first adopters. --- include/ALabel.hpp | 39 +++++++++++++++++++++++++++++++++++++ src/modules/disk.cpp | 20 ++++--------------- src/modules/temperature.cpp | 17 ++++------------ 3 files changed, 47 insertions(+), 29 deletions(-) diff --git a/include/ALabel.hpp b/include/ALabel.hpp index 4b04c00d..bb68768d 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -1,10 +1,14 @@ #pragma once +#include +#include #include #include #include #include +#include +#include #include "AModule.hpp" @@ -30,6 +34,41 @@ class ALabel : public AModule { bool setLabelMarkup(const Glib::ustring& markup); bool setTooltipMarkup(const Glib::ustring& markup); + // Resolve the tooltip format string: prefers `tooltip-format-` (when a + // non-empty state is given), then `tooltip-format`, then `defaultFormat`. + std::string resolveTooltipFormat(const std::string& defaultFormat, + const std::string& state = "") const { + if (!state.empty() && config_["tooltip-format-" + state].isString()) { + return config_["tooltip-format-" + state].asString(); + } + if (config_["tooltip-format"].isString()) { + return config_["tooltip-format"].asString(); + } + return defaultFormat; + } + + // Combined label + tooltip helper. Builds a single fmt argument store from + // `args`, renders `labelFormat` into the label and the resolved tooltip format + // into the tooltip, both through the dedup-aware setters. Honors the `tooltip` + // toggle. This replaces the label/tooltip formatting boilerplate that modules + // used to duplicate. `state` selects `tooltip-format-` when non-empty. + template + void updateLabelAndTooltipForState(const std::string& state, const std::string& labelFormat, + const std::string& tooltipDefault, Args&&... args) { + fmt::dynamic_format_arg_store store; + (store.push_back(std::forward(args)), ...); + setLabelMarkup(fmt::vformat(labelFormat, store)); + if (tooltipEnabled()) { + setTooltipMarkup(fmt::vformat(resolveTooltipFormat(tooltipDefault, state), store)); + } + } + + template + void updateLabelAndTooltip(const std::string& labelFormat, const std::string& tooltipDefault, + Args&&... args) { + updateLabelAndTooltipForState("", labelFormat, tooltipDefault, std::forward(args)...); + } + bool handleToggle(GdkEventButton* const& e) override; void copyToClipboard(const std::string&); virtual std::string getState(uint8_t value, bool lesser = false); diff --git a/src/modules/disk.cpp b/src/modules/disk.cpp index 462940b2..5b9d7e41 100644 --- a/src/modules/disk.cpp +++ b/src/modules/disk.cpp @@ -68,25 +68,13 @@ auto waybar::modules::Disk::update() -> void { event_box_.hide(); } else { event_box_.show(); - label_.set_markup(fmt::format( - fmt::runtime(format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), + updateLabelAndTooltip( + format, "{used} used out of {total} on {path} ({percentage_used}%)", + stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used), fmt::arg("percentage_used", percentage_used), fmt::arg("total", total), fmt::arg("path", path_), fmt::arg("specific_free", specific_free), - fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total))); - } - - if (tooltipEnabled()) { - std::string tooltip_format = "{used} used out of {total} on {path} ({percentage_used}%)"; - if (config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); - } - label_.set_tooltip_markup(fmt::format( - fmt::runtime(tooltip_format), stats.f_bavail * 100 / stats.f_blocks, fmt::arg("free", free), - fmt::arg("percentage_free", stats.f_bavail * 100 / stats.f_blocks), fmt::arg("used", used), - fmt::arg("percentage_used", percentage_used), fmt::arg("total", total), - fmt::arg("path", path_), fmt::arg("specific_free", specific_free), - fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total))); + fmt::arg("specific_used", specific_used), fmt::arg("specific_total", specific_total)); } // Call parent update ALabel::update(); diff --git a/src/modules/temperature.cpp b/src/modules/temperature.cpp index 1d5e6522..4b410cd3 100644 --- a/src/modules/temperature.cpp +++ b/src/modules/temperature.cpp @@ -155,19 +155,10 @@ auto waybar::modules::Temperature::update() -> void { event_box_.show(); auto max_temp = config_["critical-threshold"].isInt() ? config_["critical-threshold"].asInt() : 0; - label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("temperatureC", temperature_c), - fmt::arg("temperatureF", temperature_f), - fmt::arg("temperatureK", temperature_k), - fmt::arg("icon", getIcon(temperature_c, "", max_temp)))); - if (tooltipEnabled()) { - std::string tooltip_format = "{temperatureC}°C"; - if (config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); - } - label_.set_tooltip_markup(fmt::format( - fmt::runtime(tooltip_format), fmt::arg("temperatureC", temperature_c), - fmt::arg("temperatureF", temperature_f), fmt::arg("temperatureK", temperature_k))); - } + updateLabelAndTooltip(format, "{temperatureC}°C", fmt::arg("temperatureC", temperature_c), + fmt::arg("temperatureF", temperature_f), + fmt::arg("temperatureK", temperature_k), + fmt::arg("icon", getIcon(temperature_c, "", max_temp))); // Call parent update ALabel::update(); } From 2ac1f1c437456b6cdc39e76be661d7ac1c88eb56 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:51:10 +0200 Subject: [PATCH 259/416] Fix clang-format --- src/modules/network.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index e7cdfcaf..0981d571 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -322,8 +322,7 @@ auto waybar::modules::Network::update() -> void { if (!state_.empty() && label_.get_style_context()->has_class(state_)) { label_.get_style_context()->remove_class(state_); } - if (!threshold_state.empty() && - config_["format-" + state + "-" + threshold_state].isString()) { + if (!threshold_state.empty() && config_["format-" + state + "-" + threshold_state].isString()) { default_format_ = config_["format-" + state + "-" + threshold_state].asString(); } else if (config_["format-" + state].isString()) { default_format_ = config_["format-" + state].asString(); From 913cde915abbff9e70827e9d57cd440d87c62361 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:51:45 +0200 Subject: [PATCH 260/416] Fix clang-format --- include/modules/bluetooth.hpp | 3 ++- src/modules/bluetooth.cpp | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/include/modules/bluetooth.hpp b/include/modules/bluetooth.hpp index a06d4645..5e03b118 100644 --- a/include/modules/bluetooth.hpp +++ b/include/modules/bluetooth.hpp @@ -63,7 +63,8 @@ class Bluetooth : public ALabel { auto getDeviceGattBatteryLevels(GDBusObject*, std::optional&, std::optional&) -> void; static auto processBatteryServiceCharacteristics(GList*, const std::string&, const std::string&, - const std::string&, std::optional&, + const std::string&, + std::optional&, std::optional&) -> void; auto getDeviceProperties(GDBusObject*, DeviceInfo&) -> bool; auto getControllerProperties(GDBusObject*, ControllerInfo&) -> bool; diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 97aad8b2..d54c4d07 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -92,8 +92,9 @@ auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional std::optional(g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar))); + const auto* data = static_cast( + g_variant_get_fixed_array(value_array, &n_elements, sizeof(guchar))); std::optional result; if (data != nullptr && n_elements > 0) { @@ -136,7 +137,8 @@ auto hasUserDescriptionDescriptor(GList* objects, const std::string& char_path, auto desc_uuid = getOptionalStringProperty(proxy_desc, "UUID"); g_object_unref(proxy_desc); - if (desc_uuid.has_value() && desc_uuid.value().find(user_description_uuid) != std::string::npos) { + if (desc_uuid.has_value() && + desc_uuid.value().find(user_description_uuid) != std::string::npos) { return true; } } @@ -496,7 +498,8 @@ auto waybar::modules::Bluetooth::getDeviceGattBatteryLevels( } processBatteryServiceCharacteristics(objects, service_path, BATTERY_LEVEL_UUID, - USER_DESCRIPTION_UUID, central_battery, peripheral_battery); + USER_DESCRIPTION_UUID, central_battery, + peripheral_battery); } g_list_free_full(objects, g_object_unref); From c4c9345fef30a5a29f5ab44ad16b1d3c6413ad86 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:52:02 +0200 Subject: [PATCH 261/416] Fix clang-format --- include/AGraph.hpp | 31 +++++++++++++++---------------- include/modules/cpu_graph.hpp | 6 +++--- src/factory.cpp | 6 +++--- src/modules/custom_graph.cpp | 7 +++---- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/include/AGraph.hpp b/include/AGraph.hpp index 69045473..e2ada835 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -15,7 +15,7 @@ enum class GraphType { LINE, BAR, GAUGE }; class AGraph : public AModule { public: - AGraph(const Json::Value &, const std::string &, const std::string &, uint16_t interval = 0, + AGraph(const Json::Value&, const std::string&, const std::string&, uint16_t interval = 0, bool enable_click = false, bool enable_scroll = false); virtual ~AGraph() = default; auto update() -> void override; @@ -30,29 +30,28 @@ class AGraph : public AModule { const std::chrono::seconds interval_; - bool onDraw(const Cairo::RefPtr &cr); + bool onDraw(const Cairo::RefPtr& cr); - std::map submenus_; + std::map submenus_; std::map menuActionsMap_; - static void handleGtkMenuEvent(GtkMenuItem *menuitem, gpointer data); + static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data); private: - void drawFilledArea(const Cairo::RefPtr &cr, - const std::vector> &points, double height, - const Gdk::RGBA &bg_color); + void drawFilledArea(const Cairo::RefPtr& cr, + const std::vector>& points, double height, + const Gdk::RGBA& bg_color); - void drawLine(const Cairo::RefPtr &cr, - const std::vector> &points, const Gdk::RGBA &fg_color); + void drawLine(const Cairo::RefPtr& cr, + const std::vector>& points, const Gdk::RGBA& fg_color); - void drawPath(const Cairo::RefPtr &cr, - const std::vector> &points); + void drawPath(const Cairo::RefPtr& cr, + const std::vector>& points); - void drawBars(const Cairo::RefPtr &cr, - double width, double height, int current_value, - const Gdk::RGBA &fg_color); + void drawBars(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color); - void drawGauge(const Cairo::RefPtr &cr, double width, double height, - int current_value, const Gdk::RGBA &fg_color); + void drawGauge(const Cairo::RefPtr& cr, double width, double height, + int current_value, const Gdk::RGBA& fg_color); }; } // namespace waybar diff --git a/include/modules/cpu_graph.hpp b/include/modules/cpu_graph.hpp index df5f6ea3..cf74a3fe 100644 --- a/include/modules/cpu_graph.hpp +++ b/include/modules/cpu_graph.hpp @@ -21,9 +21,9 @@ class CpuGraph : public AGraph { auto update() -> void override; private: - static constexpr const char *MODERATE_CLASS = "cpu-moderate"; - static constexpr const char *HIGH_CLASS = "cpu-high"; - static constexpr const char *INTENSIVE_CLASS = "cpu-intensive"; + static constexpr const char* MODERATE_CLASS = "cpu-moderate"; + static constexpr const char* HIGH_CLASS = "cpu-high"; + static constexpr const char* INTENSIVE_CLASS = "cpu-intensive"; std::vector> prev_times_; util::SleeperThread thread_; diff --git a/src/factory.cpp b/src/factory.cpp index cd29554c..6969da6b 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -43,11 +43,11 @@ #include "modules/niri/workspaces.hpp" #endif #ifdef HAVE_MANGO -#include "modules/mango/language.hpp" #include "modules/mango/keymode.hpp" +#include "modules/mango/language.hpp" +#include "modules/mango/layout.hpp" #include "modules/mango/window.hpp" #include "modules/mango/workspaces.hpp" -#include "modules/mango/layout.hpp" #endif #ifdef HAVE_WAYFIRE #include "modules/wayfire/window.hpp" @@ -58,8 +58,8 @@ #endif #if defined(HAVE_CPU_LINUX) || defined(HAVE_CPU_BSD) #include "modules/cpu.hpp" -#include "modules/cpu_graph.hpp" #include "modules/cpu_frequency.hpp" +#include "modules/cpu_graph.hpp" #include "modules/cpu_usage.hpp" #include "modules/load.hpp" #endif diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index c23ed0d6..2a12f0c1 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -5,7 +5,7 @@ #include "util/scope_guard.hpp" waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, - const Json::Value& config, const std::string& output_name) + const Json::Value& config, const std::string& output_name) : AGraph(config, "custom-graph-" + name, id), name_(name), output_name_(output_name), @@ -175,9 +175,8 @@ auto waybar::modules::CustomGraph::update() -> void { if (tooltipEnabled()) { if (tooltip_format_enabled_) { auto tooltip = config_["tooltip-format"].asString(); - tooltip = fmt::format( - fmt::runtime(tooltip), fmt::arg("text", text_), fmt::arg("alt", alt_), - fmt::arg("percentage", percentage_)); + tooltip = fmt::format(fmt::runtime(tooltip), fmt::arg("text", text_), + fmt::arg("alt", alt_), fmt::arg("percentage", percentage_)); graph_.set_tooltip_markup(tooltip); } else { if (graph_.get_tooltip_markup() != tooltip_) { From 885ced58fca19cb7400969ba1b5ed5ced622ffbb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:52:13 +0200 Subject: [PATCH 262/416] Fix clang-format --- src/modules/custom.cpp | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 9b60eeb1..37198e3c 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -193,7 +193,8 @@ auto waybar::modules::Custom::update() -> void { auto str = fmt::format(fmt::runtime(format_), fmt::arg("text", text_), fmt::arg("alt", alt_), fmt::arg("icon", getIcon(percentage_, alt_)), fmt::arg("percentage", percentage_)); - if ((config_["hide-empty-text"].asBool() && text_.empty()) || (str.empty() && image_path_.empty() && image_name_.empty())) { + if ((config_["hide-empty-text"].asBool() && text_.empty()) || + (str.empty() && image_path_.empty() && image_name_.empty())) { event_box_.hide(); } else { label_.set_markup(str); @@ -231,17 +232,16 @@ auto waybar::modules::Custom::update() -> void { auto image_style = image_.get_style_context(); 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); - } else if (!image_name_.empty()) { - image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID); - image_.set_pixel_size(app_icon_size_); - } + if (!image_path_.empty()) { + auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); + image_.set(pixbuf); + } 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) { if (std::strcmp(e.what(), "cannot switch from manual to automatic argument indexing") != 0) @@ -251,9 +251,6 @@ auto waybar::modules::Custom::update() -> void { "mixing manual and automatic argument indexing is no longer supported; " "try replacing \"{}\" with \"{text}\" in your format specifier"); } - - - } // Call parent update AIconLabel::update(); @@ -322,7 +319,7 @@ void waybar::modules::Custom::parseOutputJson() { class_.push_back(c.asString()); } } - + if (!parsed["percentage"].asString().empty() && parsed["percentage"].isNumeric()) { percentage_ = (int)lround(parsed["percentage"].asFloat()); } else { From 331fe0e963ea867eb60dde2ebb6c98de6b0a1080 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:52:24 +0200 Subject: [PATCH 263/416] Fix clang-format --- include/modules/clock.hpp | 5 +++-- src/modules/clock.cpp | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/modules/clock.hpp b/include/modules/clock.hpp index 4ac91d61..5fd9df54 100644 --- a/include/modules/clock.hpp +++ b/include/modules/clock.hpp @@ -89,8 +89,9 @@ class Clock final : public ALabel { {"shift_reset", &waybar::modules::Clock::cldShift_reset}, {"tz_up", &waybar::modules::Clock::tz_up}, {"tz_down", &waybar::modules::Clock::tz_down}}; - static inline std::map actionWithArgsMap_{ - {"exec", &waybar::modules::Clock::action_exec}}; + static inline std::map + actionWithArgsMap_{{"exec", &waybar::modules::Clock::action_exec}}; }; } // namespace waybar::modules diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index b92f2264..6050df31 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -550,7 +550,6 @@ void waybar::modules::Clock::action_exec(const std::string& action) { pid_children_.push_back(util::command::forkExec(cmd)); } - #ifdef HAVE_LANGINFO_1STDAY template using deleter_from_fn = std::integral_constant; From 66ecba4e99fcbb3a91acd197c6c552d730a608a7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:58:46 +0200 Subject: [PATCH 264/416] idle_inhibitor: apply clang-format --- src/modules/idle_inhibitor.cpp | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 8dfe0b85..9638d0fc 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -23,7 +23,7 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& // Read the wait-for-activity config option if (config_["wait-for-activity"].isBool()) { wait_for_activity_ = config_["wait-for-activity"].asBool(); - + // Check if ext-idle-notify protocol is available when wait-for-activity is enabled if (wait_for_activity_ && waybar::Client::inst()->idle_notifier == nullptr) { throw std::runtime_error("wait-for-activity requires ext-idle-notify-v1 protocol support"); @@ -162,31 +162,31 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { return true; } -void waybar::modules::IdleInhibitor::handleIdled(void* data, - ext_idle_notification_v1* /*notification*/) { +void waybar::modules::IdleInhibitor::handleIdled(void* data, + ext_idle_notification_v1* /*notification*/) { spdlog::info("deactivating idle_inhibitor due to user inactivity"); status = false; - + // Clean up the notification since we're deactivating auto* self = static_cast(data); if (self != nullptr) { self->teardownIdleNotification(); } - + for (auto const& module : waybar::modules::IdleInhibitor::modules) { module->update(); } } void waybar::modules::IdleInhibitor::handleResumed(void* data, - ext_idle_notification_v1* /*notification*/) { + ext_idle_notification_v1* /*notification*/) { // User became active again - notification will continue monitoring spdlog::debug("user activity detected, idle_inhibitor still active"); } void waybar::modules::IdleInhibitor::setupIdleNotification() { spdlog::debug("idle_inhibitor: setting up idle notification"); - + // Clean up any existing notification first if (idle_notification_ != nullptr) { spdlog::debug("idle_inhibitor: cleaning up existing notification before setup"); @@ -208,11 +208,12 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { auto* wl_seat = gdk_wayland_seat_get_wl_seat(gdk_seat); // Check protocol version to determine which function to use - uint32_t version = wl_proxy_get_version(reinterpret_cast(client->idle_notifier)); - - spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})", + uint32_t version = + wl_proxy_get_version(reinterpret_cast(client->idle_notifier)); + + spdlog::debug("idle_inhibitor: creating notification with timeout {} ms (protocol version {})", idle_timeout_ms_, version); - + if (version >= 2) { // Version 2+: Use get_input_idle_notification which ignores idle inhibitors // This allows us to detect actual user inactivity even while the inhibitor is active @@ -222,10 +223,13 @@ void waybar::modules::IdleInhibitor::setupIdleNotification() { } else { // Version 1: Fall back to get_idle_notification // WARNING: This respects idle inhibitors, so it won't fire while inhibitor is active - spdlog::warn("idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support get_input_idle_notification, " - "wait-for-activity may not work correctly", version); - idle_notification_ = ext_idle_notifier_v1_get_idle_notification( - client->idle_notifier, idle_timeout_ms_, wl_seat); + spdlog::warn( + "idle_inhibitor: ext-idle-notifier-v1 version {} doesn't support " + "get_input_idle_notification, " + "wait-for-activity may not work correctly", + version); + idle_notification_ = ext_idle_notifier_v1_get_idle_notification(client->idle_notifier, + idle_timeout_ms_, wl_seat); } if (idle_notification_ == nullptr) { From cdfd3cb08ab413d5d8da66387a41d7eb1a840f49 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:59:55 +0200 Subject: [PATCH 265/416] refactor: migrate complex-tooltip modules to ALabel dedup setters Switch direct label_.set_markup/set_tooltip_markup calls to the deduplicating setLabelMarkup/setTooltipMarkup helpers in clock, custom and mpris, keeping their custom tooltip construction logic intact. taskbar is left untouched: its tooltip lives on per-task Gtk::Button, not on an ALabel label_. --- src/modules/clock.cpp | 2 +- src/modules/custom.cpp | 7 ++----- src/modules/mpris/mpris.cpp | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index b265c0ae..6f570e2d 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -162,7 +162,7 @@ auto waybar::modules::Clock::update() -> void { const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : local_zone(); const zoned_time now{tz, floor(system_clock::now())}; - label_.set_markup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now))); + setLabelMarkup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now))); if (tooltipEnabled()) { const year_month_day today{floor(now.get_local_time())}; diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index e4a4eb37..7303d8ff 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -187,7 +187,7 @@ auto waybar::modules::Custom::update() -> void { if ((config_["hide-empty-text"].asBool() && text_.empty()) || str.empty()) { event_box_.hide(); } else { - label_.set_markup(str); + setLabelMarkup(str); if (tooltipEnabled()) { std::string tooltip_markup; if (tooltip_format_enabled_) { @@ -202,10 +202,7 @@ auto waybar::modules::Custom::update() -> void { tooltip_markup = tooltip_; } - if (last_tooltip_markup_ != tooltip_markup) { - label_.set_tooltip_markup(tooltip_markup); - last_tooltip_markup_ = std::move(tooltip_markup); - } + setTooltipMarkup(tooltip_markup); } auto style = label_.get_style_context(); auto classes = style->list_classes(); diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index ce084723..c3e0235e 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -735,7 +735,7 @@ auto Mpris::update() -> void { if (label_format.empty()) { label_.hide(); } else { - label_.set_markup(label_format); + setLabelMarkup(label_format); label_.show(); } } catch (fmt::format_error const& e) { @@ -758,7 +758,7 @@ auto Mpris::update() -> void { fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string))); - label_.set_tooltip_markup(tooltip_text); + setTooltipMarkup(tooltip_text); } catch (fmt::format_error const& e) { spdlog::warn("mpris: format error (tooltip): {}", e.what()); } From db4941efe74aa2d3acc7c9c8a4f90b3a2afacac8 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 00:00:15 +0200 Subject: [PATCH 266/416] refactor(tooltip): migrate modules to ALabel generic tooltip helper Migrate idle_inhibitor, sway/language, sway/scratchpad, hyprland/language and hyprland/window to the shared label/tooltip setters. idle_inhibitor uses updateLabelAndTooltipForState; the others keep their custom label/tooltip resolution (trim, precomputed tooltip, rewriteString, per-lang/variant formats) and switch to the dedup-aware setLabelMarkup/setTooltipMarkup. --- src/modules/hyprland/language.cpp | 18 +++++++++--------- src/modules/hyprland/window.cpp | 19 +++++++++---------- src/modules/idle_inhibitor.cpp | 11 ++--------- src/modules/sway/language.cpp | 6 +++--- src/modules/sway/scratchpad.cpp | 4 ++-- 5 files changed, 25 insertions(+), 33 deletions(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index ff7cad64..dfe24b0a 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -77,9 +77,9 @@ auto Language::update() -> void { if (!format_.empty()) { label_.show(); - label_.set_markup(layoutName); + setLabelMarkup(layoutName); if (tooltip_enabled) { - label_.set_tooltip_markup(tooltipContent); + setTooltipMarkup(tooltipContent); } } else { label_.hide(); @@ -93,12 +93,11 @@ auto Language::update() -> void { } 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))); + 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(""); @@ -185,7 +184,8 @@ void Language::initLanguage() { auto Language::removeXkbLayoutCssClass() -> void { label_.get_style_context()->remove_class(layout_.short_name); - spdlog::debug("hyprland language try to remove currently short_name css class {}", layout_.short_name); + spdlog::debug("hyprland language try to remove currently short_name css class {}", + layout_.short_name); } auto Language::addXkbLayoutCssClass() -> void { label_.get_style_context()->add_class(layout_.short_name); diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 029f9722..13993482 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -67,7 +67,7 @@ auto Window::update() -> void { fmt::arg("class", windowData_.class_name), fmt::arg("initialClass", windowData_.initial_class_name)), config_["rewrite"]); - label_.set_markup(label_text); + setLabelMarkup(label_text); } else { label_.hide(); } @@ -78,13 +78,12 @@ auto Window::update() -> void { tooltip_format = config_["tooltip-format"].asString(); } if (!tooltip_format.empty()) { - label_.set_tooltip_markup( - fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName), - fmt::arg("initialTitle", windowData_.initial_title), - fmt::arg("class", windowData_.class_name), - fmt::arg("initialClass", windowData_.initial_class_name))); + setTooltipMarkup(fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName), + fmt::arg("initialTitle", windowData_.initial_title), + fmt::arg("class", windowData_.class_name), + fmt::arg("initialClass", windowData_.initial_class_name))); } else if (!label_text.empty()) { - label_.set_tooltip_markup(label_text); + setTooltipMarkup(label_text); } } @@ -222,9 +221,9 @@ void Window::queryActiveWorkspace() { std::vector visibleWindows; std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows), [&](const Json::Value& window) { return !window["hidden"].asBool(); }); - solo_ = 1 == std::count_if( - visibleWindows.begin(), visibleWindows.end(), - [&](const Json::Value& window) { return !window["floating"].asBool(); }); + solo_ = + 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(), + [&](const Json::Value& window) { return !window["floating"].asBool(); }); allFloating_ = std::ranges::all_of( visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); }); fullscreen_ = windowData_.fullscreen; diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index a5fc9ac7..e18c28f0 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -63,16 +63,9 @@ auto waybar::modules::IdleInhibitor::update() -> void { } std::string status_text = status ? "activated" : "deactivated"; - label_.set_markup(fmt::format(fmt::runtime(format_), fmt::arg("status", status_text), - fmt::arg("icon", getIcon(0, status_text)))); + updateLabelAndTooltipForState(status_text, format_, "{status}", fmt::arg("status", status_text), + fmt::arg("icon", getIcon(0, status_text))); label_.get_style_context()->add_class(status_text); - if (tooltipEnabled()) { - auto config = config_[status ? "tooltip-format-activated" : "tooltip-format-deactivated"]; - auto tooltip_format = config.isString() ? config.asString() : "{status}"; - label_.set_tooltip_markup(fmt::format(fmt::runtime(tooltip_format), - fmt::arg("status", status_text), - fmt::arg("icon", getIcon(0, status_text)))); - } // Call parent update ALabel::update(); } diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index 28f1ef24..7113d9ef 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -124,7 +124,7 @@ auto Language::update() -> void { fmt::runtime(format_), fmt::arg("short", layout_.short_name), fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name), fmt::arg("variant", layout_.variant), fmt::arg("flag", layout_.country_flag()))); - label_.set_markup(display_layout); + setLabelMarkup(display_layout); if (tooltipEnabled()) { if (tooltip_format_ != "") { auto tooltip_display_layout = trim( @@ -132,9 +132,9 @@ auto Language::update() -> void { fmt::arg("shortDescription", layout_.short_description), fmt::arg("long", layout_.full_name), fmt::arg("variant", layout_.variant), fmt::arg("flag", layout_.country_flag()))); - label_.set_tooltip_markup(tooltip_display_layout); + setTooltipMarkup(tooltip_display_layout); } else { - label_.set_tooltip_markup(display_layout); + setTooltipMarkup(display_layout); } } diff --git a/src/modules/sway/scratchpad.cpp b/src/modules/sway/scratchpad.cpp index 17dc2707..94f35999 100644 --- a/src/modules/sway/scratchpad.cpp +++ b/src/modules/sway/scratchpad.cpp @@ -31,12 +31,12 @@ Scratchpad::Scratchpad(const std::string& id, const Json::Value& config) auto Scratchpad::update() -> void { if (count_ || show_empty_) { event_box_.show(); - label_.set_markup( + setLabelMarkup( fmt::format(fmt::runtime(format_), fmt::arg("icon", getIcon(count_, "", config_["format-icons"].size())), fmt::arg("count", count_))); if (tooltip_enabled_) { - label_.set_tooltip_markup(tooltip_text_); + setTooltipMarkup(tooltip_text_); } } else { event_box_.hide(); From 836449d253a517f56311454d5639b5083b08fca8 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 00:00:17 +0200 Subject: [PATCH 267/416] refactor(modules): use ALabel dedup tooltip helper in simpleclock, systemd-failed-units, upower simpleclock: migrate to updateLabelAndTooltip (label + tooltip share the localtime arg; default tooltip falls back to the label format). systemd-failed-units: use setLabelMarkup/setTooltipMarkup dedup setters; label/tooltip formats are selected by different conditions so the combined helper does not apply. upower: use setLabelMarkup for the label; tooltip stays a custom GTK widget. mpd already uses the dedup setters (label/tooltip use different truncated args), so no change. --- src/modules/simpleclock.cpp | 13 +------------ src/modules/systemd_failed_units.cpp | 6 +++--- src/modules/upower.cpp | 2 +- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/modules/simpleclock.cpp b/src/modules/simpleclock.cpp index 59a2b1c1..b8fc2423 100644 --- a/src/modules/simpleclock.cpp +++ b/src/modules/simpleclock.cpp @@ -18,18 +18,7 @@ auto waybar::modules::Clock::update() -> void { tzset(); // Update timezone information auto now = std::chrono::system_clock::now(); auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now)); - auto text = fmt::format(fmt::runtime(format_), localtime); - label_.set_markup(text); - - if (tooltipEnabled()) { - if (config_["tooltip-format"].isString()) { - auto tooltip_format = config_["tooltip-format"].asString(); - auto tooltip_text = fmt::format(fmt::runtime(tooltip_format), localtime); - label_.set_tooltip_markup(tooltip_text); - } else { - label_.set_tooltip_markup(text); - } - } + updateLabelAndTooltip(format_, format_, localtime); // Call parent update ALabel::update(); } diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index a20580f5..faa1ca7e 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -281,7 +281,7 @@ auto SystemdFailedUnits::update() -> void { last_status_ = overall_state_; - label_.set_markup(fmt::format( + setLabelMarkup(fmt::format( fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_), fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_), @@ -290,14 +290,14 @@ auto SystemdFailedUnits::update() -> void { std::string failed_list = BuildTooltipFailedList(); auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_; if (!tooltip_template.empty()) { - label_.set_tooltip_markup(fmt::format( + setTooltipMarkup(fmt::format( fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_), fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_), fmt::arg("failed_units_list", failed_list))); } else { - label_.set_tooltip_markup(""); + setTooltipMarkup(""); } } ALabel::update(); diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp index d260ba83..f5d72639 100644 --- a/src/modules/upower.cpp +++ b/src/modules/upower.cpp @@ -223,7 +223,7 @@ auto UPower::update() -> void { return; } - label_.set_markup(getText(upDevice_, format_)); + setLabelMarkup(getText(upDevice_, format_)); // Set icon if (upDevice_.icon_name == NULL || !gtkTheme_->has_icon(upDevice_.icon_name)) upDevice_.icon_name = (char*)NO_BATTERY.c_str(); From 86b8dd6b1baba67bf663f487cdedd9950196fc1c Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 00:01:17 +0200 Subject: [PATCH 268/416] refactor: migrate backlight/battery/bluetooth/gps to updateLabelAndTooltip --- src/modules/backlight.cpp | 22 +++------------ src/modules/battery.cpp | 4 +-- src/modules/bluetooth.cpp | 4 +-- src/modules/gps.cpp | 56 +++++++++------------------------------ 4 files changed, 21 insertions(+), 65 deletions(-) diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 367d6434..24e08d54 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -55,24 +55,10 @@ auto waybar::modules::Backlight::update() -> void { } } - std::string desc = - fmt::format(fmt::runtime(current_format), fmt::arg("percent", percent), - fmt::arg("percent_exp", percent_exp), fmt::arg("icon", getIcon(percent)), - fmt::arg("icon_exp", getIcon(percent_exp))); - label_.set_markup(desc); - if (tooltipEnabled()) { - std::string tooltip_format; - if (config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); - } - if (!tooltip_format.empty()) { - label_.set_tooltip_markup(fmt::format(fmt::runtime(tooltip_format), - fmt::arg("percent", percent), - fmt::arg("icon", getIcon(percent)))); - } else { - label_.set_tooltip_markup(desc); - } - } + updateLabelAndTooltip(current_format, current_format, fmt::arg("percent", percent), + fmt::arg("percent_exp", percent_exp), + fmt::arg("icon", getIcon(percent)), + fmt::arg("icon_exp", getIcon(percent_exp))); } else { event_box_.hide(); } diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index 2fa4ded0..cec8beb8 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -767,7 +767,7 @@ auto waybar::modules::Battery::update() -> void { } else if (config_["tooltip-format"].isString()) { tooltip_format = config_["tooltip-format"].asString(); } - label_.set_tooltip_markup( + setTooltipMarkup( fmt::format(fmt::runtime(tooltip_format), fmt::arg("timeTo", tooltip_text_default), fmt::arg("power", power), fmt::arg("capacity", capacity), fmt::arg("time", time_remaining_formatted), fmt::arg("cycles", cycles), @@ -790,7 +790,7 @@ auto waybar::modules::Battery::update() -> void { } else { event_box_.show(); auto icons = std::vector{status + "-" + state, status, state}; - label_.set_markup(fmt::format( + setLabelMarkup(fmt::format( fmt::runtime(format), fmt::arg("capacity", capacity), fmt::arg("power", power), fmt::arg("icon", getIcon(capacity, icons)), fmt::arg("time", time_remaining_formatted), fmt::arg("cycles", cycles), fmt::arg("health", fmt::format("{:.3}", health)))); diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index c59af3b5..d2afd897 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -222,7 +222,7 @@ auto waybar::modules::Bluetooth::update() -> void { event_box_.hide(); } else { event_box_.show(); - label_.set_markup(fmt::format( + setLabelMarkup(fmt::format( fmt::runtime(format_), fmt::arg("status", state_), fmt::arg("num_connections", connected_devices_.size()), fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"), @@ -267,7 +267,7 @@ auto waybar::modules::Bluetooth::update() -> void { device_enumerate_.erase(0, 1); } } - label_.set_tooltip_markup(fmt::format( + setTooltipMarkup(fmt::format( fmt::runtime(tooltip_format), fmt::arg("status", state_), fmt::arg("num_connections", connected_devices_.size()), fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"), diff --git a/src/modules/gps.cpp b/src/modules/gps.cpp index a7bab659..285a6938 100644 --- a/src/modules/gps.cpp +++ b/src/modules/gps.cpp @@ -141,7 +141,7 @@ auto waybar::modules::Gps::update() -> void { // Show the module if (!event_box_.get_visible()) event_box_.set_visible(true); - std::string tooltip_format; + std::string tooltip_state; if (!alt_) { auto state = getFixModeName(); @@ -155,57 +155,27 @@ auto waybar::modules::Gps::update() -> void { } else { default_format_ = DEFAULT_FORMAT; } - if (config_["tooltip-format-" + state].isString()) { - tooltip_format = config_["tooltip-format-" + state].asString(); - } if (!label_.get_style_context()->has_class(state)) { label_.get_style_context()->add_class(state); } format_ = default_format_; state_ = state; + tooltip_state = state; } auto format = format_; - fmt::dynamic_format_arg_store store; - store.push_back(fmt::arg("mode", getFixModeString())); - store.push_back(fmt::arg("status", getFixStatusString())); - - store.push_back(fmt::arg("latitude", gps_data_.fix.latitude)); - store.push_back(fmt::arg("latitude_error", gps_data_.fix.epy)); - - store.push_back(fmt::arg("longitude", gps_data_.fix.longitude)); - store.push_back(fmt::arg("longitude_error", gps_data_.fix.epx)); - - store.push_back(fmt::arg("altitude_hae", gps_data_.fix.altHAE)); - store.push_back(fmt::arg("altitude_msl", gps_data_.fix.altMSL)); - store.push_back(fmt::arg("altitude_error", gps_data_.fix.epv)); - - store.push_back(fmt::arg("speed", gps_data_.fix.speed)); - store.push_back(fmt::arg("speed_error", gps_data_.fix.eps)); - - store.push_back(fmt::arg("climb", gps_data_.fix.climb)); - store.push_back(fmt::arg("climb_error", gps_data_.fix.epc)); - - store.push_back(fmt::arg("satellites_used", gps_data_.satellites_used)); - store.push_back(fmt::arg("satellites_visible", gps_data_.satellites_visible)); - - auto text = fmt::vformat(format, store); - - if (tooltipEnabled()) { - if (tooltip_format.empty() && config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); - } - if (!tooltip_format.empty()) { - auto tooltip_text = fmt::vformat(tooltip_format, store); - if (label_.get_tooltip_text() != tooltip_text) { - label_.set_tooltip_markup(tooltip_text); - } - } else if (label_.get_tooltip_text() != text) { - label_.set_tooltip_markup(text); - } - } - label_.set_markup(text); + updateLabelAndTooltipForState( + tooltip_state, format, format, fmt::arg("mode", getFixModeString()), + fmt::arg("status", getFixStatusString()), fmt::arg("latitude", gps_data_.fix.latitude), + fmt::arg("latitude_error", gps_data_.fix.epy), fmt::arg("longitude", gps_data_.fix.longitude), + fmt::arg("longitude_error", gps_data_.fix.epx), + fmt::arg("altitude_hae", gps_data_.fix.altHAE), + fmt::arg("altitude_msl", gps_data_.fix.altMSL), fmt::arg("altitude_error", gps_data_.fix.epv), + fmt::arg("speed", gps_data_.fix.speed), fmt::arg("speed_error", gps_data_.fix.eps), + fmt::arg("climb", gps_data_.fix.climb), fmt::arg("climb_error", gps_data_.fix.epc), + fmt::arg("satellites_used", gps_data_.satellites_used), + fmt::arg("satellites_visible", gps_data_.satellites_visible)); // Call parent update ALabel::update(); } From 014c95a9fe8a961e4c5418bfb533b12abbc6521f Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 00:04:47 +0200 Subject: [PATCH 269/416] refactor(modules): migrate audio/network/power modules to ALabel tooltip helpers Migrate network, pulseaudio, wireplumber, jack and power_profiles_daemon to the generic ALabel tooltip helpers: - jack and power-profiles-daemon use updateLabelAndTooltip() since their label/tooltip share a single arg set and a format-string tooltip default. - network, pulseaudio and wireplumber keep their rich in-module format selection but build a single fmt arg store, render the label/tooltip through the dedup-aware setLabelMarkup()/setTooltipMarkup() setters and resolveTooltipFormat(), preserving their custom fallbacks (label text, sink/node description) and visibility handling exactly. --- src/modules/jack.cpp | 17 ++--- src/modules/network.cpp | 95 ++++++++++++--------------- src/modules/power_profiles_daemon.cpp | 20 +++--- src/modules/pulseaudio.cpp | 29 ++++---- src/modules/wireplumber.cpp | 41 +++++------- 5 files changed, 85 insertions(+), 117 deletions(-) diff --git a/src/modules/jack.cpp b/src/modules/jack.cpp index 578fb4e0..c7bd5d92 100644 --- a/src/modules/jack.cpp +++ b/src/modules/jack.cpp @@ -72,19 +72,10 @@ auto JACK::update() -> void { } else format = "{load}%"; - label_.set_markup(fmt::format(fmt::runtime(format), fmt::arg("load", std::round(load_)), - fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_), - fmt::arg("latency", fmt::format("{:.2f}", latency)), - fmt::arg("xruns", xruns_))); - - if (tooltipEnabled()) { - std::string tooltip_format = "{bufsize}/{samplerate} {latency}ms"; - if (config_["tooltip-format"].isString()) tooltip_format = config_["tooltip-format"].asString(); - label_.set_tooltip_markup(fmt::format( - fmt::runtime(tooltip_format), fmt::arg("load", std::round(load_)), - fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_), - fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_))); - } + updateLabelAndTooltip( + format, "{bufsize}/{samplerate} {latency}ms", fmt::arg("load", std::round(load_)), + fmt::arg("bufsize", bufsize_), fmt::arg("samplerate", samplerate_), + fmt::arg("latency", fmt::format("{:.2f}", latency)), fmt::arg("xruns", xruns_)); // Call parent update ALabel::update(); diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 8bb54fce..04a1642d 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -350,32 +350,45 @@ auto waybar::modules::Network::update() -> void { final_ipaddr_ += ipaddr6_; } - auto text = fmt::format( - fmt::runtime(format_), fmt::arg("essid", essid_), fmt::arg("bssid", bssid_), - fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), - fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), - fmt::arg("netmask", netmask_), fmt::arg("netmask6", netmask6_), - fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), - fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), - fmt::arg("icon", getIcon(signal_strength_, state_)), - fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), - fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("essid", essid_)); + store.push_back(fmt::arg("bssid", bssid_)); + store.push_back(fmt::arg("signaldBm", signal_strength_dbm_)); + store.push_back(fmt::arg("signalStrength", signal_strength_)); + store.push_back(fmt::arg("signalStrengthApp", signal_strength_app_)); + store.push_back(fmt::arg("ifname", ifname_)); + store.push_back(fmt::arg("netmask", netmask_)); + store.push_back(fmt::arg("netmask6", netmask6_)); + store.push_back(fmt::arg("ipaddr", final_ipaddr_)); + store.push_back(fmt::arg("gwaddr", gwaddr_)); + store.push_back(fmt::arg("cidr", cidr_)); + store.push_back(fmt::arg("cidr6", cidr6_)); + store.push_back(fmt::arg("frequency", fmt::format("{:.1f}", frequency_))); + store.push_back(fmt::arg("icon", getIcon(signal_strength_, state_))); + store.push_back( + fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s"))); + store.push_back( + fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s"))); + store.push_back( fmt::arg("bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), - fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")), - fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")), - fmt::arg("bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), - fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), - fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), - fmt::arg("bandwidthDownBytesCompact", - pow_format(bandwidth_down / elapsed_seconds, "B", false, 2)), - fmt::arg("bandwidthUpBytesCompact", - pow_format(bandwidth_up / elapsed_seconds, "B", false, 2)), - fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); - if (text.compare(label_.get_label()) != 0) { - label_.set_markup(text); + pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s"))); + store.push_back( + fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s"))); + store.push_back(fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s"))); + store.push_back(fmt::arg("bandwidthTotalOctets", + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s"))); + store.push_back( + fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s"))); + store.push_back(fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s"))); + store.push_back(fmt::arg("bandwidthDownBytesCompact", + pow_format(bandwidth_down / elapsed_seconds, "B", false, 2))); + store.push_back(fmt::arg("bandwidthUpBytesCompact", + pow_format(bandwidth_up / elapsed_seconds, "B", false, 2))); + store.push_back(fmt::arg("bandwidthTotalBytes", + pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); + + auto text = fmt::vformat(format_, store); + if (setLabelMarkup(text)) { if (text.empty()) { event_box_.hide(); } else { @@ -387,35 +400,9 @@ auto waybar::modules::Network::update() -> void { tooltip_format = config_["tooltip-format"].asString(); } if (!tooltip_format.empty()) { - auto tooltip_text = fmt::format( - fmt::runtime(tooltip_format), fmt::arg("essid", essid_), fmt::arg("bssid", bssid_), - fmt::arg("signaldBm", signal_strength_dbm_), fmt::arg("signalStrength", signal_strength_), - fmt::arg("signalStrengthApp", signal_strength_app_), fmt::arg("ifname", ifname_), - fmt::arg("netmask", netmask_), fmt::arg("netmask6", netmask6_), - fmt::arg("ipaddr", final_ipaddr_), fmt::arg("gwaddr", gwaddr_), fmt::arg("cidr", cidr_), - fmt::arg("cidr6", cidr6_), fmt::arg("frequency", fmt::format("{:.1f}", frequency_)), - fmt::arg("icon", getIcon(signal_strength_, state_)), - fmt::arg("bandwidthDownBits", pow_format(bandwidth_down * 8ull / elapsed_seconds, "b/s")), - fmt::arg("bandwidthUpBits", pow_format(bandwidth_up * 8ull / elapsed_seconds, "b/s")), - fmt::arg("bandwidthTotalBits", - pow_format((bandwidth_up + bandwidth_down) * 8ull / elapsed_seconds, "b/s")), - fmt::arg("bandwidthDownOctets", pow_format(bandwidth_down / elapsed_seconds, "o/s")), - fmt::arg("bandwidthUpOctets", pow_format(bandwidth_up / elapsed_seconds, "o/s")), - fmt::arg("bandwidthTotalOctets", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "o/s")), - fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s")), - fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s")), - fmt::arg("bandwidthDownBytesCompact", - pow_format(bandwidth_down / elapsed_seconds, "B", false, 2)), - fmt::arg("bandwidthUpBytesCompact", - pow_format(bandwidth_up / elapsed_seconds, "B", false, 2)), - fmt::arg("bandwidthTotalBytes", - pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); - if (label_.get_tooltip_text() != tooltip_text) { - label_.set_tooltip_markup(tooltip_text); - } - } else if (label_.get_tooltip_text() != text) { - label_.set_tooltip_markup(text); + setTooltipMarkup(fmt::vformat(tooltip_format, store)); + } else { + setTooltipMarkup(text); } } diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index d7eb5bcd..b897cd1c 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -187,18 +187,14 @@ void PowerProfilesDaemon::switchToProfile(std::string const& str) { auto PowerProfilesDaemon::update() -> void { if (connected_ && activeProfile_ != availableProfiles_.end()) { auto profile = (*activeProfile_); - // Set label - fmt::dynamic_format_arg_store store; - store.push_back(fmt::arg("profile", profile.name)); - // Legacy placeholder, kept for backward compatibility with existing configs. - store.push_back(fmt::arg("driver", profile.driver)); - store.push_back(fmt::arg("cpu_driver", profile.cpuDriver)); - store.push_back(fmt::arg("platform_driver", profile.platformDriver)); - store.push_back(fmt::arg("icon", getIcon(0, profile.name))); - label_.set_markup(fmt::vformat(format_, store)); - if (tooltipEnabled()) { - label_.set_tooltip_markup(fmt::vformat(tooltipFormat_, store)); - } + // Set label and tooltip + updateLabelAndTooltip(format_, tooltipFormat_, fmt::arg("profile", profile.name), + // Legacy placeholder, kept for backward compatibility with existing + // configs. + fmt::arg("driver", profile.driver), + fmt::arg("cpu_driver", profile.cpuDriver), + fmt::arg("platform_driver", profile.platformDriver), + fmt::arg("icon", getIcon(0, profile.name))); // Set CSS class if (!currentStyle_.empty()) { diff --git a/src/modules/pulseaudio.cpp b/src/modules/pulseaudio.cpp index 10fb74f9..874f5d97 100644 --- a/src/modules/pulseaudio.cpp +++ b/src/modules/pulseaudio.cpp @@ -72,7 +72,6 @@ const std::vector waybar::modules::Pulseaudio::getPulseIcon() const auto waybar::modules::Pulseaudio::update() -> void { auto format = format_; - std::string tooltip_format; auto sink_volume = backend->getSinkVolume(); if (!alt_) { std::string format_name = "format"; @@ -120,29 +119,29 @@ auto waybar::modules::Pulseaudio::update() -> void { auto source_desc = backend->getSourceDesc(); format_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_volume)); - auto text = fmt::format( - fmt::runtime(format), fmt::arg("desc", sink_desc), fmt::arg("volume", sink_volume), - fmt::arg("format_source", format_source), fmt::arg("source_volume", source_volume), - fmt::arg("source_desc", source_desc), fmt::arg("icon", getIcon(sink_volume, getPulseIcon()))); + + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("desc", sink_desc)); + store.push_back(fmt::arg("volume", sink_volume)); + store.push_back(fmt::arg("format_source", format_source)); + store.push_back(fmt::arg("source_volume", source_volume)); + store.push_back(fmt::arg("source_desc", source_desc)); + store.push_back(fmt::arg("icon", getIcon(sink_volume, getPulseIcon()))); + + auto text = fmt::vformat(format, store); if (text.empty()) { label_.hide(); } else { - label_.set_markup(text); + setLabelMarkup(text); label_.show(); } if (tooltipEnabled()) { - if (tooltip_format.empty() && config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); - } + auto tooltip_format = resolveTooltipFormat(""); if (!tooltip_format.empty()) { - label_.set_tooltip_markup(fmt::format( - fmt::runtime(tooltip_format), fmt::arg("desc", sink_desc), - fmt::arg("volume", sink_volume), fmt::arg("format_source", format_source), - fmt::arg("source_volume", source_volume), fmt::arg("source_desc", source_desc), - fmt::arg("icon", getIcon(sink_volume, getPulseIcon())))); + setTooltipMarkup(fmt::vformat(tooltip_format, store)); } else { - label_.set_tooltip_markup(sink_desc); + setTooltipMarkup(sink_desc); } } diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 751e885c..e6a3c64b 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -432,7 +432,6 @@ void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { auto waybar::modules::Wireplumber::update() -> void { auto format = format_; - std::string tooltipFormat; // Handle sink mute state if (muted_) { @@ -486,32 +485,28 @@ auto waybar::modules::Wireplumber::update() -> void { std::string formatted_source = fmt::format(fmt::runtime(format_source), fmt::arg("volume", source_vol)); - std::string markup = - fmt::format(fmt::runtime(format), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), - fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), - fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_), - fmt::arg("volume_linear", volume_), fmt::arg("volume_cubic", vol_cube), - fmt::arg("volume_db", vol_db), fmt::arg("source_volume_linear", source_volume_), - fmt::arg("source_volume_cubic", source_vol_cube), - fmt::arg("source_volume_db", source_vol_db)); - label_.set_markup(markup); + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("node_name", node_name_)); + store.push_back(fmt::arg("volume", vol)); + store.push_back(fmt::arg("icon", getIcon(vol))); + store.push_back(fmt::arg("format_source", formatted_source)); + store.push_back(fmt::arg("source_volume", source_vol)); + store.push_back(fmt::arg("source_desc", source_name_)); + store.push_back(fmt::arg("volume_linear", volume_)); + store.push_back(fmt::arg("volume_cubic", vol_cube)); + store.push_back(fmt::arg("volume_db", vol_db)); + store.push_back(fmt::arg("source_volume_linear", source_volume_)); + store.push_back(fmt::arg("source_volume_cubic", source_vol_cube)); + store.push_back(fmt::arg("source_volume_db", source_vol_db)); + + setLabelMarkup(fmt::vformat(format, store)); if (tooltipEnabled()) { - if (tooltipFormat.empty() && config_["tooltip-format"].isString()) { - tooltipFormat = config_["tooltip-format"].asString(); - } - + auto tooltipFormat = resolveTooltipFormat(""); if (!tooltipFormat.empty()) { - label_.set_tooltip_markup(fmt::format( - fmt::runtime(tooltipFormat), fmt::arg("node_name", node_name_), fmt::arg("volume", vol), - fmt::arg("icon", getIcon(vol)), fmt::arg("format_source", formatted_source), - fmt::arg("source_volume", source_vol), fmt::arg("source_desc", source_name_), - fmt::arg("volume_linear", volume_), fmt::arg("volume_cubic", vol_cube), - fmt::arg("volume_db", vol_db), fmt::arg("source_volume_linear", source_volume_), - fmt::arg("source_volume_cubic", source_vol_cube), - fmt::arg("source_volume_db", source_vol_db))); + setTooltipMarkup(fmt::vformat(tooltipFormat, store)); } else { - label_.set_tooltip_markup(node_name_); + setTooltipMarkup(node_name_); } } From 8dbba448ce771ae3220c4ba5bf06958390d02120 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 00:05:17 +0200 Subject: [PATCH 270/416] refactor(modules): migrate state modules to ALabel tooltip helper Migrate cpu, cpu_usage, cpu_frequency and memory to the generic updateLabelAndTooltip/ForState helper so label and tooltip rendering go through the dedup-aware setters and shared tooltip-format resolution. Add store-accepting overloads of the helper to ALabel for modules that build a dynamic fmt argument store (per-core cpu stats). cpu keeps its tooltip-format- selection via the state overload; the others keep their existing tooltip-format-only behavior. --- include/ALabel.hpp | 20 ++++++++++++ src/modules/cpu.cpp | 16 +--------- src/modules/cpu_frequency/common.cpp | 24 ++++---------- src/modules/cpu_usage/common.cpp | 11 +------ src/modules/memory/common.cpp | 47 +++++++++------------------- 5 files changed, 42 insertions(+), 76 deletions(-) diff --git a/include/ALabel.hpp b/include/ALabel.hpp index bb68768d..60850352 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -69,6 +69,26 @@ class ALabel : public AModule { updateLabelAndTooltipForState("", labelFormat, tooltipDefault, std::forward(args)...); } + // Overloads accepting a pre-built argument store, for modules that must + // assemble a dynamic set of format arguments (e.g. per-core CPU stats) that + // cannot be expressed through a fixed variadic call. + // A non-const reference is used so this overload is preferred over the + // variadic template above (which would otherwise bind the store as a single + // forwarded argument). + void updateLabelAndTooltipForState(const std::string& state, const std::string& labelFormat, + const std::string& tooltipDefault, + fmt::dynamic_format_arg_store& store) { + setLabelMarkup(fmt::vformat(labelFormat, store)); + if (tooltipEnabled()) { + setTooltipMarkup(fmt::vformat(resolveTooltipFormat(tooltipDefault, state), store)); + } + } + + void updateLabelAndTooltip(const std::string& labelFormat, const std::string& tooltipDefault, + fmt::dynamic_format_arg_store& store) { + updateLabelAndTooltipForState("", labelFormat, tooltipDefault, store); + } + bool handleToggle(GdkEventButton* const& e) override; void copyToClipboard(const std::string&); virtual std::string getState(uint8_t value, bool lesser = false); diff --git a/src/modules/cpu.cpp b/src/modules/cpu.cpp index 1a64b4a3..ef9a13b6 100644 --- a/src/modules/cpu.cpp +++ b/src/modules/cpu.cpp @@ -62,21 +62,7 @@ auto waybar::modules::Cpu::update() -> void { store.push_back(fmt::arg(arg_names.back().c_str(), core_icon)); } store.push_back(fmt::arg("icons", all_icons)); - label_.set_markup(fmt::vformat(format, store)); - - if (tooltipEnabled()) { - std::string tooltip_format; - if (!state.empty() && config_["tooltip-format-" + state].isString()) { - tooltip_format = config_["tooltip-format-" + state].asString(); - } else if (config_["tooltip-format"].isString()) { - tooltip_format = config_["tooltip-format"].asString(); - } - if (!tooltip_format.empty()) { - label_.set_tooltip_markup(fmt::vformat(tooltip_format, store)); - } else { - label_.set_tooltip_markup(tooltip); - } - } + updateLabelAndTooltipForState(state, format, tooltip, store); } // Call parent update diff --git a/src/modules/cpu_frequency/common.cpp b/src/modules/cpu_frequency/common.cpp index 05adc2b3..a4a9c7e9 100644 --- a/src/modules/cpu_frequency/common.cpp +++ b/src/modules/cpu_frequency/common.cpp @@ -32,24 +32,12 @@ auto waybar::modules::CpuFrequency::update() -> void { } else { event_box_.show(); auto icons = std::vector{state}; - fmt::dynamic_format_arg_store store; - store.push_back(fmt::arg("icon", getIcon(avg_frequency, icons))); - store.push_back(fmt::arg("max_frequency", max_frequency)); - store.push_back(fmt::arg("min_frequency", min_frequency)); - store.push_back(fmt::arg("avg_frequency", avg_frequency)); - label_.set_markup(fmt::vformat(format, store)); - - if (tooltipEnabled()) { - std::string tooltip; - if (config_["tooltip-format"].isString()) { - tooltip = config_["tooltip-format"].asString(); - label_.set_tooltip_markup(fmt::vformat(tooltip, store)); - } else { - tooltip = "Minimum frequency: {}\nAverage frequency: {}\nMaximum frequency: {}\n"; - label_.set_tooltip_markup( - fmt::format(fmt::runtime(tooltip), min_frequency, avg_frequency, max_frequency)); - } - } + updateLabelAndTooltip( + format, + "Minimum frequency: {min_frequency}\nAverage frequency: {avg_frequency}\nMaximum " + "frequency: {max_frequency}\n", + fmt::arg("icon", getIcon(avg_frequency, icons)), fmt::arg("max_frequency", max_frequency), + fmt::arg("min_frequency", min_frequency), fmt::arg("avg_frequency", avg_frequency)); } // Call parent update diff --git a/src/modules/cpu_usage/common.cpp b/src/modules/cpu_usage/common.cpp index 79ce6941..13e9cc4e 100644 --- a/src/modules/cpu_usage/common.cpp +++ b/src/modules/cpu_usage/common.cpp @@ -49,16 +49,7 @@ auto waybar::modules::CpuUsage::update() -> void { store.push_back(fmt::arg(arg_names.back().c_str(), core_icon)); } store.push_back(fmt::arg("icons", all_icons)); - label_.set_markup(fmt::vformat(format, store)); - - if (tooltipEnabled()) { - if (config_["tooltip-format"].isString()) { - tooltip = config_["tooltip-format"].asString(); - label_.set_tooltip_markup(fmt::vformat(tooltip, store)); - } else { - label_.set_tooltip_markup(tooltip); - } - } + updateLabelAndTooltip(format, tooltip, store); } // Call parent update diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index d41b4fc2..626376e4 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -1,16 +1,14 @@ #include "modules/memory.hpp" namespace { -const std::unordered_map kUnits = { - {"kB", 1.000}, - {"kiB", 1.024}, - {"MB", 1.000 * 1000.0}, - {"MiB", 1.024 * 1024.0}, - {"GB", 1.000 * 1000.0 * 1000.0}, - {"GiB", 1.024 * 1024.0 * 1024.0}, - {"TB", 1.000 * 1000.0 * 1000.0 * 1000.0}, - {"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0} -}; +const std::unordered_map kUnits = {{"kB", 1.000}, + {"kiB", 1.024}, + {"MB", 1.000 * 1000.0}, + {"MiB", 1.024 * 1024.0}, + {"GB", 1.000 * 1000.0 * 1000.0}, + {"GiB", 1.024 * 1024.0 * 1024.0}, + {"TB", 1.000 * 1000.0 * 1000.0 * 1000.0}, + {"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0}}; } waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) @@ -54,7 +52,7 @@ auto waybar::modules::Memory::update() -> void { if (memtotal > 0 && memfree >= 0) { int used_ram_percentage = 100 * (memtotal - memfree) / memtotal; int used_swap_percentage = 0; - if ((bool) swaptotal) { + if ((bool)swaptotal) { used_swap_percentage = 100 * (swaptotal - swapfree) / swaptotal; } @@ -77,31 +75,14 @@ auto waybar::modules::Memory::update() -> void { } else { event_box_.show(); auto icons = std::vector{state}; - label_.set_markup(fmt::format( - fmt::runtime(format), used_ram_percentage, - fmt::arg("icon", getIcon(used_ram_percentage, icons)), - fmt::arg("total", total_ram), fmt::arg("swapTotal", total_swap), - fmt::arg("percentage", used_ram_percentage), + updateLabelAndTooltip( + format, fmt::format("{:.{}f}{} used", used_ram, 1, unit_), used_ram_percentage, + fmt::arg("icon", getIcon(used_ram_percentage, icons)), fmt::arg("total", total_ram), + fmt::arg("swapTotal", total_swap), fmt::arg("percentage", used_ram_percentage), fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram), fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), - fmt::arg("swapAvail", available_swap))); - } - - if (tooltipEnabled()) { - if (config_["tooltip-format"].isString()) { - auto tooltip_format = config_["tooltip-format"].asString(); - label_.set_tooltip_markup(fmt::format( - fmt::runtime(tooltip_format), used_ram_percentage, - fmt::arg("total", total_ram), fmt::arg("swapTotal", total_swap), - fmt::arg("percentage", used_ram_percentage), - fmt::arg("swapState", swaptotal == 0 ? "Off" : "On"), - fmt::arg("swapPercentage", used_swap_percentage), fmt::arg("used", used_ram), - fmt::arg("swapUsed", used_swap), fmt::arg("avail", available_ram), - fmt::arg("swapAvail", available_swap))); - } else { - label_.set_tooltip_markup(fmt::format("{:.{}f}{} used", used_ram, 1, unit_)); - } + fmt::arg("swapAvail", available_swap)); } } else { event_box_.hide(); From 003d531701cd20a43c7611ef6d90a03a5301e98f Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 00:17:14 +0200 Subject: [PATCH 271/416] refactor(AModule): hoist format/tooltip resolution to the base module Move resolveTooltipFormat() (and add resolveFormat()) from ALabel down into AModule, and add a generic updateTooltip(Gtk::Widget&, ...) helper. This lets modules that are not ALabel-derived reuse the shared tooltip logic instead of re-implementing it. Migrate gamemode (the only non-ALabel module reading tooltip-format) to updateTooltip(box_, ...). ALabel inherits the resolvers. --- include/ALabel.hpp | 13 +------------ include/AModule.hpp | 39 +++++++++++++++++++++++++++++++++++++++ src/modules/gamemode.cpp | 5 +---- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/include/ALabel.hpp b/include/ALabel.hpp index 60850352..201d640c 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -34,18 +34,7 @@ class ALabel : public AModule { bool setLabelMarkup(const Glib::ustring& markup); bool setTooltipMarkup(const Glib::ustring& markup); - // Resolve the tooltip format string: prefers `tooltip-format-` (when a - // non-empty state is given), then `tooltip-format`, then `defaultFormat`. - std::string resolveTooltipFormat(const std::string& defaultFormat, - const std::string& state = "") const { - if (!state.empty() && config_["tooltip-format-" + state].isString()) { - return config_["tooltip-format-" + state].asString(); - } - if (config_["tooltip-format"].isString()) { - return config_["tooltip-format"].asString(); - } - return defaultFormat; - } + // resolveTooltipFormat() / resolveFormat() are inherited from AModule. // Combined label + tooltip helper. Builds a single fmt argument store from // `args`, renders `labelFormat` into the label and the resolved tooltip format diff --git a/include/AModule.hpp b/include/AModule.hpp index fedcc8fe..4aef1f8f 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -1,11 +1,16 @@ #pragma once +#include +#include #include #include #include #include #include +#include +#include + #include "IModule.hpp" namespace waybar { @@ -40,6 +45,40 @@ class AModule : public IModule { SCROLL_DIR getScrollDir(GdkEventScroll* e); bool tooltipEnabled() const; + // --- Generic format/tooltip resolution (config-only, usable by any module, + // ALabel-derived or not). Prefers `-`, then ``, then default. + std::string resolveFormat(const std::string& defaultFormat, const std::string& state = "") const { + if (!state.empty() && config_["format-" + state].isString()) { + return config_["format-" + state].asString(); + } + if (config_["format"].isString()) { + return config_["format"].asString(); + } + return defaultFormat; + } + std::string resolveTooltipFormat(const std::string& defaultFormat, + const std::string& state = "") const { + if (!state.empty() && config_["tooltip-format-" + state].isString()) { + return config_["tooltip-format-" + state].asString(); + } + if (config_["tooltip-format"].isString()) { + return config_["tooltip-format"].asString(); + } + return defaultFormat; + } + + // Generic tooltip for any widget: honors the `tooltip` toggle and + // `tooltip-format`, formats with the given args and applies it. Lets modules + // that are not ALabel-derived (e.g. gamemode) reuse the shared logic. + template + void updateTooltip(Gtk::Widget& widget, const std::string& defaultFormat, Args&&... args) { + if (!tooltipEnabled()) { + return; + } + widget.set_tooltip_markup( + fmt::format(fmt::runtime(resolveTooltipFormat(defaultFormat)), std::forward(args)...)); + } + std::vector pid_children_; const std::string name_; const Json::Value& config_; diff --git a/src/modules/gamemode.cpp b/src/modules/gamemode.cpp index 691a2844..40970f0c 100644 --- a/src/modules/gamemode.cpp +++ b/src/modules/gamemode.cpp @@ -210,10 +210,7 @@ auto Gamemode::update() -> void { lastStatus = status; // Tooltip - if (tooltip) { - std::string text = fmt::format(fmt::runtime(tooltip_format), fmt::arg("count", gameCount)); - box_.set_tooltip_markup(text); - } + updateTooltip(box_, tooltip_format, fmt::arg("count", gameCount)); // Label format std::string str = fmt::format(fmt::runtime(showAltText ? format_alt : format), From efe31dfecfde3b97c5517365d2415eef83b85513 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 01:13:21 +0200 Subject: [PATCH 272/416] build: install wayfire man pages (register in meson man_files) The wayfire window/workspaces man pages existed but were never added to man_files, so they were never installed. Fixes #4264. --- meson.build | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/meson.build b/meson.build index a0fad843..5563d610 100644 --- a/meson.build +++ b/meson.build @@ -384,6 +384,10 @@ if true 'src/modules/wayfire/window.cpp', 'src/modules/wayfire/workspaces.cpp', ) + man_files += files( + 'man/waybar-wayfire-window.5.scd', + 'man/waybar-wayfire-workspaces.5.scd', + ) endif if get_option('login-proxy') From 7a84583a7632e417212078cf10b2e75f0a83b0a3 Mon Sep 17 00:00:00 2001 From: Maik Broemme Date: Fri, 10 Oct 2025 16:35:33 +0200 Subject: [PATCH 273/416] wlr/taskbar: add `truncate` to ellipsize long button text --- man/waybar-wlr-taskbar.5.scd | 5 +++++ src/modules/wlr/taskbar.cpp | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index 56d0b228..ae30316b 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -84,6 +84,11 @@ Addressed by *wlr/taskbar* default: false ++ If set to true, task buttons stretch to fill the available space in the taskbar and long titles are ellipsized to fit. Only takes effect on a horizontal bar; on a vertical bar the buttons keep their content-based size. If set to false, buttons are sized to their content. +*truncate*: ++ + typeof: bool ++ + default: false ++ + If set to true, the task button text will be ellipsized (truncated with …) when the available button width is smaller than the label text. + *on-click*: ++ typeof: string ++ The action which should be triggered when clicking on the application button with the left mouse button. diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 2cfe1736..206a6b0e 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -138,6 +138,17 @@ Task::Task(const waybar::Bar& bar, const Json::Value& config, Taskbar* tbar, content_.show(); button.add(content_); + // Apply optional label truncation (ellipsize). + if (config_["truncate"].isBool() && config_["truncate"].asBool()) { + text_before_.set_single_line_mode(true); + text_before_.set_ellipsize(Pango::ELLIPSIZE_END); + text_before_.set_line_wrap(false); + + text_after_.set_single_line_mode(true); + text_after_.set_ellipsize(Pango::ELLIPSIZE_END); + text_after_.set_line_wrap(false); + } + format_before_.clear(); format_after_.clear(); From d3ef76ccbc1a3b6b3daff4bf9016e5d293a84760 Mon Sep 17 00:00:00 2001 From: Ryan Gibb Date: Wed, 22 Dec 2021 14:53:41 +0000 Subject: [PATCH 274/416] added output-dimensions option --- include/bar.hpp | 2 ++ include/client.hpp | 1 + include/config.hpp | 3 ++- man/waybar.5.scd.in | 22 ++++++++++++++++++++++ src/client.cpp | 16 ++++++++++++++-- src/config.cpp | 43 ++++++++++++++++++++++++++++++++++++++----- test/config.cpp | 22 +++++++++++----------- 7 files changed, 90 insertions(+), 19 deletions(-) diff --git a/include/bar.hpp b/include/bar.hpp index 783dd282..f3dfa781 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -24,6 +24,8 @@ struct waybar_output { Glib::RefPtr monitor; std::string name; std::string identifier; + int32_t width; + int32_t height; std::unique_ptr xdg_output = { nullptr, &zxdg_output_v1_destroy}; diff --git a/include/client.hpp b/include/client.hpp index 09f9090f..7971c132 100644 --- a/include/client.hpp +++ b/include/client.hpp @@ -45,6 +45,7 @@ class Client { static void handleGlobal(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version); static void handleGlobalRemove(void* data, struct wl_registry* registry, uint32_t name); + static void handleOutputLogicalSize(void*, struct zxdg_output_v1*, int32_t, int32_t); static void handleOutputDone(void*, struct zxdg_output_v1*); void createBarsBatch(); static void handleOutputName(void*, struct zxdg_output_v1*, const char*); diff --git a/include/config.hpp b/include/config.hpp index 1ef9ef5f..3b3b7243 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -29,7 +29,8 @@ class Config { Json::Value& getConfig() { return config_; } - std::vector getOutputConfigs(const std::string& name, const std::string& identifier); + std::vector getOutputConfigs(const std::string& name, const std::string& identifier, + int32_t width, int32_t height); private: void setupConfig(Json::Value& dst, const std::string& config_file, int depth); diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 4209c053..e52dadee 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -50,6 +50,10 @@ The visual display elements for waybar use a CSS stylesheet. Waybar supports *st Output specification follows sway's and can either be the output port such as "HDMI-A-1" or a string consisting of the make, model, and serial such as "Some Company ABC123 0x00000000". See *sway-output(5)* for details. In an array, star '*\**' can be used at the end to accept all outputs, in case all previous entries are exclusions. +*output-dimensions* ++ + typeof: string|array ++ + If *output* is not specified, allows selecting outputs by dimensions. Format "(width/height) () (value)", e.g. "width > 1080". Multiple conditions can be chained. + *position* ++ typeof: string ++ default: top ++ @@ -417,6 +421,24 @@ To hide the power group if it is empty, apply to #power and all its children: } ``` +## Select outputs by dimensions + +``` +{ + "layer": "top", + "output-dimensions": "width > 1080", + ... +} +``` + +``` +{ + "layer": "top", + "output-dimensions": ["width < 3840", "height < 2160"], + ... +} +``` + # SUPPORTED MODULES - *waybar-backlight(5)* diff --git a/src/client.cpp b/src/client.cpp index 71aa06ef..57e0fe5c 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -58,7 +58,7 @@ void waybar::Client::handleGlobalRemove(void* data, struct wl_registry* /*regist void waybar::Client::handleOutput(struct waybar_output& output) { static const struct zxdg_output_v1_listener xdgOutputListener = { .logical_position = [](void*, struct zxdg_output_v1*, int32_t, int32_t) {}, - .logical_size = [](void*, struct zxdg_output_v1*, int32_t, int32_t) {}, + .logical_size = &handleOutputLogicalSize, .done = &handleOutputDone, .name = &handleOutputName, .description = &handleOutputDescription, @@ -79,7 +79,19 @@ struct waybar::waybar_output& waybar::Client::getOutput(void* addr) { } std::vector waybar::Client::getOutputConfigs(struct waybar_output& output) { - return config.getOutputConfigs(output.name, output.identifier); + return config.getOutputConfigs(output.name, output.identifier, output.width, output.height); +} + +void waybar::Client::handleOutputLogicalSize(void* data, struct zxdg_output_v1* object, + int32_t width, int32_t height) { + auto client = waybar::Client::inst(); + try { + auto& output = client->getOutput(data); + output.width = width; + output.height = height; + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + } } void waybar::Client::handleOutputDone(void* data, struct zxdg_output_v1* /*xdg_output*/) { diff --git a/src/config.cpp b/src/config.cpp index 7b3feaea..51bec392 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -174,8 +174,8 @@ void Config::mergeConfig(Json::Value& a_config_, Json::Value& b_config_) { spdlog::error("Cannot merge config, conflicting or invalid JSON types"); } } -bool isValidOutput(const Json::Value& config, const std::string& name, - const std::string& identifier) { +bool isValidOutput(const Json::Value& config, const std::string& name, const std::string& identifier, + int32_t width, int32_t height) { const auto isOutputMatches = [&](const std::string& output) -> bool { if (output.substr(0, 1) == "$") { auto* const environment_value = std::getenv(output.substr(1).c_str()); @@ -227,6 +227,38 @@ bool isValidOutput(const Json::Value& config, const std::string& name, } } + // if "output-dimensions" is a string, make it an array of size 1 + Json::Value config_output_dimensions = config["output-dimensions"]; + if (config_output_dimensions.isString()) { + Json::Value jsonArray(Json::arrayValue); + jsonArray.append(config_output_dimensions); + config_output_dimensions = jsonArray; + } + if (config_output_dimensions.isArray()) { + for (auto const &config_output_dimension : config_output_dimensions) { + if (!config_output_dimension.isString()) { + continue; + } + std::string str = config_output_dimension.asString(); + int i = str.find(" "); + std::string dimension = str.substr(0, i); + str = str.substr(i + 1); + i = str.find(" "); + std::string comparator = str.substr(0, i); + int value = std::stoi(str.substr(i)); + + if (dimension == "height" && comparator == "<" && height >= value) { + return false; + } else if (dimension == "height" && comparator == ">" && height <= value) { + return false; + }else if (dimension == "width" && comparator == "<" && width >= value) { + return false; + }else if (dimension == "width" && comparator == ">" && width <= value) { + return false; + } + return true; + } + } return true; } @@ -242,15 +274,16 @@ void Config::load(const std::string& config) { } std::vector Config::getOutputConfigs(const std::string& name, - const std::string& identifier) { + const std::string& identifier, int32_t width, + int32_t height) { std::vector configs; if (config_.isArray()) { for (auto const& config : config_) { - if (config.isObject() && isValidOutput(config, name, identifier)) { + if (config.isObject() && isValidOutput(config, name, identifier, width, height)) { configs.push_back(config); } } - } else if (isValidOutput(config_, name, identifier)) { + } else if (isValidOutput(config_, name, identifier, width, height)) { configs.push_back(config_); } return configs; diff --git a/test/config.cpp b/test/config.cpp index 28e7c2de..27e84b9b 100644 --- a/test/config.cpp +++ b/test/config.cpp @@ -16,11 +16,11 @@ TEST_CASE("Load simple config", "[config]") { REQUIRE(data["height"].asInt() == 30); } SECTION("select configs for configured output") { - auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0", 0, 0); REQUIRE(configs.size() == 1); } SECTION("select configs for missing output") { - auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1", 0, 0); REQUIRE(configs.empty()); } } @@ -30,7 +30,7 @@ TEST_CASE("Load config with multiple bars", "[config]") { conf.load("test/config/multi.json"); SECTION("select multiple configs #1") { - auto data = conf.getOutputConfigs("DP-0", "Fake DisplayPort output #0"); + auto data = conf.getOutputConfigs("DP-0", "Fake DisplayPort output #0", 0, 0); REQUIRE(data.size() == 4); REQUIRE(data[0]["layer"].asString() == "bottom"); REQUIRE(data[0]["height"].asInt() == 20); @@ -43,7 +43,7 @@ TEST_CASE("Load config with multiple bars", "[config]") { REQUIRE(data[3]["height"].asInt() == 24); } SECTION("select multiple configs #2") { - auto data = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + auto data = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0", 0, 0); REQUIRE(data.size() == 2); REQUIRE(data[0]["layer"].asString() == "bottom"); REQUIRE(data[0]["height"].asInt() == 20); @@ -52,7 +52,7 @@ TEST_CASE("Load config with multiple bars", "[config]") { REQUIRE(data[1]["height"].asInt() == 23); } SECTION("select single config by output description") { - auto data = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + auto data = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1", 0, 0); REQUIRE(data.size() == 1); REQUIRE(data[0]["layer"].asString() == "overlay"); REQUIRE(data[0]["position"].asString() == "left"); @@ -75,11 +75,11 @@ TEST_CASE("Load simple config with include", "[config]") { REQUIRE((data.isMember("nullOption") && data["nullOption"].isNull())); } SECTION("select configs for configured output") { - auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0"); + auto configs = conf.getOutputConfigs("HDMI-0", "Fake HDMI output #0", 0, 0); REQUIRE(configs.size() == 1); } SECTION("select configs for missing output") { - auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1"); + auto configs = conf.getOutputConfigs("HDMI-1", "Fake HDMI output #1", 0, 0); REQUIRE(configs.empty()); } } @@ -116,25 +116,25 @@ TEST_CASE("Load multiple bar config with include", "[config]") { conf.load("test/config/include-multi.json"); SECTION("bar config with sole include") { - auto data = conf.getOutputConfigs("OUT-0", "Fake output #0"); + auto data = conf.getOutputConfigs("OUT-0", "Fake output #0", 0, 0); REQUIRE(data.size() == 1); REQUIRE(data[0]["height"].asInt() == 20); } SECTION("bar config with output and include") { - auto data = conf.getOutputConfigs("OUT-1", "Fake output #1"); + auto data = conf.getOutputConfigs("OUT-1", "Fake output #1", 0, 0); REQUIRE(data.size() == 1); REQUIRE(data[0]["height"].asInt() == 21); } SECTION("bar config with output override") { - auto data = conf.getOutputConfigs("OUT-2", "Fake output #2"); + auto data = conf.getOutputConfigs("OUT-2", "Fake output #2", 0, 0); REQUIRE(data.size() == 1); REQUIRE(data[0]["height"].asInt() == 22); } SECTION("multiple levels of include") { - auto data = conf.getOutputConfigs("OUT-3", "Fake output #3"); + auto data = conf.getOutputConfigs("OUT-3", "Fake output #3", 0, 0); REQUIRE(data.size() == 1); REQUIRE(data[0]["height"].asInt() == 23); } From eddba950f83ed13b01b4a1a067e0618a4b9c6bd9 Mon Sep 17 00:00:00 2001 From: Ryan Gibb Date: Wed, 29 Dec 2021 11:33:53 +0000 Subject: [PATCH 275/416] better logic to find valid output dimensions --- src/config.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 51bec392..082e9c63 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -247,16 +247,19 @@ bool isValidOutput(const Json::Value& config, const std::string& name, const std std::string comparator = str.substr(0, i); int value = std::stoi(str.substr(i)); - if (dimension == "height" && comparator == "<" && height >= value) { - return false; - } else if (dimension == "height" && comparator == ">" && height <= value) { - return false; - }else if (dimension == "width" && comparator == "<" && width >= value) { - return false; - }else if (dimension == "width" && comparator == ">" && width <= value) { + int comparison_value; + if (dimension == "height") { + comparison_value = height; + } else if (dimension == "weight") { + comparison_value = weight; + } else { + continue; + } + + if ((comparator == "<" && comparison_value >= value) || + (comparator == ">" && comparison_value <= value)) { return false; } - return true; } } return true; From dbc0e1a00a71e0bf34cfa1c01857941151900f03 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 01:07:38 +0200 Subject: [PATCH 276/416] fix(config): correct weight->width typo in output-dimensions filter --- src/config.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 082e9c63..ad136339 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -174,8 +174,8 @@ void Config::mergeConfig(Json::Value& a_config_, Json::Value& b_config_) { spdlog::error("Cannot merge config, conflicting or invalid JSON types"); } } -bool isValidOutput(const Json::Value& config, const std::string& name, const std::string& identifier, - int32_t width, int32_t height) { +bool isValidOutput(const Json::Value& config, const std::string& name, + const std::string& identifier, int32_t width, int32_t height) { const auto isOutputMatches = [&](const std::string& output) -> bool { if (output.substr(0, 1) == "$") { auto* const environment_value = std::getenv(output.substr(1).c_str()); @@ -235,7 +235,7 @@ bool isValidOutput(const Json::Value& config, const std::string& name, const std config_output_dimensions = jsonArray; } if (config_output_dimensions.isArray()) { - for (auto const &config_output_dimension : config_output_dimensions) { + for (auto const& config_output_dimension : config_output_dimensions) { if (!config_output_dimension.isString()) { continue; } @@ -250,13 +250,13 @@ bool isValidOutput(const Json::Value& config, const std::string& name, const std int comparison_value; if (dimension == "height") { comparison_value = height; - } else if (dimension == "weight") { - comparison_value = weight; + } else if (dimension == "width") { + comparison_value = width; } else { continue; } - if ((comparator == "<" && comparison_value >= value) || + if ((comparator == "<" && comparison_value >= value) || (comparator == ">" && comparison_value <= value)) { return false; } From 87a337ecdfb743767bb1ccec9d2dce127aa73c66 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 9 Jun 2026 07:10:32 +0000 Subject: [PATCH 277/416] Update tray.hpp --- include/modules/sni/tray.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 2996078a..f8b004ce 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -28,6 +28,7 @@ class Tray : public AModule { SNI::Watcher::singleton watcher_; std::vector ignore_list_; SNI::Host host_; + std::vector items_; }; } // namespace waybar::modules::SNI From 1facd4ef95f50d15bc45dcd41fe8d410178a0d1a Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Tue, 9 Jun 2026 07:13:04 +0000 Subject: [PATCH 278/416] Update tray.cpp --- src/modules/sni/tray.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 66c7da75..e4340f77 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -68,6 +68,7 @@ void Tray::onAdd(std::unique_ptr& item) { } else { box_.pack_start(item->event_box); } + items_.push_back(item.get()); spdlog::debug("Tray::onAdd deferred check - checking ignore list"); host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); @@ -79,6 +80,7 @@ void Tray::onAdd(std::unique_ptr& item) { void Tray::onRemove(std::unique_ptr& item) { box_.remove(item->event_box); + items_.erase(std::remove(items_.begin(), items_.end(), item.get()), items_.end()); dp.emit(); } @@ -89,9 +91,11 @@ auto Tray::update() -> void { host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); } - std::vector children = box_.get_children(); - event_box_.set_visible(std::any_of(children.begin(), children.end(), - [](Gtk::Widget* child) { return child->get_visible(); })); + // Show tray only when items are visible. Iterate the managed items_ list + // instead of box_.get_children() to avoid a use-after-free on raw widget + // pointers that may dangle after items are destroyed asynchronously. + event_box_.set_visible(std::any_of(items_.begin(), items_.end(), + [](Item* item) { return item->event_box.get_visible(); })); AModule::update(); } From 41926e873fe46c0c049c12ed9dac1274efddaa26 Mon Sep 17 00:00:00 2001 From: Esensats Date: Thu, 9 Oct 2025 06:43:39 +0500 Subject: [PATCH 279/416] feat: add custom workspace sorting functionality --- include/modules/sway/workspaces.hpp | 3 ++ man/waybar-sway-workspaces.5.scd | 5 +++ src/modules/sway/workspaces.cpp | 47 +++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/include/modules/sway/workspaces.hpp b/include/modules/sway/workspaces.hpp index cea8ade5..8b902f03 100644 --- a/include/modules/sway/workspaces.hpp +++ b/include/modules/sway/workspaces.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -44,6 +45,7 @@ class Workspaces : public AModule, public sigc::trackable { std::string getCycleWorkspace(std::vector::iterator, bool prev) const; uint16_t getWorkspaceIndex(const std::string& name) const; static std::string trimWorkspaceName(const std::string&); + std::optional getCustomSortIndex(const std::string& name) const; bool handleScroll(GdkEventScroll* /*unused*/) override; const Bar& bar_; @@ -56,6 +58,7 @@ class Workspaces : public AModule, public sigc::trackable { util::RegexCollection m_windowRewriteRules; util::JsonParser parser_; std::unordered_map buttons_; + std::unordered_map custom_sort_priorities_; std::mutex mutex_; Ipc ipc_; }; diff --git a/man/waybar-sway-workspaces.5.scd b/man/waybar-sway-workspaces.5.scd index d7050ddc..7a1bd4d5 100644 --- a/man/waybar-sway-workspaces.5.scd +++ b/man/waybar-sway-workspaces.5.scd @@ -96,6 +96,11 @@ Addressed by *sway/workspaces* typeof: bool ++ Whether to sort workspaces alphabetically. Please note this can make "swaymsg workspace prev/next" move to workspaces inconsistent with the ordering shown in Waybar. +*custom-sort*: ++ + typeof: array ++ + default: [] ++ + Defines an explicit ordering for workspace names. Entries earlier in the array appear first. Both full names and the trimmed portion after ":" are matched. Any workspace not listed here falls back to the default ordering (or alphabetical ordering when *alphabetical_sort* is true). + warp-on-scroll: ++ typeof: bool ++ default: true ++ diff --git a/src/modules/sway/workspaces.cpp b/src/modules/sway/workspaces.cpp index 957187a0..86902ef5 100644 --- a/src/modules/sway/workspaces.cpp +++ b/src/modules/sway/workspaces.cpp @@ -50,6 +50,21 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& high_priority_named_.push_back(it.asString()); } } + if (config_["custom-sort"].isArray()) { + uint16_t priority = 0; + for (const auto& entry : config_["custom-sort"]) { + if (!entry.isString()) { + continue; + } + auto const name = entry.asString(); + custom_sort_priorities_.insert_or_assign(name, priority); + auto const trimmed = trimWorkspaceName(name); + if (trimmed != name) { + custom_sort_priorities_.insert_or_assign(trimmed, priority); + } + ++priority; + } + } box_.set_name("workspaces"); if (!id.empty()) { box_.get_style_context()->add_class(id); @@ -236,6 +251,20 @@ void Workspaces::onCmd(const struct Ipc::ipc_response& res) { int l = lhs["sort"].asInt(); int r = rhs["sort"].asInt(); + if (!custom_sort_priorities_.empty()) { + auto const lcustom = getCustomSortIndex(lname); + auto const rcustom = getCustomSortIndex(rname); + if (lcustom && rcustom) { + if (*lcustom != *rcustom) { + return *lcustom < *rcustom; + } + } else if (lcustom) { + return true; + } else if (rcustom) { + return false; + } + } + if (l == r || config_["alphabetical_sort"].asBool()) { // In case both integers are the same, lexicographical // sort. The code above already ensure that this will only @@ -572,6 +601,24 @@ std::string Workspaces::trimWorkspaceName(const std::string& name) { return name; } +std::optional Workspaces::getCustomSortIndex(const std::string& name) const { + if (custom_sort_priorities_.empty()) { + return std::nullopt; + } + auto it = custom_sort_priorities_.find(name); + if (it != custom_sort_priorities_.end()) { + return it->second; + } + auto trimmed = trimWorkspaceName(name); + if (trimmed != name) { + auto trimmed_it = custom_sort_priorities_.find(trimmed); + if (trimmed_it != custom_sort_priorities_.end()) { + return trimmed_it->second; + } + } + return std::nullopt; +} + bool is_focused_recursive(const Json::Value& node) { // If a workspace has a focused container then get_tree will say // that the workspace itself isn't focused. Therefore we need to From c07f8ed0a48a1847d17875c5f0780ef8382e9a82 Mon Sep 17 00:00:00 2001 From: yangyingchao Date: Tue, 31 Mar 2026 17:36:11 +0800 Subject: [PATCH 280/416] (sni) Be able to control order for system tray items. --- include/modules/sni/host.hpp | 14 +++++---- include/modules/sni/item.hpp | 10 ++++++- include/modules/sni/tray.hpp | 2 +- man/waybar-tray.5.scd | 9 ++++++ src/modules/sni/host.cpp | 58 +++++++++++++++++++++++++----------- src/modules/sni/item.cpp | 23 ++++++++++++-- src/modules/sni/watcher.cpp | 19 ++++++------ 7 files changed, 99 insertions(+), 36 deletions(-) diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index 7223b17a..49771fac 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -14,7 +14,7 @@ namespace waybar::modules::SNI { class Host { public: - Host(const std::size_t id, const Json::Value&, const Bar&, const std::vector&, + Host(std::size_t id, const Json::Value&, const Bar&, const std::vector&, const std::function&)>&, const std::function&)>&, const std::function&); ~Host(); @@ -22,11 +22,13 @@ class Host { void checkIgnoreList(const std::vector& ignore_list, const std::function&)>& on_remove); + void reorderItems(); + private: - void busAcquired(const Glib::RefPtr&, Glib::ustring); - void nameAppeared(const Glib::RefPtr&, Glib::ustring, + void busAcquired(const Glib::RefPtr&, const Glib::ustring&); + void nameAppeared(const Glib::RefPtr&, const Glib::ustring&, const Glib::ustring&); - void nameVanished(const Glib::RefPtr&, Glib::ustring); + void nameVanished(const Glib::RefPtr&, const Glib::ustring&); static void proxyReady(GObject*, GAsyncResult*, gpointer); static void registerHost(GObject*, GAsyncResult*, gpointer); static void itemRegistered(SnWatcher*, const gchar*, gpointer); @@ -36,7 +38,7 @@ class Host { void removeItem(std::vector>::iterator); void clearItems(); - std::tuple getBusNameAndObjectPath(const std::string); + static std::tuple getBusNameAndObjectPath(const std::string&); void addRegisteredItem(const std::string& service); std::vector> items_; @@ -53,6 +55,8 @@ class Host { const std::vector ignore_list_; const std::function&)> on_add_; const std::function&)> on_remove_; + + ItemOrderMap orders_; const std::function on_update_; }; diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index d1e3562d..f2e8b3ce 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -24,11 +24,15 @@ struct ToolTip { Glib::ustring text; }; +class Host; + +using ItemOrderMap = std::unordered_map; + class Item : public sigc::trackable { public: Item(const std::string&, const std::string&, const Json::Value&, const Bar&, const std::function&, const std::function&, - const std::function&); + const std::function&, Host&, const ItemOrderMap&); ~Item(); bool isReady() const; @@ -64,6 +68,7 @@ class Item : public sigc::trackable { * while compliant SNI implementation would always reset the flag to desired value. */ bool item_is_menu = true; + int order_ = -1; // -1 means not set private: void onConfigure(GdkEventConfigure* ev); @@ -114,6 +119,9 @@ class Item : public sigc::trackable { Glib::RefPtr proxy_; Glib::RefPtr cancellable_; std::set update_pending_; + + Host& host_; + const ItemOrderMap& orders_; }; } // namespace waybar::modules::SNI diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index 2996078a..848d27f4 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -13,7 +13,7 @@ namespace waybar::modules::SNI { class Tray : public AModule { public: Tray(const std::string&, const Bar&, const Json::Value&); - virtual ~Tray() = default; + ~Tray() override = default; auto update() -> void override; private: diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index 1d787e50..975ba1f2 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -49,6 +49,12 @@ Addressed by *tray* - *string*: Custom icon name or path to icon file ++ - *false*: Hide this application's tray icon completely +*orders*: ++ + typeof: object ++ + Orders for items to be placed in tray. When not set, orders are controlled by DBus. ++ + Key: name of item. ++ + Value: integer, higher value means to place this item to the right. + # EXAMPLES ``` @@ -59,6 +65,9 @@ Addressed by *tray* "blueman": "bluetooth", "TelegramDesktop": "$HOME/.local/share/icons/hicolor/16x16/apps/telegram.png", "spotify": false + }, + "orders": { + "wechat": 99 } } diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index f77d26a2..6e9e95d0 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -2,6 +2,9 @@ #include +#include + +#include "modules/sni/item.hpp" #include "util/scope_guard.hpp" namespace waybar::modules::SNI { @@ -9,7 +12,7 @@ namespace waybar::modules::SNI { static const unsigned RETRY_DELAY_MS = 200; static const unsigned MAX_RETRIES = 10; -Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, +Host::Host(std::size_t id, const Json::Value& config, const Bar& bar, const std::vector& ignore_list, const std::function&)>& on_add, const std::function&)>& on_remove, @@ -24,7 +27,18 @@ Host::Host(const std::size_t id, const Json::Value& config, const Bar& bar, ignore_list_(ignore_list), on_add_(on_add), on_remove_(on_remove), - on_update_(on_update) {} + on_update_(on_update) { + auto orders = config["orders"]; + if (!orders.isNull()) { + for (auto itr = orders.begin(); itr != orders.end(); ++itr) { + auto key = itr.name(); + auto& value = *itr; + assert(value.isInt()); + + orders_[key] = value.asInt(); + } + } +} Host::~Host() { retry_connection_.disconnect(); @@ -77,13 +91,13 @@ void Host::checkIgnoreList(const std::vector& ignore_list, } } -void Host::busAcquired(const Glib::RefPtr& conn, Glib::ustring name) { +void Host::busAcquired(const Glib::RefPtr& conn, const Glib::ustring& name) { watcher_id_ = Gio::DBus::watch_name(conn, "org.kde.StatusNotifierWatcher", sigc::mem_fun(*this, &Host::nameAppeared), sigc::mem_fun(*this, &Host::nameVanished)); } -void Host::nameAppeared(const Glib::RefPtr& conn, const Glib::ustring name, +void Host::nameAppeared(const Glib::RefPtr& conn, const Glib::ustring& name, const Glib::ustring& name_owner) { if (cancellable_ != nullptr) { // TODO @@ -94,7 +108,8 @@ void Host::nameAppeared(const Glib::RefPtr& conn, const G "/StatusNotifierWatcher", cancellable_, &Host::proxyReady, this); } -void Host::nameVanished(const Glib::RefPtr& conn, const Glib::ustring name) { +void Host::nameVanished(const Glib::RefPtr& conn, + const Glib::ustring& name) { retry_connection_.disconnect(); retry_count_ = 0; g_cancellable_cancel(cancellable_); @@ -111,11 +126,11 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { } }); SnWatcher* watcher = sn_watcher_proxy_new_finish(res, &error); - if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED) != 0) { spdlog::error("Host: {}", error->message); return; } - auto host = static_cast(data); + auto* host = static_cast(data); if (error != nullptr) { spdlog::error("Host: {}", error->message); g_clear_object(&host->cancellable_); @@ -153,18 +168,18 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { } }); sn_watcher_call_register_host_finish(SN_WATCHER(src), res, &error); - if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED) != 0) { spdlog::error("Host: {}", error->message); return; } - auto host = static_cast(data); + auto* host = static_cast(data); if (error != nullptr) { spdlog::error("Host: {}", error->message); return; } g_signal_connect(host->watcher_, "item-registered", G_CALLBACK(&Host::itemRegistered), data); g_signal_connect(host->watcher_, "item-unregistered", G_CALLBACK(&Host::itemUnregistered), data); - auto items = sn_watcher_dup_registered_items(host->watcher_); + auto* items = sn_watcher_dup_registered_items(host->watcher_); if (items != nullptr) { spdlog::info("Host: Found {} pre-registered SNI items", g_strv_length(items)); for (uint32_t i = 0; items[i] != nullptr; i += 1) { @@ -176,7 +191,7 @@ void Host::registerHost(GObject* src, GAsyncResult* res, gpointer data) { } void Host::itemRegistered(SnWatcher* watcher, const gchar* service, gpointer data) { - auto host = static_cast(data); + auto* host = static_cast(data); spdlog::info("Host::itemRegistered called with service: {}", service); host->addRegisteredItem(service); // host->checkIgnoreList(host->ignore_list_, std::bind(&Host::itemUnregistered, host, @@ -184,8 +199,8 @@ void Host::itemRegistered(SnWatcher* watcher, const gchar* service, gpointer dat } void Host::itemUnregistered(SnWatcher* watcher, const gchar* service, gpointer data) { - auto host = static_cast(data); - auto [bus_name, object_path] = host->getBusNameAndObjectPath(service); + auto* host = static_cast(data); + auto [bus_name, object_path] = waybar::modules::SNI::Host::getBusNameAndObjectPath(service); for (auto it = host->items_.begin(); it != host->items_.end(); ++it) { if ((*it)->bus_name == bus_name && (*it)->object_path == object_path) { host->removeItem(it); @@ -232,7 +247,7 @@ void Host::clearItems() { } } -std::tuple Host::getBusNameAndObjectPath(const std::string service) { +std::tuple Host::getBusNameAndObjectPath(const std::string& service) { auto it = service.find('/'); if (it != std::string::npos) { return {service.substr(0, it), service.substr(it)}; @@ -248,19 +263,28 @@ void Host::addRegisteredItem(const std::string& service) { return; } } - std::string bus_name, object_path; + std::string bus_name; + std::string object_path; std::tie(bus_name, object_path) = getBusNameAndObjectPath(service); spdlog::debug("SNI item registered: bus_name={}, object_path={}, full_service={}", bus_name, object_path, service); - auto it = std::find_if(items_.begin(), items_.end(), [&bus_name, &object_path](const auto& item) { + auto it = std::ranges::find_if(items_, [&bus_name, &object_path](const auto& item) { return bus_name == item->bus_name && object_path == item->object_path; }); if (it == items_.end()) { spdlog::debug("Adding SNI item: {}", bus_name); items_.emplace_back(std::make_unique( bus_name, object_path, config_, bar_, [this](Item& item) { itemReady(item); }, - [this](Item& item) { itemInvalidated(item); }, on_update_)); + [this](Item& item) { itemInvalidated(item); }, on_update_, *this, orders_)); } } +void Host::reorderItems() { + std::ranges::for_each(items_, on_remove_); + std::ranges::sort(items_, [](std::unique_ptr& item1, std::unique_ptr& item2) { + return item1->order_ < item2->order_; + }); + std::ranges::for_each(items_, on_add_); +} + } // namespace waybar::modules::SNI diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index b97a596b..1c28d7af 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -6,13 +6,16 @@ #include #include +#include #include #include #include +#include #include "gdk/gdk.h" +#include "modules/sni/host.hpp" #include "modules/sni/icon_manager.hpp" -#include "util/format.hpp" +#include "util/format.hpp" // IWYU pragma: keep #include "util/gtk_icon.hpp" template <> @@ -40,7 +43,8 @@ static const unsigned UPDATE_DEBOUNCE_TIME = 10; Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar, const std::function& on_ready, - const std::function& on_invalidate, const std::function& on_updated) + const std::function& on_invalidate, const std::function& on_updated, + Host& host, const ItemOrderMap& orders) : bus_name(bn), object_path(op), icon_size(16), @@ -49,7 +53,9 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf bar_(bar), on_ready_(on_ready), on_invalidate_(on_invalidate), - on_updated_(on_updated) { + on_updated_(on_updated), + host_(host), + orders_(orders) { if (config["icon-size"].isUInt()) { icon_size = config["icon-size"].asUInt(); } @@ -279,6 +285,17 @@ void Item::invalidate() { void Item::setCustomIcon(const std::string& id) { spdlog::debug("SNI tray id: {}", id); + if (order_ == -1) { + auto iter = orders_.find(id); + if (iter != orders_.end()) { + order_ = iter->second; + spdlog::debug("reordering tray item {}, order: {}", id, order_); + } else { + order_ = 0; + } + host_.reorderItems(); + } + std::string custom_icon = IconManager::instance().getIconForApp(id); if (!custom_icon.empty()) { if (std::filesystem::exists(custom_icon)) { diff --git a/src/modules/sni/watcher.cpp b/src/modules/sni/watcher.cpp index 969806cf..be52a401 100644 --- a/src/modules/sni/watcher.cpp +++ b/src/modules/sni/watcher.cpp @@ -25,11 +25,12 @@ Watcher::~Watcher() { items_ = nullptr; } Gio::DBus::unown_name(bus_name_id_); - auto iface = G_DBUS_INTERFACE_SKELETON(watcher_); + auto* iface = G_DBUS_INTERFACE_SKELETON(watcher_); g_dbus_interface_skeleton_unexport(iface); } -void Watcher::busAcquired(const Glib::RefPtr& conn, Glib::ustring name) { +void Watcher::busAcquired(const Glib::RefPtr& conn, + Glib::ustring name) { // NOLINT GError* error = nullptr; waybar::util::ScopeGuard error_deleter([&error]() { if (error) { @@ -65,7 +66,7 @@ gboolean Watcher::handleRegisterHost(Watcher* obj, GDBusMethodInvocation* invoca "D-Bus bus name '%s' is not valid", bus_name); return TRUE; } - auto watch = gfWatchFind(obj->hosts_, bus_name, object_path); + auto* watch = gfWatchFind(obj->hosts_, bus_name, object_path); if (watch != nullptr) { g_warning("Status Notifier Host with bus name '%s' and object path '%s' is already registered", bus_name, object_path); @@ -74,7 +75,7 @@ gboolean Watcher::handleRegisterHost(Watcher* obj, GDBusMethodInvocation* invoca } watch = gfWatchNew(GF_WATCH_TYPE_HOST, service, bus_name, object_path, obj); obj->hosts_ = g_slist_prepend(obj->hosts_, watch); - if (!sn_watcher_get_is_host_registered(obj->watcher_)) { + if (sn_watcher_get_is_host_registered(obj->watcher_) == 0) { sn_watcher_set_is_host_registered(obj->watcher_, TRUE); sn_watcher_emit_host_registered(obj->watcher_); } @@ -96,7 +97,7 @@ gboolean Watcher::handleRegisterItem(Watcher* obj, GDBusMethodInvocation* invoca "D-Bus bus name '%s' is not valid", bus_name); return TRUE; } - auto watch = gfWatchFind(obj->items_, bus_name, object_path); + auto* watch = gfWatchFind(obj->items_, bus_name, object_path); if (watch != nullptr) { spdlog::debug("Ignoring duplicate Status Notifier Item registration for '{}' at '{}'", bus_name, object_path); @@ -116,7 +117,7 @@ gboolean Watcher::handleRegisterItem(Watcher* obj, GDBusMethodInvocation* invoca Watcher::GfWatch* Watcher::gfWatchFind(GSList* list, const gchar* bus_name, const gchar* object_path) { for (GSList* l = list; l != nullptr; l = g_slist_next(l)) { - auto watch = static_cast(l->data); + auto* watch = static_cast(l->data); if (g_strcmp0(watch->bus_name, bus_name) == 0 && g_strcmp0(watch->object_path, object_path) == 0) { return watch; @@ -126,7 +127,7 @@ Watcher::GfWatch* Watcher::gfWatchFind(GSList* list, const gchar* bus_name, } void Watcher::gfWatchFree(gpointer data) { - auto watch = static_cast(data); + auto* watch = static_cast(data); if (watch->watch_id > 0) { g_bus_unwatch_name(watch->watch_id); @@ -153,7 +154,7 @@ Watcher::GfWatch* Watcher::gfWatchNew(GfWatchType type, const gchar* service, co } void Watcher::nameVanished(GDBusConnection* connection, const char* name, gpointer data) { - auto watch = static_cast(data); + auto* watch = static_cast(data); if (watch->type == GF_WATCH_TYPE_HOST) { watch->watcher->hosts_ = g_slist_remove(watch->watcher->hosts_, watch); if (watch->watcher->hosts_ == nullptr) { @@ -174,7 +175,7 @@ void Watcher::updateRegisteredItems(SnWatcher* obj) { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); for (GSList* l = items_; l != nullptr; l = g_slist_next(l)) { - auto watch = static_cast(l->data); + auto* watch = static_cast(l->data); gchar* item = g_strdup_printf("%s%s", watch->bus_name, watch->object_path); g_variant_builder_add(&builder, "s", item); g_free(item); From 949da4e8292601d027d73fc2988efd84c1741daa Mon Sep 17 00:00:00 2001 From: Lena Date: Fri, 10 Oct 2025 20:19:27 +0200 Subject: [PATCH 281/416] create partial wwan module --- include/modules/wwan.hpp | 51 +++++ man/waybar-wwan.5.scd | 119 +++++++++++ man/waybar-wwan.scd | 111 +++++++++++ meson.build | 10 +- meson_options.txt | 1 + src/factory.cpp | 8 + src/modules/wwan.cpp | 414 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 include/modules/wwan.hpp create mode 100644 man/waybar-wwan.5.scd create mode 100644 man/waybar-wwan.scd create mode 100644 src/modules/wwan.cpp diff --git a/include/modules/wwan.hpp b/include/modules/wwan.hpp new file mode 100644 index 00000000..d1635654 --- /dev/null +++ b/include/modules/wwan.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include + +#include + +#include "ALabel.hpp" +#include "util/format.hpp" +#include "util/sleeper_thread.hpp" + +namespace waybar::modules { + + class Wwan : public ALabel { + public: + Wwan(const std::string&, const Json::Value&); + virtual ~Wwan(); + auto update() -> void override; + + private: + + const void updateCurrentModem(); + const MMModemState getModemState() const; + const std::string getModemStateString() const; + const std::string getModemStateFormatString() const; + const std::string getAccessTechnologiesString() const; + const int getSignalQuality() const; + const std::string getPowerStateString() const; + const std::string getImeiString() const; + + const MMModemModeCombination getCurrentModes() const; + const std::string getPreferredModeString() const; + const std::string getCurrentModesString() const; + const std::string getOperatorNameString() const; + + util::SleeperThread thread_; + std::string state_; + GDBusConnection* connection; + MMManager* manager; + MMModem* current_modem; + + bool hideDisconnected = true; + + const std::string dbus_name = "org.freedesktop.ModemManager1"; + const std::string dbus_obj_path = "/org/freedesktop/ModemManager1/"; + const std::string dbus_modems_path = "/org/freedesktop/ModemManager1/Modems/"; + }; + +} // namespace waybar::modules diff --git a/man/waybar-wwan.5.scd b/man/waybar-wwan.5.scd new file mode 100644 index 00000000..3f6e4870 --- /dev/null +++ b/man/waybar-wwan.5.scd @@ -0,0 +1,119 @@ +waybar-wwan(5) "waybar-wwan" "User Manual" + +# NAME + +waybar - wwan module + +# DESCRIPTION + +*wwan* module for ModemManager. + + +# FILES + +$XDG_CONFIG_HOME/waybar/config ++ + Per user configuration file + +# ADDITIONAL FILES + +libmm-glib lives in: + +. /usr/lib/libmm-glib.so or /usr/lib64/libmm-glib.so +. /usr/lib/pkgconfig/libgps.pc or /usr/lib64/pkgconfig/libgps.pc +. /usr/include/libmm-glib + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: {glyph} ++ + The text format. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: Games running: {glyph} ++ + The text format of the tooltip. + +*interval*: ++ + typeof: integer ++ + default: 5 ++ + The interval in which the WWAN information gets polled (e.g. carrier name). + +*hide-failed*: ++ + typeof: bool ++ + default: true ++ + Defines if the module should be hidden if the modem cannot start. + +*hide-disabled*: ++ + typeof: bool ++ + default: false ++ + Defines if the module should be hidden if the modem is disabled. + +*hide-not-registered*: ++ + typeof: bool ++ + default: false ++ + Defines if the module should be hidden if the modem isn't registered with any carrier network. + + +# FORMAT REPLACEMENTS + +*{mode}*: Fix mode + +*{status}*: Technology used for GPS fix. Not all GPS receivers report this. + +*{latitude}*: Latitude, decimal degrees. Can be NaN. + +*{latitude_error}*: Latitude uncertainty, meters. Can be NaN. + +*{longitude}*: Longitude, decimal degrees. Can be NaN. + +*{longitude_error}*: Longitude uncertainty, meters. Can be NaN. + +*{altitude_hae}*: Altitude, height above ellipsoid, meters. Can be NaN. + +*{altitude_msl}*: Longitude, MSL, meters. Can be NaN. + +*{altitude_error}*: Altitude uncertainty, meters. Can be NaN. + +*{speed}*: Speed over ground, meters/sec. Can be NaN. + +*{speed_error}*: Speed uncertainty, meters/sec. Can be NaN. + +*{climb}*: Vertical speed, meters/sec. Can be NaN. + +*{climb_error}*: Vertical speed uncertainty, meters/sec. Can be NaN. + +*{satellites_visible}*: Number of satellites visible from the GPS receiver. + +*{satellites_used}*: Number of satellites used for the GPS fix. + +# EXAMPLES + +``` +"wwan": { + "format": "{state}", + "format-disabled": "", // an empty format will hide the module + "format-locked": "SIM Locked", + "format-connected": "{operator_name} {preferred_mode}" + "tooltip-format": "{mode}", + "tooltip-format-locked": "{satellites_visible} satellites visible", + "tooltip-format-registered": "{satellites_used}/{satellites_visible} satellites used", + "tooltip-format-connected": "", + "hide-disconnected": false +} +``` +# STYLE + +- *#wwan* +- *#wwan.failed* Applied when the modem cannot start. +- *#wwan.locked* Applied when the SIM on the modem is locked. +- *#wwan.disabled* Applied when the modem is disabled. +- *#wwan.enabled* Applied when the modem is enabled, but isn't registered to any carrier network. +- *#wwan.searching* Applied when the modem is looking for a carrier network to register to. +- *#wwan.registered* Applied when the modem is registered to a network, but no data connection is active. +- *#wwan.connected* Applied when the modem is registered to a network, and there is an active data connection. diff --git a/man/waybar-wwan.scd b/man/waybar-wwan.scd new file mode 100644 index 00000000..e7473601 --- /dev/null +++ b/man/waybar-wwan.scd @@ -0,0 +1,111 @@ +waybar-gps(5) "waybar-gps" "User Manual" + +# NAME + +waybar - gps module + +# DESCRIPTION + +*gps* module for gpsd. + + +# FILES + +$XDG_CONFIG_HOME/waybar/config ++ + Per user configuration file + +# ADDITIONAL FILES + +libgps lives in: + +. /usr/lib/libgps.so or /usr/lib64/libgps.so +. /usr/lib/pkgconfig/libgps.pc or /usr/lib64/pkgconfig/libgps.pc +. /usr/include/gps + +# CONFIGURATION + +*format*: ++ + typeof: string ++ + default: {glyph} ++ + The text format. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: Games running: {glyph} ++ + The text format of the tooltip. + +*interval*: ++ + typeof: integer ++ + default: 5 ++ + The interval in which the GPS information gets polled (e.g. current speed). + Significant updates (e.g. the current fix mode) are updated immediately. + +*hide-disconnected*: ++ + typeof: bool ++ + default: true ++ + Defines if the module should be hidden if there is no GPS receiver. + +*hide-no-fix*: ++ + typeof: bool ++ + default: false ++ + Defines if the module should be hidden if there is no GPS fix. + +# FORMAT REPLACEMENTS + +*{mode}*: Fix mode + +*{status}*: Technology used for GPS fix. Not all GPS receivers report this. + +*{latitude}*: Latitude, decimal degrees. Can be NaN. + +*{latitude_error}*: Latitude uncertainty, meters. Can be NaN. + +*{longitude}*: Longitude, decimal degrees. Can be NaN. + +*{longitude_error}*: Longitude uncertainty, meters. Can be NaN. + +*{altitude_hae}*: Altitude, height above ellipsoid, meters. Can be NaN. + +*{altitude_msl}*: Longitude, MSL, meters. Can be NaN. + +*{altitude_error}*: Altitude uncertainty, meters. Can be NaN. + +*{speed}*: Speed over ground, meters/sec. Can be NaN. + +*{speed_error}*: Speed uncertainty, meters/sec. Can be NaN. + +*{climb}*: Vertical speed, meters/sec. Can be NaN. + +*{climb_error}*: Vertical speed uncertainty, meters/sec. Can be NaN. + +*{satellites_visible}*: Number of satellites visible from the GPS receiver. + +*{satellites_used}*: Number of satellites used for the GPS fix. + +# EXAMPLES + +``` +"gps": { + "format": "{mode}", + "format-disabled": "", // an empty format will hide the module + "format-no-fix": "No fix", + "format-fix-3d": "{status}", + "tooltip-format": "{mode}", + "tooltip-format-no-fix": "{satellites_visible} satellites visible", + "tooltip-format-fix-2d": "{satellites_used}/{satellites_visible} satellites used", + "tooltip-format-fix-3d": "Altitude: {altitude_hae}m", + "hide-disconnected": false +} +``` +# STYLE + +- *#gps* +- *#gps.disabled* Applied when GPS is disabled. +- *#gps.fix-none* Applied when GPS is present, but there is no fix. +- *#gps.fix-2d* Applied when there is a 2D fix. +- *#gps.fix-3d* Applied when there is a 3D fix. diff --git a/meson.build b/meson.build index 5563d610..737a4199 100644 --- a/meson.build +++ b/meson.build @@ -94,6 +94,7 @@ xkbregistry = dependency('xkbregistry') libjack = dependency('jack', required: get_option('jack')) libwireplumber = dependency('wireplumber-0.5', required: get_option('wireplumber')) libgps = dependency('libgps', required: get_option('gps')) +libmm_glib = dependency('libmm-glib', required: get_option('wwan')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() @@ -557,6 +558,12 @@ if libgps.found() man_files += files('man/waybar-gps.5.scd') endif +if libmm_glib.found() + add_project_arguments('-DHAVE_LIBMM_GLIB', language: 'cpp') + src_files += files('src/modules/wwan.cpp') + #man_files += files('man/waybar-wwan.5.scd') +endif + subdir('protocol') app_resources = [] @@ -598,7 +605,8 @@ executable( xkbregistry, cava, eproxy, - libgps + libgps, + libmm_glib ], include_directories: inc_dirs, install: true, diff --git a/meson_options.txt b/meson_options.txt index 278fdd02..139ed6cb 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -23,3 +23,4 @@ option('niri', type: 'boolean', description: 'Enable support for niri') option('mango', type: 'boolean', description: 'Enable support for mango') option('login-proxy', type: 'boolean', description: 'Enable interfacing with dbus login interface') option('gps', type: 'feature', value: 'auto', description: 'Enable support for gps') +option('wwan', type: 'feature', value: 'auto', description: 'Enable support for WWAN') diff --git a/src/factory.cpp b/src/factory.cpp index 6969da6b..096729ac 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -123,6 +123,9 @@ #include "modules/gps.hpp" #endif #include "modules/cava/cava_frontend.hpp" +#ifdef HAVE_LIBMM_GLIB +#include "modules/wwan.hpp" +#endif #include "modules/cffi.hpp" #include "modules/custom.hpp" #include "modules/custom_graph.hpp" @@ -380,6 +383,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, if (ref == "gps") { return new waybar::modules::Gps(id, config_[name]); } +#endif +#ifdef HAVE_LIBMM_GLIB + if (ref == "wwan") { + return new waybar::modules::Wwan(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp new file mode 100644 index 00000000..f829df17 --- /dev/null +++ b/src/modules/wwan.cpp @@ -0,0 +1,414 @@ +#include "modules/wwan.hpp" +#include + +#include +#include +#include "ModemManager.h" +#include "libmm-glib.h" + +// In the 80000 version of fmt library authors decided to optimize imports +// and moved declarations required for fmt::dynamic_format_arg_store in new +// header fmt/args.h +#if (FMT_VERSION >= 80000) +#include +#else +#include +#endif + +namespace { + using namespace waybar::util; + constexpr const char *DEFAULT_FORMAT = "{state}"; +} // namespace + + +waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config) +: ALabel(config, "wwan", id, "{}", 5) { + thread_ = [this] { + dp.emit(); + thread_.sleep_for(interval_); + }; + + GError* error = nullptr; + connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, &error); + if (error) { + g_error_free(error); + return; + } + + + manager = mm_manager_new_sync( + connection, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, + nullptr, // cancellable + &error + ); + + if (error) { + spdlog::error("Failed to create ModemManager proxy: " + std::string(error->message)); + g_error_free(error); + g_object_unref(connection); + return; + } + + updateCurrentModem(); + + + if (config_["hide-disconnected"].isBool()) { + hideDisconnected = config_["hide-disconnected"].asBool(); + } + +} + +const void waybar::modules::Wwan::updateCurrentModem(){ + // get 1st modem (or modem with the specified hardware path) + + GList* modems = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(manager)); + if (!modems) { + spdlog::warn("No modems found"); + g_object_unref(manager); + g_object_unref(connection); + return; + } + + for (GList* m = modems; m; m = g_list_next(m)) { + MMObject* modem_obj = MM_OBJECT(m->data); + + MMModem* modem = mm_object_get_modem(modem_obj); + + if (config_["path"].isString()) { + std::string path = config_["path"].asString(); + std::string other = mm_modem_dup_path(modem); + + if(path != other) { + g_object_unref(modem); + continue; + } + } + + if (config_["imei"].isString()) { + std::string imei = config_["imei"].asString(); + std::string other = mm_modem_dup_equipment_identifier(modem); + + if(imei != other) { + g_object_unref(modem); + continue; + } + } + + current_modem = modem; + g_list_free_full(modems, g_object_unref); + return; +} +// no modems found +g_list_free_full(modems, g_object_unref); +} + +const MMModemState waybar::modules::Wwan::getModemState() const { + if(current_modem == nullptr) { + return MMModemState::MM_MODEM_STATE_UNKNOWN; + } + return mm_modem_get_state(current_modem); +} + +const std::string waybar::modules::Wwan::getModemStateString() const { + switch (getModemState()) { + case MMModemState::MM_MODEM_STATE_FAILED: + return "Failed"; + case MMModemState::MM_MODEM_STATE_INITIALIZING: + return "Initializing"; + case MMModemState::MM_MODEM_STATE_LOCKED: + return "Locked"; + case MMModemState::MM_MODEM_STATE_DISABLED: + return "Disabled"; + case MMModemState::MM_MODEM_STATE_DISABLING: + return "Disabling"; + case MMModemState::MM_MODEM_STATE_ENABLED: + return "Enabled"; + case MMModemState::MM_MODEM_STATE_ENABLING: + return "Enabling"; + case MMModemState::MM_MODEM_STATE_SEARCHING: + return "Searching"; + case MMModemState::MM_MODEM_STATE_REGISTERED: + return "Registered"; + case MMModemState::MM_MODEM_STATE_DISCONNECTING: + return "Disconnecting"; + case MMModemState::MM_MODEM_STATE_CONNECTING: + return "Connecting"; + case MMModemState::MM_MODEM_STATE_CONNECTED: + return "Connected"; + default: + return "Unknown"; + } +} + +const std::string waybar::modules::Wwan::getModemStateFormatString() const { + switch (getModemState()) { + case MMModemState::MM_MODEM_STATE_FAILED: + return "failed"; + case MMModemState::MM_MODEM_STATE_LOCKED: + return "locked"; + case MMModemState::MM_MODEM_STATE_DISABLED: + return "disabled"; + case MMModemState::MM_MODEM_STATE_ENABLED: + return "enabled"; + case MMModemState::MM_MODEM_STATE_SEARCHING: + return "searching"; + case MMModemState::MM_MODEM_STATE_REGISTERED: + return "registered"; + case MMModemState::MM_MODEM_STATE_CONNECTED: + return "connected"; + default: + return ""; + } +} + +const int waybar::modules::Wwan::getSignalQuality() const { + if(current_modem == nullptr) { + return MMModemState::MM_MODEM_STATE_UNKNOWN; + } + + gboolean recent; + + return mm_modem_get_signal_quality(current_modem, &recent); +} + +const std::string stringifyAccessTechnologies(MMModemAccessTechnology technologies) { + std::string buffer; + + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_POTS) + buffer += "POTS, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_GSM) + buffer += "GSM, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_GSM_COMPACT) + buffer += "GSM Compact, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_GPRS) + buffer += "GPRS, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_EDGE) + buffer += "EDGE, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_UMTS) + buffer += "UMTS, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_HSDPA) + buffer += "HSDPA, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_HSUPA) + buffer += "HSUPA, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_HSPA) + buffer += "HSPA, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_HSPA_PLUS) + buffer += "HSPA+, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_1XRTT) + buffer += "1xRTT, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_EVDO0) + buffer += "EVDO0, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_EVDOA) + buffer += "EVDOA, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_EVDOB) + buffer += "EVDOB, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_LTE) + buffer += "LTE, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_5GNR) + buffer += "5GNR, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_LTE_CAT_M) + buffer += "LTE CAT-M, "; + if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_LTE_NB_IOT) + buffer += "LTE NB-IoT, "; + + if (buffer.length() > 2) { + buffer.erase(buffer.length()-2); + } + return buffer; + +} + +const std::string stringifyModemMode(MMModemMode mmode){ + + switch (mmode) { + case MM_MODEM_MODE_CS: + return "CS"; + case MM_MODEM_MODE_2G: + return "2G"; + case MM_MODEM_MODE_3G: + return "3G"; + case MM_MODEM_MODE_4G: + return "4G"; + case MM_MODEM_MODE_5G: + return "5G"; + default: + return "No Service"; + } +} + +const std::string waybar::modules::Wwan::getAccessTechnologiesString() const { + + if(current_modem == nullptr) { + return ""; + } + + MMModemAccessTechnology technologies = mm_modem_get_access_technologies(current_modem); + + return stringifyAccessTechnologies(technologies); +} + +const MMModemModeCombination waybar::modules::Wwan::getCurrentModes() const { + + MMModemModeCombination combination; + + mm_modem_get_current_modes(current_modem, &combination.allowed, &combination.preferred); + + return combination; +} + +const std::string waybar::modules::Wwan::getPreferredModeString() const { + + if(current_modem == nullptr) { + return ""; + } + + MMModemModeCombination combination = getCurrentModes(); + + return stringifyModemMode(combination.preferred); +} + + +const std::string waybar::modules::Wwan::getCurrentModesString() const { + + if(current_modem == nullptr) { + return ""; + } + + MMModemModeCombination combination = getCurrentModes(); + + // TODO: fix + return stringifyModemMode(combination.allowed); +} + +const std::string waybar::modules::Wwan::getPowerStateString() const { + + if(current_modem == nullptr) { + return ""; + } + + MMModemPowerState state = mm_modem_get_power_state(current_modem); + + switch (state) { + case MM_MODEM_POWER_STATE_ON: + return "On"; + case MM_MODEM_POWER_STATE_OFF: + return "Off"; + case MM_MODEM_POWER_STATE_LOW: + return "Low"; + default: + return "Unknown"; + } +} + +const std::string waybar::modules::Wwan::getImeiString() const { + + if(current_modem == nullptr) { + return ""; + } + + return mm_modem_dup_equipment_identifier(current_modem); +} + +const std::string waybar::modules::Wwan::getOperatorNameString() const { + + if(current_modem == nullptr) { + return ""; + } + + MMSim* sim = mm_modem_get_sim_sync(current_modem, nullptr, nullptr); + + if (sim == nullptr) { + return "No SIM"; + } + + std::string name = mm_sim_dup_operator_name(sim); + g_object_unref(sim); + return name; +} + +auto waybar::modules::Wwan::update() -> void { + + if (current_modem == nullptr) { + event_box_.set_visible(false); + updateCurrentModem(); + return; + } + + if (hideDisconnected) { + event_box_.set_visible(false); + + return; + } + + // Show the module + if (!event_box_.get_visible()) event_box_.set_visible(true); + + std::string tooltip_format; + + if (!alt_) { + std::string state = getModemStateFormatString(); + if (!state_.empty() && label_.get_style_context()->has_class(state_)) { + label_.get_style_context()->remove_class(state_); + } + if (config_["format-" + state].isString()) { + default_format_ = config_["format-" + state].asString(); + } else if (config_["format"].isString()) { + default_format_ = config_["format"].asString(); + } else { + default_format_ = DEFAULT_FORMAT; + } + if (config_["tooltip-format-" + state].isString()) { + tooltip_format = config_["tooltip-format-" + state].asString(); + } + if (!label_.get_style_context()->has_class(state)) { + label_.get_style_context()->add_class(state); + } + format_ = default_format_; + state_ = state; + } + + + auto format = format_; + + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("state", getModemStateString())); + + store.push_back(fmt::arg("access_technologies", getAccessTechnologiesString())); + + store.push_back(fmt::arg("current_modes", getCurrentModesString())); + store.push_back(fmt::arg("preferred_mode", getPreferredModeString())); + //store.push_back(fmt::arg("supported_modes", getSupportedModesString())); + + //store.push_back(fmt::arg("current_bands", getCurrentBandsString())); + //store.push_back(fmt::arg("supported_bands", getSupportedBandsString())); + + store.push_back(fmt::arg("signal_quality", getSignalQuality())); + store.push_back(fmt::arg("power_state", getPowerStateString())); + store.push_back(fmt::arg("imei", getImeiString())); + + store.push_back(fmt::arg("operator_name", getOperatorNameString())); + //store.push_back(fmt::arg("registration_state", getRegistrationStateString())); + + auto text = fmt::vformat(format, store); + + if (tooltipEnabled()) { + if (tooltip_format.empty() && config_["tooltip-format"].isString()) { + tooltip_format = config_["tooltip-format"].asString(); + } + if (!tooltip_format.empty()) { + auto tooltip_text = fmt::vformat(tooltip_format, store); + if (label_.get_tooltip_text() != tooltip_text) { + label_.set_tooltip_markup(tooltip_text); + } + } else if (label_.get_tooltip_text() != text) { + label_.set_tooltip_markup(text); + } + } + label_.set_markup(text); + // Call parent update + ALabel::update(); +} + +waybar::modules::Wwan::~Wwan() { +} From 90a0f46e37f7a659f02775c8db1b4f7e820a998d Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 11 Oct 2025 15:27:56 +0200 Subject: [PATCH 282/416] almost done --- include/modules/wwan.hpp | 12 ---- man/waybar-wwan.5.scd | 34 +++++---- man/waybar-wwan.scd | 111 ----------------------------- src/modules/wwan.cpp | 148 +++++++++++++-------------------------- 4 files changed, 69 insertions(+), 236 deletions(-) delete mode 100644 man/waybar-wwan.scd diff --git a/include/modules/wwan.hpp b/include/modules/wwan.hpp index d1635654..0cbb5d8d 100644 --- a/include/modules/wwan.hpp +++ b/include/modules/wwan.hpp @@ -22,18 +22,6 @@ namespace waybar::modules { private: const void updateCurrentModem(); - const MMModemState getModemState() const; - const std::string getModemStateString() const; - const std::string getModemStateFormatString() const; - const std::string getAccessTechnologiesString() const; - const int getSignalQuality() const; - const std::string getPowerStateString() const; - const std::string getImeiString() const; - - const MMModemModeCombination getCurrentModes() const; - const std::string getPreferredModeString() const; - const std::string getCurrentModesString() const; - const std::string getOperatorNameString() const; util::SleeperThread thread_; std::string state_; diff --git a/man/waybar-wwan.5.scd b/man/waybar-wwan.5.scd index 3f6e4870..74eda493 100644 --- a/man/waybar-wwan.5.scd +++ b/man/waybar-wwan.5.scd @@ -19,7 +19,7 @@ $XDG_CONFIG_HOME/waybar/config ++ libmm-glib lives in: . /usr/lib/libmm-glib.so or /usr/lib64/libmm-glib.so -. /usr/lib/pkgconfig/libgps.pc or /usr/lib64/pkgconfig/libgps.pc +. /usr/lib/pkgconfig/libmm-glib.pc or /usr/lib64/pkgconfig/libmm-glib.pc . /usr/include/libmm-glib # CONFIGURATION @@ -35,7 +35,7 @@ libmm-glib lives in: Option to disable tooltip on hover. *tooltip-format*: ++ - typeof: string ++ + typeof: strirng ++ default: Games running: {glyph} ++ The text format of the tooltip. @@ -44,6 +44,16 @@ libmm-glib lives in: default: 5 ++ The interval in which the WWAN information gets polled (e.g. carrier name). +*imei*: ++ + typeof: string ++ + default: {glyph} ++ + Select only the modem with the corresponding imei. + +*path*: ++ + typeof: string ++ + default: {glyph} ++ + Select only the modem with the corresponding hardware path. + *hide-failed*: ++ typeof: bool ++ default: true ++ @@ -62,25 +72,23 @@ libmm-glib lives in: # FORMAT REPLACEMENTS -*{mode}*: Fix mode +*{state}*: Modem state. -*{status}*: Technology used for GPS fix. Not all GPS receivers report this. +*{access_technologies}*: List of all the supported access technologies. -*{latitude}*: Latitude, decimal degrees. Can be NaN. +*{current_modes}*: List of all the currently active connection modes. -*{latitude_error}*: Latitude uncertainty, meters. Can be NaN. +*{preferred_mode}*: The preferred connection mode in use. -*{longitude}*: Longitude, decimal degrees. Can be NaN. +*{supported_modes}*: List of all the supported connection modes. -*{longitude_error}*: Longitude uncertainty, meters. Can be NaN. +*{current_bands}*: List of all the currently active frequency bands. -*{altitude_hae}*: Altitude, height above ellipsoid, meters. Can be NaN. +*{supported_bands}*: List of all the supported frequency bands. -*{altitude_msl}*: Longitude, MSL, meters. Can be NaN. +*{signal_quality}*: Signal quality (0-100). -*{altitude_error}*: Altitude uncertainty, meters. Can be NaN. - -*{speed}*: Speed over ground, meters/sec. Can be NaN. +*{operator_name}*: Name of the network operator. *{speed_error}*: Speed uncertainty, meters/sec. Can be NaN. diff --git a/man/waybar-wwan.scd b/man/waybar-wwan.scd deleted file mode 100644 index e7473601..00000000 --- a/man/waybar-wwan.scd +++ /dev/null @@ -1,111 +0,0 @@ -waybar-gps(5) "waybar-gps" "User Manual" - -# NAME - -waybar - gps module - -# DESCRIPTION - -*gps* module for gpsd. - - -# FILES - -$XDG_CONFIG_HOME/waybar/config ++ - Per user configuration file - -# ADDITIONAL FILES - -libgps lives in: - -. /usr/lib/libgps.so or /usr/lib64/libgps.so -. /usr/lib/pkgconfig/libgps.pc or /usr/lib64/pkgconfig/libgps.pc -. /usr/include/gps - -# CONFIGURATION - -*format*: ++ - typeof: string ++ - default: {glyph} ++ - The text format. - -*tooltip*: ++ - typeof: bool ++ - default: true ++ - Option to disable tooltip on hover. - -*tooltip-format*: ++ - typeof: string ++ - default: Games running: {glyph} ++ - The text format of the tooltip. - -*interval*: ++ - typeof: integer ++ - default: 5 ++ - The interval in which the GPS information gets polled (e.g. current speed). - Significant updates (e.g. the current fix mode) are updated immediately. - -*hide-disconnected*: ++ - typeof: bool ++ - default: true ++ - Defines if the module should be hidden if there is no GPS receiver. - -*hide-no-fix*: ++ - typeof: bool ++ - default: false ++ - Defines if the module should be hidden if there is no GPS fix. - -# FORMAT REPLACEMENTS - -*{mode}*: Fix mode - -*{status}*: Technology used for GPS fix. Not all GPS receivers report this. - -*{latitude}*: Latitude, decimal degrees. Can be NaN. - -*{latitude_error}*: Latitude uncertainty, meters. Can be NaN. - -*{longitude}*: Longitude, decimal degrees. Can be NaN. - -*{longitude_error}*: Longitude uncertainty, meters. Can be NaN. - -*{altitude_hae}*: Altitude, height above ellipsoid, meters. Can be NaN. - -*{altitude_msl}*: Longitude, MSL, meters. Can be NaN. - -*{altitude_error}*: Altitude uncertainty, meters. Can be NaN. - -*{speed}*: Speed over ground, meters/sec. Can be NaN. - -*{speed_error}*: Speed uncertainty, meters/sec. Can be NaN. - -*{climb}*: Vertical speed, meters/sec. Can be NaN. - -*{climb_error}*: Vertical speed uncertainty, meters/sec. Can be NaN. - -*{satellites_visible}*: Number of satellites visible from the GPS receiver. - -*{satellites_used}*: Number of satellites used for the GPS fix. - -# EXAMPLES - -``` -"gps": { - "format": "{mode}", - "format-disabled": "", // an empty format will hide the module - "format-no-fix": "No fix", - "format-fix-3d": "{status}", - "tooltip-format": "{mode}", - "tooltip-format-no-fix": "{satellites_visible} satellites visible", - "tooltip-format-fix-2d": "{satellites_used}/{satellites_visible} satellites used", - "tooltip-format-fix-3d": "Altitude: {altitude_hae}m", - "hide-disconnected": false -} -``` -# STYLE - -- *#gps* -- *#gps.disabled* Applied when GPS is disabled. -- *#gps.fix-none* Applied when GPS is present, but there is no fix. -- *#gps.fix-2d* Applied when there is a 2D fix. -- *#gps.fix-3d* Applied when there is a 3D fix. diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp index f829df17..522b2e9c 100644 --- a/src/modules/wwan.cpp +++ b/src/modules/wwan.cpp @@ -52,7 +52,6 @@ waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config) updateCurrentModem(); - if (config_["hide-disconnected"].isBool()) { hideDisconnected = config_["hide-disconnected"].asBool(); } @@ -64,9 +63,6 @@ const void waybar::modules::Wwan::updateCurrentModem(){ GList* modems = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(manager)); if (!modems) { - spdlog::warn("No modems found"); - g_object_unref(manager); - g_object_unref(connection); return; } @@ -77,10 +73,11 @@ const void waybar::modules::Wwan::updateCurrentModem(){ if (config_["path"].isString()) { std::string path = config_["path"].asString(); - std::string other = mm_modem_dup_path(modem); + std::string other = mm_modem_dup_physdev(modem); if(path != other) { g_object_unref(modem); + modem = nullptr; continue; } } @@ -91,6 +88,7 @@ const void waybar::modules::Wwan::updateCurrentModem(){ if(imei != other) { g_object_unref(modem); + modem = nullptr; continue; } } @@ -98,51 +96,45 @@ const void waybar::modules::Wwan::updateCurrentModem(){ current_modem = modem; g_list_free_full(modems, g_object_unref); return; -} -// no modems found -g_list_free_full(modems, g_object_unref); -} - -const MMModemState waybar::modules::Wwan::getModemState() const { - if(current_modem == nullptr) { - return MMModemState::MM_MODEM_STATE_UNKNOWN; } - return mm_modem_get_state(current_modem); + // no modems found + current_modem = nullptr; + g_list_free_full(modems, g_object_unref); } -const std::string waybar::modules::Wwan::getModemStateString() const { - switch (getModemState()) { +const std::string getModemStateString(MMModem* modem) { + switch (mm_modem_get_state(modem)) { case MMModemState::MM_MODEM_STATE_FAILED: - return "Failed"; + return "Failed"; case MMModemState::MM_MODEM_STATE_INITIALIZING: - return "Initializing"; + return "Initializing"; case MMModemState::MM_MODEM_STATE_LOCKED: - return "Locked"; + return "Locked"; case MMModemState::MM_MODEM_STATE_DISABLED: - return "Disabled"; + return "Disabled"; case MMModemState::MM_MODEM_STATE_DISABLING: - return "Disabling"; + return "Disabling"; case MMModemState::MM_MODEM_STATE_ENABLED: - return "Enabled"; + return "Enabled"; case MMModemState::MM_MODEM_STATE_ENABLING: - return "Enabling"; + return "Enabling"; case MMModemState::MM_MODEM_STATE_SEARCHING: - return "Searching"; + return "Searching"; case MMModemState::MM_MODEM_STATE_REGISTERED: - return "Registered"; + return "Registered"; case MMModemState::MM_MODEM_STATE_DISCONNECTING: - return "Disconnecting"; + return "Disconnecting"; case MMModemState::MM_MODEM_STATE_CONNECTING: - return "Connecting"; + return "Connecting"; case MMModemState::MM_MODEM_STATE_CONNECTED: - return "Connected"; + return "Connected"; default: - return "Unknown"; + return "Unknown"; } } -const std::string waybar::modules::Wwan::getModemStateFormatString() const { - switch (getModemState()) { +const std::string getModemStateFormatString(MMModem* modem) { + switch (mm_modem_get_state(modem)) { case MMModemState::MM_MODEM_STATE_FAILED: return "failed"; case MMModemState::MM_MODEM_STATE_LOCKED: @@ -162,17 +154,9 @@ const std::string waybar::modules::Wwan::getModemStateFormatString() const { } } -const int waybar::modules::Wwan::getSignalQuality() const { - if(current_modem == nullptr) { - return MMModemState::MM_MODEM_STATE_UNKNOWN; - } +const std::string getAccessTechnologiesString(MMModem* modem) { + MMModemAccessTechnology technologies = mm_modem_get_access_technologies(modem); - gboolean recent; - - return mm_modem_get_signal_quality(current_modem, &recent); -} - -const std::string stringifyAccessTechnologies(MMModemAccessTechnology technologies) { std::string buffer; if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_POTS) @@ -219,7 +203,7 @@ const std::string stringifyAccessTechnologies(MMModemAccessTechnology technologi } -const std::string stringifyModemMode(MMModemMode mmode){ +const std::string stringifyModemMode(MMModemMode mmode) { switch (mmode) { case MM_MODEM_MODE_CS: @@ -237,57 +221,33 @@ const std::string stringifyModemMode(MMModemMode mmode){ } } -const std::string waybar::modules::Wwan::getAccessTechnologiesString() const { - - if(current_modem == nullptr) { - return ""; - } - - MMModemAccessTechnology technologies = mm_modem_get_access_technologies(current_modem); - - return stringifyAccessTechnologies(technologies); -} - -const MMModemModeCombination waybar::modules::Wwan::getCurrentModes() const { +const MMModemModeCombination getCurrentModes(MMModem* modem) { MMModemModeCombination combination; - mm_modem_get_current_modes(current_modem, &combination.allowed, &combination.preferred); + mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); return combination; } -const std::string waybar::modules::Wwan::getPreferredModeString() const { +const std::string getPreferredModeString(MMModem* modem) { - if(current_modem == nullptr) { - return ""; - } - - MMModemModeCombination combination = getCurrentModes(); + MMModemModeCombination combination = getCurrentModes(modem); return stringifyModemMode(combination.preferred); } -const std::string waybar::modules::Wwan::getCurrentModesString() const { +const std::string getCurrentModesString(MMModem* modem) { - if(current_modem == nullptr) { - return ""; - } - - MMModemModeCombination combination = getCurrentModes(); + MMModemModeCombination combination = getCurrentModes(modem); // TODO: fix return stringifyModemMode(combination.allowed); } -const std::string waybar::modules::Wwan::getPowerStateString() const { - - if(current_modem == nullptr) { - return ""; - } - - MMModemPowerState state = mm_modem_get_power_state(current_modem); +const std::string getPowerStateString(MMModem* modem) { + MMModemPowerState state = mm_modem_get_power_state(modem); switch (state) { case MM_MODEM_POWER_STATE_ON: @@ -301,22 +261,9 @@ const std::string waybar::modules::Wwan::getPowerStateString() const { } } -const std::string waybar::modules::Wwan::getImeiString() const { +const std::string getOperatorNameString(MMModem* modem) { - if(current_modem == nullptr) { - return ""; - } - - return mm_modem_dup_equipment_identifier(current_modem); -} - -const std::string waybar::modules::Wwan::getOperatorNameString() const { - - if(current_modem == nullptr) { - return ""; - } - - MMSim* sim = mm_modem_get_sim_sync(current_modem, nullptr, nullptr); + MMSim* sim = mm_modem_get_sim_sync(modem, nullptr, nullptr); if (sim == nullptr) { return "No SIM"; @@ -328,7 +275,6 @@ const std::string waybar::modules::Wwan::getOperatorNameString() const { } auto waybar::modules::Wwan::update() -> void { - if (current_modem == nullptr) { event_box_.set_visible(false); updateCurrentModem(); @@ -347,7 +293,7 @@ auto waybar::modules::Wwan::update() -> void { std::string tooltip_format; if (!alt_) { - std::string state = getModemStateFormatString(); + std::string state = getModemStateFormatString(current_modem); if (!state_.empty() && label_.get_style_context()->has_class(state_)) { label_.get_style_context()->remove_class(state_); } @@ -372,23 +318,22 @@ auto waybar::modules::Wwan::update() -> void { auto format = format_; fmt::dynamic_format_arg_store store; - store.push_back(fmt::arg("state", getModemStateString())); + store.push_back(fmt::arg("state", getModemStateString(current_modem))); - store.push_back(fmt::arg("access_technologies", getAccessTechnologiesString())); + store.push_back(fmt::arg("access_technologies", getAccessTechnologiesString(current_modem))); - store.push_back(fmt::arg("current_modes", getCurrentModesString())); - store.push_back(fmt::arg("preferred_mode", getPreferredModeString())); + store.push_back(fmt::arg("current_modes", getCurrentModesString(current_modem))); + store.push_back(fmt::arg("preferred_mode", getPreferredModeString(current_modem))); //store.push_back(fmt::arg("supported_modes", getSupportedModesString())); //store.push_back(fmt::arg("current_bands", getCurrentBandsString())); //store.push_back(fmt::arg("supported_bands", getSupportedBandsString())); - store.push_back(fmt::arg("signal_quality", getSignalQuality())); - store.push_back(fmt::arg("power_state", getPowerStateString())); - store.push_back(fmt::arg("imei", getImeiString())); + store.push_back(fmt::arg("signal_quality", mm_modem_get_signal_quality(current_modem, nullptr))); + store.push_back(fmt::arg("power_state", getPowerStateString(current_modem))); + store.push_back(fmt::arg("imei", mm_modem_dup_equipment_identifier(current_modem))); - store.push_back(fmt::arg("operator_name", getOperatorNameString())); - //store.push_back(fmt::arg("registration_state", getRegistrationStateString())); + store.push_back(fmt::arg("operator_name", getOperatorNameString(current_modem))); auto text = fmt::vformat(format, store); @@ -411,4 +356,7 @@ auto waybar::modules::Wwan::update() -> void { } waybar::modules::Wwan::~Wwan() { + g_object_unref(current_modem); + g_object_unref(manager); + g_object_unref(connection); } From fa5abc37f6c4ac5a1a5ef1c465cb12c7796446ad Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 11 Oct 2025 15:51:12 +0200 Subject: [PATCH 283/416] remove useless junk --- man/waybar-wwan.5.scd | 18 ------- src/modules/wwan.cpp | 108 +++++++++++------------------------------- 2 files changed, 28 insertions(+), 98 deletions(-) diff --git a/man/waybar-wwan.5.scd b/man/waybar-wwan.5.scd index 74eda493..4363911b 100644 --- a/man/waybar-wwan.5.scd +++ b/man/waybar-wwan.5.scd @@ -74,32 +74,14 @@ libmm-glib lives in: *{state}*: Modem state. -*{access_technologies}*: List of all the supported access technologies. - *{current_modes}*: List of all the currently active connection modes. *{preferred_mode}*: The preferred connection mode in use. -*{supported_modes}*: List of all the supported connection modes. - -*{current_bands}*: List of all the currently active frequency bands. - -*{supported_bands}*: List of all the supported frequency bands. - *{signal_quality}*: Signal quality (0-100). *{operator_name}*: Name of the network operator. -*{speed_error}*: Speed uncertainty, meters/sec. Can be NaN. - -*{climb}*: Vertical speed, meters/sec. Can be NaN. - -*{climb_error}*: Vertical speed uncertainty, meters/sec. Can be NaN. - -*{satellites_visible}*: Number of satellites visible from the GPS receiver. - -*{satellites_used}*: Number of satellites used for the GPS fix. - # EXAMPLES ``` diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp index 522b2e9c..93fca11a 100644 --- a/src/modules/wwan.cpp +++ b/src/modules/wwan.cpp @@ -154,96 +154,50 @@ const std::string getModemStateFormatString(MMModem* modem) { } } -const std::string getAccessTechnologiesString(MMModem* modem) { - MMModemAccessTechnology technologies = mm_modem_get_access_technologies(modem); +const std::string getPreferredModeString(MMModem* modem) { - std::string buffer; + MMModemModeCombination combination; + mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_POTS) - buffer += "POTS, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_GSM) - buffer += "GSM, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_GSM_COMPACT) - buffer += "GSM Compact, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_GPRS) - buffer += "GPRS, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_EDGE) - buffer += "EDGE, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_UMTS) - buffer += "UMTS, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_HSDPA) - buffer += "HSDPA, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_HSUPA) - buffer += "HSUPA, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_HSPA) - buffer += "HSPA, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_HSPA_PLUS) - buffer += "HSPA+, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_1XRTT) - buffer += "1xRTT, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_EVDO0) - buffer += "EVDO0, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_EVDOA) - buffer += "EVDOA, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_EVDOB) - buffer += "EVDOB, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_LTE) - buffer += "LTE, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_5GNR) - buffer += "5GNR, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_LTE_CAT_M) - buffer += "LTE CAT-M, "; - if(technologies & MMModemAccessTechnology::MM_MODEM_ACCESS_TECHNOLOGY_LTE_NB_IOT) - buffer += "LTE NB-IoT, "; - - if (buffer.length() > 2) { - buffer.erase(buffer.length()-2); - } - return buffer; - -} - -const std::string stringifyModemMode(MMModemMode mmode) { - - switch (mmode) { - case MM_MODEM_MODE_CS: + switch (combination.preferred) { + case MMModemMode::MM_MODEM_MODE_CS: return "CS"; - case MM_MODEM_MODE_2G: + case MMModemMode::MM_MODEM_MODE_2G: return "2G"; - case MM_MODEM_MODE_3G: + case MMModemMode::MM_MODEM_MODE_3G: return "3G"; - case MM_MODEM_MODE_4G: + case MMModemMode::MM_MODEM_MODE_4G: return "4G"; - case MM_MODEM_MODE_5G: + case MMModemMode::MM_MODEM_MODE_5G: return "5G"; default: return "No Service"; } } -const MMModemModeCombination getCurrentModes(MMModem* modem) { - - MMModemModeCombination combination; - - mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); - - return combination; -} - -const std::string getPreferredModeString(MMModem* modem) { - - MMModemModeCombination combination = getCurrentModes(modem); - - return stringifyModemMode(combination.preferred); -} - const std::string getCurrentModesString(MMModem* modem) { - MMModemModeCombination combination = getCurrentModes(modem); + MMModemModeCombination combination; + mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); - // TODO: fix - return stringifyModemMode(combination.allowed); + std::string buffer; + + if(combination.allowed & MMModemMode::MM_MODEM_MODE_CS) + buffer += "CS | "; + if(combination.allowed & MMModemMode::MM_MODEM_MODE_2G) + buffer += "2G | "; + if(combination.allowed & MMModemMode::MM_MODEM_MODE_3G) + buffer += "3G | "; + if(combination.allowed & MMModemMode::MM_MODEM_MODE_4G) + buffer += "4G | "; + if(combination.allowed & MMModemMode::MM_MODEM_MODE_5G) + buffer += "5G | "; + + if (buffer.length() > 2) { + buffer.erase(buffer.length()-2); + } + return buffer; } const std::string getPowerStateString(MMModem* modem) { @@ -320,14 +274,8 @@ auto waybar::modules::Wwan::update() -> void { fmt::dynamic_format_arg_store store; store.push_back(fmt::arg("state", getModemStateString(current_modem))); - store.push_back(fmt::arg("access_technologies", getAccessTechnologiesString(current_modem))); - store.push_back(fmt::arg("current_modes", getCurrentModesString(current_modem))); store.push_back(fmt::arg("preferred_mode", getPreferredModeString(current_modem))); - //store.push_back(fmt::arg("supported_modes", getSupportedModesString())); - - //store.push_back(fmt::arg("current_bands", getCurrentBandsString())); - //store.push_back(fmt::arg("supported_bands", getSupportedBandsString())); store.push_back(fmt::arg("signal_quality", mm_modem_get_signal_quality(current_modem, nullptr))); store.push_back(fmt::arg("power_state", getPowerStateString(current_modem))); From 7e65b091f25c54619e9900eae25450bdea333e47 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 11 Oct 2025 15:57:45 +0200 Subject: [PATCH 284/416] cleanup --- src/modules/wwan.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp index 93fca11a..7cb8ba8b 100644 --- a/src/modules/wwan.cpp +++ b/src/modules/wwan.cpp @@ -58,7 +58,7 @@ waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config) } -const void waybar::modules::Wwan::updateCurrentModem(){ +void waybar::modules::Wwan::updateCurrentModem(){ // get 1st modem (or modem with the specified hardware path) GList* modems = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(manager)); @@ -102,7 +102,7 @@ const void waybar::modules::Wwan::updateCurrentModem(){ g_list_free_full(modems, g_object_unref); } -const std::string getModemStateString(MMModem* modem) { +std::string getModemStateString(MMModem* modem) { switch (mm_modem_get_state(modem)) { case MMModemState::MM_MODEM_STATE_FAILED: return "Failed"; @@ -133,7 +133,7 @@ const std::string getModemStateString(MMModem* modem) { } } -const std::string getModemStateFormatString(MMModem* modem) { +std::string getModemStateFormatString(MMModem* modem) { switch (mm_modem_get_state(modem)) { case MMModemState::MM_MODEM_STATE_FAILED: return "failed"; @@ -154,7 +154,7 @@ const std::string getModemStateFormatString(MMModem* modem) { } } -const std::string getPreferredModeString(MMModem* modem) { +std::string getPreferredModeString(MMModem* modem) { MMModemModeCombination combination; mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); @@ -176,7 +176,7 @@ const std::string getPreferredModeString(MMModem* modem) { } -const std::string getCurrentModesString(MMModem* modem) { +std::string getCurrentModesString(MMModem* modem) { MMModemModeCombination combination; mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); @@ -200,7 +200,7 @@ const std::string getCurrentModesString(MMModem* modem) { return buffer; } -const std::string getPowerStateString(MMModem* modem) { +std::string getPowerStateString(MMModem* modem) { MMModemPowerState state = mm_modem_get_power_state(modem); switch (state) { @@ -215,7 +215,7 @@ const std::string getPowerStateString(MMModem* modem) { } } -const std::string getOperatorNameString(MMModem* modem) { +std::string getOperatorNameString(MMModem* modem) { MMSim* sim = mm_modem_get_sim_sync(modem, nullptr, nullptr); From d7274a041f331efd32247c54dddbcb20f367e6df Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 11 Oct 2025 16:14:44 +0200 Subject: [PATCH 285/416] improve manpage --- man/waybar-wwan.5.scd | 77 +++++++++++++++++++++++++++++++++++++------ src/modules/wwan.cpp | 5 ++- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/man/waybar-wwan.5.scd b/man/waybar-wwan.5.scd index 4363911b..6f5168cd 100644 --- a/man/waybar-wwan.5.scd +++ b/man/waybar-wwan.5.scd @@ -25,9 +25,42 @@ libmm-glib lives in: # CONFIGURATION *format*: ++ + typeof: string ++ + default: *{state}* ++ + The format, how information should be displayed. This format is used when other formats aren't specified. + +*format-failed*: ++ typeof: string ++ - default: {glyph} ++ - The text format. + This format is used when the modem cannot start. + +*format-locked*: ++ + typeof: string ++ + This format is used when the SIM on the modem is locked. + +*format-disabled*: ++ + typeof: string ++ + This format is used when the modem is disabled. + +*format-enabled*: ++ + typeof: string ++ + This format is used when the modem is enabled, but isn't registered to any carrier network. + +*format-searching*: ++ + typeof: string ++ + This format is used when the modem is looking for a carrier network to register to. + +*format-registered*: ++ + typeof: string ++ + This format is used when the modem is registered to a network, but no data connection is active. + +*format-connected*: ++ + typeof: string ++ + This format is used when the modem is registered to a network, and there is an active data connection. + +*format-icons*: ++ + typeof: array/object ++ + Based on the current signal strength, the corresponding icon gets selected. ++ + The order is *low* to *high*. Or by the state if it is an object. *tooltip*: ++ typeof: bool ++ @@ -36,8 +69,36 @@ libmm-glib lives in: *tooltip-format*: ++ typeof: strirng ++ - default: Games running: {glyph} ++ - The text format of the tooltip. + default: {state} ++ + The format, how information should be displayed in the tooltip. This format is used when other formats aren't specified. + +*tooltip-format-failed*: ++ + typeof: string ++ + This format is used when the modem cannot start. + +*tooltip-format-locked*: ++ + typeof: string ++ + This format is used when the SIM on the modem is locked. + +*tooltip-format-disabled*: ++ + typeof: string ++ + This format is used when the modem is disabled. + +*tooltip-format-enabled*: ++ + typeof: string ++ + This format is used when the modem is enabled, but isn't registered to any carrier network. + +*tooltip-format-searching*: ++ + typeof: string ++ + This format is used when the modem is looking for a carrier network to register to. + +*tooltip-format-registered*: ++ + typeof: string ++ + This format is used when the modem is registered to a network, but no data connection is active. + +*tooltip-format-connected*: ++ + typeof: string ++ + This format is used when the modem is registered to a network, and there is an active data connection. *interval*: ++ typeof: integer ++ @@ -46,12 +107,10 @@ libmm-glib lives in: *imei*: ++ typeof: string ++ - default: {glyph} ++ Select only the modem with the corresponding imei. *path*: ++ typeof: string ++ - default: {glyph} ++ Select only the modem with the corresponding hardware path. *hide-failed*: ++ @@ -89,11 +148,9 @@ libmm-glib lives in: "format": "{state}", "format-disabled": "", // an empty format will hide the module "format-locked": "SIM Locked", - "format-connected": "{operator_name} {preferred_mode}" + "format-connected": "{operator_name} {preferred_mode} | {signal_quality}%" "tooltip-format": "{mode}", - "tooltip-format-locked": "{satellites_visible} satellites visible", - "tooltip-format-registered": "{satellites_used}/{satellites_visible} satellites used", - "tooltip-format-connected": "", + "tooltip-format-connected": "{current_modes}", "hide-disconnected": false } ``` diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp index 7cb8ba8b..5a328a73 100644 --- a/src/modules/wwan.cpp +++ b/src/modules/wwan.cpp @@ -277,7 +277,10 @@ auto waybar::modules::Wwan::update() -> void { store.push_back(fmt::arg("current_modes", getCurrentModesString(current_modem))); store.push_back(fmt::arg("preferred_mode", getPreferredModeString(current_modem))); - store.push_back(fmt::arg("signal_quality", mm_modem_get_signal_quality(current_modem, nullptr))); + int percentage = mm_modem_get_signal_quality(current_modem, nullptr); + store.push_back(fmt::arg("signal_quality", percentage)); + store.push_back(fmt::arg("icon", getIcon(percentage))); + store.push_back(fmt::arg("power_state", getPowerStateString(current_modem))); store.push_back(fmt::arg("imei", mm_modem_dup_equipment_identifier(current_modem))); From cd73e6cdfb05d9252179da9d2987a3bf4e11adf6 Mon Sep 17 00:00:00 2001 From: Lena Date: Tue, 14 Oct 2025 13:32:58 +0200 Subject: [PATCH 286/416] clang complained --- include/modules/wwan.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/modules/wwan.hpp b/include/modules/wwan.hpp index 0cbb5d8d..8b18e999 100644 --- a/include/modules/wwan.hpp +++ b/include/modules/wwan.hpp @@ -21,7 +21,7 @@ namespace waybar::modules { private: - const void updateCurrentModem(); + void updateCurrentModem(); util::SleeperThread thread_; std::string state_; From 11fdbe8ae3b0de90119ff53a7681562c36c34f83 Mon Sep 17 00:00:00 2001 From: Lena Date: Tue, 14 Oct 2025 13:42:48 +0200 Subject: [PATCH 287/416] clang-format --- include/modules/wwan.hpp | 38 +++++++++++++++---------------- src/modules/wwan.cpp | 48 ++++++++++++++-------------------------- 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/include/modules/wwan.hpp b/include/modules/wwan.hpp index 8b18e999..4ef851af 100644 --- a/include/modules/wwan.hpp +++ b/include/modules/wwan.hpp @@ -1,39 +1,37 @@ #pragma once #include +#include #include #include -#include - #include "ALabel.hpp" #include "util/format.hpp" #include "util/sleeper_thread.hpp" namespace waybar::modules { - class Wwan : public ALabel { - public: - Wwan(const std::string&, const Json::Value&); - virtual ~Wwan(); - auto update() -> void override; +class Wwan : public ALabel { + public: + Wwan(const std::string&, const Json::Value&); + virtual ~Wwan(); + auto update() -> void override; - private: + private: + void updateCurrentModem(); - void updateCurrentModem(); + util::SleeperThread thread_; + std::string state_; + GDBusConnection* connection; + MMManager* manager; + MMModem* current_modem; - util::SleeperThread thread_; - std::string state_; - GDBusConnection* connection; - MMManager* manager; - MMModem* current_modem; + bool hideDisconnected = true; - bool hideDisconnected = true; - - const std::string dbus_name = "org.freedesktop.ModemManager1"; - const std::string dbus_obj_path = "/org/freedesktop/ModemManager1/"; - const std::string dbus_modems_path = "/org/freedesktop/ModemManager1/Modems/"; - }; + const std::string dbus_name = "org.freedesktop.ModemManager1"; + const std::string dbus_obj_path = "/org/freedesktop/ModemManager1/"; + const std::string dbus_modems_path = "/org/freedesktop/ModemManager1/Modems/"; +}; } // namespace waybar::modules diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp index 5a328a73..8cd3aefe 100644 --- a/src/modules/wwan.cpp +++ b/src/modules/wwan.cpp @@ -1,8 +1,10 @@ #include "modules/wwan.hpp" + #include #include #include + #include "ModemManager.h" #include "libmm-glib.h" @@ -16,13 +18,12 @@ #endif namespace { - using namespace waybar::util; - constexpr const char *DEFAULT_FORMAT = "{state}"; +using namespace waybar::util; +constexpr const char* DEFAULT_FORMAT = "{state}"; } // namespace - waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config) -: ALabel(config, "wwan", id, "{}", 5) { + : ALabel(config, "wwan", id, "{}", 5) { thread_ = [this] { dp.emit(); thread_.sleep_for(interval_); @@ -35,13 +36,9 @@ waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config) return; } - - manager = mm_manager_new_sync( - connection, - G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, - nullptr, // cancellable - &error - ); + manager = mm_manager_new_sync(connection, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, + nullptr, // cancellable + &error); if (error) { spdlog::error("Failed to create ModemManager proxy: " + std::string(error->message)); @@ -55,10 +52,9 @@ waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config) if (config_["hide-disconnected"].isBool()) { hideDisconnected = config_["hide-disconnected"].asBool(); } - } -void waybar::modules::Wwan::updateCurrentModem(){ +void waybar::modules::Wwan::updateCurrentModem() { // get 1st modem (or modem with the specified hardware path) GList* modems = g_dbus_object_manager_get_objects(G_DBUS_OBJECT_MANAGER(manager)); @@ -75,7 +71,7 @@ void waybar::modules::Wwan::updateCurrentModem(){ std::string path = config_["path"].asString(); std::string other = mm_modem_dup_physdev(modem); - if(path != other) { + if (path != other) { g_object_unref(modem); modem = nullptr; continue; @@ -86,7 +82,7 @@ void waybar::modules::Wwan::updateCurrentModem(){ std::string imei = config_["imei"].asString(); std::string other = mm_modem_dup_equipment_identifier(modem); - if(imei != other) { + if (imei != other) { g_object_unref(modem); modem = nullptr; continue; @@ -155,7 +151,6 @@ std::string getModemStateFormatString(MMModem* modem) { } std::string getPreferredModeString(MMModem* modem) { - MMModemModeCombination combination; mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); @@ -175,27 +170,20 @@ std::string getPreferredModeString(MMModem* modem) { } } - std::string getCurrentModesString(MMModem* modem) { - MMModemModeCombination combination; mm_modem_get_current_modes(modem, &combination.allowed, &combination.preferred); std::string buffer; - if(combination.allowed & MMModemMode::MM_MODEM_MODE_CS) - buffer += "CS | "; - if(combination.allowed & MMModemMode::MM_MODEM_MODE_2G) - buffer += "2G | "; - if(combination.allowed & MMModemMode::MM_MODEM_MODE_3G) - buffer += "3G | "; - if(combination.allowed & MMModemMode::MM_MODEM_MODE_4G) - buffer += "4G | "; - if(combination.allowed & MMModemMode::MM_MODEM_MODE_5G) - buffer += "5G | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_CS) buffer += "CS | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_2G) buffer += "2G | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_3G) buffer += "3G | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_4G) buffer += "4G | "; + if (combination.allowed & MMModemMode::MM_MODEM_MODE_5G) buffer += "5G | "; if (buffer.length() > 2) { - buffer.erase(buffer.length()-2); + buffer.erase(buffer.length() - 2); } return buffer; } @@ -216,7 +204,6 @@ std::string getPowerStateString(MMModem* modem) { } std::string getOperatorNameString(MMModem* modem) { - MMSim* sim = mm_modem_get_sim_sync(modem, nullptr, nullptr); if (sim == nullptr) { @@ -268,7 +255,6 @@ auto waybar::modules::Wwan::update() -> void { state_ = state; } - auto format = format_; fmt::dynamic_format_arg_store store; From 2aa74a523ce69992e63d6197211c1e94cbaebad6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:12:39 +0200 Subject: [PATCH 288/416] wwan: fix hide-disconnected default, plug dup() leaks, meson/man cleanup - update(): only hide the module when hide-disconnected is set AND the modem is not in the CONNECTED state, instead of hiding it unconditionally (the module was invisible by default). - free the gchar* returned by mm_modem_dup_physdev/ mm_modem_dup_equipment_identifier/mm_sim_dup_operator_name. - meson.build: drop duplicate libgps dependency, install the wwan man page. - man: document hide-disconnected (replacing the unimplemented hide-failed/hide-disabled/hide-not-registered options). - remove stray empty subprojects/.wraplock. --- man/waybar-wwan.5.scd | 14 ++------------ meson.build | 2 +- src/modules/wwan.cpp | 19 ++++++++++++++----- subprojects/.wraplock | 0 4 files changed, 17 insertions(+), 18 deletions(-) delete mode 100644 subprojects/.wraplock diff --git a/man/waybar-wwan.5.scd b/man/waybar-wwan.5.scd index 6f5168cd..0169de07 100644 --- a/man/waybar-wwan.5.scd +++ b/man/waybar-wwan.5.scd @@ -113,20 +113,10 @@ libmm-glib lives in: typeof: string ++ Select only the modem with the corresponding hardware path. -*hide-failed*: ++ +*hide-disconnected*: ++ typeof: bool ++ default: true ++ - Defines if the module should be hidden if the modem cannot start. - -*hide-disabled*: ++ - typeof: bool ++ - default: false ++ - Defines if the module should be hidden if the modem is disabled. - -*hide-not-registered*: ++ - typeof: bool ++ - default: false ++ - Defines if the module should be hidden if the modem isn't registered with any carrier network. + Defines if the module should be hidden while the modem does not have an active data connection. # FORMAT REPLACEMENTS diff --git a/meson.build b/meson.build index 737a4199..f1b9cb75 100644 --- a/meson.build +++ b/meson.build @@ -561,7 +561,7 @@ endif if libmm_glib.found() add_project_arguments('-DHAVE_LIBMM_GLIB', language: 'cpp') src_files += files('src/modules/wwan.cpp') - #man_files += files('man/waybar-wwan.5.scd') + man_files += files('man/waybar-wwan.5.scd') endif subdir('protocol') diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp index 8cd3aefe..86e68624 100644 --- a/src/modules/wwan.cpp +++ b/src/modules/wwan.cpp @@ -69,7 +69,9 @@ void waybar::modules::Wwan::updateCurrentModem() { if (config_["path"].isString()) { std::string path = config_["path"].asString(); - std::string other = mm_modem_dup_physdev(modem); + gchar* physdev = mm_modem_dup_physdev(modem); + std::string other = physdev ? physdev : ""; + g_free(physdev); if (path != other) { g_object_unref(modem); @@ -80,7 +82,9 @@ void waybar::modules::Wwan::updateCurrentModem() { if (config_["imei"].isString()) { std::string imei = config_["imei"].asString(); - std::string other = mm_modem_dup_equipment_identifier(modem); + gchar* equipment_id = mm_modem_dup_equipment_identifier(modem); + std::string other = equipment_id ? equipment_id : ""; + g_free(equipment_id); if (imei != other) { g_object_unref(modem); @@ -210,7 +214,9 @@ std::string getOperatorNameString(MMModem* modem) { return "No SIM"; } - std::string name = mm_sim_dup_operator_name(sim); + gchar* operator_name = mm_sim_dup_operator_name(sim); + std::string name = operator_name ? operator_name : ""; + g_free(operator_name); g_object_unref(sim); return name; } @@ -222,7 +228,7 @@ auto waybar::modules::Wwan::update() -> void { return; } - if (hideDisconnected) { + if (hideDisconnected && mm_modem_get_state(current_modem) != MM_MODEM_STATE_CONNECTED) { event_box_.set_visible(false); return; @@ -268,7 +274,10 @@ auto waybar::modules::Wwan::update() -> void { store.push_back(fmt::arg("icon", getIcon(percentage))); store.push_back(fmt::arg("power_state", getPowerStateString(current_modem))); - store.push_back(fmt::arg("imei", mm_modem_dup_equipment_identifier(current_modem))); + gchar* imei_raw = mm_modem_dup_equipment_identifier(current_modem); + std::string imei = imei_raw ? imei_raw : ""; + g_free(imei_raw); + store.push_back(fmt::arg("imei", imei)); store.push_back(fmt::arg("operator_name", getOperatorNameString(current_modem))); diff --git a/subprojects/.wraplock b/subprojects/.wraplock deleted file mode 100644 index e69de29b..00000000 From 41d86d92a884184ca051a7b33cad65cdde87c59f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:17:26 +0200 Subject: [PATCH 289/416] fix(wwan): null-initialize pointer members to avoid UB in destructor --- include/modules/wwan.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/modules/wwan.hpp b/include/modules/wwan.hpp index 4ef851af..0aa7112c 100644 --- a/include/modules/wwan.hpp +++ b/include/modules/wwan.hpp @@ -23,9 +23,9 @@ class Wwan : public ALabel { util::SleeperThread thread_; std::string state_; - GDBusConnection* connection; - MMManager* manager; - MMModem* current_modem; + GDBusConnection* connection = nullptr; + MMManager* manager = nullptr; + MMModem* current_modem = nullptr; bool hideDisconnected = true; From 247e18fb0b7aab4a083c06ee34d831d4a3693fe7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 23:51:14 +0200 Subject: [PATCH 290/416] Fix clang-format --- src/factory.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/factory.cpp b/src/factory.cpp index 096729ac..c0d018b7 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -385,9 +385,9 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, } #endif #ifdef HAVE_LIBMM_GLIB - if (ref == "wwan") { - return new waybar::modules::Wwan(id, config_[name]); - } + if (ref == "wwan") { + return new waybar::modules::Wwan(id, config_[name]); + } #endif if (ref == "temperature") { return new waybar::modules::Temperature(id, config_[name]); From 8fa58b1000240b6bf9ff601d74f1ed8d8ebc6bde Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 00:34:42 +0200 Subject: [PATCH 291/416] nix: add libmm-glib for the wwan module --- nix/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/default.nix b/nix/default.nix index 1850a414..8102363a 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -36,6 +36,7 @@ waybar.overrideAttrs (oldAttrs: { ++ [ pkgs.wireplumber pkgs.gpsd + pkgs.modemmanager ]; postUnpack = '' From 5ecda401667e946540140b40f1cc809c2b1b1e57 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 01:37:11 +0200 Subject: [PATCH 292/416] meson: fix libmm-glib dependency to use correct pkg-config name 'mm-glib' The ModemManager libmm-glib library installs its pkg-config file as mm-glib.pc (not libmm-glib.pc), so dependency('libmm-glib') never resolved and the nix-flake-check build failed with the wwan module enabled. --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index f1b9cb75..7bbad3fe 100644 --- a/meson.build +++ b/meson.build @@ -94,7 +94,7 @@ xkbregistry = dependency('xkbregistry') libjack = dependency('jack', required: get_option('jack')) libwireplumber = dependency('wireplumber-0.5', required: get_option('wireplumber')) libgps = dependency('libgps', required: get_option('gps')) -libmm_glib = dependency('libmm-glib', required: get_option('wwan')) +libmm_glib = dependency('mm-glib', required: get_option('wwan')) libsndio = compiler.find_library('sndio', required: get_option('sndio')) if libsndio.found() From 9e10a9aec973b25a24b5592eb3009c348eb1bd35 Mon Sep 17 00:00:00 2001 From: Abhijeet Date: Sat, 4 Jul 2026 01:42:23 +0200 Subject: [PATCH 293/416] feat(niri/workspaces): add window icons via window-rewrite Adds format-window-separator, window-rewrite and window-rewrite-default options plus the {windows} format replacement to the niri/workspaces module (rebased onto master; coexists with ignore-workspaces). --- include/modules/niri/workspaces.hpp | 11 +++ man/waybar-niri-workspaces.5.scd | 47 ++++++--- resources/config.jsonc | 21 ++++ src/modules/niri/workspaces.cpp | 144 +++++++++++++++++++++++++++- 4 files changed, 211 insertions(+), 12 deletions(-) diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index 9e548c98..7b717e9f 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -9,6 +9,7 @@ #include "AModule.hpp" #include "bar.hpp" #include "modules/niri/backend.hpp" +#include "util/regex_collection.hpp" // Added for rewrite rules namespace waybar::modules::niri { @@ -26,6 +27,10 @@ class Workspaces : public AModule, public EventHandler { std::string getIcon(const std::string& value, const Json::Value& ws); bool isWorkspaceIgnored(const std::string& name); bool handleScroll(GdkEventScroll* /*unused*/) override; + // Added for window rewrite + void populateWindowRewriteConfig(); + void populateFormatWindowSeparatorConfig(); + std::string getRewrite(const std::string& app_id, const std::string& title); const Bar& bar_; Gtk::Box box_; @@ -37,6 +42,12 @@ class Workspaces : public AModule, public EventHandler { bool sort_by_id_ = false; bool sort_by_name_ = false; bool sort_by_coordinates_ = false; + + // Added for window rewrite + util::RegexCollection m_windowRewriteRules; + std::string m_windowRewriteDefault; + std::string m_formatWindowSeparator; + }; } // namespace waybar::modules::niri diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 977d6948..13e2d27c 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -97,6 +97,21 @@ If none of the sorting options are enabled, workspaces keep their output/index o default: [] ++ Regexes to match against workspaces names and index's. If there's a match, the workspace will not be shown. +*format-window-separator*: ++ + typeof: string ++ + default: " " ++ + Separator used between window representations generated by *window-rewrite*. + +*window-rewrite*: ++ + typeof: object ++ + default: {} ++ + Rules to map window app_id and/or title to a specific string representation (e.g., an icon). Keys are regular expressions matching `app_id` and/or `title`. Values are the replacement strings, which can contain `{app_id}` and `{title}` placeholders. + +*window-rewrite-default*: ++ + typeof: string ++ + default: "?" ++ + The default representation for windows that don't match any rule in *window-rewrite*. + # FORMAT REPLACEMENTS *{value}*: Name of the workspace, or index for unnamed workspaces, @@ -112,6 +127,8 @@ as defined by niri. *{total}*: The total number of workspaces. +*{windows}*: Concatenated representation of windows in the workspace, using *window-rewrite* rules and *format-window-separator*. + # ICONS Additional to workspace name matching, the following *format-icons* can be set. @@ -126,18 +143,26 @@ Additional to workspace name matching, the following *format-icons* can be set. ``` "niri/workspaces": { - "format": "{icon}", - "format-icons": { - // Named workspaces - // (you need to configure them in niri) - "browser": "", - "discord": "", - "chat": "", + "format": "{icon} {windows}", + "format-window-separator": "|", + "window-rewrite-default": "", + "window-rewrite": { + "app_id": "", + "app_id": "", + "app_id title<.*Downloads.*>": "", + "app_id": "" + }, + "format-icons": { + // Named workspaces + // (you need to configure them in niri) + "browser": "", + "discord": "", + "chat": "", - // Icons by state - "active": "", - "default": "" - } + // Icons by state + "active": "", + "default": "" + } } ``` diff --git a/resources/config.jsonc b/resources/config.jsonc index b616d182..e816ee2f 100644 --- a/resources/config.jsonc +++ b/resources/config.jsonc @@ -50,6 +50,27 @@ // "default": "" // } // }, + "niri/workspaces": { + "format": "{icon} {windows}", + "format-window-separator": " ", + "window-rewrite-default": "", + "window-rewrite": { + "app_id": "", + "app_id": "", + "app_id": "", + "app_id": "", + "app_id": "" + }, + "format-icons": { + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "active": "", + "default": "" + } + }, "keyboard-state": { "numlock": true, "capslock": true, diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index a3b9be9a..21e12c0e 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -1,5 +1,6 @@ #include "modules/niri/workspaces.hpp" +#include // Needed for joining window representations #include #include #include @@ -7,6 +8,8 @@ #include #include +#include "util/rewrite_string.hpp" // Needed for rewrite logic + namespace waybar::modules::niri { Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config) @@ -59,6 +62,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& if (!gIPC) gIPC = std::make_unique(); + // Parse new config options first + populateWindowRewriteConfig(); + populateFormatWindowSeparatorConfig(); + + // Niri's WorkspacesChanged event already includes window info, + // so no need to register for separate window events like in Hyprland. gIPC->registerForIPC("WorkspacesChanged", this); gIPC->registerForIPC("WorkspaceActivated", this); gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); @@ -80,6 +89,10 @@ void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); } void Workspaces::doUpdate() { auto ipcLock = gIPC->lockData(); + // Debug: log global IPC lists + spdlog::debug("[niri/workspaces] gIPC workspaces count: {}", gIPC->workspaces().size()); + spdlog::debug("[niri/workspaces] gIPC windows count: {}", gIPC->windows().size()); + const auto alloutputs = config_["all-outputs"].asBool(); const auto display_cond = config_["display-condition"].asString(); std::vector my_workspaces; @@ -123,6 +136,10 @@ void Workspaces::doUpdate() { // Add buttons for new workspaces, update existing ones. for (const auto& ws : my_workspaces) { + // Debug: print workspace JSON + spdlog::debug("[niri/workspaces] workspace id={} json={} ", ws["id"].asUInt64(), + ws.toStyledString()); + auto bit = buttons_.find(ws["id"].asUInt64()); auto& button = bit == buttons_.end() ? addButton(ws) : bit->second; auto style_context = button.get_style_context(); @@ -156,6 +173,52 @@ void Workspaces::doUpdate() { else style_context->remove_class("empty"); + // --- Start Window Rewrite Logic --- + std::vector window_reps; + if (ws.isMember("windows") && ws["windows"].isArray()) { + spdlog::debug("[niri/workspaces] workspace id={} has {} windows", ws["id"].asUInt64(), + ws["windows"].size()); + for (const auto& win : ws["windows"]) { + spdlog::debug("[niri/workspaces] window json: {}", win.toStyledString()); + std::string app_id = + win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; + std::string title = + win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; + if (!app_id.empty() || !title.empty()) { // Only add if we have some identifier + auto rep = getRewrite(app_id, title); + spdlog::debug("[niri/workspaces] rewrite: app_id='{}' title='{}' => '{}'", app_id, title, + rep); + window_reps.push_back(rep); + } + } + } else { + spdlog::debug( + "[niri/workspaces] workspace id={} has no 'windows' array, collecting from global " + "windows", + ws["id"].asUInt64()); + // Fallback: collect from global windows list by matching workspace_id + for (const auto& win : gIPC->windows()) { + if (!win.isMember("workspace_id")) continue; + if (win["workspace_id"].asUInt64() != ws["id"].asUInt64()) continue; + spdlog::debug("[niri/workspaces] global window json: {}", win.toStyledString()); + std::string app_id = + win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; + std::string title = + win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; + if (!app_id.empty() || !title.empty()) { + auto rep = getRewrite(app_id, title); + spdlog::debug("[niri/workspaces] rewrite (global): app_id='{}' title='{}' => '{}'", + app_id, title, rep); + window_reps.push_back(rep); + } + } + } + // Join representations with the separator + auto windows_str = fmt::format("{}", fmt::join(window_reps, m_formatWindowSeparator)); + spdlog::debug("[niri/workspaces] workspace id={} windows_str='{}'", ws["id"].asUInt64(), + windows_str); + // --- End Window Rewrite Logic --- + std::string name; if (ws["name"]) { name = ws["name"].asString(); @@ -169,7 +232,8 @@ void Workspaces::doUpdate() { name = fmt::format( fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name), fmt::arg("name", ws["name"].asString()), fmt::arg("index", ws["idx"].asUInt()), - fmt::arg("output", ws["output"].asString()), fmt::arg("total", my_workspaces.size())); + fmt::arg("output", ws["output"].asString()), fmt::arg("total", my_workspaces.size()), + fmt::arg("windows", windows_str)); // Added windows arg } if (!config_["disable-markup"].asBool()) { auto* child = gtk_bin_get_child(GTK_BIN(button.gobj())); @@ -243,6 +307,84 @@ Gtk::Button& Workspaces::addButton(const Json::Value& ws) { return button; } +// --- Start New Helper Functions --- +void Workspaces::populateWindowRewriteConfig() { + // Reconstruct RegexCollection instead of clearing/adding + const Json::Value& rewrite_rules_config = config_["window-rewrite"]; + if (rewrite_rules_config.isObject()) { + // Assuming a constructor that takes the Json::Value object exists. + // If Niri needs rule prioritization like Hyprland, a priority function + // would be needed as a second argument here. + try { + m_windowRewriteRules = util::RegexCollection(rewrite_rules_config); + } catch (const std::exception& e) { + spdlog::error("Error initializing RegexCollection: {}", e.what()); + // Initialize with an empty collection if error occurs + m_windowRewriteRules = util::RegexCollection(Json::Value(Json::objectValue)); + } + + } else { + // Initialize with an empty collection if config is not an object + m_windowRewriteRules = util::RegexCollection(Json::Value(Json::objectValue)); + } + + if (config_.isMember("window-rewrite-default") && config_["window-rewrite-default"].isString()) { + m_windowRewriteDefault = config_["window-rewrite-default"].asString(); + } else { + m_windowRewriteDefault = "?"; // Default fallback + } +} + +void Workspaces::populateFormatWindowSeparatorConfig() { + if (config_.isMember("format-window-separator") && + config_["format-window-separator"].isString()) { + m_formatWindowSeparator = config_["format-window-separator"].asString(); + } else { + m_formatWindowSeparator = " "; // Default fallback + } +} + +std::string Workspaces::getRewrite(const std::string& app_id, const std::string& title) { + // Niri uses app_id, Hyprland uses class. Adapt the key format. + std::string lookup_key = "app_id<" + app_id + "> title<" + title + ">"; + std::string res = m_windowRewriteRules.get(lookup_key); + if (!res.empty()) { + // Create Json::Value for substitutions + Json::Value substitutions(Json::objectValue); + substitutions["app_id"] = app_id; + substitutions["title"] = title; + return util::rewriteString(res, substitutions); + } + // Fallback to app_id only + lookup_key = "app_id<" + app_id + ">"; + res = m_windowRewriteRules.get(lookup_key); + if (!res.empty()) { + // Create Json::Value for substitutions + Json::Value substitutions(Json::objectValue); + substitutions["app_id"] = app_id; + substitutions["title"] = title; + return util::rewriteString(res, substitutions); + } + // Fallback to title only + lookup_key = "title<" + title + ">"; + res = m_windowRewriteRules.get(lookup_key); + if (!res.empty()) { + // Create Json::Value for substitutions + Json::Value substitutions(Json::objectValue); + substitutions["app_id"] = app_id; + substitutions["title"] = title; + return util::rewriteString(res, substitutions); + } + + // No rule matched, return default + // Apply substitutions to default as well, in case it uses placeholders + Json::Value substitutions(Json::objectValue); + substitutions["app_id"] = app_id; + substitutions["title"] = title; + return util::rewriteString(m_windowRewriteDefault, substitutions); +} +// --- End New Helper Functions --- + std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) { const auto& icons = config_["format-icons"]; if (!icons) return value; From eeb7bc702e8e20ec097e30f4fe0703b153b7e9dd Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Sat, 4 Jul 2026 01:37:06 +0200 Subject: [PATCH 294/416] Added GeoClue2 privacy item Rebased onto current master and fixed the privacy module build: - privacy_item.hpp no longer includes privacy.hpp (it does not use the Privacy class); this broke the circular include that left PrivacyItem undeclared when privacy.hpp was reached first from privacy_item.cpp. - privacy_item.cpp now includes gtkmm/label.h explicitly, since Gtk::Label was previously only pulled in transitively via privacy.hpp. --- include/modules/privacy/privacy.hpp | 22 +++-- include/modules/privacy/privacy_item.hpp | 45 +++++++--- include/util/geoclue_backend.hpp | 43 +++++++++ include/util/pipewire/pipewire_backend.hpp | 2 +- include/util/pipewire/privacy_node_info.hpp | 5 +- man/waybar-privacy.5.scd | 13 ++- meson.build | 1 + .../waybar-privacy-location-symbolic.svg | 2 + resources/icons/waybar_icons.gresource.xml | 1 + resources/style.css | 4 + src/modules/privacy/privacy.cpp | 89 ++++++++++++++----- src/modules/privacy/privacy_item.cpp | 51 ++++++----- src/util/geoclue_backend.cpp | 88 ++++++++++++++++++ src/util/pipewire/pipewire_backend.cpp | 13 +-- src/util/pipewire/privacy_node_info.cpp | 8 +- 15 files changed, 314 insertions(+), 73 deletions(-) create mode 100644 include/util/geoclue_backend.hpp create mode 100644 resources/icons/waybar-privacy-location-symbolic.svg create mode 100644 src/util/geoclue_backend.cpp diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index 729fcc6a..7307afc9 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -1,13 +1,16 @@ #pragma once +#include #include #include "gtkmm/box.h" #include "modules/privacy/privacy_item.hpp" +#include "util/geoclue_backend.hpp" #include "util/pipewire/pipewire_backend.hpp" #include "util/pipewire/privacy_node_info.hpp" -using waybar::util::PipewireBackend::PrivacyNodeInfo; +using waybar::util::PipewireBackend::PrivacyNodeType; +using waybar::util::PipewireBackend::PWPrivacyNodeInfo; namespace waybar::modules::privacy { @@ -16,15 +19,15 @@ class Privacy : public AModule { Privacy(const std::string&, const Json::Value&, Gtk::Orientation, const std::string& pos); auto update() -> void override; - void onPrivacyNodesChanged(); - private: - std::list nodes_screenshare; // Screen is being shared - std::list nodes_audio_in; // Application is using the microphone - std::list nodes_audio_out; // Application is outputting audio + std::list nodes_screenshare; // Screen is being shared + std::list nodes_audio_in; // Application is using the microphone + std::list nodes_audio_out; // Application is outputting audio + std::atomic location_in_use; // GeoClue is being used std::mutex mutex_; sigc::connection visibility_conn; + sigc::connection geoclue_timeout_conn; // Config Gtk::Box box_; @@ -35,7 +38,12 @@ class Privacy : public AModule { std::set> ignore; bool ignore_monitor = true; - std::shared_ptr backend = nullptr; + std::shared_ptr pw_backend = nullptr; + std::shared_ptr geoclue_backend = nullptr; + + void onPWPrivacyNodesChanged(); + bool locationTimeout(bool in_use); + void onGeoCluePrivacyNodesChanged(); }; } // namespace waybar::modules::privacy diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp index 31e200e3..2a9cafcd 100644 --- a/include/modules/privacy/privacy_item.hpp +++ b/include/modules/privacy/privacy_item.hpp @@ -9,35 +9,36 @@ #include "gtkmm/revealer.h" #include "util/pipewire/privacy_node_info.hpp" -using waybar::util::PipewireBackend::PrivacyNodeInfo; using waybar::util::PipewireBackend::PrivacyNodeType; +using waybar::util::PipewireBackend::PWPrivacyNodeInfo; namespace waybar::modules::privacy { class PrivacyItem : public Gtk::Revealer { - public: + protected: PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privacy_type_, - std::list* nodes, Gtk::Orientation orientation, - const std::string& pos, const uint icon_size, const uint transition_duration); + Gtk::Orientation orientation, const std::string& pos, const uint icon_size, + const uint transition_duration); + + public: + virtual void set_tooltip() = 0; enum PrivacyNodeType privacy_type; void set_in_use(bool in_use); - private: - std::list* nodes; - - sigc::connection signal_conn; - + uint tooltipIconSize = 24; Gtk::Box tooltip_window; + private: + sigc::connection signal_conn; + bool init = false; bool in_use = false; // Config std::string iconName = "image-missing-symbolic"; bool tooltip = true; - uint tooltipIconSize = 24; Gtk::Box box_; Gtk::Image icon_; @@ -45,4 +46,28 @@ class PrivacyItem : public Gtk::Revealer { void update_tooltip(); }; +class GeoCluePrivacyItem : public PrivacyItem { + public: + GeoCluePrivacyItem(const Json::Value& config_, Gtk::Orientation orientation, + const std::string& pos, const uint icon_size, const uint transition_duration) + : PrivacyItem(config_, util::PipewireBackend::PRIVACY_NODE_TYPE_LOCATION, orientation, pos, + icon_size, transition_duration) {} + + void set_tooltip() override; +}; + +class PWPrivacyItem : public PrivacyItem { + public: + PWPrivacyItem(const Json::Value& config_, enum PrivacyNodeType privacy_type_, + std::list* nodes_, Gtk::Orientation orientation, + const std::string& pos, const uint icon_size, const uint transition_duration) + : PrivacyItem(config_, privacy_type_, orientation, pos, icon_size, transition_duration), + nodes(nodes_) {} + + void set_tooltip() override; + + private: + std::list* nodes; +}; + } // namespace waybar::modules::privacy diff --git a/include/util/geoclue_backend.hpp b/include/util/geoclue_backend.hpp new file mode 100644 index 00000000..4bf19091 --- /dev/null +++ b/include/util/geoclue_backend.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include + +#include "giomm/dbusproxy.h" + +namespace waybar::util::GeoClueBackend { + +class GeoClueBackend { + private: + guint watcherID_; + sigc::connection signal_conn; + Glib::RefPtr proxy; + bool connected; + + /* Hack to keep constructor inaccessible but still public. + * This is required to be able to use std::make_shared. + * It is important to keep this class only accessible via a reference-counted + * pointer because the destructor will manually free memory, and this could be + * a problem with C++20's copy and move semantics. + */ + struct PrivateConstructorTag {}; + + public: + sigc::signal in_use_changed_signal_event; + + std::atomic location_in_use; // GeoClue is being used + + static std::shared_ptr getInstance(); + + // DBus callbacks + void onAppear(const Glib::RefPtr&, const Glib::ustring&, + const Glib::ustring&); + void onVanished(const Glib::RefPtr&, const Glib::ustring&); + void propertyChanged(const Gio::DBus::Proxy::MapChangedProperties& changedProperties, + const std::vector& invalidatedProperties); + + GeoClueBackend(PrivateConstructorTag tag); + ~GeoClueBackend(); +}; +} // namespace waybar::util::GeoClueBackend diff --git a/include/util/pipewire/pipewire_backend.hpp b/include/util/pipewire/pipewire_backend.hpp index 90fb2bb2..7ea0ffee 100644 --- a/include/util/pipewire/pipewire_backend.hpp +++ b/include/util/pipewire/pipewire_backend.hpp @@ -29,7 +29,7 @@ class PipewireBackend { public: sigc::signal privacy_nodes_changed_signal_event; - std::unordered_map privacy_nodes; + std::unordered_map privacy_nodes; std::mutex mutex_; static std::shared_ptr getInstance(); diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp index 54acad42..cd45313b 100644 --- a/include/util/pipewire/privacy_node_info.hpp +++ b/include/util/pipewire/privacy_node_info.hpp @@ -12,10 +12,11 @@ enum PrivacyNodeType { PRIVACY_NODE_TYPE_NONE, PRIVACY_NODE_TYPE_VIDEO_INPUT, PRIVACY_NODE_TYPE_AUDIO_INPUT, - PRIVACY_NODE_TYPE_AUDIO_OUTPUT + PRIVACY_NODE_TYPE_AUDIO_OUTPUT, + PRIVACY_NODE_TYPE_LOCATION }; -class PrivacyNodeInfo { +class PWPrivacyNodeInfo { public: PrivacyNodeType type = PRIVACY_NODE_TYPE_NONE; uint32_t id; diff --git a/man/waybar-privacy.5.scd b/man/waybar-privacy.5.scd index 1c4ef59d..72191dfe 100644 --- a/man/waybar-privacy.5.scd +++ b/man/waybar-privacy.5.scd @@ -28,7 +28,7 @@ the screen or playing audio. *modules* ++ typeof: array of objects ++ - default: [{"type": "screenshare"}, {"type": "audio-in"}] ++ + default: [{"type": "screenshare"}, {"type": "audio-in"}, {"type": "location"}] ++ Which privacy modules to monitor. See *MODULES CONFIGURATION* for++ more information. @@ -52,9 +52,13 @@ the screen or playing audio. *type*: ++ typeof: string ++ - values: "screenshare", "audio-in", "audio-out" ++ + values: "screenshare", "audio-in", "audio-out", "location" ++ Specifies which module to use and configure. +*icon-name*: ++ + typeof: string ++ + If set, changes the modules icon. + *tooltip*: ++ typeof: bool ++ default: true ++ @@ -95,6 +99,10 @@ the screen or playing audio. "type": "audio-in", "tooltip": true, "tooltip-icon-size": 24 + }, + { + "type": "location", + "icon-name": "location-services-active-symbolic" } ], "ignore-monitor": true, @@ -118,3 +126,4 @@ the screen or playing audio. - *#privacy-item.screenshare* - *#privacy-item.audio-in* - *#privacy-item.audio-out* +- *#privacy-item.location* diff --git a/meson.build b/meson.build index 5563d610..c4abfaa4 100644 --- a/meson.build +++ b/meson.build @@ -426,6 +426,7 @@ if pipewire.found() 'src/modules/privacy/privacy_item.cpp', 'src/util/pipewire/pipewire_backend.cpp', 'src/util/pipewire/privacy_node_info.cpp', + 'src/util/geoclue_backend.cpp', ) man_files += files('man/waybar-privacy.5.scd') endif diff --git a/resources/icons/waybar-privacy-location-symbolic.svg b/resources/icons/waybar-privacy-location-symbolic.svg new file mode 100644 index 00000000..3b843e79 --- /dev/null +++ b/resources/icons/waybar-privacy-location-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/icons/waybar_icons.gresource.xml b/resources/icons/waybar_icons.gresource.xml index 077049bf..a8584eef 100644 --- a/resources/icons/waybar_icons.gresource.xml +++ b/resources/icons/waybar_icons.gresource.xml @@ -4,5 +4,6 @@ waybar-privacy-audio-input-symbolic.svg waybar-privacy-audio-output-symbolic.svg waybar-privacy-screen-share-symbolic.svg + waybar-privacy-location-symbolic.svg diff --git a/resources/style.css b/resources/style.css index bcfcb5c8..fe29f8d5 100644 --- a/resources/style.css +++ b/resources/style.css @@ -325,3 +325,7 @@ label:focus { #privacy-item.audio-out { background-color: #0069d4; } + +#privacy-item.location { + background-color: #00afff; +} diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 4e613971..6b0a019c 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -10,10 +10,11 @@ namespace waybar::modules::privacy { -using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT; -using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT; -using util::PipewireBackend::PRIVACY_NODE_TYPE_NONE; -using util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT; +using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT; +using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT; +using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_LOCATION; +using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_NONE; +using waybar::util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT; Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientation orientation, const std::string& pos) @@ -21,7 +22,9 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat nodes_screenshare(), nodes_audio_in(), nodes_audio_out(), + location_in_use(false), visibility_conn(), + geoclue_timeout_conn(), box_(orientation, 0) { box_.set_name(name_); @@ -45,20 +48,21 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat // Initialize each privacy module Json::Value modules = config_["modules"]; - // Add Screenshare and Mic usage as default modules if none are specified + // Add Screenshare, Mic, and Location usage as default modules if none are specified if (!modules.isArray() || modules.empty()) { modules = Json::Value(Json::arrayValue); - for (const auto& type : {"screenshare", "audio-in"}) { + for (const auto& type : {"screenshare", "audio-in", "location"}) { Json::Value obj = Json::Value(Json::objectValue); obj["type"] = type; modules.append(obj); } } - std::map > typeMap = { + std::map*, PrivacyNodeType>> typeMap = { {"screenshare", {&nodes_screenshare, PRIVACY_NODE_TYPE_VIDEO_INPUT}}, {"audio-in", {&nodes_audio_in, PRIVACY_NODE_TYPE_AUDIO_INPUT}}, {"audio-out", {&nodes_audio_out, PRIVACY_NODE_TYPE_AUDIO_OUTPUT}}, + {"location", {nullptr, PRIVACY_NODE_TYPE_LOCATION}}, }; for (const auto& module : modules) { @@ -68,8 +72,21 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat auto iter = typeMap.find(type); if (iter != typeMap.end()) { auto& [nodePtr, nodeType] = iter->second; - auto* item = Gtk::make_managed(module, nodeType, nodePtr, orientation, pos, + PrivacyItem* item = nullptr; + switch (nodeType) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + item = Gtk::make_managed(module, nodeType, nodePtr, orientation, pos, iconSize, transition_duration); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_LOCATION: + item = Gtk::make_managed(module, orientation, pos, iconSize, + transition_duration); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + continue; + } box_.add(*item); modules_.push_back(item); } @@ -93,20 +110,24 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat ignore_monitor = config_["ignore-monitor"].asBool(); } - backend = util::PipewireBackend::PipewireBackend::getInstance(); - backend->privacy_nodes_changed_signal_event.connect( - sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); + pw_backend = util::PipewireBackend::PipewireBackend::getInstance(); + pw_backend->privacy_nodes_changed_signal_event.connect( + sigc::mem_fun(*this, &Privacy::onPWPrivacyNodesChanged)); + + geoclue_backend = util::GeoClueBackend::GeoClueBackend::getInstance(); + geoclue_backend->in_use_changed_signal_event.connect( + sigc::mem_fun(*this, &Privacy::onGeoCluePrivacyNodesChanged)); dp.emit(); } -void Privacy::onPrivacyNodesChanged() { +void Privacy::onPWPrivacyNodesChanged() { mutex_.lock(); nodes_audio_out.clear(); nodes_audio_in.clear(); nodes_screenshare.clear(); - for (auto& node : backend->privacy_nodes) { + for (auto& node : pw_backend->privacy_nodes) { if (ignore_monitor && node.second->is_monitor) continue; auto iter = ignore.find(std::pair(node.second->type, node.second->node_name)); @@ -124,7 +145,7 @@ void Privacy::onPrivacyNodesChanged() { case PRIVACY_NODE_TYPE_AUDIO_OUTPUT: nodes_audio_out.push_back(node.second); break; - case PRIVACY_NODE_TYPE_NONE: + default: continue; } break; @@ -137,36 +158,63 @@ void Privacy::onPrivacyNodesChanged() { dp.emit(); } +bool Privacy::locationTimeout(bool in_use) { + location_in_use.store(geoclue_backend->location_in_use); + dp.emit(); + + return false; +} +void Privacy::onGeoCluePrivacyNodesChanged() { + geoclue_timeout_conn.disconnect(); + + const bool in_use = geoclue_backend->location_in_use; + if (in_use) { + locationTimeout(in_use); + } else { + // The GeoClue service is really fast, so wait before hiding, so that the + // widget is at least visible for 3 seconds. + geoclue_timeout_conn = Glib::signal_timeout().connect_seconds( + sigc::bind(sigc::mem_fun(*this, &Privacy::locationTimeout), in_use), 3); + } +} + auto Privacy::update() -> void { // set in modules or not bool setScreenshare = false; bool setAudioIn = false; bool setAudioOut = false; + bool setLocation = false; // used or not bool useScreenshare = false; bool useAudioIn = false; bool useAudioOut = false; + bool useLocation = false; mutex_.lock(); for (PrivacyItem* module : modules_) { switch (module->privacy_type) { - case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + case PRIVACY_NODE_TYPE_VIDEO_INPUT: setScreenshare = true; useScreenshare = !nodes_screenshare.empty(); module->set_in_use(useScreenshare); break; - case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + case PRIVACY_NODE_TYPE_AUDIO_INPUT: setAudioIn = true; useAudioIn = !nodes_audio_in.empty(); module->set_in_use(useAudioIn); break; - case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + case PRIVACY_NODE_TYPE_AUDIO_OUTPUT: setAudioOut = true; useAudioOut = !nodes_audio_out.empty(); module->set_in_use(useAudioOut); break; - case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + case PRIVACY_NODE_TYPE_LOCATION: + setLocation = true; + useLocation = location_in_use; + module->set_in_use(useLocation); + break; + case PRIVACY_NODE_TYPE_NONE: break; } } @@ -174,7 +222,7 @@ auto Privacy::update() -> void { // Hide the whole widget if none are in use bool isVisible = (setScreenshare && useScreenshare) || (setAudioIn && useAudioIn) || - (setAudioOut && useAudioOut); + (setAudioOut && useAudioOut) || (setLocation && useLocation); if (isVisible != event_box_.get_visible()) { // Disconnect any previous connection so that it doesn't get activated in @@ -187,12 +235,13 @@ auto Privacy::update() -> void { // have finished animating visibility_conn = Glib::signal_timeout().connect( sigc::track_obj( - [this, setScreenshare, setAudioOut, setAudioIn]() { + [this, setScreenshare, setAudioOut, setAudioIn, setLocation]() { mutex_.lock(); bool visible = false; visible |= setScreenshare && !nodes_screenshare.empty(); visible |= setAudioIn && !nodes_audio_in.empty(); visible |= setAudioOut && !nodes_audio_out.empty(); + visible |= setLocation && location_in_use; mutex_.unlock(); event_box_.set_visible(visible); return false; diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index 3a55eee7..880d026e 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -8,19 +8,16 @@ #include "gtkmm/label.h" #include "gtkmm/revealer.h" #include "gtkmm/tooltip.h" -#include "util/pipewire/privacy_node_info.hpp" namespace waybar::modules::privacy { PrivacyItem::PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privacy_type_, - std::list* nodes_, Gtk::Orientation orientation, - const std::string& pos, const uint icon_size, + Gtk::Orientation orientation, const std::string& pos, const uint icon_size, const uint transition_duration) : Gtk::Revealer(), privacy_type(privacy_type_), - nodes(nodes_), - signal_conn(), tooltip_window(Gtk::ORIENTATION_VERTICAL, 0), + signal_conn(), box_(Gtk::ORIENTATION_HORIZONTAL, 0), icon_() { switch (privacy_type) { @@ -36,7 +33,10 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privac box_.get_style_context()->add_class("screenshare"); iconName = "waybar-privacy-screen-share-symbolic"; break; - default: + case util::PipewireBackend::PRIVACY_NODE_TYPE_LOCATION: + box_.get_style_context()->add_class("location"); + iconName = "waybar-privacy-location-symbolic"; + break; case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: return; } @@ -81,7 +81,6 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privac set_has_tooltip(tooltip); if (tooltip) { // Sets the window to use when showing the tooltip - update_tooltip(); this->signal_query_tooltip().connect(sigc::track_obj( [this](int x, int y, bool keyboard_tooltip, const Glib::RefPtr& tooltip) { tooltip->set_custom(tooltip_window); @@ -103,21 +102,8 @@ void PrivacyItem::update_tooltip() { // work differently in GTK4. delete child; } - for (auto* node : *nodes) { - auto* box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL, 4); - // Set device icon - auto* node_icon = Gtk::make_managed(); - node_icon->set_pixel_size(tooltipIconSize); - node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID); - box->add(*node_icon); - - // Set model - auto* nodeName = Gtk::make_managed(node->getName()); - box->add(*nodeName); - - tooltip_window.add(*box); - } + set_tooltip(); tooltip_window.show_all(); } @@ -161,4 +147,27 @@ void PrivacyItem::set_in_use(bool in_use) { this->init = true; } +void GeoCluePrivacyItem::set_tooltip() { + auto* nodeName = Gtk::make_managed("Location in use"); + tooltip_window.add(*nodeName); +} + +void PWPrivacyItem::set_tooltip() { + for (auto* node : *nodes) { + auto* box = Gtk::make_managed(Gtk::ORIENTATION_HORIZONTAL, 4); + + // Set device icon + auto* node_icon = Gtk::make_managed(); + node_icon->set_pixel_size(tooltipIconSize); + node_icon->set_from_icon_name(node->getIconName(), Gtk::ICON_SIZE_INVALID); + box->add(*node_icon); + + // Set model + auto* nodeName = Gtk::make_managed(node->getName()); + box->add(*nodeName); + + tooltip_window.add(*box); + } +} + } // namespace waybar::modules::privacy diff --git a/src/util/geoclue_backend.cpp b/src/util/geoclue_backend.cpp new file mode 100644 index 00000000..dbe09074 --- /dev/null +++ b/src/util/geoclue_backend.cpp @@ -0,0 +1,88 @@ +#include "util/geoclue_backend.hpp" + +#include + +#include + +namespace waybar::util::GeoClueBackend { + +GeoClueBackend::GeoClueBackend(PrivateConstructorTag tag) + : watcherID_(0), signal_conn(), connected(false), location_in_use(false) { + // Start watching DBUS + watcherID_ = + Gio::DBus::watch_name(Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.GeoClue2", + sigc::mem_fun(*this, &GeoClueBackend::onAppear), + sigc::mem_fun(*this, &GeoClueBackend::onVanished), + Gio::DBus::BusNameWatcherFlags::BUS_NAME_WATCHER_FLAGS_AUTO_START); +} + +GeoClueBackend::~GeoClueBackend() { + if (watcherID_ > 0) { + Gio::DBus::unwatch_name(watcherID_); + watcherID_ = 0; + } + signal_conn.disconnect(); +} + +std::shared_ptr GeoClueBackend::getInstance() { + PrivateConstructorTag tag; + return std::make_shared(tag); +} + +void GeoClueBackend::propertyChanged( + const Gio::DBus::Proxy::MapChangedProperties& changedProperties, + const std::vector& invalidatedProperties) { + if (!connected) { + return; + } + + if (auto in_use_variant = changedProperties.find("InUse"); + in_use_variant != changedProperties.end()) { + bool in_use = + Glib::VariantBase::cast_dynamic>(in_use_variant->second).get(); + location_in_use.store(in_use); + in_use_changed_signal_event.emit(); + } +} + +void GeoClueBackend::onAppear(const Glib::RefPtr& conn, + const Glib::ustring& name, const Glib::ustring& name_owner) { + location_in_use.store(false); + signal_conn.disconnect(); + connected = true; + + proxy = Gio::DBus::Proxy::create_sync(conn, "org.freedesktop.GeoClue2", + "/org/freedesktop/GeoClue2/Manager", + "org.freedesktop.GeoClue2.Manager"); + + // Subscribe DBus events + signal_conn = proxy->signal_properties_changed().connect( + sigc::mem_fun(*this, &GeoClueBackend::propertyChanged)); + + // Get the initial value + auto callArgs = Glib::Variant>::create( + std::make_tuple("org.freedesktop.GeoClue2.Manager", "InUse")); + Glib::VariantContainerBase base = + proxy->call_sync("org.freedesktop.DBus.Properties.Get", callArgs); + if (base.is_of_type(Glib::VariantType("(v)"))) { + Glib::Variant> variant_value; + base.get_child(variant_value, 0); + auto [value] = variant_value.get(); + if (location_in_use != value) { + location_in_use.store(value); + } + } + + in_use_changed_signal_event.emit(); +} + +void GeoClueBackend::onVanished(const Glib::RefPtr& conn, + const Glib::ustring& name) { + signal_conn.disconnect(); + connected = false; + + location_in_use.store(false); + in_use_changed_signal_event.emit(); +} + +} // namespace waybar::util::GeoClueBackend diff --git a/src/util/pipewire/pipewire_backend.cpp b/src/util/pipewire/pipewire_backend.cpp index 82ec0c0f..72528fb4 100644 --- a/src/util/pipewire/pipewire_backend.cpp +++ b/src/util/pipewire/pipewire_backend.cpp @@ -5,7 +5,7 @@ namespace waybar::util::PipewireBackend { static void getNodeInfo(void* data_, const struct pw_node_info* info) { - auto* pNodeInfo = static_cast(data_); + auto* pNodeInfo = static_cast(data_); pNodeInfo->handleNodeEventInfo(info); static_cast(pNodeInfo->data)->privacy_nodes_changed_signal_event.emit(); @@ -17,7 +17,7 @@ static const struct pw_node_events NODE_EVENTS = { }; static void proxyDestroy(void* data) { - static_cast(data)->handleProxyEventDestroy(); + static_cast(data)->handleProxyEventDestroy(); } static const struct pw_proxy_events PROXY_EVENTS = { @@ -127,12 +127,13 @@ void PipewireBackend::handleRegistryEventGlobal(uint32_t id, uint32_t permission return; } - auto* proxy = (pw_proxy*)pw_registry_bind(registry_, id, type, version, sizeof(PrivacyNodeInfo)); + auto* proxy = + (pw_proxy*)pw_registry_bind(registry_, id, type, version, sizeof(PWPrivacyNodeInfo)); if (proxy == nullptr) return; - auto* pNodeInfo = (PrivacyNodeInfo*)pw_proxy_get_user_data(proxy); - new (pNodeInfo) PrivacyNodeInfo{}; + auto* pNodeInfo = (PWPrivacyNodeInfo*)pw_proxy_get_user_data(proxy); + new (pNodeInfo) PWPrivacyNodeInfo{}; pNodeInfo->id = id; pNodeInfo->data = this; pNodeInfo->type = mediaType; @@ -152,7 +153,7 @@ void PipewireBackend::handleRegistryEventGlobalRemove(uint32_t id) { mutex_.lock(); auto iter = privacy_nodes.find(id); if (iter != privacy_nodes.end()) { - privacy_nodes[id]->~PrivacyNodeInfo(); + privacy_nodes[id]->~PWPrivacyNodeInfo(); privacy_nodes.erase(id); } mutex_.unlock(); diff --git a/src/util/pipewire/privacy_node_info.cpp b/src/util/pipewire/privacy_node_info.cpp index dbd6ed85..0da4b25b 100644 --- a/src/util/pipewire/privacy_node_info.cpp +++ b/src/util/pipewire/privacy_node_info.cpp @@ -2,7 +2,7 @@ namespace waybar::util::PipewireBackend { -std::string PrivacyNodeInfo::getName() { +std::string PWPrivacyNodeInfo::getName() { const std::vector names{&application_name, &node_name}; std::string name = "Unknown Application"; for (const auto& item : names) { @@ -15,7 +15,7 @@ std::string PrivacyNodeInfo::getName() { return name; } -std::string PrivacyNodeInfo::getIconName() { +std::string PWPrivacyNodeInfo::getIconName() { const std::vector names{&application_icon_name, &pipewire_access_portal_app_id, &application_name, &node_name}; std::string name = "application-x-executable-symbolic"; @@ -27,12 +27,12 @@ std::string PrivacyNodeInfo::getIconName() { return name; } -void PrivacyNodeInfo::handleProxyEventDestroy() { +void PWPrivacyNodeInfo::handleProxyEventDestroy() { spa_hook_remove(&proxy_listener); spa_hook_remove(&object_listener); } -void PrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info* info) { +void PWPrivacyNodeInfo::handleNodeEventInfo(const struct pw_node_info* info) { state = info->state; const struct spa_dict_item* item; From 7c86f1ccb47f6c33fc0f11d2497fe0805354388e Mon Sep 17 00:00:00 2001 From: Emiliano Deustua Date: Thu, 17 Jul 2025 10:59:33 -0500 Subject: [PATCH 295/416] fix: Use workspace ID to handle workspace updates --- src/modules/hyprland/workspaces.cpp | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 46402055..50eba51b 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -79,18 +79,14 @@ Json::Value Workspaces::createMonitorWorkspaceData(std::string const& name, void Workspaces::createWorkspace(Json::Value const& workspace_data, Json::Value const& clients_data) { - auto workspaceName = workspace_data["name"].asString(); auto workspaceId = workspace_data["id"].asInt(); - spdlog::debug("Creating workspace {}", workspaceName); + spdlog::debug("Creating workspace {}", workspaceId); // avoid recreating existing workspaces - auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr const& w) { - if (workspaceId > 0) { - return w->id() == workspaceId; - } - return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) || - workspaceName == w->name(); - }); + auto workspace = + std::ranges::find_if(m_workspaces, [workspaceId](std::unique_ptr const& w) { + return workspaceId == w->id(); + }); if (workspace != m_workspaces.end()) { // don't recreate workspace, but update persistency if necessary @@ -98,14 +94,14 @@ void Workspaces::createWorkspace(Json::Value const& workspace_data, const auto* k = "persistent-rule"; if (std::ranges::find(keys, k) != keys.end()) { - spdlog::debug("Set dynamic persistency of workspace {} to: {}", workspaceName, + spdlog::debug("Set dynamic persistency of workspace {} to: {}", workspaceId, workspace_data[k].asBool() ? "true" : "false"); (*workspace)->setPersistentRule(workspace_data[k].asBool()); } k = "persistent-config"; if (std::ranges::find(keys, k) != keys.end()) { - spdlog::debug("Set config persistency of workspace {} to: {}", workspaceName, + spdlog::debug("Set config persistency of workspace {} to: {}", workspaceId, workspace_data[k].asBool() ? "true" : "false"); (*workspace)->setPersistentConfig(workspace_data[k].asBool()); } @@ -1101,8 +1097,7 @@ void Workspaces::updateWindowCount() { const Json::Value workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); for (auto const& workspace : m_workspaces) { auto workspaceJson = std::ranges::find_if(workspacesJson, [&](Json::Value const& x) { - return x["name"].asString() == workspace->name() || - (workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name()); + return x["id"].asInt() == workspace->id(); }); uint32_t count = 0; if (workspaceJson != workspacesJson.end()) { @@ -1171,9 +1166,7 @@ void Workspaces::updateWorkspaceStates() { workspaceTooltip = workspace->selectString(m_tooltipMap); } auto updatedWorkspace = std::ranges::find_if(updatedWorkspaces, [&workspace](const auto& w) { - auto wNameRaw = w["name"].asString(); - auto wName = wNameRaw.starts_with("special:") ? wNameRaw.substr(8) : wNameRaw; - return wName == workspace->name(); + return w["id"].asInt() == workspace->id(); }); if (updatedWorkspace != updatedWorkspaces.end()) { workspace->setOutput((*updatedWorkspace)["monitor"].asString()); From 95664c7662c16de57b0c763030565551e0dcacf5 Mon Sep 17 00:00:00 2001 From: Emiliano Deustua Date: Tue, 14 Oct 2025 20:18:50 -0500 Subject: [PATCH 296/416] fix: Remove load persistent workspaces from workspace rules --- src/modules/hyprland/workspaces.cpp | 47 ----------------------------- 1 file changed, 47 deletions(-) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 50eba51b..8d217972 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -217,8 +217,6 @@ void Workspaces::initializeWorkspaces() { // a persistent workspace config is defined, so use that instead of workspace rules loadPersistentWorkspacesFromConfig(clientsJson); } - // load Hyprland's workspace rules - loadPersistentWorkspacesFromWorkspaceRules(clientsJson); } bool isDoubleSpecial(std::string const& workspace_name) { @@ -293,51 +291,6 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const& clientsJs } } -void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value& clientsJson) { - spdlog::info("Loading persistent workspaces from Hyprland workspace rules"); - - auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules"); - for (Json::Value const& rule : workspaceRules) { - if (!rule["workspaceString"].isString()) { - spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule); - continue; - } - if (!rule["persistent"].asBool()) { - continue; - } - auto workspace = rule.isMember("defaultName") ? rule["defaultName"].asString() - : rule["workspaceString"].asString(); - - // There could be persistent special workspaces, only show those when show-special is enabled. - if (workspace.starts_with("special:") && !showSpecial()) { - continue; - } - - // The prefix "name:" cause mismatches with workspace names taken anywhere else. - if (workspace.starts_with("name:")) { - workspace = workspace.substr(5); - } - auto const& monitor = rule["monitor"].asString(); - // create this workspace persistently if: - // 1. the allOutputs config option is enabled - // 2. the rule's monitor is the current monitor - // 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor - if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) { - // => skip ignore-workspaces even if its a persistent - if (isWorkspaceIgnored(workspace)) { - continue; - } - // => persistent workspace should be shown on this monitor - auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name); - workspaceData["persistent-rule"] = true; - m_workspacesToCreate.emplace_back(workspaceData, clientsJson); - } else { - // This can be any workspace selector. - m_workspacesToRemove.emplace_back(workspace); - } - } -} - void Workspaces::onEvent(const std::string& ev) { { std::lock_guard lock(m_mutex); From 5f7629a36e4fca584f66167e244b0ee593db788a Mon Sep 17 00:00:00 2001 From: Emiliano Deustua Date: Tue, 14 Oct 2025 21:33:11 -0500 Subject: [PATCH 297/416] format: Fix clang formatting --- src/modules/hyprland/workspaces.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 8d217972..4cd0d2ae 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -83,10 +83,9 @@ void Workspaces::createWorkspace(Json::Value const& workspace_data, spdlog::debug("Creating workspace {}", workspaceId); // avoid recreating existing workspaces - auto workspace = - std::ranges::find_if(m_workspaces, [workspaceId](std::unique_ptr const& w) { - return workspaceId == w->id(); - }); + auto workspace = std::ranges::find_if( + m_workspaces, + [workspaceId](std::unique_ptr const& w) { return workspaceId == w->id(); }); if (workspace != m_workspaces.end()) { // don't recreate workspace, but update persistency if necessary @@ -1049,9 +1048,8 @@ auto Workspaces::update() -> void { void Workspaces::updateWindowCount() { const Json::Value workspacesJson = m_ipc.getSocket1JsonReply("workspaces"); for (auto const& workspace : m_workspaces) { - auto workspaceJson = std::ranges::find_if(workspacesJson, [&](Json::Value const& x) { - return x["id"].asInt() == workspace->id(); - }); + auto workspaceJson = std::ranges::find_if( + workspacesJson, [&](Json::Value const& x) { return x["id"].asInt() == workspace->id(); }); uint32_t count = 0; if (workspaceJson != workspacesJson.end()) { try { From 9cd09c25691c973c9f55d136da58ed9db7d8c2b9 Mon Sep 17 00:00:00 2001 From: sewaustav Date: Sat, 4 Jul 2026 01:50:16 +0200 Subject: [PATCH 298/416] feat(niri): add workspace-taskbar support Adds a per-workspace app-icon taskbar (Workspace class) to the niri/workspaces module. Rebased onto master: integrated with the existing window-rewrite feature so the {windows} and {total} format replacements continue to work alongside the new taskbar. --- include/modules/niri/workspace.hpp | 47 +++++ include/modules/niri/workspaces.hpp | 25 ++- man/waybar-niri-workspaces.5.scd | 26 +++ meson.build | 3 + src/modules/niri/workspace.cpp | 261 ++++++++++++++++++++++++ src/modules/niri/workspaces.cpp | 302 ++++++++++------------------ 6 files changed, 458 insertions(+), 206 deletions(-) create mode 100644 include/modules/niri/workspace.hpp create mode 100644 src/modules/niri/workspace.cpp diff --git a/include/modules/niri/workspace.hpp b/include/modules/niri/workspace.hpp new file mode 100644 index 00000000..e1bf7ba6 --- /dev/null +++ b/include/modules/niri/workspace.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace waybar::modules::niri { + +class Workspaces; + +class Workspace { + public: + Workspace(const Json::Value& workspace_data, Workspaces& manager); + ~Workspace() = default; + + Workspace(const Workspace&) = delete; + Workspace& operator=(const Workspace&) = delete; + + Gtk::Button& button() { return button_; } + uint64_t id() const { return id_; } + + void update(const Json::Value& workspace_data, const std::vector& all_windows, + const std::string& windows_str, std::size_t total); + + private: + void rebuildTaskbar(const std::vector& my_windows); + + Glib::RefPtr loadIcon(const std::string& app_id, int size); + + Workspaces& manager_; + uint64_t id_; + + // Layout: button_ + // └─ box_ (horizontal) + // ├─ label_ workspace label / icon + // └─ taskbar_box_ app icon buttons (shown only when taskbar enabled) + Gtk::Button button_; + Gtk::Box box_; + Gtk::Label label_; + Gtk::Box taskbar_box_; +}; + +} // namespace waybar::modules::niri \ No newline at end of file diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index 7b717e9f..97030223 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -1,41 +1,50 @@ #pragma once -#include +#include #include +#include #include #include #include "AModule.hpp" #include "bar.hpp" #include "modules/niri/backend.hpp" -#include "util/regex_collection.hpp" // Added for rewrite rules +#include "modules/niri/workspace.hpp" +#include "util/regex_collection.hpp" // Added for rewrite rules namespace waybar::modules::niri { class Workspaces : public AModule, public EventHandler { public: - Workspaces(const std::string&, const Bar&, const Json::Value&); + Workspaces(const std::string& id, const Bar& bar, const Json::Value& config); ~Workspaces() override; + void update() override; + const Json::Value& config() const { return config_; } + const Bar& bar() const { return bar_; } + + std::string getIcon(const std::string& value, const Json::Value& ws) const; + private: void onEvent(const Json::Value& ev) override; void doUpdate(); - void sortWorkspaces(std::vector& workspaces) const; - Gtk::Button& addButton(const Json::Value& ws); - std::string getIcon(const std::string& value, const Json::Value& ws); + void createWorkspace(const Json::Value& workspace_data); + void sortWorkspaces(std::vector& workspaces) const; bool isWorkspaceIgnored(const std::string& name); bool handleScroll(GdkEventScroll* /*unused*/) override; // Added for window rewrite void populateWindowRewriteConfig(); void populateFormatWindowSeparatorConfig(); std::string getRewrite(const std::string& app_id, const std::string& title); + std::string getWindowsRepresentation(const Json::Value& ws); const Bar& bar_; Gtk::Box box_; - // Map from niri workspace id to button. - std::unordered_map buttons_; + + std::vector> workspaces_; + // Vec of regex rules to ignore workspaces. std::vector ignoreWorkspaces_; diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 13e2d27c..0627776c 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -91,6 +91,20 @@ If none of the sorting options are enabled, workspaces keep their output/index o typeof: bool ++ default: false ++ Enables this module to consume all left over space dynamically. + +*workspace-taskbar*: ++ + 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. + + *icon-size*: ++ + typeof: int ++ + default: 18 ++ + Size of the icons in the workspace taskbar. *ignore-workspaces*: ++ typeof: array ++ @@ -166,6 +180,16 @@ Additional to workspace name matching, the following *format-icons* can be set. } ``` +``` +"niri/workspaces": { + "format": "{icon}", + "workspace-taskbar": { + "enable": true, + "icon-size": 18 + } +} +``` + # Style - *#workspaces button* @@ -177,3 +201,5 @@ Additional to workspace name matching, the following *format-icons* can be set. the bar that it is displayed on. - *#workspaces button#niri-workspace-*: Workspaces named this, or index for unnamed workspaces. +- *#workspaces button.niri-workspace*: The main container for the workspace. +- *#workspaces button .niri-taskbar-btn*: The icon buttons within the taskbar. diff --git a/meson.build b/meson.build index 5563d610..a7b42ece 100644 --- a/meson.build +++ b/meson.build @@ -74,6 +74,7 @@ wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') wayland_protos = dependency('wayland-protocols') gtkmm = dependency('gtkmm-3.0', version : ['>=3.22.0']) +giomm = dependency('giomm-2.4', version : ['>=2.4.0']) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4', required: get_option('dbusmenu-gtk')) giounix = dependency('gio-unix-2.0') jsoncpp = dependency('jsoncpp', version : ['>=1.9.2'], fallback : ['jsoncpp', 'jsoncpp_dep']) @@ -350,6 +351,7 @@ if get_option('niri') 'src/modules/niri/language.cpp', 'src/modules/niri/window.cpp', 'src/modules/niri/workspaces.cpp', + 'src/modules/niri/workspace.cpp', ) man_files += files( 'man/waybar-niri-language.5.scd', @@ -575,6 +577,7 @@ executable( jsoncpp, wayland_cursor, gtkmm, + giomm, dbusmenu_gtk, giounix, libinput, diff --git a/src/modules/niri/workspace.cpp b/src/modules/niri/workspace.cpp new file mode 100644 index 00000000..73445b50 --- /dev/null +++ b/src/modules/niri/workspace.cpp @@ -0,0 +1,261 @@ +#include "modules/niri/workspace.hpp" + +#include +#include +#include +#include +#include +#include + +#include "modules/niri/backend.hpp" +#include "modules/niri/workspaces.hpp" + +namespace waybar::modules::niri { + +Workspace::Workspace(const Json::Value& workspace_data, Workspaces& manager) + : manager_(manager), + id_(workspace_data["id"].asUInt64()), + box_(Gtk::ORIENTATION_HORIZONTAL, 0), + taskbar_box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + button_.add(box_); + box_.pack_start(label_, false, false, 0); + box_.pack_start(taskbar_box_, false, false, 0); + + button_.set_relief(Gtk::RELIEF_NONE); + button_.get_style_context()->add_class("niri-workspace"); + + if (!manager_.config()["disable-click"].asBool()) { + const auto ws_id = id_; + button_.signal_pressed().connect([ws_id] { + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + auto& focusWorkspace = (action["FocusWorkspace"] = Json::Value(Json::objectValue)); + auto& reference = (focusWorkspace["reference"] = Json::Value(Json::objectValue)); + reference["Id"] = ws_id; + IPC::send(request); + } catch (const std::exception& e) { + spdlog::error("Niri: error focusing workspace: {}", e.what()); + } + }); + } + + button_.show_all(); +} + +void Workspace::update(const Json::Value& data, const std::vector& all_windows, + const std::string& windows_str, std::size_t total) { + // ── CSS classes ────────────────────────────────────────────────────────── + auto style = button_.get_style_context(); + + auto setClass = [&](const char* cls, bool on) { + if (on) + style->add_class(cls); + else + style->remove_class(cls); + }; + + setClass("focused", data["is_focused"].asBool()); + setClass("active", data["is_active"].asBool()); + setClass("urgent", data["is_urgent"].asBool()); + setClass("empty", data["active_window_id"].isNull()); + setClass("current_output", + data["output"] && data["output"].asString() == manager_.bar().output->name); + + // ── Workspace label ─────────────────────────────────────────────────────── + std::string name; + if (data["name"]) { + name = data["name"].asString(); + } else { + name = std::to_string(data["idx"].asUInt()); + } + + button_.set_name("niri-workspace-" + name); + + const auto& cfg = manager_.config(); + + if (cfg["format"].isString()) { + auto format = cfg["format"].asString(); + name = fmt::format(fmt::runtime(format), fmt::arg("icon", manager_.getIcon(name, data)), + fmt::arg("value", name), fmt::arg("name", data["name"].asString()), + fmt::arg("index", data["idx"].asUInt()), + fmt::arg("output", data["output"].asString()), fmt::arg("total", total), + fmt::arg("windows", windows_str)); + } + + if (!cfg["disable-markup"].asBool()) { + label_.set_markup(name); + } else { + label_.set_text(name); + } + + // ── Visibility ─────────────────────────────────────────────────────────── + const bool alloutputs = cfg["all-outputs"].asBool(); + if (cfg["current-only"].asBool()) { + const auto* prop = alloutputs ? "is_focused" : "is_active"; + data[prop].asBool() ? button_.show() : button_.hide(); + } else if (cfg["hide-empty"].asBool()) { + (data["active_window_id"].isNull() && !data["is_focused"].asBool()) ? button_.hide() + : button_.show(); + } else { + button_.show(); + } + + // ── Taskbar ─────────────────────────────────────────────────────────────── + const auto& taskbar_cfg = cfg["workspace-taskbar"]; + if (taskbar_cfg.isObject() && taskbar_cfg["enable"].asBool()) { + std::vector my_windows; + for (const auto& win : all_windows) { + if (win["workspace_id"].asUInt64() == id_) { + my_windows.push_back(win); + } + } + + std::sort(my_windows.begin(), my_windows.end(), [](const Json::Value& a, const Json::Value& b) { + const auto& la = a["layout"]; + const auto& lb = b["layout"]; + const bool ha = la.isObject() && la["pos_in_scrolling_layout"].isArray(); + const bool hb = lb.isObject() && lb["pos_in_scrolling_layout"].isArray(); + if (!ha && !hb) return false; + if (!ha) return false; + if (!hb) return true; + const int col_a = la["pos_in_scrolling_layout"][0].asInt(); + const int col_b = lb["pos_in_scrolling_layout"][0].asInt(); + if (col_a != col_b) return col_a < col_b; + return la["pos_in_scrolling_layout"][1].asInt() < lb["pos_in_scrolling_layout"][1].asInt(); + }); + + rebuildTaskbar(my_windows); + taskbar_box_.show(); + label_.hide(); + } else { + for (auto* child : taskbar_box_.get_children()) { + taskbar_box_.remove(*child); + } + taskbar_box_.hide(); + } +} + +// ── Taskbar rebuild ────────────────────────────────────────────────────────── + +void Workspace::rebuildTaskbar(const std::vector& my_windows) { + for (auto* child : taskbar_box_.get_children()) { + taskbar_box_.remove(*child); + } + + const auto& taskbar_cfg = manager_.config()["workspace-taskbar"]; + const int icon_size = taskbar_cfg["icon-size"].isInt() ? taskbar_cfg["icon-size"].asInt() : 16; + + for (const auto& win : my_windows) { + const auto win_id = win["id"].asUInt64(); + const std::string app_id = win["app_id"].isString() ? win["app_id"].asString() : ""; + const std::string title = win["title"].isString() ? win["title"].asString() : app_id; + const bool is_focused = win["is_focused"].asBool(); + + auto* btn = Gtk::make_managed(); + btn->set_relief(Gtk::RELIEF_NONE); + btn->get_style_context()->add_class("niri-taskbar-btn"); + if (is_focused) btn->get_style_context()->add_class("focused"); + btn->set_tooltip_text(title); + + auto pixbuf = loadIcon(app_id, icon_size); + if (pixbuf) { + auto* img = Gtk::make_managed(pixbuf); + btn->add(*img); + } else { + std::string fallback = app_id.empty() ? title : app_id; + if (!fallback.empty()) { + fallback = fallback.substr(0, 3); + } else { + fallback = "?"; + } + auto* lbl = Gtk::make_managed(fallback); + btn->add(*lbl); + } + + // Left click → focus window. + btn->signal_clicked().connect([win_id] { + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + auto& focusWindow = (action["FocusWindow"] = Json::Value(Json::objectValue)); + focusWindow["id"] = win_id; + IPC::send(request); + } catch (const std::exception& e) { + spdlog::error("Niri: error focusing window {}: {}", win_id, e.what()); + } + }); + + // Middle click → close window. + btn->signal_button_release_event().connect([win_id](GdkEventButton* event) -> bool { + if (event->button == GDK_BUTTON_MIDDLE) { + try { + Json::Value request(Json::objectValue); + auto& action = (request["Action"] = Json::Value(Json::objectValue)); + auto& closeWindow = (action["CloseWindow"] = Json::Value(Json::objectValue)); + closeWindow["id"] = win_id; + IPC::send(request); + } catch (const std::exception& e) { + spdlog::error("Niri: error closing window {}: {}", win_id, e.what()); + } + return true; + } + return false; + }); + + taskbar_box_.pack_start(*btn, false, false, 0); + btn->show_all(); + } +} + +// ── Icon loading ───────────────────────────────────────────────────────────── + +Glib::RefPtr Workspace::loadIcon(const std::string& app_id, int size) { + if (app_id.empty()) return {}; + auto app_info = Gio::DesktopAppInfo::create(app_id + ".desktop"); + + if (app_info) { + auto icon = app_info->get_icon(); + if (icon) { + auto theme = Gtk::IconTheme::get_default(); + auto icon_info = theme->lookup_icon(icon, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + + if (icon_info) { + try { + + return icon_info.load_icon(); + } catch (...) { + + } + } + } + } + + auto theme = Gtk::IconTheme::get_default(); + + auto tryLoad = [&](const std::string& name) -> Glib::RefPtr { + if (!theme->has_icon(name)) return {}; + try { + return theme->load_icon(name, size, Gtk::ICON_LOOKUP_FORCE_SIZE); + } catch (...) { + return {}; + } + }; + + if (auto pb = tryLoad(app_id)) return pb; + + std::string lower = app_id; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + if (auto pb = tryLoad(lower)) return pb; + + auto dot = app_id.rfind('.'); + if (dot != std::string::npos) { + std::string last = app_id.substr(dot + 1); + std::transform(last.begin(), last.end(), last.begin(), ::tolower); + if (auto pb = tryLoad(last)) return pb; + } + + return {}; +} + +} // namespace waybar::modules::niri \ No newline at end of file diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 21e12c0e..0bdcf85b 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -1,8 +1,6 @@ #include "modules/niri/workspaces.hpp" #include // Needed for joining window representations -#include -#include #include #include @@ -73,6 +71,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); gIPC->registerForIPC("WorkspaceUrgencyChanged", this); + gIPC->registerForIPC("WindowsChanged", this); + gIPC->registerForIPC("WindowOpenedOrChanged", this); + gIPC->registerForIPC("WindowLayoutsChanged", this); + gIPC->registerForIPC("WindowFocusChanged", this); + gIPC->registerForIPC("WindowClosed", this); + if (config["enable-bar-scroll"].asBool()) { auto& window = const_cast(bar_).window; window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); @@ -84,7 +88,7 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& Workspaces::~Workspaces() { gIPC->unregisterForIPC(this); } -void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); } +void Workspaces::onEvent(const Json::Value& /*ev*/) { dp.emit(); } void Workspaces::doUpdate() { auto ipcLock = gIPC->lockData(); @@ -95,178 +99,78 @@ void Workspaces::doUpdate() { const auto alloutputs = config_["all-outputs"].asBool(); const auto display_cond = config_["display-condition"].asString(); - std::vector my_workspaces; - const auto& workspaces = gIPC->workspaces(); - std::copy_if( - workspaces.cbegin(), workspaces.cend(), std::back_inserter(my_workspaces), - [&](const auto& ws) { - std::string name; - if (ws["name"]) { - name = ws["name"].asString(); - } else { - name = std::to_string(ws["idx"].asUInt()); - } - if (isWorkspaceIgnored(name)) { - return false; - } - - if (display_cond == "only-populated") { - if (ws["active_window_id"].isNull() && !ws["is_active"].asBool()) return false; - } else if (display_cond == "keep-named") { - if (ws["name"].isNull() && ws["active_window_id"].isNull() && !ws["is_active"].asBool()) - return false; - } - - if (alloutputs) return true; - return ws["output"].asString() == bar_.output->name; - }); - - sortWorkspaces(my_workspaces); - - // Remove buttons for removed workspaces. - for (auto it = buttons_.begin(); it != buttons_.end();) { - auto ws = std::find_if(my_workspaces.begin(), my_workspaces.end(), - [it](const auto& ws) { return ws["id"].asUInt64() == it->first; }); - if (ws == my_workspaces.end()) { - it = buttons_.erase(it); - } else { - ++it; - } - } - - // Add buttons for new workspaces, update existing ones. - for (const auto& ws : my_workspaces) { - // Debug: print workspace JSON - spdlog::debug("[niri/workspaces] workspace id={} json={} ", ws["id"].asUInt64(), - ws.toStyledString()); - - auto bit = buttons_.find(ws["id"].asUInt64()); - auto& button = bit == buttons_.end() ? addButton(ws) : bit->second; - auto style_context = button.get_style_context(); - - if (ws["is_focused"].asBool()) - style_context->add_class("focused"); - else - style_context->remove_class("focused"); - - if (ws["is_active"].asBool()) - style_context->add_class("active"); - else - style_context->remove_class("active"); - - if (ws["is_urgent"].asBool()) - style_context->add_class("urgent"); - else - style_context->remove_class("urgent"); - - if (ws["output"]) { - if (ws["output"].asString() == bar_.output->name) - style_context->add_class("current_output"); - else - style_context->remove_class("current_output"); - } else { - style_context->remove_class("current_output"); - } - - if (ws["active_window_id"].isNull()) - style_context->add_class("empty"); - else - style_context->remove_class("empty"); - - // --- Start Window Rewrite Logic --- - std::vector window_reps; - if (ws.isMember("windows") && ws["windows"].isArray()) { - spdlog::debug("[niri/workspaces] workspace id={} has {} windows", ws["id"].asUInt64(), - ws["windows"].size()); - for (const auto& win : ws["windows"]) { - spdlog::debug("[niri/workspaces] window json: {}", win.toStyledString()); - std::string app_id = - win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; - std::string title = - win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; - if (!app_id.empty() || !title.empty()) { // Only add if we have some identifier - auto rep = getRewrite(app_id, title); - spdlog::debug("[niri/workspaces] rewrite: app_id='{}' title='{}' => '{}'", app_id, title, - rep); - window_reps.push_back(rep); - } - } - } else { - spdlog::debug( - "[niri/workspaces] workspace id={} has no 'windows' array, collecting from global " - "windows", - ws["id"].asUInt64()); - // Fallback: collect from global windows list by matching workspace_id - for (const auto& win : gIPC->windows()) { - if (!win.isMember("workspace_id")) continue; - if (win["workspace_id"].asUInt64() != ws["id"].asUInt64()) continue; - spdlog::debug("[niri/workspaces] global window json: {}", win.toStyledString()); - std::string app_id = - win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; - std::string title = - win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; - if (!app_id.empty() || !title.empty()) { - auto rep = getRewrite(app_id, title); - spdlog::debug("[niri/workspaces] rewrite (global): app_id='{}' title='{}' => '{}'", - app_id, title, rep); - window_reps.push_back(rep); - } - } - } - // Join representations with the separator - auto windows_str = fmt::format("{}", fmt::join(window_reps, m_formatWindowSeparator)); - spdlog::debug("[niri/workspaces] workspace id={} windows_str='{}'", ws["id"].asUInt64(), - windows_str); - // --- End Window Rewrite Logic --- + const auto& all_workspaces = gIPC->workspaces(); + const auto& all_windows = gIPC->windows(); + std::vector my_workspaces; + my_workspaces.reserve(all_workspaces.size()); + for (const auto& ws : all_workspaces) { std::string name; if (ws["name"]) { name = ws["name"].asString(); } else { name = std::to_string(ws["idx"].asUInt()); } - button.set_name("niri-workspace-" + name); - - if (config_["format"].isString()) { - auto format = config_["format"].asString(); - name = fmt::format( - fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name), - fmt::arg("name", ws["name"].asString()), fmt::arg("index", ws["idx"].asUInt()), - fmt::arg("output", ws["output"].asString()), fmt::arg("total", my_workspaces.size()), - fmt::arg("windows", windows_str)); // Added windows arg - } - if (!config_["disable-markup"].asBool()) { - auto* child = gtk_bin_get_child(GTK_BIN(button.gobj())); - if (child != nullptr && GTK_IS_LABEL(child)) - gtk_label_set_markup(GTK_LABEL(child), name.c_str()); - } else { - button.set_label(name); + if (isWorkspaceIgnored(name)) { + continue; } - if (config_["current-only"].asBool()) { - const auto* property = alloutputs ? "is_focused" : "is_active"; - if (ws[property].asBool()) - button.show(); - else - button.hide(); - } else if (config_["hide-empty"].asBool()) { - if (ws["active_window_id"].isNull() && !ws["is_focused"].asBool()) - button.hide(); - else - button.show(); - } else { - button.show(); + if (display_cond == "only-populated") { + if (ws["active_window_id"].isNull() && !ws["is_active"].asBool()) continue; + } else if (display_cond == "keep-named") { + if (ws["name"].isNull() && ws["active_window_id"].isNull() && !ws["is_active"].asBool()) + continue; + } + + if (alloutputs || ws["output"].asString() == bar_.output->name) { + my_workspaces.push_back(&ws); } } - // Refresh the button order. - for (auto it = my_workspaces.cbegin(); it != my_workspaces.cend(); ++it) { - const auto& ws = *it; + sortWorkspaces(my_workspaces); - const auto pos = static_cast(std::distance(my_workspaces.cbegin(), it)); + workspaces_.erase(std::remove_if(workspaces_.begin(), workspaces_.end(), + [&](const std::unique_ptr& w) { + bool gone = std::none_of( + my_workspaces.begin(), my_workspaces.end(), + [&](const Json::Value* ws) { + return ws->operator[]("id").asUInt64() == w->id(); + }); + if (gone) box_.remove(w->button()); + return gone; + }), + workspaces_.end()); - auto& button = buttons_[ws["id"].asUInt64()]; - box_.reorder_child(button, pos); + for (const auto* ws_ptr : my_workspaces) { + const auto& ws = *ws_ptr; + const auto ws_id = ws.isMember("id") ? ws["id"].asUInt64() : 0; + + auto it = + std::find_if(workspaces_.begin(), workspaces_.end(), + [ws_id](const std::unique_ptr& w) { return w->id() == ws_id; }); + + if (it == workspaces_.end()) { + createWorkspace(ws); + it = workspaces_.end() - 1; + } + + std::vector windows_vec(all_windows.begin(), all_windows.end()); + const auto windows_str = getWindowsRepresentation(ws); + (*it)->update(ws, windows_vec, windows_str, my_workspaces.size()); + } + + for (auto pos_it = my_workspaces.cbegin(); pos_it != my_workspaces.cend(); ++pos_it) { + const auto& ws = **pos_it; + const auto ws_id = ws.isMember("id") ? ws["id"].asUInt64() : 0; + + const auto pos = static_cast(std::distance(my_workspaces.cbegin(), pos_it)); + + auto it = + std::find_if(workspaces_.begin(), workspaces_.end(), + [ws_id](const std::unique_ptr& w) { return w->id() == ws_id; }); + if (it != workspaces_.end()) { + box_.reorder_child((*it)->button(), pos); + } } } @@ -275,36 +179,10 @@ void Workspaces::update() { AModule::update(); } -Gtk::Button& Workspaces::addButton(const Json::Value& ws) { - std::string name; - if (ws["name"]) { - name = ws["name"].asString(); - } else { - name = std::to_string(ws["idx"].asUInt()); - } - - auto pair = buttons_.emplace(ws["id"].asUInt64(), name); - auto&& button = pair.first->second; - box_.pack_start(button, false, false, 0); - button.set_relief(Gtk::RELIEF_NONE); - if (!config_["disable-click"].asBool()) { - const auto id = ws["id"].asUInt64(); - button.signal_pressed().connect([=] { - try { - // {"Action":{"FocusWorkspace":{"reference":{"Id":1}}}} - Json::Value request(Json::objectValue); - auto& action = (request["Action"] = Json::Value(Json::objectValue)); - auto& focusWorkspace = (action["FocusWorkspace"] = Json::Value(Json::objectValue)); - auto& reference = (focusWorkspace["reference"] = Json::Value(Json::objectValue)); - reference["Id"] = id; - - IPC::send(request); - } catch (const std::exception& e) { - spdlog::error("Error switching workspace: {}", e.what()); - } - }); - } - return button; +void Workspaces::createWorkspace(const Json::Value& workspace_data) { + auto ws = std::make_unique(workspace_data, *this); + box_.pack_start(ws->button(), false, false, 0); + workspaces_.push_back(std::move(ws)); } // --- Start New Helper Functions --- @@ -383,18 +261,44 @@ std::string Workspaces::getRewrite(const std::string& app_id, const std::string& substitutions["title"] = title; return util::rewriteString(m_windowRewriteDefault, substitutions); } + +// Build the "{windows}" replacement string for a workspace using window-rewrite rules. +std::string Workspaces::getWindowsRepresentation(const Json::Value& ws) { + std::vector window_reps; + auto collect = [&](const Json::Value& win) { + std::string app_id = + win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; + std::string title = + win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; + if (!app_id.empty() || !title.empty()) { + window_reps.push_back(getRewrite(app_id, title)); + } + }; + + if (ws.isMember("windows") && ws["windows"].isArray()) { + for (const auto& win : ws["windows"]) { + collect(win); + } + } else { + // Fallback: collect from global windows list by matching workspace_id. + for (const auto& win : gIPC->windows()) { + if (!win.isMember("workspace_id")) continue; + if (win["workspace_id"].asUInt64() != ws["id"].asUInt64()) continue; + collect(win); + } + } + + return fmt::format("{}", fmt::join(window_reps, m_formatWindowSeparator)); +} // --- End New Helper Functions --- -std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) { +std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) const { const auto& icons = config_["format-icons"]; if (!icons) return value; if (ws["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString(); - if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString(); - if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].asString(); - if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString(); if (ws["name"]) { @@ -458,7 +362,7 @@ bool Workspaces::handleScroll(GdkEventScroll* e) { return true; } -void Workspaces::sortWorkspaces(std::vector& workspaces) const { +void Workspaces::sortWorkspaces(std::vector& workspaces) const { auto get_name = [](const Json::Value& ws) -> std::string { if (ws["name"]) return ws["name"].asString(); return std::to_string(ws["idx"].asUInt()); @@ -471,14 +375,16 @@ void Workspaces::sortWorkspaces(std::vector& workspaces) const { const bool names_are_numeric = std::all_of(workspaces.begin(), workspaces.end(), - [&](const auto& ws) { return is_numeric(get_name(ws)); }); + [&](const auto* ws) { return is_numeric(get_name(*ws)); }); auto compare_numeric_strings = [](const std::string& a, const std::string& b) { if (a.size() != b.size()) return a.size() < b.size(); return a < b; }; - std::sort(workspaces.begin(), workspaces.end(), [&](const auto& a, const auto& b) { + std::sort(workspaces.begin(), workspaces.end(), [&](const auto* ap, const auto* bp) { + const auto& a = *ap; + const auto& b = *bp; if (sort_by_id_) { return a["id"].asUInt64() < b["id"].asUInt64(); } From 810a8c0f941d3b732edf09879bc801d6568d06c4 Mon Sep 17 00:00:00 2001 From: Piotr Wegrzyn Date: Sat, 4 Jul 2026 01:54:30 +0200 Subject: [PATCH 299/416] feat(net): add {linkSpeed} format replacement Adds an ethernet link speed ({linkSpeed}) read from /sys/class/net//speed, plus a skip-decimal option on pow_format to drop a trailing .0. Rebased onto master's store-based network formatting; existing compact bandwidth args updated for the new pow_format signature. --- include/modules/network.hpp | 2 ++ include/util/format.hpp | 28 +++++++++++++++++++--------- man/waybar-network.5.scd | 2 ++ src/modules/network.cpp | 22 ++++++++++++++++++++-- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/include/modules/network.hpp b/include/modules/network.hpp index 2b625f6c..abab16c2 100644 --- a/include/modules/network.hpp +++ b/include/modules/network.hpp @@ -54,6 +54,7 @@ class Network : public ALabel { const std::string getNetworkState() const; void clearIface(); std::optional> readBandwidthUsage(); + uint32_t readLinkSpeed() const; int ifid_{-1}; ip_addr_pref addr_pref_{ip_addr_pref::IPV4}; @@ -94,6 +95,7 @@ class Network : public ALabel { uint8_t signal_strength_; std::string signal_strength_app_; uint32_t route_priority; + uint32_t link_speed_{0}; util::SleeperThread thread_; util::SleeperThread thread_timer_; diff --git a/include/util/format.hpp b/include/util/format.hpp index 2eb148c4..14bf3ef3 100644 --- a/include/util/format.hpp +++ b/include/util/format.hpp @@ -5,12 +5,18 @@ class pow_format { public: - pow_format(long long val, std::string&& unit, bool binary = false, int min_pow_for_decimal = 0) - : val_(val), unit_(unit), binary_(binary), min_pow_for_decimal_(min_pow_for_decimal) {}; + pow_format(long long val, std::string&& unit, bool binary = false, bool skip_decimal = false, + int min_pow_for_decimal = 0) + : val_(val), + unit_(unit), + binary_(binary), + skip_decimal_(skip_decimal), + min_pow_for_decimal_(min_pow_for_decimal) {}; long long val_; std::string unit_; bool binary_; + bool skip_decimal_; int min_pow_for_decimal_; }; @@ -50,17 +56,21 @@ struct formatter { const char* units[] = {"", "k", "M", "G", "T", "P", nullptr}; auto base = s.binary_ ? 1024ull : 1000ll; + auto div = 1ll; auto fraction = (double)s.val_; int pow; for (pow = 0; units[pow + 1] != nullptr && fraction / base >= 1; ++pow) { fraction /= base; + div *= base; } - auto number_width = 5 // coeff in {:.1f} format - + s.binary_; // potential 4th digit before the decimal point - auto max_width = number_width + 1 // prefix from units array - + s.binary_ // for the 'i' in GiB. + auto fixed_precision = (s.skip_decimal_ && ((s.val_ % div) == 0)) ? 0 : 1; + auto number_width = 3 + fixed_precision // coeff in {:.{fixed_precision}f} format + + (fixed_precision != 0) // float dot + + s.binary_; // potential digit before the decimal point + auto max_width = number_width + 1 // prefix from units array + + s.binary_ // for the 'i' in GiB. + s.unit_.length(); const char* format; @@ -71,17 +81,17 @@ struct formatter { case '<': return fmt::format_to(ctx.out(), "{:<{}}", fmt::format("{}", s), max_width); case '=': - format = "{coefficient:<{number_width}.1f}{padding}{prefix}{unit}"; + format = "{coefficient:<{number_width}.{fixed_precision}f}{padding}{prefix}{unit}"; break; case 0: default: format = pow < s.min_pow_for_decimal_ ? "{coefficient:.0f}{prefix}{unit}" - : "{coefficient:.1f}{prefix}{unit}"; + : "{coefficient:.{fixed_precision}f}{prefix}{unit}"; break; } return fmt::format_to( ctx.out(), fmt::runtime(format), fmt::arg("coefficient", fraction), - fmt::arg("number_width", number_width), + fmt::arg("fixed_precision", fixed_precision), fmt::arg("number_width", number_width), fmt::arg("prefix", std::string() + units[pow] + ((s.binary_ && pow) ? "i" : "")), fmt::arg("unit", s.unit_), fmt::arg("padding", pow ? "" diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index d089adff..2a7200fe 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -200,6 +200,8 @@ Addressed by *network* *{rx_bitrate}*: Link receive bitrate (e.g., 866.7 Mb/s). +*{linkSpeed}*: Ethernet link speed. + *{icon}*: Icon, as defined in *format-icons*. # EXAMPLES diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 58922724..f69d4f33 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -83,6 +83,21 @@ waybar::modules::Network::readBandwidthUsage() { return {{receivedBytes, transmittedBytes}}; } +uint32_t waybar::modules::Network::readLinkSpeed() const { + auto path = fmt::format("/sys/class/net/{}/speed", ifname_); + std::ifstream sysfs_speed(path); + + if (!sysfs_speed) return 0; + + uint32_t speed; + sysfs_speed >> speed; + + if (sysfs_speed.bad()) // read fails on incompatible devices + return 0; + + return speed; +} + waybar::modules::Network::Network(const std::string& id, const Json::Value& config) : ALabel(config, "network", id, DEFAULT_FORMAT, 60) { // Start with some "text" in the module's label_. update() will then @@ -329,6 +344,7 @@ auto waybar::modules::Network::update() -> void { elapsed_seconds = std::chrono::duration(interval_).count(); } + link_speed_ = readLinkSpeed(); auto threshold_state = getState(signal_strength_); if (!alt_) { @@ -401,13 +417,14 @@ auto waybar::modules::Network::update() -> void { fmt::arg("bandwidthDownBytes", pow_format(bandwidth_down / elapsed_seconds, "B/s"))); store.push_back(fmt::arg("bandwidthUpBytes", pow_format(bandwidth_up / elapsed_seconds, "B/s"))); store.push_back(fmt::arg("bandwidthDownBytesCompact", - pow_format(bandwidth_down / elapsed_seconds, "B", false, 2))); + pow_format(bandwidth_down / elapsed_seconds, "B", false, false, 2))); store.push_back(fmt::arg("bandwidthUpBytesCompact", - pow_format(bandwidth_up / elapsed_seconds, "B", false, 2))); + pow_format(bandwidth_up / elapsed_seconds, "B", false, false, 2))); store.push_back(fmt::arg("bandwidthTotalBytes", pow_format((bandwidth_up + bandwidth_down) / elapsed_seconds, "B/s"))); store.push_back(fmt::arg("rxBitrate", pow_format(rx_bitrate_, "b/s"))); store.push_back(fmt::arg("txBitrate", pow_format(tx_bitrate_, "b/s"))); + store.push_back(fmt::arg("linkSpeed", pow_format(link_speed_ * 1000000ull, "b/s", false, true))); auto text = fmt::vformat(format_, store); if (setLabelMarkup(text)) { @@ -511,6 +528,7 @@ void waybar::modules::Network::clearIface() { signal_strength_dbm_ = 0; signal_strength_ = 0; signal_strength_app_.clear(); + link_speed_ = 0; frequency_ = 0.0; rx_bitrate_ = 0; tx_bitrate_ = 0; From f72f84e01101ba0d38e0624e528728b7ecb3f1cf Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 02:10:39 +0200 Subject: [PATCH 300/416] fix: backward-compat + bug fixes and man-page build fix for 0.16.0 Post-0.15.0 review of the 0.15.0..HEAD range surfaced regressions and bugs. This restores backward compatibility for existing configs/CSS, fixes confirmed defects, and repairs the scdoc man-page build break on master. Pango-markup tooltips are intentional and were kept. Backward-compat restorations: - AModule: honor legacy numeric Gdk::CursorType cursor values (int overload) - memory: correct GiB divisor (was ~2.3% low); round bare {} placeholders - wireplumber: scale max-volume into the linear domain so the cap works again - idle_inhibitor: gate right/middle-click deactivate & scroll on dynamic-timeouts; accept both dynamic-timeout(s); widen timeout to double (no fractional truncation) - custom: keep #custom-. CSS selectors working (classes on box_) - image: don't wordexp-split a single path; fall back to the literal path - niri/window: restore hide-when-empty (new show-empty opt-in); escape tooltip - wlr/taskbar: plain-text tooltip when markup is disabled Bug fixes: - tray: fix use-after-free in onAdd; guard the watcher retry timeout - hyprland: clamp max-windows iterator (OOB); drop duplicate language tooltip block - niri/window: supply {col}/{max_col} args in the empty branch (fmt::format_error) - mpris: escape {dynamic}/{player} tooltip; fix dangling player; albumArtist source - mango: fix use-after-free race (dispatch under callback_mutex_) - mpd: contain throwing checkErrors in noexcept idle paths (no std::terminate/UAF) - keyboard_state: always render every lock label, with guarded defaults - bluetooth: bound GATT ReadValue timeout, opt-in + services-resolved gating, preserve authoritative Battery1 percentage - wireplumber: fix WpDevice reference leak / NULL handling - battery, clock, dwl, wayfire, graph, custom_graph, transform, river: assorted crash/logic fixes Man page / build: - niri-workspaces: fix scdoc "indented by an amount greater than 1" (workspace-taskbar sub-options were mis-indented; breaks man-page build) - document new show-empty (niri/window); correct network {txBitrate}/{rxBitrate} Not compiled locally (no gtkmm on this host); C++ build relies on CI. Man pages validated with scdoc 1.11.4. Co-Authored-By: Claude Opus 4.8 (1M context) --- include/AGraph.hpp | 2 +- include/AModule.hpp | 2 + include/modules/idle_inhibitor.hpp | 6 +-- man/waybar-idle-inhibitor.5.scd | 2 +- man/waybar-network.5.scd | 4 +- man/waybar-niri-window.5.scd | 5 +++ man/waybar-niri-workspaces.5.scd | 16 ++++---- man/waybar-styles.5.scd.in | 5 +++ src/AGraph.cpp | 12 ++++-- src/AIconLabel.cpp | 17 ++++++--- src/AModule.cpp | 25 +++++++++++++ src/modules/battery.cpp | 2 +- src/modules/bluetooth.cpp | 37 +++++++++++++++++-- src/modules/clock.cpp | 8 +++- src/modules/custom.cpp | 30 +++++++++++++-- src/modules/custom_graph.cpp | 21 +++++++++++ src/modules/dwl/window.cpp | 4 +- src/modules/hyprland/language.cpp | 57 +++++++++++------------------ src/modules/hyprland/workspace.cpp | 3 +- src/modules/idle_inhibitor.cpp | 25 ++++++++++--- src/modules/image.cpp | 29 +++++++++++---- src/modules/keyboard_state.cpp | 25 ++++--------- src/modules/mango/backend.cpp | 7 +--- src/modules/memory/common.cpp | 31 +++++++++------- src/modules/mpd/mpd.cpp | 2 +- src/modules/mpd/state.cpp | 54 ++++++++++++++++----------- src/modules/mpris/mpris.cpp | 9 +++-- src/modules/niri/window.cpp | 16 +++++--- src/modules/river/tags.cpp | 16 +++++--- src/modules/sni/host.cpp | 18 +++++++-- src/modules/sni/tray.cpp | 7 +++- src/modules/wayfire/backend.cpp | 34 +++++++++-------- src/modules/wireplumber.cpp | 27 ++++++++++---- src/modules/wlr/taskbar.cpp | 2 +- src/util/transform_8bit_to_rgba.cpp | 9 +++-- 35 files changed, 377 insertions(+), 192 deletions(-) diff --git a/include/AGraph.hpp b/include/AGraph.hpp index e2ada835..e65cd450 100644 --- a/include/AGraph.hpp +++ b/include/AGraph.hpp @@ -28,7 +28,7 @@ class AGraph : public AModule { void addValue(const int n); - const std::chrono::seconds interval_; + const std::chrono::milliseconds interval_; bool onDraw(const Cairo::RefPtr& cr); diff --git a/include/AModule.hpp b/include/AModule.hpp index 659aa68e..e99bec9c 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -86,6 +86,8 @@ class AModule : public IModule { Gtk::EventBox event_box_; virtual void setCursor(std::string const& c); + // Backward-compat overload for legacy numeric Gdk::CursorType configs (pre-0.16) + virtual void setCursor(Gdk::CursorType const& c); virtual bool handleToggle(GdkEventButton* const& ev); virtual bool handleMouseEnter(GdkEventCrossing* const& ev); diff --git a/include/modules/idle_inhibitor.hpp b/include/modules/idle_inhibitor.hpp index 5fbfbc5b..8fef1985 100644 --- a/include/modules/idle_inhibitor.hpp +++ b/include/modules/idle_inhibitor.hpp @@ -38,9 +38,9 @@ class IdleInhibitor : public ALabel { struct zwp_idle_inhibitor_v1* idle_inhibitor_; int pid_; - bool dynamicTimeout; - short timeout; - short timeout_step; + bool dynamicTimeout = false; + double timeout; + double timeout_step; bool wait_for_activity_; }; diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 4fd5f04f..8cbba56e 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -76,7 +76,7 @@ screensaver, also known as "presentation mode". typeof: double ++ The number of minutes the inhibition should last. -*timeout_step*: ++ +*timeout-step*: ++ typeof: double ++ default: 10 ++ The number of minutes to add or subtract when scrolling (when dynamic timeouts are enabled). diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index 2a7200fe..a7860e5c 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -196,9 +196,9 @@ Addressed by *network* *{bandwidthTotalBytes}*: Instant total speed in bytes/seconds. -*{tx_bitrate}*: Link transmit bitrate (e.g., 866.7 Mb/s). +*{txBitrate}*: Link transmit bitrate (e.g., 866.7 Mb/s). -*{rx_bitrate}*: Link receive bitrate (e.g., 866.7 Mb/s). +*{rxBitrate}*: Link receive bitrate (e.g., 866.7 Mb/s). *{linkSpeed}*: Ethernet link speed. diff --git a/man/waybar-niri-window.5.scd b/man/waybar-niri-window.5.scd index c836f057..784c3b39 100644 --- a/man/waybar-niri-window.5.scd +++ b/man/waybar-niri-window.5.scd @@ -40,6 +40,11 @@ Addressed by *niri/window* default: false ++ Enables this module to consume all left over space dynamically. +*show-empty*: ++ + typeof: bool ++ + default: false ++ + When no window is focused (empty workspace or all windows closed), keep the module visible and render *format* with empty {title}/{app_id} and {col}/{max_col} set to -1. When false (the default), the module is hidden while no window is focused. + # FORMAT REPLACEMENTS See the output of "niri msg windows" for examples diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 0627776c..d10691e8 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -96,15 +96,15 @@ If none of the sorting options are enabled, workspaces keep their output/index o typeof: object ++ Contains settings for the workspace taskbar, which displays app icons within each workspace. - *enable*: ++ - typeof: bool ++ - default: false ++ - Enables the workspace taskbar mode. + *enable*: ++ + typeof: bool ++ + default: false ++ + Enables the workspace taskbar mode. - *icon-size*: ++ - typeof: int ++ - default: 18 ++ - Size of the icons in the workspace taskbar. + *icon-size*: ++ + typeof: int ++ + default: 18 ++ + Size of the icons in the workspace taskbar. *ignore-workspaces*: ++ typeof: array ++ diff --git a/man/waybar-styles.5.scd.in b/man/waybar-styles.5.scd.in index ca9ee0a5..f01d4cf0 100644 --- a/man/waybar-styles.5.scd.in +++ b/man/waybar-styles.5.scd.in @@ -67,6 +67,11 @@ see the cursor-shape-v1 protocol for all possible cursor types: https://wayland.app/protocols/cursor-shape-v1#wp_cursor_shape_device_v1:enum:shape. Depending on the compositor and cursor theme used, cursors not listed in the protocol may also work. +Prior to 0.16.0 the `cursor` option accepted a numeric _Gdk::CursorType_ value +(e.g. `"cursor": 8`). Numeric values are deprecated but still honored for +backward compatibility; a cursor-shape-v1 name string (e.g. `"grab"`) is now +preferred. + Example of disabling the cursor on a custom module: ``` diff --git a/src/AGraph.cpp b/src/AGraph.cpp index f79e7218..ea1f754d 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -17,10 +18,13 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st : AModule(config, name, id, config["format-alt"].isString() || config["menu"].isString() || enable_click, enable_scroll), - interval_(config_["interval"] == "once" - ? std::chrono::seconds::max() - : std::chrono::seconds( - config_["interval"].isUInt() ? config_["interval"].asUInt() : interval)) { + interval_( + config_["interval"] == "once" + ? std::chrono::milliseconds::max() + : std::chrono::milliseconds( + config_["interval"].isNumeric() + ? std::max(1L, static_cast(config_["interval"].asDouble() * 1000)) + : 1000L * static_cast(interval))) { graph_.signal_draw().connect(sigc::mem_fun(*this, &AGraph::onDraw)); graph_.set_name(name); if (!id.empty()) { diff --git a/src/AIconLabel.cpp b/src/AIconLabel.cpp index c17eb8d4..a54266ac 100644 --- a/src/AIconLabel.cpp +++ b/src/AIconLabel.cpp @@ -90,13 +90,18 @@ auto AIconLabel::update() -> void { label_.set_markup(cleanLabel); if (iconLabel.front() == '/') { - int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); - auto pixbuf = Gdk::Pixbuf::create_from_file(iconLabel, scaled_icon_size, scaled_icon_size); + try { + int scaled_icon_size = app_icon_size_ * image_.get_scale_factor(); + auto pixbuf = Gdk::Pixbuf::create_from_file(iconLabel, scaled_icon_size, scaled_icon_size); - auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), - image_.get_window()); - image_.set(surface); - image_.set_visible(true); + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), + image_.get_window()); + image_.set(surface); + image_.set_visible(true); + } catch (const Glib::Exception& e) { + spdlog::warn("Failed to load embedded icon {}: {}", iconLabel, std::string(e.what())); + image_.set_visible(false); + } } else { image_.set_from_icon_name(iconLabel, Gtk::ICON_SIZE_INVALID); image_.set_visible(true); diff --git a/src/AModule.cpp b/src/AModule.cpp index 407d5a66..b53f6cee 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -85,6 +85,13 @@ AModule::AModule(const Json::Value& config, const std::string& name, const std:: } } else if (config_["cursor"].isString()) { setCursor(config_["cursor"].asString()); + } else if (config_["cursor"].isInt() || config_["cursor"].isUInt()) { + // Backward-compat: legacy numeric Gdk::CursorType values (pre-0.16) + setCursor(Gdk::CursorType(config_["cursor"].asInt())); + spdlog::warn( + "Numeric 'cursor' values are deprecated; use a cursor-shape-v1 name string instead " + "(module {})", + name_); } else { spdlog::warn("unknown cursor option configured on module {}", name_); } @@ -140,6 +147,24 @@ void AModule::setCursor(std::string const& c) { } } +// Backward-compat overload: honor legacy numeric Gdk::CursorType configs (pre-0.16) +void AModule::setCursor(Gdk::CursorType const& c) { + auto gdk_window = event_box_.get_window(); + if (gdk_window) { + auto cursor = Gdk::Cursor::create(gdk_window->get_display(), c); + gdk_window->set_cursor(cursor); + } else { + // window may not be accessible yet, in this case, + // schedule another call for setting the cursor in 1 sec + cursor_timeout_conn_ = Glib::signal_timeout().connect_seconds( + [this, c]() { + setCursor(c); + return false; + }, + 1); + } +} + bool AModule::handleMouseEnter(GdkEventCrossing* const& e) { if (auto* module = event_box_.get_child(); module != nullptr) { module->set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); diff --git a/src/modules/battery.cpp b/src/modules/battery.cpp index cec8beb8..ffa6ff39 100644 --- a/src/modules/battery.cpp +++ b/src/modules/battery.cpp @@ -609,7 +609,7 @@ waybar::modules::Battery::getInfos() { if (status == "Discharging" && time_to_empty_now_exists) { if (time_to_empty_now != 0) time_remaining = (float)time_to_empty_now / 3600.0f; } else if (status == "Discharging" && total_power_exists && total_energy_exists) { - if (smooth_power_ != 0) time_remaining = (float)total_energy / smooth_power_; + if (total_power != 0) time_remaining = (float)total_energy / smooth_power_; } else if (status == "Charging" && time_to_full_now_exists) { if (time_to_full_now_exists && (time_to_full_now != 0)) time_remaining = -(float)time_to_full_now / 3600.0f; diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 7436967d..b092c29c 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -87,14 +87,29 @@ auto isChildPath(const std::string& child, const std::string& parent) -> bool { return child.starts_with(parent); } +// Returns true only if some configured format/tooltip string actually references the peripheral +// battery placeholder. The GATT battery scan issues over-the-air BLE reads, so it must stay +// opt-in: users who don't display {device_battery_percentage_peripheral} pay zero cost. +auto configUsesPeripheralBattery(const Json::Value& config) -> bool { + for (const auto& key : config.getMemberNames()) { + if (config[key].isString() && + config[key].asString().find("device_battery_percentage_peripheral") != std::string::npos) { + return true; + } + } + return false; +} + auto readBatteryCharacteristicValue(GDBusProxy* proxy_char) -> std::optional { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); GError* error = nullptr; + // Use a small finite timeout (2s) instead of the default (-1 == 25s) so a sleeping or + // dropped BLE peripheral cannot stall the Waybar main thread for ~25s on a synchronous read. GVariant* gvar = g_dbus_proxy_call_sync(proxy_char, "ReadValue", g_variant_new("(a{sv})", &builder), - G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + G_DBUS_CALL_FLAGS_NONE, 2000, nullptr, &error); if (error != nullptr) { g_error_free(error); return std::nullopt; @@ -538,7 +553,9 @@ auto waybar::modules::Bluetooth::processBatteryServiceCharacteristics( if (hasUserDescriptionDescriptor(objects, char_path, user_description_uuid)) { peripheral_battery = battery_value.value(); - } else { + } else if (!central_battery.has_value()) { + // Only fill the central sink once so multiple non-described 0x2a19 characteristics don't + // clobber each other (order-dependent last-writer-wins) or an already-set Battery1 value. central_battery = battery_value.value(); } } @@ -564,8 +581,20 @@ auto waybar::modules::Bluetooth::getDeviceProperties(GDBusObject* object, Device g_object_unref(proxy_device); device_info.battery_percentage = getDeviceBatteryPercentage(object); - getDeviceGattBatteryLevels(object, device_info.battery_percentage, - device_info.battery_percentage_peripheral); + // Only perform the (synchronous, over-the-air) GATT battery scan when the peripheral battery + // placeholder is actually used and the device's services are resolved. This keeps the scan off + // the frequent property-changed hot path (RSSI/TxPower updates leave ServicesResolved false or + // unchanged) and opt-in for split-keyboard style peripherals. + if (device_info.services_resolved && configUsesPeripheralBattery(config_)) { + // Read the GATT central level into a separate local; only fall back to it when the + // authoritative org.bluez.Battery1 percentage is absent, so the GATT scan can never overwrite + // the Battery1 value used by {device_battery_percentage}. + std::optional gatt_central; + getDeviceGattBatteryLevels(object, gatt_central, device_info.battery_percentage_peripheral); + if (!device_info.battery_percentage.has_value()) { + device_info.battery_percentage = gatt_central; + } + } return true; } diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 89ae5eba..016d1d75 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -568,8 +568,12 @@ void waybar::modules::Clock::tz_down() { tzCurrIdx_ = (tzCurrIdx_ == 0) ? tzSize - 1 : tzCurrIdx_ - 1; } void waybar::modules::Clock::action_exec(const std::string& action) { - auto cmd = action.substr(strlen("exec ")); - pid_children_.push_back(util::command::forkExec(cmd)); + const auto pos = action.find(" "); + if (pos == std::string::npos || action.find_first_not_of(" ", pos) == std::string::npos) { + spdlog::error("Clock: exec action requires a command argument"); + return; + } + pid_children_.push_back(util::command::forkExec(action.substr(pos + 1))); } #ifdef HAVE_LANGINFO_1STDAY diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index bdd9aa65..01781036 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -223,6 +223,17 @@ auto waybar::modules::Custom::update() -> void { for (auto const& c : class_) { style->add_class(c); } + // Mirror the dynamic script classes onto box_, which now carries the + // #custom- widget name (see AIconLabel), so #custom-. + // CSS selectors keep resolving as they did in 0.15.0. + auto box_style = box_.get_style_context(); + for (auto const& c : box_style->list_classes()) { + if (c == id_ || c == MODULE_CLASS) continue; + box_style->remove_class(c); + } + for (auto const& c : class_) { + box_style->add_class(c); + } style->add_class("flat"); style->add_class("text-button"); style->add_class(MODULE_CLASS); @@ -230,14 +241,20 @@ auto waybar::modules::Custom::update() -> void { image_style->add_class("image-button"); event_box_.show(); if (!image_path_.empty()) { - auto pixbuf = Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); - image_.set(pixbuf); + try { + auto pixbuf = + Gdk::Pixbuf::create_from_file(image_path_, app_icon_size_, app_icon_size_); + image_.set(pixbuf); + } catch (const Glib::Error& e) { + spdlog::warn("custom {}: failed to load image-path '{}': {}", name_, image_path_, + std::string(e.what())); + image_.clear(); + } } else if (!image_name_.empty()) { image_.set_from_icon_name(image_name_, Gtk::ICON_SIZE_INVALID); image_.set_pixel_size(app_icon_size_); } - image_.set_visible(!image_name_.empty() || !image_path_.empty()); label_.set_visible(!str.empty()); } } catch (const fmt::format_error& e) { @@ -251,6 +268,13 @@ auto waybar::modules::Custom::update() -> void { } // Call parent update AIconLabel::update(); + + // Show a configured image-path/image-name image after the base update() so + // AIconLabel::update()'s icon gate cannot re-hide it. Leave the embedded-icon + // and "icon" cases to the base class (they have no image-path/image-name). + if (!image_name_.empty() || !image_path_.empty()) { + image_.set_visible(true); + } } void waybar::modules::Custom::parseOutputRaw() { diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index 2a12f0c1..0f236304 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -2,6 +2,11 @@ #include +#include +#include +#include +#include + #include "util/scope_guard.hpp" waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::string& id, @@ -228,6 +233,22 @@ void waybar::modules::CustomGraph::parseOutputRaw() { tooltip_ = validated_line; } class_.clear(); + // Derive the graph value from the first line's leading numeric value, + // since the raw (i3blocks) format has no dedicated percentage field. + std::string value = validated_line; + if (!value.empty() && value.back() == '%') { + value.pop_back(); + } + try { + double parsed = std::stod(value); + if (std::isnan(parsed)) { + percentage_ = 0; + } else { + percentage_ = static_cast(std::clamp(std::lround(parsed), 0L, 100L)); + } + } catch (const std::exception&) { + percentage_ = 0; + } } else if (i == 1) { if (config_["escape"].isBool() && config_["escape"].asBool()) { tooltip_ = Glib::Markup::escape_text(validated_line); diff --git a/src/modules/dwl/window.cpp b/src/modules/dwl/window.cpp index 15104b2e..677d2ca0 100644 --- a/src/modules/dwl/window.cpp +++ b/src/modules/dwl/window.cpp @@ -138,9 +138,7 @@ void Window::handle_frame() { box_.set_visible(true); } else { box_.get_style_context()->remove_class("active"); - if (hide_inactive_) { - box_.set_visible(false); - } + box_.set_visible(!hide_inactive_); } } } diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index dfe24b0a..9c082d3a 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -53,24 +53,29 @@ auto Language::update() -> void { std::string tooltipContent = std::string{}; bool tooltip_enabled = tooltipEnabled(); if (tooltip_enabled) { - if (config_.isMember("tooltip-format")) { - auto tooltip_format = config_["tooltip-format"].asString(); - if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) { - const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant; - tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); - } else if (config_.isMember("tooltip-format-" + layout_.short_description)) { - const auto propName = "tooltip-format-" + layout_.short_description; - tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString()); - } else { - tooltipContent = - trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), - fmt::arg("short", layout_.short_name), - fmt::arg("shortDescription", layout_.short_description), - fmt::arg("variant", layout_.variant))); - } + // Default to "{long}" when no tooltip-format is provided, matching the man page + auto tooltip_format = + config_.isMember("tooltip-format") ? config_["tooltip-format"].asString() : "{long}"; + if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) { + const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant; + tooltipContent = trim(fmt::format(fmt::runtime(tooltip_format), config_[propName].asString(), + fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); + } else if (config_.isMember("tooltip-format-" + layout_.short_description)) { + const auto propName = "tooltip-format-" + layout_.short_description; + tooltipContent = trim(fmt::format(fmt::runtime(tooltip_format), config_[propName].asString(), + fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); } else { - // if no tooltip format is provided, use the same text as the module - tooltipContent = layoutName; + tooltipContent = + trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name), + fmt::arg("short", layout_.short_name), + fmt::arg("shortDescription", layout_.short_description), + fmt::arg("variant", layout_.variant))); } spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent); } @@ -85,24 +90,6 @@ auto Language::update() -> void { label_.hide(); } - // Tooltip support - if (tooltipEnabled()) { - std::string tooltipFormat; - if (config_["tooltip-format"].isString()) { - tooltipFormat = config_["tooltip-format"].asString(); - } else { - tooltipFormat = "{long}"; - } - auto tooltipText = - trim(fmt::format(fmt::runtime(tooltipFormat), fmt::arg("long", layout_.full_name), - fmt::arg("short", layout_.short_name), - fmt::arg("shortDescription", layout_.short_description), - fmt::arg("variant", layout_.variant))); - label_.set_tooltip_text(tooltipText); - } else { - label_.set_tooltip_text(""); - } - ALabel::update(); } diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index ade7a10f..79ff5c3d 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -438,7 +438,8 @@ void Workspace::update(const std::string& workspace_icon, const std::string& wor auto windowSeparator = m_workspaceManager.getWindowSeparator(); auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold(); - auto end_it = m_workspaceManager.maxWindows() == 0 + auto end_it = (m_workspaceManager.maxWindows() <= 0 || + static_cast(m_workspaceManager.maxWindows()) >= m_windowMap.size()) ? m_windowMap.end() : m_windowMap.begin() + m_workspaceManager.maxWindows(); diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 525095fa..170701ce 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -44,7 +44,14 @@ waybar::modules::IdleInhibitor::IdleInhibitor(const std::string& id, const Bar& event_box_.signal_button_press_event().connect( sigc::mem_fun(*this, &IdleInhibitor::handleToggle)); - event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &IdleInhibitor::handleScroll)); + // Only connect our own scroll handler when the user hasn't configured on-scroll-* + // commands. When on-scroll-* is set, AModule already connects a scroll handler that + // (via virtual dispatch) reaches IdleInhibitor::handleScroll; a second connection here + // would fire handleScroll twice per scroll event. + if (!(config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString() || + config_["on-scroll-left"].isString() || config_["on-scroll-right"].isString())) { + event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &IdleInhibitor::handleScroll)); + } // Add this to the modules list waybar::modules::IdleInhibitor::modules.push_back(this); @@ -157,8 +164,11 @@ void waybar::modules::IdleInhibitor::toggleStatus(int force_status) { } bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { + // Accept both the documented "dynamic-timeouts" (plural) and the legacy + // "dynamic-timeout" (singular) key spellings. + const bool dynamic = config_["dynamic-timeouts"].asBool() || config_["dynamic-timeout"].asBool(); if (e->button == 1) { - if (config_["dynamic-timeout"].asBool()) { + if (dynamic) { toggleStatus(1); } else { toggleStatus(); @@ -171,7 +181,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } } } - if (e->button == 3) { + if (e->button == 3 && dynamic) { toggleStatus(0); // Make all other idle inhibitor modules update @@ -181,7 +191,7 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } } } - if (e->button == 2) { + if (e->button == 2 && dynamic) { toggleStatus(0); timeout = config_["timeout"].asDouble(); } @@ -190,8 +200,11 @@ bool waybar::modules::IdleInhibitor::handleToggle(GdkEventButton* const& e) { } bool waybar::modules::IdleInhibitor::handleScroll(GdkEventScroll* e) { - if (!config_["dynamic-timeout"].asBool()) { - return true; + // Accept both the documented "dynamic-timeouts" (plural) and the legacy + // "dynamic-timeout" (singular) key spellings. + if (!(config_["dynamic-timeouts"].asBool() || config_["dynamic-timeout"].asBool())) { + // Delegate to the base handler so any configured on-scroll-* command still runs. + return ALabel::handleScroll(e); } auto dir = AModule::getScrollDir(e); if (dir == SCROLL_DIR::NONE) { diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 6372dd4e..7c733880 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -126,7 +126,12 @@ void MultipleImageStrategy::setupAndDraw() { bool has_onclick = !data.on_click.empty(); Glib::RefPtr pixbuf; - pixbuf = Gdk::Pixbuf::create_from_file(path, size_, size_); + try { + pixbuf = Gdk::Pixbuf::create_from_file(path, size_, size_); + } catch (const Glib::Error& e) { + spdlog::error("failed to load image '{}': {}", path, std::string(e.what())); + pixbuf.reset(); // fall through to the .empty branch + } if (has_onclick) { auto btn = images_data_[i].btn; @@ -234,22 +239,32 @@ SingleImageStrategy::SingleImageStrategy(const std::string& id, const Json::Valu void SingleImageStrategy::update() { if (config_["path"].isString()) { - auto result = Config::tryExpandPath(config_["path"].asString(), ""); - path_ = result.empty() ? "" : result.front(); + auto p = config_["path"].asString(); + auto result = Config::tryExpandPath(p, ""); + // Only use the expanded path when it resolves to exactly one existing match; + // otherwise keep the literal path so paths with spaces/metacharacters still work. + path_ = (result.size() == 1) ? result.front() : p; } else if (config_["exec"].isString()) { output_ = util::command::exec(config_["exec"].asString(), ""); parseOutputRaw(); // expand path if "~" or "$HOME" is present in original path auto result = Config::tryExpandPath(path_, ""); - path_ = result.empty() ? "" : result.front(); + path_ = (result.size() == 1) ? result.front() : path_; } + Glib::RefPtr pixbuf; if (Glib::file_test(path_, Glib::FILE_TEST_EXISTS)) { - Glib::RefPtr pixbuf; - int scaled_icon_size = size_ * image_.get_scale_factor(); - pixbuf = Gdk::Pixbuf::create_from_file(path_, scaled_icon_size, scaled_icon_size); + try { + pixbuf = Gdk::Pixbuf::create_from_file(path_, scaled_icon_size, scaled_icon_size); + } catch (const Glib::Exception& e) { + // Existing but corrupt/non-image file: degrade to the empty state instead of crashing. + spdlog::warn("Failed to load image {}: {}", path_, std::string(e.what())); + pixbuf.reset(); + } + } + if (pixbuf) { auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), image_.get_window()); image_.set(surface); diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 49cf863f..7e43c74c 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -86,7 +86,6 @@ auto isCommonFormatIcons(const Json::Value& config) -> bool { auto keyStateToIcons(const Json::Value& config) -> std::unordered_map> { std::unordered_map> key_icon_states; - std::vector default_icons = {"unlocked", "locked"}; if (isCommonFormatIcons(config)) { std::vector icons = { @@ -100,26 +99,18 @@ auto keyStateToIcons(const Json::Value& config) return key_icon_states; } - bool found_any = false; + const auto& format_icons = config["format-icons"]; for (const auto& key : std::vector{"numlock", "capslock", "scrolllock"}) { std::string map_key = key.substr(0, key.length() - 4); map_key[0] = std::toupper(map_key[0]); - if (config["format-icons"].isObject() && config["format-icons"][key].isObject()) { - std::string unlocked = config["format-icons"][key]["unlocked"].isString() - ? config["format-icons"][key]["unlocked"].asString() - : "unlocked"; - std::string locked = config["format-icons"][key]["locked"].isString() - ? config["format-icons"][key]["locked"].asString() - : "locked"; - key_icon_states[map_key] = {unlocked, locked}; - found_any = true; + std::string unlocked = "unlocked"; + std::string locked = "locked"; + if (format_icons.isObject() && format_icons[key].isObject()) { + const auto& obj = format_icons[key]; + if (obj["unlocked"].isString()) unlocked = obj["unlocked"].asString(); + if (obj["locked"].isString()) locked = obj["locked"].asString(); } - } - - if (!found_any) { - key_icon_states["Num"] = default_icons; - key_icon_states["Caps"] = default_icons; - key_icon_states["Scroll"] = default_icons; + key_icon_states[map_key] = {unlocked, locked}; } return key_icon_states; diff --git a/src/modules/mango/backend.cpp b/src/modules/mango/backend.cpp index a8976f21..5083a7fc 100644 --- a/src/modules/mango/backend.cpp +++ b/src/modules/mango/backend.cpp @@ -204,20 +204,15 @@ void IPC::parseIPC(const std::string& line) { } } - std::vector handlers_to_notify; { std::lock_guard lock(callback_mutex_); for (auto& [ev, handler] : callbacks_) { if (ev == "monitor") { - handlers_to_notify.push_back(handler); + handler->onEvent(root); } } } - for (auto* handler : handlers_to_notify) { - handler->onEvent(root); - } - return; } diff --git a/src/modules/memory/common.cpp b/src/modules/memory/common.cpp index 626376e4..610c3830 100644 --- a/src/modules/memory/common.cpp +++ b/src/modules/memory/common.cpp @@ -1,14 +1,14 @@ +#include + #include "modules/memory.hpp" namespace { -const std::unordered_map kUnits = {{"kB", 1.000}, - {"kiB", 1.024}, - {"MB", 1.000 * 1000.0}, - {"MiB", 1.024 * 1024.0}, - {"GB", 1.000 * 1000.0 * 1000.0}, - {"GiB", 1.024 * 1024.0 * 1024.0}, - {"TB", 1.000 * 1000.0 * 1000.0 * 1000.0}, - {"TiB", 1.024 * 1024.0 * 1024.0 * 1024.0}}; +// /proc/meminfo reports values in kiB (1024 bytes) despite the 'kB' label. +// These divisors convert a kiB count to the requested unit. +const std::unordered_map kUnits = { + {"B", 1.0 / 1024.0}, {"kB", 1000.0 / 1024.0}, {"kiB", 1.0}, + {"MB", 1000000.0 / 1024.0}, {"MiB", 1024.0}, {"GB", 1000000000.0 / 1024.0}, + {"GiB", 1024.0 * 1024.0}, {"TB", 1e12 / 1024.0}, {"TiB", 1024.0 * 1024.0 * 1024.0}}; } waybar::modules::Memory::Memory(const std::string& id, const Json::Value& config) @@ -57,12 +57,15 @@ auto waybar::modules::Memory::update() -> void { } float divisor = kUnits.at(unit_); - float total_ram = memtotal / divisor; - float total_swap = swaptotal / divisor; - float used_ram = (memtotal - memfree) / divisor; - float used_swap = (swaptotal - swapfree) / divisor; - float available_ram = memfree / divisor; - float available_swap = swapfree / divisor; + // Pre-round to 2 decimals so bare {total}/{used}/{avail} placeholders print + // cleanly, matching the 0.15.0 behavior (an explicit spec like {used:.1f} + // still overrides this). + float total_ram = 0.01f * std::round((memtotal / divisor) * 100.0f); + float total_swap = 0.01f * std::round((swaptotal / divisor) * 100.0f); + float used_ram = 0.01f * std::round(((memtotal - memfree) / divisor) * 100.0f); + float used_swap = 0.01f * std::round(((swaptotal - swapfree) / divisor) * 100.0f); + float available_ram = 0.01f * std::round((memfree / divisor) * 100.0f); + float available_swap = 0.01f * std::round((swapfree / divisor) * 100.0f); auto format = format_; auto state = getState(used_ram_percentage); diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 407fa599..106c54eb 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -28,7 +28,7 @@ waybar::modules::MPD::MPD(const std::string& id, const Json::Value& config) connection_(nullptr, &mpd_connection_free), status_(nullptr, &mpd_status_free), song_(nullptr, &mpd_song_free), - ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "\u2026") { + ellipsis_(config_["ellipsis"].isString() ? config_["ellipsis"].asString() : "") { if (!config_["port"].isNull() && !config_["port"].isUInt()) { spdlog::warn("{}: `port` configuration should be an unsigned int", module_name_); } diff --git a/src/modules/mpd/state.cpp b/src/modules/mpd/state.cpp index 16cd529b..834f97c9 100644 --- a/src/modules/mpd/state.cpp +++ b/src/modules/mpd/state.cpp @@ -58,16 +58,21 @@ void Idle::entry() noexcept { auto conn = ctx_->connection().get(); assert(conn != nullptr); - if (!mpd_send_idle_mask( - conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { - ctx_->checkErrors(conn); - spdlog::error("mpd: Idle: failed to register for IDLE events"); - } else { - spdlog::debug("mpd: Idle: watching FD"); - sigc::slot idle_slot = sigc::mem_fun(*this, &Idle::on_io); - idle_connection_ = - Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), - Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + try { + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Idle: failed to register for IDLE events"); + } else { + spdlog::debug("mpd: Idle: watching FD"); + sigc::slot idle_slot = sigc::mem_fun(*this, &Idle::on_io); + idle_connection_ = + Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), + Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + } + } catch (std::exception const& e) { + spdlog::warn("mpd: Idle: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); } } @@ -114,8 +119,10 @@ bool Idle::on_io(Glib::IOCondition const&) { void Playing::entry() noexcept { timer(); - idle(); spdlog::debug("mpd: Playing: enabled {}ms periodic timer.", ctx_->playing_interval()); + // NB: idle() may transition to Disconnected (destroying *this) on error, so it + // must be the last statement here — do not access members after this call. + idle(); } void Playing::exit() noexcept { @@ -143,16 +150,21 @@ void Playing::idle() noexcept { auto conn = ctx_->connection().get(); assert(conn != nullptr); - if (!mpd_send_idle_mask( - conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { - ctx_->checkErrors(conn); - spdlog::error("mpd: Playing: failed to register for IDLE events"); - } else if (!idle_connection_.connected()) { - spdlog::trace("mpd: Playing: watching FD"); - sigc::slot idle_slot = sigc::mem_fun(*this, &Playing::on_io); - idle_connection_ = - Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), - Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + try { + if (!mpd_send_idle_mask( + conn, static_cast(MPD_IDLE_PLAYER | MPD_IDLE_OPTIONS | MPD_IDLE_QUEUE))) { + ctx_->checkErrors(conn); + spdlog::error("mpd: Playing: failed to register for IDLE events"); + } else if (!idle_connection_.connected()) { + spdlog::trace("mpd: Playing: watching FD"); + sigc::slot idle_slot = sigc::mem_fun(*this, &Playing::on_io); + idle_connection_ = + Glib::signal_io().connect(idle_slot, mpd_connection_get_fd(conn), + Glib::IO_IN | Glib::IO_PRI | Glib::IO_ERR | Glib::IO_HUP); + } + } catch (std::exception const& e) { + spdlog::warn("mpd: Playing: error: {}", e.what()); + ctx_->setState(std::make_unique(ctx_)); } } diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index c3e0235e..7eb11594 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -385,6 +385,7 @@ auto Mpris::onPlayerNameAppeared(PlayerctlPlayerManager* manager, PlayerctlPlaye if (mpris->player != nullptr) { g_signal_handlers_disconnect_by_data(mpris->player, mpris); + if (mpris->last_active_player_ == mpris->player) mpris->last_active_player_ = nullptr; g_clear_object(&mpris->player); } mpris->player = playerctl_player_new_from_name(player_name, nullptr); @@ -544,11 +545,12 @@ auto Mpris::getPlayerInfo() -> std::optional { if (error) goto errorexit; if (auto* album_artist_ = - playerctl_player_print_metadata_prop(player, "xesam:albumArtist", &error)) { + playerctl_player_print_metadata_prop(last_active_player_, "xesam:albumArtist", &error)) { spdlog::debug("mpris[{}]: albumArtist = {}", info.name, album_artist_); info.album_artist = album_artist_; g_free(album_artist_); } + if (error) goto errorexit; if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) { spdlog::debug("mpris[{}]: album = {}", info.name, album_); @@ -745,7 +747,8 @@ auto Mpris::update() -> void { if (tooltipEnabled()) { try { auto tooltip_text = fmt::format( - fmt::runtime(tooltipstr), fmt::arg("player", info.name), + fmt::runtime(tooltipstr), + fmt::arg("player", std::string(Glib::Markup::escape_text(info.name))), fmt::arg("status", info.status_string), fmt::arg("artist", std::string(Glib::Markup::escape_text(getArtistStr(info, tooltip_len_limits_)))), @@ -754,7 +757,7 @@ auto Mpris::update() -> void { fmt::arg("album", std::string(Glib::Markup::escape_text(getAlbumStr(info, tooltip_len_limits_)))), fmt::arg("length", tooltipLength), fmt::arg("position", tooltipPosition), - fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, false)), + fmt::arg("dynamic", getDynamicStr(info, tooltip_len_limits_, true)), fmt::arg("player_icon", getIconFromJson(config_["player-icons"], info.name)), fmt::arg("status_icon", getIconFromJson(config_["status-icons"], info.status_string))); diff --git a/src/modules/niri/window.cpp b/src/modules/niri/window.cpp index 38ce2f49..5785e6cb 100644 --- a/src/modules/niri/window.cpp +++ b/src/modules/niri/window.cpp @@ -78,7 +78,7 @@ void Window::doUpdate() { updateAppIconName(appId, ""); - if (tooltipEnabled()) label_.set_tooltip_markup(title); + if (tooltipEnabled()) label_.set_tooltip_markup(sanitizedTitle); const auto id = window["id"].asUInt64(); const auto workspaceId = window["workspace_id"].asUInt64(); @@ -93,11 +93,15 @@ void Window::doUpdate() { oldAppId_ = appId; } } else { - label_.show(); - label_.set_markup(waybar::util::rewriteString( - fmt::format(fmt::runtime(format_), fmt::arg("title", ""), - fmt::arg("app_id", "")), - config_["rewrite"])); + if (config_["show-empty"].asBool()) { + label_.show(); + label_.set_markup(waybar::util::rewriteString( + fmt::format(fmt::runtime(format_), fmt::arg("title", ""), fmt::arg("app_id", ""), + fmt::arg("col", -1), fmt::arg("max_col", -1)), + config_["rewrite"])); + } else { + label_.hide(); + } updateAppIconName("", ""); setClass("solo", false); diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index f08fdac5..d7f87cf8 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -129,13 +129,15 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con } if (!control_) { + // Keep going: without river_control_v1 the tags are still displayed read-only + // (0.15.0 behavior); only the click-to-select/toggle wiring is disabled below. spdlog::error("river_control_v1 not advertised"); - return; } if (!seat_) { + // Keep going: wl_seat is only required for the focused-output ("output") class + // and for issuing control commands. Read-only tag display still works without it. spdlog::error("wl_seat not advertised"); - return; } // Store the output this module belongs to; the river_output_status and @@ -169,7 +171,7 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con button.set_relief(Gtk::RELIEF_NONE); box_.pack_start(button, false, false, 0); - if (!config_["disable-click"].asBool()) { + if (control_ && seat_ && !config_["disable-click"].asBool()) { if (set_tags.isArray() && !set_tags.empty()) button.signal_clicked().connect(sigc::bind( sigc::mem_fun(*this, &Tags::handle_primary_clicked), set_tags[tag].asUInt())); @@ -213,14 +215,17 @@ void Tags::handle_show() { output_status_ = zriver_status_manager_v1_get_river_output_status(status_manager_, output_); zriver_output_status_v1_add_listener(output_status_, &output_status_listener_impl, this); - seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); - zriver_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this); + if (seat_) { + seat_status_ = zriver_status_manager_v1_get_river_seat_status(status_manager_, seat_); + zriver_seat_status_v1_add_listener(seat_status_, &seat_status_listener_impl, this); + } zriver_status_manager_v1_destroy(status_manager_); status_manager_ = nullptr; } void Tags::handle_primary_clicked(uint32_t tag) { + if (!control_ || !seat_) return; // Send river command to select tag on left mouse click zriver_command_callback_v1* callback; zriver_control_v1_add_argument(control_, "set-focused-tags"); @@ -230,6 +235,7 @@ void Tags::handle_primary_clicked(uint32_t tag) { } bool Tags::handle_button_press(GdkEventButton* event_button, uint32_t tag) { + if (!control_ || !seat_) return true; if (event_button->type == GDK_BUTTON_PRESS && event_button->button == 3) { // Send river command to toggle tag on right mouse click zriver_command_callback_v1* callback; diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 6e9e95d0..272e05aa 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -143,12 +143,24 @@ void Host::proxyReady(GObject* src, GAsyncResult* res, gpointer data) { // Store the timeout connection so it is disconnected in ~Host, avoiding a // use-after-free if the Host is destroyed before the retry fires. host->retry_connection_ = Glib::signal_timeout().connect( - [host]() { + [host]() -> bool { if (host->watcher_ != nullptr) { return false; } - auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); - host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + try { + auto conn = Gio::DBus::Connection::get_sync(Gio::DBus::BusType::BUS_TYPE_SESSION); + host->nameAppeared(conn, "org.kde.StatusNotifierWatcher", ""); + } catch (const Glib::Error& e) { + spdlog::error("Host: retry get_sync failed: {}", static_cast(e.what())); + if (host->retry_count_ < MAX_RETRIES) { + host->retry_count_ += 1; + return true; // re-arm this timer; never let the exception escape + } + spdlog::warn("Host: giving up on watcher proxy creation after {} retries", + host->retry_count_); + } catch (const std::exception& e) { + spdlog::error("Host: retry failed: {}", e.what()); + } return false; }, RETRY_DELAY_MS); diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index e4340f77..f3e413a8 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -70,11 +70,14 @@ void Tray::onAdd(std::unique_ptr& item) { } items_.push_back(item.get()); + item->event_box.signal_show().connect([this] { dp.emit(); }); + item->event_box.signal_hide().connect([this] { dp.emit(); }); + + // After this point `item` may be erased/invalidated by the ignore-list check; + // do not touch it again below. spdlog::debug("Tray::onAdd deferred check - checking ignore list"); host_.checkIgnoreList(ignore_list_, std::bind(&Tray::onRemove, this, std::placeholders::_1)); - item->event_box.signal_show().connect([this] { dp.emit(); }); - item->event_box.signal_hide().connect([this] { dp.emit(); }); dp.emit(); } diff --git a/src/modules/wayfire/backend.cpp b/src/modules/wayfire/backend.cpp index 037a61e1..b281b0f5 100644 --- a/src/modules/wayfire/backend.cpp +++ b/src/modules/wayfire/backend.cpp @@ -168,25 +168,29 @@ auto IPC::start() -> void { send("window-rules/get-focused-output", {}); std::thread([self = shared_from_this()] { - auto sock = connect(); + try { + auto sock = connect(); - { - Json::Value json; - json["method"] = "window-rules/events/watch"; + { + Json::Value json; + json["method"] = "window-rules/events/watch"; - pack_and_write(sock, Json::writeString(self->writer_builder, json)); - if (self->receive(sock)["result"] != "ok") { - spdlog::error( - "Wayfire IPC: method \"window-rules/events/watch\"" - " have failed"); - return; + pack_and_write(sock, Json::writeString(self->writer_builder, json)); + if (self->receive(sock)["result"] != "ok") { + spdlog::error( + "Wayfire IPC: method \"window-rules/events/watch\"" + " have failed"); + return; + } } - } - while (auto json = self->receive(sock)) { - auto ev = json["event"].asString(); - spdlog::debug("Wayfire IPC: received event \"{}\"", ev); - self->root_event_handler(ev, json); + while (auto json = self->receive(sock)) { + auto ev = json["event"].asString(); + spdlog::debug("Wayfire IPC: received event \"{}\"", ev); + self->root_event_handler(ev, json); + } + } catch (const std::exception& e) { + spdlog::error("Wayfire IPC event thread stopped: {}", e.what()); } }).detach(); } diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 06a3dd62..876c199d 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -118,15 +118,23 @@ void waybar::modules::Wireplumber::updateNodeName(waybar::modules::Wireplumber* // find form-factor const auto* devid = wp_properties_get(properties, "device.id"); - spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); + if (devid != nullptr) { + spdlog::debug("[{}]: '{}' device.id is {}", self->name_, self->type_, devid); - auto* dev = static_cast(wp_object_manager_lookup( - self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, "bound-id", "=s", devid, nullptr)); + g_autoptr(WpDevice) dev = static_cast( + wp_object_manager_lookup(self->om_, WP_TYPE_DEVICE, WP_CONSTRAINT_TYPE_G_PROPERTY, + "bound-id", "=s", devid, nullptr)); - if (const auto* ff = - wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), "device.form-factor")) { - self->form_factor_ = ff; - spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); + const gchar* ff = dev != nullptr ? wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(dev), + "device.form-factor") + : nullptr; + + if (ff != nullptr) { + self->form_factor_ = ff; + spdlog::debug("[{}]: Updating node form factor to: {}", self->name_, self->form_factor_); + } else { + self->form_factor_ = ""; + } } else { self->form_factor_ = ""; } @@ -614,7 +622,10 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { step = config_["scroll-step"].asDouble(); } if (config_["max-volume"].isDouble()) { - maxVolume = config_["max-volume"].asDouble(); + // {volume} is displayed as cubic-percent (pow(volume_, 3) * 100), while volume_/newVol are + // linear gains. Map the documented cubic-percent ceiling into the linear domain the clamp + // operates in, restoring the 0.15.0 cap semantics (e.g. 130 -> cbrt(1.3) linear -> 130%). + maxVolume = cbrt(config_["max-volume"].asDouble() / 100.0); } double vol = volume_; diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 206a6b0e..62371c15 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -627,7 +627,7 @@ void Task::update() { if (markup) button.set_tooltip_markup(txt); else - button.set_tooltip_markup(txt); + button.set_tooltip_text(txt); } } diff --git a/src/util/transform_8bit_to_rgba.cpp b/src/util/transform_8bit_to_rgba.cpp index dc87a88e..b354a640 100644 --- a/src/util/transform_8bit_to_rgba.cpp +++ b/src/util/transform_8bit_to_rgba.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace fs = std::filesystem; struct TransformResult { @@ -12,11 +14,12 @@ struct TransformResult { TransformResult transform_8bit_to_hex(const std::string& file_path) { std::ifstream f(file_path, std::ios::in | std::ios::binary); - const auto size = fs::file_size(file_path); - std::string result(size, '\0'); - if (!f.is_open() || !f.good()) { + std::error_code ec; + const auto size = fs::file_size(file_path, ec); + if (ec || !f.is_open() || !f.good()) { throw std::runtime_error("Cannot open file: " + file_path); } + std::string result(size, '\0'); if (size == 0) { return {.css = result, .was_transformed = false}; From 45276f217e5f5bd6cc1ab1fd76bbfbe7445029a8 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 02:32:33 +0200 Subject: [PATCH 301/416] ci: add feature-complete build job to guarantee all modules compile The linux matrix ran every distro with features in `auto` mode, so any module whose dependency was missing from the image was silently skipped and never compiled. Three modules (privacy/pipewire, gps, wwan) were built by no CI job at all, and several others only on one or two distros. Add a dedicated `build-full` job (Debian image) that installs the missing dependencies inline and force-enables every optional feature, turning a broken include or a missing dep into a hard build error instead of a silent skip. Drop `debian` from the auto matrix since `build-full` uses the same image and builds a strict superset. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/linux.yml | 66 ++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 12a995d7..69f670cd 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -14,10 +14,12 @@ jobs: distro: - alpine - archlinux - - debian - fedora - opensuse - gentoo + # NOTE: debian is intentionally omitted here — it is covered by the + # dedicated `build-full` job below, which uses the same Debian image + # but force-enables *every* optional module (a strict superset). cpp_std: [c++20] runs-on: ubuntu-latest @@ -32,3 +34,65 @@ jobs: run: ninja -C build - name: test run: make test + + # Feature-complete build: force-enable *every* optional module so a broken + # #include or regression in any module fails CI loudly, instead of being + # silently skipped by an `auto` feature when its dependency is absent. + # Runs on a Debian image with all optional dependencies installed inline + # (including pipewire/gps/wwan, which no CI image otherwise ships). + build-full: + runs-on: ubuntu-latest + container: + image: alexays/waybar:debian + + steps: + - uses: actions/checkout@v6 + - name: install extra dependencies + # Deps not present in the base image, required to build the modules + # that are otherwise never compiled by CI. + run: | + apt-get update + apt-get install --no-install-recommends --no-install-suggests -y \ + libpipewire-0.3-dev \ + libgps-dev \ + libmm-glib-dev \ + libevdev-dev \ + libsystemd-dev \ + libepoxy-dev \ + libfftw3-dev + - name: configure + # Every feature forced to `enabled`/`true`: any missing dependency or + # broken module is now a hard configure/build error, not a silent skip. + run: | + meson setup build \ + -Dcpp_std=c++20 \ + -Dexperimental=true \ + -Dman-pages=enabled \ + -Dlibcxx=false \ + -Dlibinput=enabled \ + -Dlibnl=enabled \ + -Dlibudev=enabled \ + -Dlibevdev=enabled \ + -Dpulseaudio=enabled \ + -Dupower_glib=enabled \ + -Dpipewire=enabled \ + -Dmpris=enabled \ + -Dsystemd=enabled \ + -Ddbusmenu-gtk=enabled \ + -Dmpd=enabled \ + -Drfkill=enabled \ + -Dsndio=enabled \ + -Dlogind=enabled \ + -Dtests=enabled \ + -Djack=enabled \ + -Dwireplumber=enabled \ + -Dcava=enabled \ + -Dgps=enabled \ + -Dwwan=enabled \ + -Dniri=true \ + -Dmango=true \ + -Dlogin-proxy=true + - name: build + run: ninja -C build + - name: test + run: make test From 14cb78b8354524fd6ffb07637474e569459af623 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 02:34:31 +0200 Subject: [PATCH 302/416] ci: install systemd-dev for systemd.pc in build-full job On Debian sid, `dependency('systemd')` needs systemd.pc, which is shipped by the `systemd-dev` package, not `libsystemd-dev`. Add it so the forced -Dsystemd=enabled configure step succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/linux.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 69f670cd..fb4ab451 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -58,6 +58,7 @@ jobs: libmm-glib-dev \ libevdev-dev \ libsystemd-dev \ + systemd-dev \ libepoxy-dev \ libfftw3-dev - name: configure From 72fdf77c192631f545f9ca77270cd6a792904aaa Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 02:39:38 +0200 Subject: [PATCH 303/416] docs: align man pages with code + fill documentation gaps (0.16.0 audit) A full man<->code consistency audit surfaced options, format placeholders and CSS classes that were implemented but undocumented, documented but not implemented (some causing fmt crashes when copied from examples), and defaults that disagreed with the code. This aligns the docs with the code and fixes a few genuine code gaps. Docs: - Add the missing waybar-user(5) man page (and register it in meson.build) - Document previously-undocumented options/placeholders/CSS across many modules (custom image-path/image-name/icon-size, graph_type/width/ datapoints; battery smooth-power; wireplumber format-source/only-physical; mpris {position}/prefer-album-artist; network {signalStrengthApp}/compact bandwidth; pulseaudio {source_volume}/{source_desc}; upower {temperature}/ {model}/{native-path}; wwan {power_state}/{imei}; tray ignore-list; and many CSS state classes: .sink-muted, .source-muted, .workspace-hover, etc.) - Correct documented defaults to match the code (hyprland format {name}, gamemode {count}, cpu-graph interval 5, cava input_delay 4, niri taskbar icon-size 16, disk/gps/wayfire formats, menu-actions object type, ...) - Remove placeholders/options that do not apply (custom-graph {icon}/format/ format-icons/rotate) and fix crashing examples (wwan {mode}, gps format-no-fix); note cava background/foreground/continuous_rendering are cava-config-file options Code: - bluetooth: accept the documented `controller` key as a synonym of `controller-alias` (the option was silently ignored) - mango/workspaces: supply the documented `{name}` fmt arg (was missing -> fmt::format threw) - privacy: read `tooltip` as a bool (was guarded on isString(), so the documented `tooltip: false` was silently ignored) All man pages validated with scdoc 1.11.4. Not compiled locally (no gtkmm); C++ build relies on CI. Co-Authored-By: Claude Opus 4.8 (1M context) --- man/waybar-backlight-slider.5.scd | 5 ++ man/waybar-backlight.5.scd | 14 ++++ man/waybar-battery.5.scd | 10 +++ man/waybar-bluetooth.5.scd | 5 ++ man/waybar-cava.5.scd | 29 +++++--- man/waybar-clock.5.scd | 17 ++++- man/waybar-cpu-graph.5.scd | 9 ++- man/waybar-cpu.5.scd | 2 +- man/waybar-custom-graph.5.scd | 33 +++++---- man/waybar-custom.5.scd | 16 +++++ man/waybar-disk.5.scd | 6 +- man/waybar-dwl-window.5.scd | 2 +- man/waybar-ext-workspaces.5.scd | 16 +++++ man/waybar-gamemode.5.scd | 2 +- man/waybar-gps.5.scd | 9 +-- man/waybar-hyprland-language.5.scd | 3 +- man/waybar-hyprland-window.5.scd | 4 +- man/waybar-hyprland-workspaces.5.scd | 10 ++- man/waybar-idle-inhibitor.5.scd | 3 +- man/waybar-image.5.scd | 16 ++--- man/waybar-inhibitor.5.scd | 6 ++ man/waybar-mango-language.5.scd | 4 +- man/waybar-mango-layout.5.scd | 12 ++-- man/waybar-mango-window.5.scd | 11 +-- man/waybar-menu.5.scd | 2 +- man/waybar-mpd.5.scd | 5 ++ man/waybar-mpris.5.scd | 21 +++++- man/waybar-network.5.scd | 6 ++ man/waybar-niri-language.5.scd | 4 +- man/waybar-niri-workspaces.5.scd | 4 +- man/waybar-power-profiles-daemon.5.scd | 9 +++ man/waybar-pulseaudio-slider.5.scd | 4 ++ man/waybar-pulseaudio.5.scd | 6 ++ man/waybar-river-layout.5.scd | 2 +- man/waybar-sway-language.5.scd | 1 + man/waybar-sway-window.5.scd | 14 +++- man/waybar-systemd-failed-units.5.scd | 4 +- man/waybar-tray.5.scd | 4 ++ man/waybar-upower.5.scd | 12 ++++ man/waybar-user.5.scd | 94 ++++++++++++++++++++++++++ man/waybar-wayfire-workspaces.5.scd | 4 +- man/waybar-wireplumber.5.scd | 37 ++++++++++ man/waybar-wlr-taskbar.5.scd | 5 ++ man/waybar-wwan.5.scd | 8 ++- man/waybar.5.scd.in | 12 +++- meson.build | 1 + src/modules/bluetooth.cpp | 25 +++++-- src/modules/mango/workspaces.cpp | 3 +- src/modules/privacy/privacy_item.cpp | 2 +- 49 files changed, 445 insertions(+), 88 deletions(-) create mode 100644 man/waybar-user.5.scd diff --git a/man/waybar-backlight-slider.5.scd b/man/waybar-backlight-slider.5.scd index c6f027a1..e15b4026 100644 --- a/man/waybar-backlight-slider.5.scd +++ b/man/waybar-backlight-slider.5.scd @@ -31,6 +31,11 @@ The brightness can be controlled by dragging the slider across the bar or clicki typeof: string ++ The name of the preferred device to control. If left empty, a device will be chosen automatically. +*interval*: ++ + typeof: uint ++ + default: 1000 ++ + The interval in milliseconds at which the brightness is polled and the slider is updated. + *expand*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-backlight.5.scd b/man/waybar-backlight.5.scd index 20810051..27e97863 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -15,6 +15,10 @@ The *backlight* module displays the current backlight level. default: 2 ++ The interval in which information gets polled. +*device*: ++ + typeof: string ++ + The name of the preferred backlight device to display. If left empty, a device will be chosen automatically. + *format*: ++ typeof: string ++ default: {percent}% ++ @@ -104,6 +108,16 @@ The *backlight* module displays the current backlight level. default: false ++ Enables this module to consume all left over space dynamically. +# FORMAT REPLACEMENTS + +*{percent}*: The current brightness in percent. + +*{percent_exp}*: The current brightness in percent, adjusted with a power curve to better match perceived brightness. + +*{icon}*: The icon from *format-icons* chosen according to *{percent}*. + +*{icon_exp}*: The icon from *format-icons* chosen according to *{percent_exp}*. + # EXAMPLE: ``` diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 0c71518c..136bee0c 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -37,6 +37,16 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: 60 ++ The interval in which the information gets polled. +*smooth-power*: ++ + typeof: bool ++ + default: false ++ + Enable exponential smoothing of the battery power reading used to estimate the *{time}* remaining, so the estimate fluctuates less between refreshes. + +*smooth-power-time-constant*: ++ + typeof: double ++ + default: 260.0 ++ + The time constant (in seconds) of the *smooth-power* exponential filter. Only used when *smooth-power* is enabled. Values below 1.0 are clamped to 1.0. + *states*: ++ typeof: object ++ A number of battery states which get activated on certain capacity levels. See *waybar-states(5)*. diff --git a/man/waybar-bluetooth.5.scd b/man/waybar-bluetooth.5.scd index 36c6443a..2fb6b90f 100644 --- a/man/waybar-bluetooth.5.scd +++ b/man/waybar-bluetooth.5.scd @@ -16,6 +16,10 @@ Addressed by *bluetooth* typeof: string ++ Use the controller with the defined alias. Otherwise, a random controller is used. Recommended to define when there is more than 1 controller available to the system. +*controller-alias*: ++ + typeof: string ++ + Synonym for *controller*. If both are set, *controller-alias* takes precedence. + *format-device-preference*: ++ typeof: array ++ A ranking of bluetooth devices, addressed by their alias. The order is from *first displayed* to *last displayed*. ++ @@ -241,6 +245,7 @@ Split keyboard with separate central/peripheral batteries (e.g., ZMK): - *#bluetooth.off* - *#bluetooth.on* - *#bluetooth.connected* +- *#bluetooth.no-controller* - *#bluetooth.discoverable* - *#bluetooth.discovering* - *#bluetooth.pairable* diff --git a/man/waybar-cava.5.scd b/man/waybar-cava.5.scd index 95336e24..439858a1 100644 --- a/man/waybar-cava.5.scd +++ b/man/waybar-cava.5.scd @@ -37,7 +37,7 @@ libcava lives in: |[ *method* \[output\] :[ string :[ -:< Manages which frontend Waybar cava module should use. Values: raw, sdl_glsl +:< Manages which frontend Waybar cava module should use. Values: raw, sdl_glsl. Not a waybar JSON key: it is set through the *method* option in the *\[output\]* section of the cava configuration file (*cava_config*) |[ *framerate* :[ integer :[ 30 @@ -111,12 +111,20 @@ libcava lives in: :[ false :[ Disables or enables the so-called "Monstercat smoothing" with or without "waves" |[ *noise_reduction* +:[ double +:[ 0.77 +:[ Fractional value between 0.0 - 1.0. The raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth. Values near 1.0 will be very slow and smooth, near 0.0 will be fast but noisy +|[ *gravity* :[ integer -:[ 77 -:[ Range between 0 - 100. The raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth. 100 will be very slow and smooth, 0 will be fast but noisy +:[ +:[ Gravity smoothing filter. Higher values make the bars drop faster. Adjusted by *noise_reduction* when set +|[ *integral* +:[ integer +:[ +:[ Integral smoothing filter. Higher values make the visualization smoother but less precise. Adjusted by *noise_reduction* when set |[ *input_delay* :[ integer -:[ 2 +:[ 4 :[ Sets the delay before fetching audio source thread start working. On author's machine, Waybar starts much faster than pipewire audio server, and without a little delay cava module fails because pipewire is not ready |[ *ascii_max_range* :[ integer @@ -124,7 +132,7 @@ libcava lives in: :[ It's impossible to set it directly. The value is dictated by the number of icons in the array *format-icons* |[ *data_format* :[ string -:[ asci +:[ ascii :[ Raw data format. Can be 'binary' or 'ascii' |[ *raw_target* :[ string @@ -157,11 +165,11 @@ libcava lives in: |[ *background* :[ string :[ -:[ GLSL actual. Support hex code colors only. Must be within '' +:[ GLSL actual. Support hex code colors only. Must be within ''. Not a waybar JSON key: set it as *background* in the *\[color\]* section of the cava configuration file (*cava_config*) |[ *foreground* :[ string :[ -:[ GLSL actual. Support hex code colors only. Must be within '' +:[ GLSL actual. Support hex code colors only. Must be within ''. Not a waybar JSON key: set it as *foreground* in the *\[color\]* section of the cava configuration file (*cava_config*) |[ *gradient* :[ integer :[ 0 @@ -185,7 +193,7 @@ libcava lives in: |[ *continuous_rendering* :[ integer :[ 0 -:[ GLSL actual. Keep rendering even if no audio. Recommended to set to 1 +:[ GLSL actual. Keep rendering even if no audio. Recommended to set to 1. Not a waybar JSON key: set it as *continuous_rendering* in the *\[output\]* section of the cava configuration file (*cava_config*) Configuration can be provided as: - The only cava configuration file which is provided through *cava_config*. The rest configuration can be skipped @@ -253,9 +261,12 @@ In case when cava releases new version and you're wanna get it, it should be rai ``` # STYLE -- *#cava* +- *#cava* Raw frontend widget - *#cava.silent* Applied after no sound has been detected for sleep_timer seconds - *#cava.updated* Applied when a new frame is shown +- *#cavaGLSL* GLSL frontend widget (used instead of *#cava* when the cava *method* is *sdl_glsl*) +- *#cavaGLSL.silent* Applied after no sound has been detected for sleep_timer seconds +- *#cavaGLSL.updated* Applied when a new frame is shown # FRONTENDS ## RAW diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 62d1394d..0901098b 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -120,8 +120,9 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ Calendar view mode. Possible values: year|month |[ *mode-mon-col* :[ integer -:[ 3 -:[ Relevant for *mode=year*. Count of months per row +:[ 1 +:[ Relevant for *mode=year*. Count of months per row. Must be a divisor of 12 + (one of 1, 2, 3, 4, 6, 12); an invalid value falls back to 3 |[ *weeks-pos* :[ string :[ @@ -193,6 +194,8 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe :[ Switch to the next calendar month/year |[ *shift_down* :[ Switch to the previous calendar month/year +|[ *shift_reset* +:[ Reset the calendar shift back to the current month/year |[ *exec * :[ Execute the specified command @@ -292,6 +295,16 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe - *#clock* +The following classes are used only inside the *{calendar}* tooltip. Their +foreground *color* is read and applied to the corresponding calendar cells in +the tooltip markup: + +- *.calendar-today*: The current day +- *.calendar-days*: The day numbers +- *.calendar-weeks*: The week numbers +- *.calendar-weekdays*: The weekday header (Su, Mo, ...) +- *.calendar-months*: The month header (January, February, ...) + # Troubleshooting If clock module is disabled at startup with locale::facet::\_S\_create\_c\_locale ++ diff --git a/man/waybar-cpu-graph.5.scd b/man/waybar-cpu-graph.5.scd index 1877aeeb..59d39cc1 100644 --- a/man/waybar-cpu-graph.5.scd +++ b/man/waybar-cpu-graph.5.scd @@ -11,10 +11,15 @@ The *cpu graph* module displays a line graph with the CPU utilization. # CONFIGURATION *interval*: ++ - typeof: integer ++ - default: 10 ++ + typeof: integer or float ++ + default: 5 ++ The interval in which the information gets polled. +*graph_type*: ++ + typeof: string ++ + default: line ++ + The rendering style of the graph. One of 'line', 'bar', or 'gauge'. + *width*: ++ typeof: integer ++ The length in pixels the module should display. diff --git a/man/waybar-cpu.5.scd b/man/waybar-cpu.5.scd index 3f68998f..dda55daf 100644 --- a/man/waybar-cpu.5.scd +++ b/man/waybar-cpu.5.scd @@ -124,7 +124,7 @@ The *cpu* module displays the current CPU utilization. *{icon*{n}*}*: Icon for CPU core n usage. Use like {icon0}. -*{icon0}{icon1}{icon2}{icon3}*: All per-core icons concatenated. Equivalent to {icon0}{icon1}...{icon*N*} but adapts to the number of cores automatically. +*{icons}*: All per-core icons concatenated. Equivalent to {icon0}{icon1}...{icon*N*} but adapts to the number of cores automatically. # EXAMPLES diff --git a/man/waybar-custom-graph.5.scd b/man/waybar-custom-graph.5.scd index 4f020a6c..9a134fd6 100644 --- a/man/waybar-custom-graph.5.scd +++ b/man/waybar-custom-graph.5.scd @@ -38,9 +38,8 @@ Addressed by *custom-graph/* If a *signal* is defined then the script will run once on startup and will only update with a signal. *restart-interval*: ++ - typeof: integer or float ++ - The restart interval (in seconds). ++ - Minimum value is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + typeof: integer ++ + The restart interval (in whole seconds). ++ Can't be used with the *interval* option, so only with continuous scripts. ++ Once the script exits, it'll be re-executed after the *restart-interval*. @@ -50,18 +49,20 @@ Addressed by *custom-graph/* The number is valid between 1 and N, where *SIGRTMIN+N* = *SIGRTMAX*. ++ If no interval is defined then a signal will be the only way to update the module. -*format*: ++ +*graph_type*: ++ typeof: string ++ - default: {text} ++ - The format, how information should be displayed. On {text} data gets inserted. + default: line ++ + The style of graph to render. One of *line*, *bar* or *gauge*. -*format-icons*: ++ - typeof: array ++ - Based on the set percentage, the corresponding icon gets selected. The order is *low* to *high*. - -*rotate*: ++ +*width*: ++ typeof: integer ++ - Positive value to rotate the text label (in 90 degree increments). + default: 100 ++ + The width of the graph in pixels. + +*datapoints*: ++ + typeof: integer ++ + default: 20 ++ + The number of most recent values to retain and plot on the graph. *on-click*: ++ typeof: string ++ @@ -99,7 +100,7 @@ Addressed by *custom-graph/* *tooltip-format*: ++ typeof: string ++ The tooltip format. If specified, overrides any tooltip output from the script in *exec*. ++ - Uses the same format replacements as *format*. + See *FORMAT REPLACEMENTS*. *escape*: ++ typeof: bool ++ @@ -146,11 +147,13 @@ $text\\n$tooltip\\n$class* # FORMAT REPLACEMENTS +These replacements are available in *tooltip-format*. + *{text}*: Output of the script. -*{percentage}* Percentage which can be set via a json return type. +*{alt}*: The *alt* value from a json return type. -*{icon}*: An icon from 'format-icons' according to percentage. +*{percentage}* Percentage which can be set via a json return type. # EXAMPLES diff --git a/man/waybar-custom.5.scd b/man/waybar-custom.5.scd index 88b79151..2d8161ca 100644 --- a/man/waybar-custom.5.scd +++ b/man/waybar-custom.5.scd @@ -145,6 +145,18 @@ Addressed by *custom/* default: false ++ Enables this module to consume all left over space dynamically. +*image-path*: ++ + typeof: string ++ + Path to an image file to display in the module. + +*image-name*: ++ + typeof: string ++ + Name of a themed icon to display in the module. + +*icon-size*: ++ + typeof: integer ++ + The size (in pixels) of the image set via *image-path* or *image-name*. + # RETURN-TYPE When *return-type* is set to *json*, Waybar expects the *exec*-script to output its data in JSON format. @@ -169,6 +181,8 @@ $text\\n$tooltip\\n$class* *{text}*: Output of the script. +*{alt}*: The *alt* value from a json return type. + *{percentage}* Percentage which can be set via a json return type. *{icon}*: An icon from 'format-icons' according to percentage. @@ -248,3 +262,5 @@ Under the premise that interval is not defined, you can use the signal and updat - *#custom-* - *#custom-.* - ** can be set by the script. For more information see *return-type* +- *.flat* and *.text-button* are always applied to the module's label. +- *.image-button* is always applied to the module's image (see *image-path*/*image-name*). diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index dab6af7e..16e0d360 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -19,8 +19,8 @@ Addressed by *disk* *format*: ++ typeof: string ++ - default: "{percentage_used}%" ++ - The format, how information for each disk should be displayed. + default: "{}%" ++ + The format, how information for each disk should be displayed. Note: in the default the positional field "{}" resolves to *{percentage_free}*. *rotate*: ++ typeof: integer ++ @@ -101,7 +101,7 @@ Addressed by *disk* *tooltip-format*: ++ typeof: string ++ - default: "{used} out of {total} used ({percentage_used}%)" ++ + default: "{used} used out of {total} on {path} ({percentage_used}%)" ++ The format of the information displayed in the tooltip. *unit*: ++ diff --git a/man/waybar-dwl-window.5.scd b/man/waybar-dwl-window.5.scd index 907d5332..b347b1d8 100644 --- a/man/waybar-dwl-window.5.scd +++ b/man/waybar-dwl-window.5.scd @@ -14,7 +14,7 @@ Addressed by *dwl/window* *format*: ++ typeof: string ++ - default: {title} ++ + default: {} ++ The format, how information should be displayed. *hide-empty*: ++ diff --git a/man/waybar-ext-workspaces.5.scd b/man/waybar-ext-workspaces.5.scd index 3fdae606..0ee17097 100644 --- a/man/waybar-ext-workspaces.5.scd +++ b/man/waybar-ext-workspaces.5.scd @@ -37,6 +37,10 @@ Addressed by *ext/workspaces* default: false ++ Should workspaces be sorted by ID. Workspace ID will be sorted numerically when all ID are numbers. Takes precedence over any other sort-by option. +*sort-by-number*: ++ + typeof: bool ++ + Deprecated alias of *sort-by-id*. Setting it emits a warning; prefer *sort-by-id*. + *all-outputs*: ++ typeof: bool ++ default: false ++ @@ -52,6 +56,18 @@ Addressed by *ext/workspaces* default: true ++ If set to false hidden workspaces will be shown. +*on-click*: ++ + typeof: string ++ + The action to perform on left-click. See *CLICK ACTIONS* for the possible values. + +*on-click-middle*: ++ + typeof: string ++ + The action to perform on middle-click. See *CLICK ACTIONS* for the possible values. + +*on-click-right*: ++ + typeof: string ++ + The action to perform on right-click. See *CLICK ACTIONS* for the possible values. + # FORMAT REPLACEMENTS *{name}*: Name of workspace assigned by compositor. diff --git a/man/waybar-gamemode.5.scd b/man/waybar-gamemode.5.scd index bb82e0aa..d7e1991b 100644 --- a/man/waybar-gamemode.5.scd +++ b/man/waybar-gamemode.5.scd @@ -28,7 +28,7 @@ Feral Gamemode optimizations. *tooltip-format*: ++ typeof: string ++ - default: Games running: {glyph} ++ + default: Games running: {count} ++ The text format of the tooltip. *hide-not-running*: ++ diff --git a/man/waybar-gps.5.scd b/man/waybar-gps.5.scd index e7473601..d25204ea 100644 --- a/man/waybar-gps.5.scd +++ b/man/waybar-gps.5.scd @@ -26,7 +26,7 @@ libgps lives in: *format*: ++ typeof: string ++ - default: {glyph} ++ + default: {mode} ++ The text format. *tooltip*: ++ @@ -36,7 +36,7 @@ libgps lives in: *tooltip-format*: ++ typeof: string ++ - default: Games running: {glyph} ++ + default: The value of *format* ++ The text format of the tooltip. *interval*: ++ @@ -93,10 +93,10 @@ libgps lives in: "gps": { "format": "{mode}", "format-disabled": "", // an empty format will hide the module - "format-no-fix": "No fix", + "format-fix-none": "No fix", "format-fix-3d": "{status}", "tooltip-format": "{mode}", - "tooltip-format-no-fix": "{satellites_visible} satellites visible", + "tooltip-format-fix-none": "{satellites_visible} satellites visible", "tooltip-format-fix-2d": "{satellites_used}/{satellites_visible} satellites used", "tooltip-format-fix-3d": "Altitude: {altitude_hae}m", "hide-disconnected": false @@ -106,6 +106,7 @@ libgps lives in: - *#gps* - *#gps.disabled* Applied when GPS is disabled. +- *#gps.disconnected* Applied when no GPS receiver is present. - *#gps.fix-none* Applied when GPS is present, but there is no fix. - *#gps.fix-2d* Applied when there is a 2D fix. - *#gps.fix-3d* Applied when there is a 3D fix. diff --git a/man/waybar-hyprland-language.5.scd b/man/waybar-hyprland-language.5.scd index 88dd82d7..12ae0acc 100644 --- a/man/waybar-hyprland-language.5.scd +++ b/man/waybar-hyprland-language.5.scd @@ -68,7 +68,7 @@ Addressed by *hyprland/language* # FORMAT REPLACEMENTS -*{short}*: Short name of layout (e.g. "us"). Equals to {}. +*{short}*: Short name of layout (e.g. "us"). *{shortDescription}*: Short description of layout (e.g. "en"). @@ -113,3 +113,4 @@ Addressed by *hyprland/language* # STYLE - *#language* +- *#language.* (per-layout class derived from the layout short name, e.g. '.us') diff --git a/man/waybar-hyprland-window.5.scd b/man/waybar-hyprland-window.5.scd index ecbacd4b..a79596c0 100644 --- a/man/waybar-hyprland-window.5.scd +++ b/man/waybar-hyprland-window.5.scd @@ -31,8 +31,8 @@ Addressed by *hyprland/window* *tooltip-format*: ++ typeof: string ++ - default: {title} ++ - The format of the tooltip shown on hover. Supports the same replacements as *format*. Requires *tooltip* to be enabled. + The format of the tooltip shown on hover. Supports the same replacements as *format*. Requires *tooltip* to be enabled. When unset, the tooltip falls back to the formatted label text. ++ + default: (the formatted label text) *icon*: ++ typeof: bool ++ diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index e296b6b9..f85ac382 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -14,11 +14,11 @@ Addressed by *hyprland/workspaces* *format*: ++ typeof: string ++ - default: {id} ++ + default: {name} ++ The format, how information should be displayed. *format-icons*: ++ - typeof: array ++ + typeof: object ++ Based on the workspace ID and state, the corresponding icon gets selected. See *icons*. *window-rewrite*: ++ @@ -201,7 +201,7 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. Option to disable tooltip on hover. *tooltips*: ++ - typeof: array ++ + typeof: object ++ Based on the workspace ID and state, the corresponding tooltip gets selected. Selection works the same as *format-icons* do. Format replacements are supported. See *icons*. *expand*: ++ @@ -217,6 +217,8 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. *{icon}*: Icon, as defined in *format-icons*. +*{windows}*: The windows in the workspace, formatted according to *window-rewrite* and joined with *format-window-separator*. + # ICONS Additional to workspace name matching, the following *format-icons* can be set. @@ -348,6 +350,8 @@ Additional to workspace name matching, the following *format-icons* can be set. - *#workspaces button.persistent* - *#workspaces button.special* - *#workspaces button.urgent* +- *#workspaces button.workspace-hover* (applied while the pointer is over the button) +- *#workspaces button.* (per-workspace class derived from the workspace name, sanitized to a valid CSS class name, e.g. a workspace named "1" yields '.ws-1'; special workspaces also get the raw name class) - *#workspaces button.hosting-monitor* (gets applied if workspace-monitor == waybar-monitor) - *#workspaces .workspace-label* - *#workspaces .taskbar-window* (each window in the taskbar, only if 'workspace-taskbar.enable' is true) diff --git a/man/waybar-idle-inhibitor.5.scd b/man/waybar-idle-inhibitor.5.scd index 8cbba56e..405258c1 100644 --- a/man/waybar-idle-inhibitor.5.scd +++ b/man/waybar-idle-inhibitor.5.scd @@ -84,7 +84,8 @@ screensaver, also known as "presentation mode". *dynamic-timeouts*: ++ typeof: bool ++ default: false ++ - Option to enable dynamic timeouts. + Option to enable dynamic timeouts. ++ + The legacy singular spelling *dynamic-timeout* is also accepted as an alias. *wait-for-activity*: ++ typeof: bool ++ diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index 029f6ad2..3a7813c2 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -110,16 +110,16 @@ $path\\n$tooltip For multiple *image* *size*: ++ - typeof: string ++ + typeof: integer ++ default: 16 ++ Minumum size of the rendered image in pixels. *interval*: ++ - typeof: interger ++ - default: INT_MAX ++ - The interval in seconds to redraw module ++ - Default value is max value of int and changing it is only recommended ++ - if image path or other property is being changed overtime ++ + typeof: integer or float ++ + The interval (in seconds) to re-render the module. ++ + If set to a positive value, the minimum is 0.001 (1ms). Values smaller than 1ms will be set to 1ms. ++ + Zero or negative values (and the literal "once") are treated as "once". ++ + This is useful if image path or other property is being changed over time. ++ If no *interval* is provided, the module will only be rendered once. *multiple*: ++ @@ -246,7 +246,7 @@ print(res) - *#image* - *#image*.empty /\*when image could not be rendered\*/ -- *#image*.button /\*when on-click is provided\*/ +- *#image* button /\*when on-click is provided; a Gtk::Button is matched by the element selector, not a class\*/ - *#image*.*marker* /\*where *marker* is parameter from config\*/ -- *#image*.button.*marker* +- *#image* button.*marker* diff --git a/man/waybar-inhibitor.5.scd b/man/waybar-inhibitor.5.scd index fce6f4f8..2e9759ad 100644 --- a/man/waybar-inhibitor.5.scd +++ b/man/waybar-inhibitor.5.scd @@ -100,6 +100,12 @@ See *systemd-inhibit*(1) for more information. *{icon}*: Icon, as defined in *format-icons* +# STYLE + +- *#inhibitor* +- *#inhibitor.activated* +- *#inhibitor.deactivated* + # EXAMPLES ``` diff --git a/man/waybar-mango-language.5.scd b/man/waybar-mango-language.5.scd index b153f0d3..7b50f4ad 100644 --- a/man/waybar-mango-language.5.scd +++ b/man/waybar-mango-language.5.scd @@ -47,11 +47,11 @@ Addressed by *mango/language* # FORMAT REPLACEMENTS -*{short}*: Short name of the layout (e.g. "us"). This is also the default when no format is specified. +*{short}*: Short name of the layout (e.g. "us"). *{shortDescription}*: Short description of the layout (same as *{short}* in most cases). -*{long}*: Full name of the layout as reported by Mango (e.g. "English (US)"). +*{long}*: Full name of the layout as reported by Mango (e.g. "English (US)"). The empty positional *{}* in the default format resolves to this value, so the long name is shown when no *format* is specified. *{variant}*: Variant of the layout, if any. diff --git a/man/waybar-mango-layout.5.scd b/man/waybar-mango-layout.5.scd index 6cf3290b..fedff008 100644 --- a/man/waybar-mango-layout.5.scd +++ b/man/waybar-mango-layout.5.scd @@ -15,8 +15,8 @@ Addressed by *mango/layout* *format*: ++ typeof: string ++ - default: {symbol} ++ - The default format, how the layout symbol should be displayed. *{symbol}* is replaced by the current layout symbol. + default: {} ++ + The default format, how the layout symbol should be displayed. The positional *{}* (the default) and *{symbol}* are both replaced by the current layout symbol. *format-*: ++ typeof: string ++ @@ -42,7 +42,7 @@ For example, if your Mango compositor reports the symbol "S" for a spiral layout The layout module provides dynamic CSS classes based on the current layout symbol, allowing you to style each layout differently. -* *#mango-layout* +* *#layout* * *.* - The current layout symbol is dynamically added as a CSS class name (e.g., *.S*, *.M*). # EXAMPLES @@ -58,18 +58,18 @@ The layout module provides dynamic CSS classes based on the current layout symbo ## CSS Example ``` -#mango-layout { +#layout { color: #ffffff; padding: 0 5px; } /* Specific color for the "S" layout */ -#mango-layout.S { +#layout.S { color: #a6e3a1; } /* Specific color for the "M" layout */ -#mango-layout.M { +#layout.M { color: #f38ba8; } ``` diff --git a/man/waybar-mango-window.5.scd b/man/waybar-mango-window.5.scd index 7ee9b02c..8d026204 100644 --- a/man/waybar-mango-window.5.scd +++ b/man/waybar-mango-window.5.scd @@ -63,8 +63,11 @@ If the title matches a regular expression from the *rewrite* object, it is repla # STYLE - *#window* -- *#window.empty* – applied when no window is focused (module hidden by default) -- *#window.solo* – applied when only one window is present on the active workspace -- *#window.* – applied when a single window with the given app ID is on the workspace -The classes *.empty*, *.solo*, and the app‑ID class are set on the module’s event box. \ No newline at end of file +The following classes are set on the module's event box (the container that +wraps *#window*), not on *#window* itself. Target them directly or with a +descendant selector such as *.solo #window*: + +- *.empty* – applied when no window is focused (module hidden by default) +- *.solo* – applied when only one window is present on the active workspace +- *.* – applied when a single window with the given app ID is on the workspace \ No newline at end of file diff --git a/man/waybar-menu.5.scd b/man/waybar-menu.5.scd index 798917f0..00a5f53c 100644 --- a/man/waybar-menu.5.scd +++ b/man/waybar-menu.5.scd @@ -67,7 +67,7 @@ A module that implements a 'menu' needs 3 properties defined in its config : GtkMenu with id *menu*. *menu-actions*: ++ - typeof: array ++ + typeof: object ++ The actions corresponding to the buttons of the menu. The identifiers of each actions needs to exists as an id in the 'menu-file' for it to be linked properly. diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index d5ba6405..01c86e05 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -94,6 +94,11 @@ Addressed by *mpd* typeof: integer ++ Maximum length of the Title tag. +*ellipsis*: ++ + typeof: string ++ + default: "" ++ + The string appended to the Artist, Album, Album Artist and Title tags when they are truncated by the corresponding length option (`artist-len`, `album-len`, `album-artist-len`, `title-len`). + *rotate*: ++ typeof: integer ++ Positive value to rotate the text label (in 90 degree increments). diff --git a/man/waybar-mpris.5.scd b/man/waybar-mpris.5.scd index 9c192bd1..e03ac0eb 100644 --- a/man/waybar-mpris.5.scd +++ b/man/waybar-mpris.5.scd @@ -26,7 +26,7 @@ The *mpris* module displays currently playing media via libplayerctl. *format*: ++ typeof: string ++ - default: {player} ({status}) {dynamic} ++ + default: {player} ({status}): {dynamic} ++ The text format. *format-[status]*: ++ @@ -40,7 +40,7 @@ The *mpris* module displays currently playing media via libplayerctl. *tooltip-format*: ++ typeof: string ++ - default: {player} ({status}) {dynamic} ++ + default: {player} ({status}): {dynamic} ++ The tooltip text format. *tooltip-format-[status]*: ++ @@ -140,6 +140,21 @@ The *mpris* module displays currently playing media via libplayerctl. default: next track ++ Overwrite default action toggles. +*on-click-backward*: ++ + typeof: string ++ + default: previous track ++ + Overwrite the default action for the backward mouse button (button 8). + +*on-click-forward*: ++ + typeof: string ++ + default: next track ++ + Overwrite the default action for the forward mouse button (button 9). + +*prefer-album-artist*: ++ + typeof: bool ++ + default: false ++ + When true, use the album artist instead of the track artist for the _{artist}_ and _{dynamic}_ tags, falling back to the track artist when no album artist is available. + *player-icons*: ++ typeof: map[string]string ++ Allows setting _{player_icon}_ based on player-name property. @@ -168,6 +183,8 @@ The *mpris* module displays currently playing media via libplayerctl. *{length}*: Length of the track, formatted as HH:MM:SS +*{position}*: Current playback position of the track, formatted as HH:MM:SS + *{dynamic}*: Use _{artist}_, _{album}_, _{title}_ and _{length}_, automatically omit++ empty values diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index a7860e5c..f86ed717 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -176,6 +176,8 @@ Addressed by *network* *{signaldBm}*: Signal strength of the wireless network in dBm. +*{signalStrengthApp}*: Human-readable descriptor of the wireless connectivity derived from the signal strength (e.g. "Great Connectivity", "Streaming", "Poor Connectivity"). + *{frequency}*: Frequency of the wireless network in GHz. *{bandwidthUpBits}*: Instant up speed in bits/seconds. @@ -194,6 +196,10 @@ Addressed by *network* *{bandwidthDownBytes}*: Instant down speed in bytes/seconds. +*{bandwidthUpBytesCompact}*: Instant up speed in bytes/seconds, formatted compactly (e.g. 1.2K). + +*{bandwidthDownBytesCompact}*: Instant down speed in bytes/seconds, formatted compactly (e.g. 1.2K). + *{bandwidthTotalBytes}*: Instant total speed in bytes/seconds. *{txBitrate}*: Link transmit bitrate (e.g., 866.7 Mb/s). diff --git a/man/waybar-niri-language.5.scd b/man/waybar-niri-language.5.scd index d58408d8..ae5c7e28 100644 --- a/man/waybar-niri-language.5.scd +++ b/man/waybar-niri-language.5.scd @@ -44,11 +44,11 @@ Addressed by *niri/language* # FORMAT REPLACEMENTS -*{short}*: Short name of layout (e.g. "us"). Equals to {}. +*{short}*: Short name of layout (e.g. "us"). *{shortDescription}*: Short description of layout (e.g. "en"). -*{long}*: Long name of layout (e.g. "English (Dvorak)"). +*{long}*: Long name of layout (e.g. "English (Dvorak)"). Equals to {}. *{variant}*: Variant of layout (e.g. "dvorak"). diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index d10691e8..636081a0 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -103,7 +103,7 @@ If none of the sorting options are enabled, workspaces keep their output/index o *icon-size*: ++ typeof: int ++ - default: 18 ++ + default: 16 ++ Size of the icons in the workspace taskbar. *ignore-workspaces*: ++ @@ -203,3 +203,5 @@ Additional to workspace name matching, the following *format-icons* can be set. for unnamed workspaces. - *#workspaces button.niri-workspace*: The main container for the workspace. - *#workspaces button .niri-taskbar-btn*: The icon buttons within the taskbar. +- *#workspaces button .niri-taskbar-btn.focused*: The taskbar icon button of the + currently focused window. diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd index da331795..a5489444 100644 --- a/man/waybar-power-profiles-daemon.5.scd +++ b/man/waybar-power-profiles-daemon.5.scd @@ -77,3 +77,12 @@ Display the full profile name: } } ``` + +# STYLE + +- *#power-profiles-daemon* +- *#power-profiles-daemon.performance* Applied when the *performance* profile is active. +- *#power-profiles-daemon.balanced* Applied when the *balanced* profile is active. +- *#power-profiles-daemon.power-saver* Applied when the *power-saver* profile is active. + +*Note*: the module applies a CSS class equal to the name of the active profile, so any additional profile exposed by power-profiles-daemon can be styled by its name. diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index 08a39018..381ab867 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -59,6 +59,10 @@ The volume can be controlled by dragging the slider across the bar or clicking o default: empty ++ A list of sink descriptions to ignore when tracking the default sink, so switching to those sinks does not update the slider. +*sink-mapping*: ++ + typeof: object ++ + Sinks named by the values of this mapping will be considered to be the current sink instead of the sinks named by the respective keys. + # EXAMPLES ``` diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index 553069c5..71b618f7 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -150,6 +150,10 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c *{volume}*: Volume in percentage. +*{source_volume}*: Source (input/microphone) volume in percentage. + +*{source_desc}*: Source's description, for bluetooth it'll be the device name. + *{icon}*: Icon, as defined in *format-icons*. *{format_source}*: Source format, *format-source*, *format-source-muted*. @@ -205,3 +209,5 @@ to be selected when the corresponding audio device is muted. This applies to *de - *#pulseaudio* - *#pulseaudio.bluetooth* - *#pulseaudio.muted* +- *#pulseaudio.sink-muted* +- *#pulseaudio.source-muted* diff --git a/man/waybar-river-layout.5.scd b/man/waybar-river-layout.5.scd index e3540217..a81bf63c 100644 --- a/man/waybar-river-layout.5.scd +++ b/man/waybar-river-layout.5.scd @@ -16,7 +16,7 @@ Addressed by *river/layout* *format*: ++ typeof: string ++ - default: {layout} ++ + default: {} ++ The format, how information should be displayed. On {} data gets inserted. *format-icons*: ++ diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd index 5710e69d..8515dbaf 100644 --- a/man/waybar-sway-language.5.scd +++ b/man/waybar-sway-language.5.scd @@ -77,3 +77,4 @@ Addressed by *sway/language* # STYLE - *#language* +- *#language.* Where *layout-short-name* is the short name of the active layout (e.g. *#language.us* or *#language.de*). diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index 6fa9170b..ad5195c7 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -14,7 +14,7 @@ Addressed by *sway/window* *format*: ++ typeof: string ++ - default: {title} ++ + default: {} ++ The format, how information should be displayed. *rotate*: ++ @@ -101,13 +101,23 @@ Addressed by *sway/window* *icon*: ++ typeof: bool ++ default: false ++ - Option to hide the application icon. + Option to display the application icon. *icon-size*: ++ typeof: integer ++ default: 24 ++ Option to change the size of the application icon. +*icon-spacing*: ++ + typeof: integer ++ + default: 8 ++ + Spacing between the application icon and the label. + +*swap-icon-label*: ++ + typeof: bool ++ + default: false ++ + Option to swap the order of the application icon and the label. + *expand*: ++ typeof: bool ++ default: false ++ diff --git a/man/waybar-systemd-failed-units.5.scd b/man/waybar-systemd-failed-units.5.scd index aaeee2d1..044dbc9a 100644 --- a/man/waybar-systemd-failed-units.5.scd +++ b/man/waybar-systemd-failed-units.5.scd @@ -84,7 +84,9 @@ Addressed by *systemd-failed-units* *{overall_state}:* Overall state of the systemd and user session. ("ok" or "degraded") *{failed_units_list}:* Bulleted list of failed units using *tooltip-unit-format*. Empty when -there are no failed units. +there are no failed units. This replacement is only available in *tooltip-format* and +*tooltip-format-ok*; it is not supplied to *format*/*format-ok* and using it there breaks +label rendering. The *tooltip-unit-format* string supports the following replacements: diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index 975ba1f2..3037ecb0 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -55,6 +55,10 @@ Addressed by *tray* Key: name of item. ++ Value: integer, higher value means to place this item to the right. +*ignore-list*: ++ + typeof: array ++ + List of strings used to hide matching tray items. An item is hidden when any string in the list is found (as a substring) in the item's bus name, category, icon name, id or title. + # EXAMPLES ``` diff --git a/man/waybar-upower.5.scd b/man/waybar-upower.5.scd index 303ee65e..68c2d3ed 100644 --- a/man/waybar-upower.5.scd +++ b/man/waybar-upower.5.scd @@ -59,6 +59,12 @@ compatible devices in the tooltip. default: 4 ++ Defines the spacing between the tooltip window edge and the tooltip content. +*tooltip-format*: ++ + typeof: string ++ + default: ++ + The format used for each device line shown in the tooltip. Accepts the ++ + same replacements as *format*. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. @@ -88,6 +94,12 @@ compatible devices in the tooltip. *{time}*: An estimated time either until empty or until fully charged ++ depending on the charging state. +*{temperature}*: The battery temperature in degrees Celsius. + +*{model}*: The model name of the device. + +*{native-path}*: The native path of the device. + # EXAMPLES ``` diff --git a/man/waybar-user.5.scd b/man/waybar-user.5.scd new file mode 100644 index 00000000..b1603336 --- /dev/null +++ b/man/waybar-user.5.scd @@ -0,0 +1,94 @@ +waybar-user(5) "waybar-user" "User Manual" + +# NAME + +waybar - user module + +# DESCRIPTION + +The *user* module displays the currently logged in user, the system uptime and, +optionally, the user's avatar image. + +# CONFIGURATION + +Addressed by *user* + +[- *Option* +:- *Typeof* +:- *Default* +:- *Description* +|[ *interval* +:[ integer +:[ 60 +:[ The interval in which the information gets polled +|[ *format* +:[ string +:[ *{user} {work_H}:{work_M}* +:[ The format, how the information should be displayed. See *FORMAT REPLACEMENTS* below +|[ *icon* +:[ bool +:[ false +:[ When enabled, displays the user's avatar image next to the label +|[ *avatar* +:[ string +:[ *$HOME/.face* +:[ Path to the avatar image to display. Only used when *icon* is enabled. When unset, the default path *$HOME/.face* is used +|[ *height* +:[ integer +:[ 20 +:[ The height of the avatar image in pixels +|[ *width* +:[ integer +:[ 20 +:[ The width of the avatar image in pixels +|[ *open-on-click* +:[ bool +:[ false +:[ When enabled, left-clicking the module opens the user's home directory (or *open-path*) with the default file manager +|[ *open-path* +:[ string +:[ +:[ Overrides the path that is opened when *open-on-click* is enabled. When empty, the user's home directory is used +|[ *rotate* +:[ integer +:[ +:[ Positive value to rotate the text label (in 90 degree increments) +|[ *tooltip* +:[ bool +:[ true +:[ Option to enable tooltip on hover + +# FORMAT REPLACEMENTS + +- *{user}*: The login name of the currently logged in user, in uppercase +- *{work_d}*: Whole days the system has been up +- *{work_H}*: Hours part of the system uptime (00-23) +- *{work_M}*: Minutes part of the system uptime (00-59) +- *{work_S}*: Seconds part of the system uptime (00-59) +- *{up_H}*: Hour the system was booted (00-23) +- *{up_M}*: Minute the system was booted (00-59) +- *{up_d}*: Day of the month the system was booted (01-31) +- *{up_m}*: Month the system was booted (01-12) +- *{up_Y}*: Year the system was booted + +# EXAMPLES + +``` +"user": { + "format": "{user} up {work_H}:{work_M}:{work_S}", + "interval": 60, + "icon": true, + "avatar": "/home/alice/.face", + "height": 24, + "width": 24, + "open-on-click": true +} +``` + +# STYLE + +- *#user* + +# AUTHOR + +Alexis Rouillard diff --git a/man/waybar-wayfire-workspaces.5.scd b/man/waybar-wayfire-workspaces.5.scd index 04f3be52..89d67f9c 100644 --- a/man/waybar-wayfire-workspaces.5.scd +++ b/man/waybar-wayfire-workspaces.5.scd @@ -14,11 +14,11 @@ Addressed by *wayfire/workspaces* *format*: ++ typeof: string ++ - default: {value} ++ + default: {index} ++ The format, how information should be displayed. *format-icons*: ++ - typeof: array ++ + typeof: object ++ Based on the workspace name, index and state, the corresponding icon gets selected. See *icons*. *disable-click*: ++ diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index c1a69e18..9d6517db 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -19,11 +19,33 @@ The *wireplumber* module displays the current volume reported by WirePlumber. typeof: string ++ This format is used when the sound is muted. +*format-bluetooth*: ++ + typeof: string ++ + This format is used when the active sink is a Bluetooth device (its node name contains *bluez* or *a2dp-sink*). + +*format-bluetooth-muted*: ++ + typeof: string ++ + This format is used when the active sink is a Bluetooth device and the sound is muted. + +*format-source*: ++ + typeof: string ++ + default: *{volume}%* ++ + The format used to render the source (microphone) volume sub-string that is exposed through the *{format_source}* replacement. + +*format-source-muted*: ++ + typeof: string ++ + The format used for the *{format_source}* sub-string when the source (microphone) is muted. + *node-type*: ++ typeof: string ++ default: *Audio/Sink* ++ The WirePlumber node type to attach to. Use *Audio/Source* to manage microphones etc. +*only-physical*: ++ + typeof: bool ++ + default: *false* ++ + When enabled, resolve through virtual/filter-chain sinks to the underlying physical sink and report its volume instead. + *tooltip*: ++ typeof: bool ++ default: *true* ++ @@ -122,6 +144,18 @@ The *wireplumber* module displays the current volume reported by WirePlumber. *{node_name}*: The node's nickname as reported by WirePlumber (*node.nick* property) +*{source_volume}*: Source (microphone) volume in percentage, cubic scale as integer. + +*{source_volume_linear}*: Source (microphone) volume in linear scale as float. + +*{source_volume_cubic}*: Source (microphone) volume in cubic scale as float. + +*{source_volume_db}*: Source (microphone) volume in decibel scale as float. + +*{source_desc}*: The source node's nickname/description as reported by WirePlumber. + +*{format_source}*: The pre-rendered source (microphone) volume sub-string, formatted with *format-source*/*format-source-muted*. + # EXAMPLES ## Basic: @@ -168,3 +202,6 @@ The *wireplumber* module displays the current volume reported by WirePlumber. - *#wireplumber* - *#wireplumber.muted* +- *#wireplumber.sink-muted* +- *#wireplumber.source-muted* +- *#wireplumber.bluetooth* diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index ae30316b..b8c661ff 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -109,6 +109,10 @@ Addressed by *wlr/taskbar* typeof: array ++ List of app_id/titles to be invisible. +*squash-list*: ++ + typeof: array ++ + List of app_id/titles whose multiple instances are collapsed into a single button. When more than one instance of a listed app is open, only one button is shown; when one instance closes, the next hidden instance reappears. The special value '\*' matches all applications. + *app_ids-mapping*: ++ typeof: object ++ Dictionary of app_id to be replaced with @@ -183,6 +187,7 @@ Invalid expressions (e.g., mismatched parentheses) are skipped. # Style - *#taskbar* +- *#taskbar.empty* - *#taskbar button* - *#taskbar button.maximized* - *#taskbar button.minimized* diff --git a/man/waybar-wwan.5.scd b/man/waybar-wwan.5.scd index 0169de07..5d931cc8 100644 --- a/man/waybar-wwan.5.scd +++ b/man/waybar-wwan.5.scd @@ -131,6 +131,12 @@ libmm-glib lives in: *{operator_name}*: Name of the network operator. +*{icon}*: Icon selected from *format-icons* according to the signal quality. + +*{power_state}*: Modem power state (On/Off/Low/Unknown). + +*{imei}*: Modem equipment identifier (IMEI). + # EXAMPLES ``` @@ -139,7 +145,7 @@ libmm-glib lives in: "format-disabled": "", // an empty format will hide the module "format-locked": "SIM Locked", "format-connected": "{operator_name} {preferred_mode} | {signal_quality}%" - "tooltip-format": "{mode}", + "tooltip-format": "{preferred_mode}", "tooltip-format-connected": "{current_modes}", "hide-disconnected": false } diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index e52dadee..78dc433e 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -360,9 +360,19 @@ A group may hide all but one element, showing them only on mouse hover. In order *children-class*: ++ typeof: string ++ - default: "hidden" ++ + default: "drawer-child" ++ Defines the CSS class to be applied to the hidden elements. +*always-visible-class*: ++ + typeof: string ++ + default: "" ++ + Defines the CSS class that marks a child module as always visible. The module carrying this class remains shown while the drawer is collapsed and drives the drawer's visibility management. When empty, the leader (first) module is used. + +*reveal-delay*: ++ + typeof: integer ++ + default: 0 ++ + Delay in milliseconds to wait after the mouse enters the group before revealing the drawer. When 0, the drawer is revealed immediately. + *click-to-reveal*: ++ typeof: bool ++ default: false ++ diff --git a/meson.build b/meson.build index 7d9cf5c1..f96139a0 100644 --- a/meson.build +++ b/meson.build @@ -207,6 +207,7 @@ man_files = files( 'man/waybar-states.5.scd', 'man/waybar-menu.5.scd', 'man/waybar-temperature.5.scd', + 'man/waybar-user.5.scd', ) inc_dirs = ['include'] diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index b092c29c..8adfff26 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -87,6 +87,18 @@ auto isChildPath(const std::string& child, const std::string& parent) -> bool { return child.starts_with(parent); } +// Returns the configured controller alias, accepting either the documented "controller" key or +// its "controller-alias" synonym ("controller-alias" takes precedence when both are set). +auto getConfiguredControllerAlias(const Json::Value& config) -> std::optional { + if (config["controller-alias"].isString()) { + return config["controller-alias"].asString(); + } + if (config["controller"].isString()) { + return config["controller"].asString(); + } + return std::nullopt; +} + // 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. @@ -176,9 +188,8 @@ waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& } if (cur_controller_ = findCurController(); !cur_controller_) { - if (config_["controller-alias"].isString()) { - spdlog::warn("no bluetooth controller found with alias '{}'", - config_["controller-alias"].asString()); + if (auto controller_alias = getConfiguredControllerAlias(config_)) { + spdlog::warn("no bluetooth controller found with alias '{}'", *controller_alias); } else { spdlog::warn("no bluetooth controller found"); } @@ -372,9 +383,9 @@ auto waybar::modules::Bluetooth::onObjectAdded(GDBusObjectManager* manager, GDBu ControllerInfo info; Bluetooth* bt = static_cast(user_data); + auto controller_alias = getConfiguredControllerAlias(bt->config_); if (!bt->cur_controller_.has_value() && bt->getControllerProperties(object, info) && - (!bt->config_["controller-alias"].isString() || - bt->config_["controller-alias"].asString() == info.alias)) { + (!controller_alias.has_value() || controller_alias.value() == info.alias)) { bt->cur_controller_ = std::move(info); bt->dp.emit(); } @@ -634,9 +645,9 @@ auto waybar::modules::Bluetooth::findCurController() -> std::optionalnext) { GDBusObject* object = G_DBUS_OBJECT(l->data); ControllerInfo info; + auto controller_alias = getConfiguredControllerAlias(config_); if (getControllerProperties(object, info) && - (!config_["controller-alias"].isString() || - config_["controller-alias"].asString() == info.alias)) { + (!controller_alias.has_value() || controller_alias.value() == info.alias)) { controller_info = std::move(info); break; } diff --git a/src/modules/mango/workspaces.cpp b/src/modules/mango/workspaces.cpp index f8849228..7424a352 100644 --- a/src/modules/mango/workspaces.cpp +++ b/src/modules/mango/workspaces.cpp @@ -175,7 +175,8 @@ void Workspaces::updateButtonState(Gtk::Button& button, const Json::Value& tag, if (config_["format"].isString()) { name = fmt::format(fmt::runtime(config_["format"].asString()), fmt::arg("icon", getIcon(name, tag)), fmt::arg("value", name), - fmt::arg("index", idx), fmt::arg("output", monitor["name"].asString())); + fmt::arg("name", name), fmt::arg("index", idx), + fmt::arg("output", monitor["name"].asString())); } if (!config_["disable-markup"].asBool()) { diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index 880d026e..aa650bf8 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -75,7 +75,7 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, enum PrivacyNodeType privac tooltipIconSize = config_["tooltip-icon-size"].asUInt(); } // Tooltip - if (config_["tooltip"].isString()) { + if (config_["tooltip"].isBool()) { tooltip = config_["tooltip"].asBool(); } set_has_tooltip(tooltip); From a3b39cfcf607c7955cacfdff06d4faabd5ed3869 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 02:55:57 +0200 Subject: [PATCH 304/416] docs(man): backfill man pages with wiki-only info (source-of-truth pass) Port information that previously lived only in the GitHub wiki into the authoritative scdoc man pages, so the man pages become the single source of truth for module documentation. 33 man pages enriched (options, format replacements, actions, style selectors, troubleshooting and implementation notes). Additions were verified against the source; stale/incorrect wiki entries were deliberately not ported. All pages still compile with scdoc. Co-Authored-By: Claude Opus 4.8 (1M context) --- man/waybar-backlight-slider.5.scd | 11 ++++++ man/waybar-backlight.5.scd | 17 +++++++- man/waybar-battery.5.scd | 30 ++++++++++++++ man/waybar-cava.5.scd | 7 +++- man/waybar-cffi.5.scd | 13 +++++++ man/waybar-clock.5.scd | 23 ++++++++++- man/waybar-custom.5.scd | 54 ++++++++++++++++++++++++-- man/waybar-dwl-tags.5.scd | 6 ++- man/waybar-dwl-window.5.scd | 3 ++ man/waybar-ext-workspaces.5.scd | 3 ++ man/waybar-gamemode.5.scd | 2 +- man/waybar-hyprland-window.5.scd | 4 ++ man/waybar-hyprland-workspaces.5.scd | 30 +++++++++++++- man/waybar-image.5.scd | 23 +++++++++++ man/waybar-jack.5.scd | 8 +++- man/waybar-keyboard-state.5.scd | 3 +- man/waybar-memory.5.scd | 5 +++ man/waybar-mpd.5.scd | 6 ++- man/waybar-network.5.scd | 4 ++ man/waybar-power-profiles-daemon.5.scd | 12 ++++++ man/waybar-privacy.5.scd | 5 ++- man/waybar-pulseaudio-slider.5.scd | 6 +++ man/waybar-pulseaudio.5.scd | 8 +++- man/waybar-river-tags.5.scd | 17 ++++++++ man/waybar-river-window.5.scd | 5 +++ man/waybar-states.5.scd | 2 +- man/waybar-sway-language.5.scd | 13 +++++++ man/waybar-sway-window.5.scd | 8 ++++ man/waybar-temperature.5.scd | 23 +++++++++++ man/waybar-tray.5.scd | 23 +++++++++-- man/waybar-upower.5.scd | 2 +- man/waybar-wireplumber.5.scd | 14 +++++++ man/waybar-wlr-taskbar.5.scd | 7 ++++ 33 files changed, 375 insertions(+), 22 deletions(-) diff --git a/man/waybar-backlight-slider.5.scd b/man/waybar-backlight-slider.5.scd index e15b4026..d357ff80 100644 --- a/man/waybar-backlight-slider.5.scd +++ b/man/waybar-backlight-slider.5.scd @@ -41,6 +41,17 @@ The brightness can be controlled by dragging the slider across the bar or clicki default: false ++ Enables this module to consume all left over space dynamically. +*Note*: As well as the JSON configuration, the slider modules are special in +that they *require* styling to work. You *need* to set *min-width* and/or +*min-height* (depending on whether your slider is vertical or not) for it to +display correctly. That is a GTK detail, not an issue with Waybar. See the +*STYLE* section below. + +*Warning*: If *min* is set to *0* (default), the slider can set brightness to +*0*, which may completely disable the backlight on some devices, making the +screen fully black. Consider setting a small minimum value (e.g. *10*) or +configuring brightness keybinds as a fallback. + # EXAMPLES ``` diff --git a/man/waybar-backlight.5.scd b/man/waybar-backlight.5.scd index 27e97863..5a4d30db 100644 --- a/man/waybar-backlight.5.scd +++ b/man/waybar-backlight.5.scd @@ -78,7 +78,13 @@ The *backlight* module displays the current backlight level. *reverse-scrolling*: ++ typeof: bool ++ - Option to reverse the scroll direction. + default: false ++ + Option to reverse the scroll direction for devices other than a mouse (touchpad, trackpad, etc). + +*reverse-mouse-scrolling*: ++ + typeof: bool ++ + default: false ++ + Option to reverse the scroll direction for mice. *scroll-step*: ++ typeof: float ++ @@ -90,6 +96,15 @@ The *backlight* module displays the current backlight level. default: 0.0 ++ The minimum brightness of the backlight. +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + Text to be displayed in the tooltip. + *menu*: ++ typeof: string ++ Action that popups the menu. diff --git a/man/waybar-battery.5.scd b/man/waybar-battery.5.scd index 136bee0c..0708634d 100644 --- a/man/waybar-battery.5.scd +++ b/man/waybar-battery.5.scd @@ -124,6 +124,11 @@ The *battery* module displays the current capacity and state (eg. charging) of y default: true ++ Option to disable tooltip on hover. +*tooltip-format*: ++ + typeof: string ++ + default: {timeTo} ++ + The tooltip format. See *CUSTOM FORMATS* for status/state-specific variants. + *bat-compatibility*: ++ typeof: bool ++ default: false ++ @@ -165,6 +170,12 @@ The *battery* module displays the current capacity and state (eg. charging) of y *{health}*: The percentage of the highest-capacity battery's original maximum charge it can still hold. +The following additional replacement is available in *tooltip-format* (and its +custom variants) only: + +*{timeTo}*: Either an estimate of time until full or empty, or "Full", "Plugged" +or "Empty" depending on the current battery status. + # TIME FORMAT The *battery* module allows you to define how time should be formatted via *format-time*. @@ -184,6 +195,11 @@ The *battery* module allows one to define custom formats based on up to two fact *format--*: You can also set a custom format depending on both values. +The tooltip format can be customized the same way. The best fitting from +*tooltip-format*, *tooltip-format-*, *tooltip-format-* and +*tooltip-format--* will be used (using the same logic as +*format-\**). + # STATES - Every entry (*state*) consists of a ** (typeof: *string*) and a ** (typeof: *integer*). @@ -229,6 +245,20 @@ Where: } ``` +Custom icon set depending on the status (*format-icons* as an object): + +``` +"battery": { + "bat": "BAT2", + "interval": 60, + "format": "{capacity}% {icon}", + "format-icons": { + "default": ["󰂎", "󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"], + "charging": ["󰢟", "󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅"] + } +} +``` + # STYLE - *#battery* diff --git a/man/waybar-cava.5.scd b/man/waybar-cava.5.scd index 439858a1..60aa32ee 100644 --- a/man/waybar-cava.5.scd +++ b/man/waybar-cava.5.scd @@ -216,7 +216,12 @@ Configuration can be provided as: # SOLVING ISSUES . On start Waybar throws an exception "error while loading shared libraries: libcava.so: cannot open shared object file: No such file or directory". - It might happen when libcava for some reason hasn't been registered in the system. sudo ldconfig should help + It might happen when libcava for some reason hasn't been registered in the system. sudo ldconfig should help. + It might also happen when Waybar was installed into /usr/local while libcava lives elsewhere. In that case: + 1. Drop the local cava library: sudo rm -rfv /usr/local/include/cava /usr/local/lib64/pkgconfig/cava.pc /usr/local/lib64/libcava.so + 2. Set the prefix where Waybar should be installed: meson configure build -Dprefix="/usr" + 3. Build Waybar: make + 4. Install Waybar into the system: sudo meson install -C build . Waybar is starting but cava module doesn't react to the music 1. In such cases at first need to make sure usual cava application is working as well 2. If so, need to comment all configuration options. Uncomment cava_config and provide the path to the working cava config diff --git a/man/waybar-cffi.5.scd b/man/waybar-cffi.5.scd index 0203ef77..9ac4aa79 100644 --- a/man/waybar-cffi.5.scd +++ b/man/waybar-cffi.5.scd @@ -37,6 +37,19 @@ Waybar config to enable the module: ``` +# DEVELOPING CFFI MODULES + +CFFI modules require a handful of functions and constants to be defined with C +linkage. The way to achieve this depends on the programming language being used +(search for FFI / Foreign Function Interface for that language). + +The complete list of symbols to define can be found in the header file shipped with +Waybar at *resources/custom_modules/cffi_example/waybar_cffi_module.h*, and a full +example written in C is provided in *resources/custom_modules/cffi_example/*. + +Language bindings exist for several languages, including Rust (the *waybar-cffi* +crate) and Zig. + # STYLE The classes and IDs are managed by the cffi dynamic library. diff --git a/man/waybar-clock.5.scd b/man/waybar-clock.5.scd index 0901098b..8537779c 100644 --- a/man/waybar-clock.5.scd +++ b/man/waybar-clock.5.scd @@ -6,7 +6,16 @@ waybar - clock module # DESCRIPTION -*clock* module displays current date and time +*clock* module displays current date and time. + +There are two implementations: + +- *clock*: Full-featured implementation, including the calendar and timezone + support described below. Enabled at build time when either C++20 concepts + (\_\_cpp\_concepts >= 201907, gcc >= 13) are available, or the HowardHinnant date + library is installed. +- *simpleclock*: Fallback that provides date and time display only. Used when the + above build conditions are not met. # FILES @@ -27,7 +36,8 @@ $XDG_CONFIG_HOME/waybar/config ++ |[ *format* :[ string :[ *{:%H:%M}* -:[ The format, how the date and time should be displayed. See format options below +:[ The format, how the date and time should be displayed. See format options + below. Split the braces to insert literal text, e.g. *{0:%H}text{0:%M}* |[ *timezone* :[ string :[ @@ -295,6 +305,11 @@ View all valid format options in *strftime(3)* or have a look https://en.cpprefe - *#clock* +If displaying seconds causes other modules to shift side to side, the cause is +usually proportional-width digits. If your font supports it, add +*font-feature-settings: "tnum";* to the *#clock* style (or wherever you set the bar +font) to use fixed-width numbers. + The following classes are used only inside the *{calendar}* tooltip. Their foreground *color* is read and applied to the corresponding calendar cells in the tooltip markup: @@ -313,6 +328,10 @@ name not valid error message try one of the following: - check if LC_TIME is set properly (glibc) - set locale to C in the config file (musl) +When using *clock* instead of *simpleclock*, the locale defaults to *C* regardless +of the locale settings. To override this, prepend *L* to the format string, e.g. +*{:%a %m %d}* becomes *{:L%a %m %d}*. + The locale option must be set for {calendar} to use the correct start-of-week, regardless of system locale. ## Calendar in Chinese. Alignment diff --git a/man/waybar-custom.5.scd b/man/waybar-custom.5.scd index 2d8161ca..bf19b955 100644 --- a/man/waybar-custom.5.scd +++ b/man/waybar-custom.5.scd @@ -61,8 +61,11 @@ Addressed by *custom/* The format, how information should be displayed. On {text} data gets inserted. *format-icons*: ++ - typeof: array ++ - Based on the set percentage, the corresponding icon gets selected. The order is *low* to *high*. + typeof: array or object or string ++ + If the type is an array, then based on the set percentage, the corresponding icon gets selected (the order is *low* to *high*). ++ + If the type is an object, then the icon is selected according to the *alt* string from the output. ++ + If the type is a string, it is pasted as is. ++ + Arrays can be nested into objects: icons are then selected first according to *alt*, then percentage. *rotate*: ++ typeof: integer ++ @@ -163,11 +166,18 @@ When *return-type* is set to *json*, Waybar expects the *exec*-script to output This should look like this: ``` -{"text": "$text", "tooltip": "$tooltip", "class": "$class", "percentage": $percentage } +{"text": "$text", "alt": "$alt", "tooltip": "$tooltip", "class": "$class", "percentage": $percentage } ``` +The whole JSON object must be printed on a single line. This can be achieved by +piping the output of your script through *jq --unbuffered --compact-output*. + The *class* parameter also accepts an array of strings. +To have multiline tooltips, use *\\r* in your script to separate the lines. If +using PowerShell for scripting, use the standard newline operator "\`n" in double +quotes; *\\r* and *\\n* will not work. + If nothing or an invalid option is specified, Waybar expects i3blocks style output. Values are *newline* separated. This should look like this: @@ -179,6 +189,8 @@ $text\\n$tooltip\\n$class* # FORMAT REPLACEMENTS +*{}*: Output of the script. Equivalent to *{text}*. + *{text}*: Output of the script. *{alt}*: The *alt* value from a json return type. @@ -187,6 +199,11 @@ $text\\n$tooltip\\n$class* *{icon}*: An icon from 'format-icons' according to percentage. +The *{}* placeholder is special: it automatically displays the text output of your +script, but it cannot be combined with other placeholders like *{icon}* in the same +format string. To display both an icon and text, use *{icon}* together with *{text}* +explicitly (e.g. *"format": "{icon} {text}"*). + # EXAMPLES ## Spotify: @@ -264,3 +281,34 @@ Under the premise that interval is not defined, you can use the signal and updat - ** can be set by the script. For more information see *return-type* - *.flat* and *.text-button* are always applied to the module's label. - *.image-button* is always applied to the module's image (see *image-path*/*image-name*). + +# CONTINUOUS SCRIPTS + +The *exec* script may be continuous (i.e. contain some kind of infinite loop). The +display is updated for each new line of data printed on stdout (following the chosen +*return-type*). The *interval* option does not apply to a continuous script; use +*restart-interval* instead to restart the script if it stops after some time. + +Be aware that some languages buffer their output. If your module displays nothing +even though your script works as expected, the output may be held in a buffer. Look +up how to flush the output buffer for your language of choice (for example, in Ruby +call *$stdout.flush* after each print). + +# OUTPUT NAME + +The *exec* script is run with the *WAYBAR_OUTPUT_NAME* environment variable set to +the name of the output (monitor) the bar is displayed on. + +# TROUBLESHOOTING + +*Self-looping module does not show up* + +If your module is self-looping and it does not even show up in the bar, check that: + +- Its configuration does *not* include an *interval* parameter. +- Output to stdout is not buffered. + +*Custom json class not displayed* + +If a class set in your custom script is not picked up by *style.css*, ensure that +the output carrying the most variables is emitted first. diff --git a/man/waybar-dwl-tags.5.scd b/man/waybar-dwl-tags.5.scd index c301f49a..e331eef9 100644 --- a/man/waybar-dwl-tags.5.scd +++ b/man/waybar-dwl-tags.5.scd @@ -8,6 +8,9 @@ waybar - dwl tags module The *tags* module displays the current state of tags in dwl. +Using this module requires patching dwl with the IPC patch +. + # CONFIGURATION Addressed by *dwl/tags* @@ -15,7 +18,8 @@ Addressed by *dwl/tags* *num-tags*: ++ typeof: uint ++ default: 9 ++ - The number of tags that should be displayed. Max 32. + The number of tags that should be displayed. Max 32. This should match the + number of tags configured in dwl. *tag-labels*: ++ typeof: array ++ diff --git a/man/waybar-dwl-window.5.scd b/man/waybar-dwl-window.5.scd index b347b1d8..696eadb6 100644 --- a/man/waybar-dwl-window.5.scd +++ b/man/waybar-dwl-window.5.scd @@ -8,6 +8,9 @@ waybar - dwl window module The *window* module displays the title of the currently focused window in DWL +Using this module requires patching dwl with the IPC patch +. + # CONFIGURATION Addressed by *dwl/window* diff --git a/man/waybar-ext-workspaces.5.scd b/man/waybar-ext-workspaces.5.scd index 0ee17097..15bf4760 100644 --- a/man/waybar-ext-workspaces.5.scd +++ b/man/waybar-ext-workspaces.5.scd @@ -8,6 +8,9 @@ waybar - wlr workspaces module The *workspaces* module displays the currently used workspaces in wayland compositor. +To use this module, your compositor must implement the *ext-workspace-v1* +Wayland protocol. + # CONFIGURATION Addressed by *ext/workspaces* diff --git a/man/waybar-gamemode.5.scd b/man/waybar-gamemode.5.scd index d7e1991b..bcc5592f 100644 --- a/man/waybar-gamemode.5.scd +++ b/man/waybar-gamemode.5.scd @@ -54,7 +54,7 @@ Feral Gamemode optimizations. *icon-size*: ++ typeof: unsigned integer ++ default: 20 ++ - Defines the size of the icons. + Defines the size of the icons. Set to *0* for auto size. *icon-spacing*: ++ typeof: unsigned integer ++ diff --git a/man/waybar-hyprland-window.5.scd b/man/waybar-hyprland-window.5.scd index a79596c0..0ef9bdea 100644 --- a/man/waybar-hyprland-window.5.scd +++ b/man/waybar-hyprland-window.5.scd @@ -17,6 +17,10 @@ Addressed by *hyprland/window* default: {title} ++ The format, how information should be displayed. On {} the current window title is displayed. +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + *rewrite*: ++ typeof: object ++ Rules to rewrite window title. See *rewrite rules*. diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index f85ac382..3010fb45 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -139,7 +139,8 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. *persistent-workspaces*: ++ typeof: object ++ default: empty ++ - Lists workspaces that should always be shown, even when they do not exist. Keys are workspace names and values are arrays of output names on which the workspace should be shown (an empty array means all outputs). See the examples below. + Lists workspaces that should always be shown, even when they do not exist. Keys are workspace names and values are arrays of output names on which the workspace should be shown (an empty array means all outputs). See the examples below. ++ + Note: for persistent workspaces to actually work you must also declare them in your Hyprland config, e.g. *workspace = 1, monitor:eDP-1, persistent:true*. *all-outputs*: ++ typeof: bool ++ @@ -219,6 +220,32 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. *{windows}*: The windows in the workspace, formatted according to *window-rewrite* and joined with *format-window-separator*. +# WINDOW REWRITE RULES + +The rules in *window-rewrite* are regexes that may match against a window's +class, title, or both. There are four categories of rule, distinguished by how +they are written in the config: + +[- *Rule* +:- *Category* +|[ *something* +:[ Vague +|[ *class* +:[ Class-only +|[ *title* +:[ Title-only +|[ *class title* +:[ Hybrid + +When the config contains only "vague" rules, they are matched against window +*classes* only. This is both for backwards compatibility and for performance: +matching against the title requires listening to window title changes via +Hyprland's IPC, which is unnecessary when no title rule is in use. + +When the config contains *at least one* "title-only" or "hybrid" rule, then all +"vague" rules match against *both* class and title. This lets you define vague +rules where it does not matter whether the class or the title matched. + # ICONS Additional to workspace name matching, the following *format-icons* can be set. @@ -229,6 +256,7 @@ Additional to workspace name matching, the following *format-icons* can be set. - *empty*: Will be shown on non-active, non-special empty persistent workspaces - *visible*: Will be shown on workspaces that are visible but not active. For example: this is useful if you want your visible workspaces on other monitors to have the same look as active. - *persistent*: Will be shown on non-empty persistent workspaces +- *urgent*: Will be shown on non-active urgent workspaces # EXAMPLES diff --git a/man/waybar-image.5.scd b/man/waybar-image.5.scd index 3a7813c2..b75a2015 100644 --- a/man/waybar-image.5.scd +++ b/man/waybar-image.5.scd @@ -100,6 +100,29 @@ $path\\n$tooltip } ``` +Using *exec* to fetch the image via a script: + +``` +"image#album-art": { + "exec": "~/.config/waybar/custom/spotify/album_art.sh", + "size": 32, + "interval": 30 +} +``` + +Where *album\_art.sh* looks like this: + +``` +#!/bin/bash +album_art=$(playerctl -p spotify metadata mpris:artUrl) +if [[ -z $album_art ]]; then + # spotify is dead, we should die too. + exit +fi +curl -s "${album_art}" --output "/tmp/cover.jpeg" +echo "/tmp/cover.jpeg" +``` + # STYLE - *#image* diff --git a/man/waybar-jack.5.scd b/man/waybar-jack.5.scd index 98adc8db..929f2d58 100644 --- a/man/waybar-jack.5.scd +++ b/man/waybar-jack.5.scd @@ -6,7 +6,13 @@ waybar - JACK module # DESCRIPTION -The *jack* module displays the current state of the JACK server. +The *jack* module displays the current state of the JACK server. It supports both +the JACK2 and PipeWire +implementations of the JACK API. + +PipeWire users are advised to run at least v0.3.57; earlier versions cause the +Waybar client to hang indefinitely if the server shuts down while the module is +running. # CONFIGURATION diff --git a/man/waybar-keyboard-state.5.scd b/man/waybar-keyboard-state.5.scd index 7babe336..6dcd590e 100644 --- a/man/waybar-keyboard-state.5.scd +++ b/man/waybar-keyboard-state.5.scd @@ -8,7 +8,8 @@ waybar - keyboard-state module The *keyboard-state* module displays the state of number lock, caps lock, and scroll lock. -You must be a member of the input group to use this module. +You must be a member of the input group to use this module. Add yourself with +*usermod -a -G input * (as root), then reboot. # CONFIGURATION diff --git a/man/waybar-memory.5.scd b/man/waybar-memory.5.scd index 99f57299..8ff51688 100644 --- a/man/waybar-memory.5.scd +++ b/man/waybar-memory.5.scd @@ -84,6 +84,11 @@ Addressed by *memory* default: true ++ Option to disable tooltip on hover. +*tooltip-format*: ++ + typeof: string ++ + default: {used:0.1f}GiB used ++ + The format of the tooltip shown on hover. Supports the same replacements as *format*. + *menu*: ++ typeof: string ++ Action that popups the menu. diff --git a/man/waybar-mpd.5.scd b/man/waybar-mpd.5.scd index 01c86e05..bf331f08 100644 --- a/man/waybar-mpd.5.scd +++ b/man/waybar-mpd.5.scd @@ -8,17 +8,19 @@ waybar - mpd module The *mpd* module displays information about a running "Music Player Daemon" instance. +Note that Waybar must be compiled with MPD support in order to use this module. + # CONFIGURATION Addressed by *mpd* *server*: ++ typeof: string ++ - The network address or Unix socket path of the MPD server. If empty, connect to the default host. + The network address or Unix socket path of the MPD server. If empty, connect to the default host (`MPD_HOST`). *port*: ++ typeof: integer ++ - The port MPD listens to. If empty, use the default port. + The port MPD listens to. If empty, use the default port (`MPD_PORT`). *password*: ++ typeof: string ++ diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index f86ed717..efeed0ce 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -56,6 +56,10 @@ Addressed by *network* typeof: string ++ This format is used when rfkill is blocking wlan interfaces. +*format-alt*: ++ + typeof: string ++ + On click, toggle between this format and the regular format. The click button is set with *format-alt-click* (defaults to the left button). + *format-icons*: ++ typeof: array/object ++ Based on the current signal strength, the corresponding icon gets selected. ++ diff --git a/man/waybar-power-profiles-daemon.5.scd b/man/waybar-power-profiles-daemon.5.scd index a5489444..808fe894 100644 --- a/man/waybar-power-profiles-daemon.5.scd +++ b/man/waybar-power-profiles-daemon.5.scd @@ -44,6 +44,18 @@ $XDG_CONFIG_HOME/waybar/config +# FORMAT REPLACEMENTS + +*{profile}*: Active profile full name. + +*{icon}*: Icon (from *format-icons*) representing the active profile. + +*{cpu_driver}*: The CPU driver reported by recent power-profiles-daemon versions. *(tooltip)* + +*{platform_driver}*: The platform driver reported by recent power-profiles-daemon versions. *(tooltip)* + +*{driver}*: Kept for backward compatibility: resolves to the legacy single driver on older daemons and falls back to the CPU driver on recent ones. *(tooltip)* + # CONFIGURATION EXAMPLES Compact display (default config): diff --git a/man/waybar-privacy.5.scd b/man/waybar-privacy.5.scd index 72191dfe..754cdfa4 100644 --- a/man/waybar-privacy.5.scd +++ b/man/waybar-privacy.5.scd @@ -19,7 +19,7 @@ the screen or playing audio. *icon-size*: ++ typeof: integer ++ default: 20 ++ - The size of each privacy icon. + The size of each privacy icon. Set to *0* for auto size. *transition-duration*: ++ typeof: integer ++ @@ -57,7 +57,8 @@ the screen or playing audio. *icon-name*: ++ typeof: string ++ - If set, changes the modules icon. + If set, changes the modules icon. Accepts any valid GTK icon name. ++ + Note: the icon is only visible when the resource is actively in use; the item is hidden when idle. *tooltip*: ++ typeof: bool ++ diff --git a/man/waybar-pulseaudio-slider.5.scd b/man/waybar-pulseaudio-slider.5.scd index 381ab867..9830b37c 100644 --- a/man/waybar-pulseaudio-slider.5.scd +++ b/man/waybar-pulseaudio-slider.5.scd @@ -87,6 +87,12 @@ The volume can be controlled by dragging the slider across the bar or clicking o # STYLE +Unlike most modules, the slider modules *require* styling to display correctly. +In particular you *need* to set *min-width* and/or *min-height* (depending on +whether the slider is vertical or horizontal) on the *trough* node, otherwise +the slider renders too small to be usable. This is a GTK detail, not a Waybar +issue. + The slider is a component with multiple CSS Nodes, of which the following are exposed: *#pulseaudio-slider*: ++ diff --git a/man/waybar-pulseaudio.5.scd b/man/waybar-pulseaudio.5.scd index 71b618f7..f72f9c1d 100644 --- a/man/waybar-pulseaudio.5.scd +++ b/man/waybar-pulseaudio.5.scd @@ -108,6 +108,10 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c default: true ++ Option to disable tooltip on hover. +*tooltip-format*: ++ + typeof: string ++ + The format of the tooltip shown on hover. When unset, the tooltip shows the current sink's description. + *max-volume*: ++ typeof: integer ++ default: 100 ++ @@ -115,7 +119,7 @@ Additionally, you can control the volume by scrolling *up* or *down* while the c *ignored-sinks*: ++ typeof: array ++ - Sinks in this list will not be shown as active sink by Waybar. Entries should be the sink's description field. + Sinks in this list will not be shown as active sink by Waybar. Entries should be the sink's description field. Use *pactl list sinks* to find the correct description. *sink-mapping*: ++ typeof: object ++ @@ -164,6 +168,8 @@ The following strings for *format-icons* are supported. - the device name +The device name looks something like *alsa\_output.pci-0000\_00\_1f.3.analog-stereo*. You can find it with a PulseAudio frontend such as *pacmd list-sinks* or *pamixer --list-sinks*. + If they are found in the current PulseAudio port name, the corresponding icons will be selected. - *default* (Shown, when no other port is found) diff --git a/man/waybar-river-tags.5.scd b/man/waybar-river-tags.5.scd index 7d6e9c7c..23ce305b 100644 --- a/man/waybar-river-tags.5.scd +++ b/man/waybar-river-tags.5.scd @@ -52,6 +52,23 @@ Addressed by *river/tags* } ``` +Sticky tag: this enables 5 tags and, to support a sticky tag (tag 32), +*set-tags* is given bitmasks with the 32nd bit set so that tag stays selected +whichever button is clicked. + +``` +"river/tags": { + "num-tags": 5, + "set-tags": [ + 2147483649, + 2147483650, + 2147483652, + 2147483656, + 2147483664 + ] +} +``` + # STYLE - *#tags button* diff --git a/man/waybar-river-window.5.scd b/man/waybar-river-window.5.scd index 82eee0a5..e5bed511 100644 --- a/man/waybar-river-window.5.scd +++ b/man/waybar-river-window.5.scd @@ -49,6 +49,11 @@ Addressed by *river/window* typeof: string ++ Command to execute when you right-click on the module. +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to enable tooltip on hover. The tooltip shows the same text as the module label. + *menu*: ++ typeof: string ++ Action that popups the menu. diff --git a/man/waybar-states.5.scd b/man/waybar-states.5.scd index dea1beea..90ea500d 100644 --- a/man/waybar-states.5.scd +++ b/man/waybar-states.5.scd @@ -14,7 +14,7 @@ apply a class when the value matches the declared state value. Every entry (*state*) consists of a ** (typeof: *string*) and a ** (typeof: *integer*). - The state can be addressed as a CSS class in the *style.css*. The name of the CSS class is the ** of the state. - Each class gets activated when the current value is equal to or less than the configured ** for the *battery* module, or equal to or greater than the configured ** for all other modules. + Each class gets activated when the current value is equal to or less than the configured ** for the *battery*, *pulseaudio* and *wireplumber* modules, or equal to or greater than the configured ** for all other modules. - Also, each state can have its own *format*. Those can be configured via *format-*, or if you want to differentiate a bit more, as *format--*. diff --git a/man/waybar-sway-language.5.scd b/man/waybar-sway-language.5.scd index 8515dbaf..e008fa92 100644 --- a/man/waybar-sway-language.5.scd +++ b/man/waybar-sway-language.5.scd @@ -32,6 +32,18 @@ Addressed by *sway/language* default: true ++ Option to disable tooltip on hover. +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when you middle clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right clicked on the module. + *menu*: ++ typeof: string ++ Action that popups the menu. @@ -67,6 +79,7 @@ Addressed by *sway/language* ``` "sway/language": { "format": "{}", + "on-click": "swaymsg input type:keyboard xkb_switch_layout next", }, "sway/language": { diff --git a/man/waybar-sway-window.5.scd b/man/waybar-sway-window.5.scd index ad5195c7..c8bb160b 100644 --- a/man/waybar-sway-window.5.scd +++ b/man/waybar-sway-window.5.scd @@ -173,3 +173,11 @@ window widget: - *window#waybar.tabbed* When there is more than one window in the workspace and the workspace layout is tabbed - *window#waybar.tiled* When there is more than one window in the workspace and the workspace layout is splith or splitv - *window#waybar.* Where *app_id* is the app_id or *instance* name like (*chromium*) of the only window in the workspace + +These additional classes are applied to the whole Waybar (an element with the +name *window* and the id *waybar*) rather than to the module widget itself (an +element with the name *box* and the id *window*). In most cases you therefore +want to target *window#waybar. #window* to style the module. + +Switching layouts does not change the style unless the focus is changed. Sway +does not provide events for layout switching and the module does not poll. diff --git a/man/waybar-temperature.5.scd b/man/waybar-temperature.5.scd index 0494e989..cc8a3e71 100644 --- a/man/waybar-temperature.5.scd +++ b/man/waybar-temperature.5.scd @@ -156,6 +156,8 @@ Addressed by *temperature* *{temperatureK}*: Temperature in Kelvin. +*{icon}*: Icon, as selected from *format-icons* based on the current temperature. + # EXAMPLES ``` @@ -173,3 +175,24 @@ Addressed by *temperature* - *#temperature* - *#temperature.warning* - *#temperature.critical* + +# TROUBLESHOOTING + +## Finding your thermal zone + +To list all the thermal zone types, run: + +``` +for i in /sys/class/thermal/thermal_zone*; do echo "$i: $(<$i/type)"; done +``` + +## Finding an hwmon path + +If you do not have a thermal zone, use *sensors* to find the preferred +temperature source, then run: + +``` +for i in /sys/class/hwmon/hwmon*/temp*_input; do echo "$(<$(dirname $i)/name): $(cat ${i%_*}_label 2>/dev/null || echo $(basename ${i%_*})) $(readlink -f $i)"; done +``` + +to find the path to the desired file, then set it in *hwmon-path*. diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index 3037ecb0..30aeed99 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -55,6 +55,9 @@ Addressed by *tray* Key: name of item. ++ Value: integer, higher value means to place this item to the right. +Note: custom *icons* only work with actual image files, not font-based icons. They +might not work for some Electron apps (see https://github.com/electron/electron/issues/40936). + *ignore-list*: ++ typeof: array ++ List of strings used to hide matching tray items. An item is hidden when any string in the list is found (as a substring) in the item's bus name, category, icon name, id or title. @@ -80,6 +83,20 @@ Addressed by *tray* # STYLE - *#tray* -- *#tray > .passive* -- *#tray > .active* -- *#tray > .needs-attention* +- *#tray menu* for the context menu +- *#tray widget>image* for icons +- *#tray > .passive* for icons with status *Passive* +- *#tray > .active* for icons with status *Active* +- *#tray > .needs-attention* for icons with status *NeedsAttention* + +# TRAY APPLETS + +The following applets are known to work with the *tray* module: + +- *NetworkManager*: install networkmanager, enable it with + *systemctl enable NetworkManager.service*, and start the applet with + *nm-applet --indicator*. +- *Blueman*: enable the Bluetooth daemon, install blueman, and start + *blueman-applet*. +- *Electron apps*: apps like Slack, Telegram, Caprine and Discord should work + out of the box. diff --git a/man/waybar-upower.5.scd b/man/waybar-upower.5.scd index 68c2d3ed..64074891 100644 --- a/man/waybar-upower.5.scd +++ b/man/waybar-upower.5.scd @@ -26,7 +26,7 @@ compatible devices in the tooltip. *icon-size*: ++ typeof: integer ++ default: 20 ++ - Defines the size of the icons. + Defines the size of the icons. Set to *0* for automatic sizing. *format*: ++ typeof: string ++ diff --git a/man/waybar-wireplumber.5.scd b/man/waybar-wireplumber.5.scd index 9d6517db..b360cae6 100644 --- a/man/waybar-wireplumber.5.scd +++ b/man/waybar-wireplumber.5.scd @@ -36,6 +36,10 @@ The *wireplumber* module displays the current volume reported by WirePlumber. typeof: string ++ The format used for the *{format_source}* sub-string when the source (microphone) is muted. +*format-icons*: ++ + typeof: array ++ + Based on the current volume, the corresponding icon gets selected. The order is *low* to *high*. + *node-type*: ++ typeof: string ++ default: *Audio/Sink* ++ @@ -90,6 +94,14 @@ The *wireplumber* module displays the current volume reported by WirePlumber. default: cubic_percent The scale to use for the scrolling volume change. Options are 'linear', 'db', 'cubic', and 'cubic_percent'. +*reverse-scrolling*: ++ + typeof: bool ++ + Option to reverse the scroll direction for touchpads. + +*reverse-mouse-scrolling*: ++ + typeof: bool ++ + Option to reverse the scroll direction for mice. + *on-click*: ++ typeof: string ++ Command to execute when clicked on the module. @@ -142,6 +154,8 @@ The *wireplumber* module displays the current volume reported by WirePlumber. *{volume_db}*: Volume in decibel scale as float. +*{icon}*: Icon, as defined in *format-icons*. + *{node_name}*: The node's nickname as reported by WirePlumber (*node.nick* property) *{source_volume}*: Source (microphone) volume in percentage, cubic scale as integer. diff --git a/man/waybar-wlr-taskbar.5.scd b/man/waybar-wlr-taskbar.5.scd index b8c661ff..5e20a541 100644 --- a/man/waybar-wlr-taskbar.5.scd +++ b/man/waybar-wlr-taskbar.5.scd @@ -135,6 +135,11 @@ Addressed by *wlr/taskbar* *{short_state}*: The state (minimize == m, maximized == M, active == A, fullscreen == F) represented as one character of the application. +As with all format replacements in Waybar, a length limit such as *{title:.15}* is +measured in bytes, not characters. This may produce invalid text if a multi-byte +character is split, or (when *markup* is true) if the truncation cuts through a +value that must be escaped in XML. + # CLICK ACTIONS *activate*: Bring the application into foreground. @@ -157,6 +162,8 @@ captures of the expression. Regular expression and replacement follow ECMA-script rules. +An expression must match the format output *fully* to trigger its replacement. + If no expression matches, the format output is left unchanged. Invalid expressions (e.g., mismatched parentheses) are skipped. From c76bfc018b6423644808e4f13ca3c1459e6437e7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:02:10 +0200 Subject: [PATCH 305/416] ci: auto-sync GitHub wiki module pages from man pages Add tooling that makes the scdoc man pages the single source of truth and regenerates the wiki module pages from them: - .github/wiki/mapping.json: man-page -> wiki-page mapping (aggregation-aware) - .github/wiki/generate.py: scdoc -> pandoc -> gfm, strips man-only sections, concatenates aggregated pages, appends optional extras/.md - .github/wiki/extras/: hand-kept appendices (screenshots) with no man equivalent - .github/workflows/wiki.yml: on push to man/** (or the tooling), regenerate and push the wiki; other wiki pages are left untouched Covers all 65 man pages -> 45 wiki pages (5 new: GPS, Inhibitor, Mango, Menu, WWAN). Non-module and hand-written wiki pages are never modified. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/wiki/README.md | 42 ++++++++ .github/wiki/extras/Module:-Clock.md | 3 + .github/wiki/extras/Module:-Hyprland.md | 6 ++ .github/wiki/generate.py | 137 ++++++++++++++++++++++++ .github/wiki/mapping.json | 49 +++++++++ .github/workflows/wiki.yml | 56 ++++++++++ 6 files changed, 293 insertions(+) create mode 100644 .github/wiki/README.md create mode 100644 .github/wiki/extras/Module:-Clock.md create mode 100644 .github/wiki/extras/Module:-Hyprland.md create mode 100755 .github/wiki/generate.py create mode 100644 .github/wiki/mapping.json create mode 100644 .github/workflows/wiki.yml diff --git a/.github/wiki/README.md b/.github/wiki/README.md new file mode 100644 index 00000000..6588bec4 --- /dev/null +++ b/.github/wiki/README.md @@ -0,0 +1,42 @@ +# Wiki sync + +The scdoc man pages under [`man/`](../../man) are the **single source of truth** +for module documentation. The GitHub wiki module pages are generated from them by +[`generate.py`](generate.py) and kept in sync automatically by the +[`wiki.yml`](../workflows/wiki.yml) workflow on every push to `master` that +touches `man/**` or this tooling. + +## Files + +- `mapping.json` — maps each wiki page to the ordered list of man-page basenames + that compose it. Several man pages are concatenated into one aggregated page + (e.g. `Module:-Hyprland` ← the four `waybar-hyprland-*` pages). **Add an entry + here when you add a new man page** — the workflow fails the mapping check + otherwise. +- `generate.py` — `scdoc → roff → pandoc → gfm`, strips the `NAME`/`FILES`/`AUTHOR` + man sections, and writes one `Module:-*.md` per mapping entry. +- `extras/.md` — optional hand-maintained appendix (screenshots, showcase + snippets that have no man equivalent), appended verbatim after the generated body. + +## What it touches + +Only the pages listed in `mapping.json` are (re)written. Every other wiki page +(`Home`, `Installation`, user showcases, the hand-written `Module:-Cava:-GLSL`, +`Module:-Group`, `Module:-Load`, …) is left untouched. + +## To edit a module's docs + +Edit the man page under `man/`, not the wiki. The wiki page is overwritten on the +next sync. Put anything with no man equivalent (images, etc.) in `extras/`. + +## Run locally + +```sh +python3 .github/wiki/generate.py --check # validate mapping only +python3 .github/wiki/generate.py --out-dir /tmp/wiki-out # generate a preview +``` + +## One-time setup + +The repository wiki must be enabled (Settings → Features → Wikis) with at least +one initial page so the `.wiki.git` remote exists. diff --git a/.github/wiki/extras/Module:-Clock.md b/.github/wiki/extras/Module:-Clock.md new file mode 100644 index 00000000..02c31a8a --- /dev/null +++ b/.github/wiki/extras/Module:-Clock.md @@ -0,0 +1,3 @@ +## Screenshots + +![calendar](https://github.com/Alexays/Waybar/assets/6098822/e5c38984-6b3c-43ef-8cac-05122a078d58) diff --git a/.github/wiki/extras/Module:-Hyprland.md b/.github/wiki/extras/Module:-Hyprland.md new file mode 100644 index 00000000..ea1a8046 --- /dev/null +++ b/.github/wiki/extras/Module:-Hyprland.md @@ -0,0 +1,6 @@ +## Screenshots + +![](https://user-images.githubusercontent.com/25804378/270065881-acef7b73-e528-4648-82c1-ba70ea9f7804.png) +![Workspace taskbars example](https://github.com/user-attachments/assets/c5387d8b-6612-442a-83af-2a0d881fa6db) +![image](https://github.com/Alexays/Waybar/assets/1778670/6421e9ce-50be-4086-8045-688338d5fbe7) +![image](https://github.com/Alexays/Waybar/assets/1778670/48c58198-aae4-42ac-802a-33b1e3db5e33) diff --git a/.github/wiki/generate.py b/.github/wiki/generate.py new file mode 100755 index 00000000..7e84e0d9 --- /dev/null +++ b/.github/wiki/generate.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Generate GitHub wiki pages from the scdoc man pages. + +The man pages under man/ are the single source of truth for module +documentation. This script converts them to GitHub-flavoured Markdown +(scdoc -> roff -> pandoc -> gfm) and writes one wiki page per entry in +mapping.json, concatenating several man pages into one page where the +wiki keeps an aggregated page (e.g. Module:-Hyprland). + +Only the pages listed in mapping.json are (re)written; every other wiki +page (Home, Installation, user showcases, ...) is left untouched. + +Usage: + generate.py --man-dir man --out-dir [--check] + +Requires: scdoc, pandoc. +""" +import argparse +import json +import os +import re +import subprocess +import sys + +# Man-page sections that are meaningless on the wiki and must be dropped. +DROP_SECTIONS = {"NAME", "FILES", "AUTHOR", "AUTHORS"} + +HEADING_RE = re.compile(r"^(#+)\s+(.*)$") + + +def sh(cmd, stdin=None): + return subprocess.run( + cmd, input=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True, + ).stdout + + +def man_to_gfm(scd_path): + """scdoc -> roff -> pandoc gfm, as raw markdown text.""" + roff = sh(["scdoc"], stdin=open(scd_path, "rb").read()) + gfm = sh(["pandoc", "-f", "man", "-t", "gfm", "--wrap=none"], stdin=roff) + return gfm.decode("utf-8") + + +def strip_sections(md): + """Remove top-level man-only sections (NAME/FILES/AUTHOR).""" + out, drop = [], False + for line in md.splitlines(): + m = HEADING_RE.match(line) + if m and len(m.group(1)) == 1: + drop = m.group(2).strip().upper() in DROP_SECTIONS + if not drop: + out.append(line) + return "\n".join(out).strip("\n") + + +def demote(md, levels=1): + """Add `levels` extra '#' to every heading (for aggregated pages).""" + out = [] + for line in md.splitlines(): + m = HEADING_RE.match(line) + out.append("#" * levels + line if m else line) + return "\n".join(out) + + +def pretty(basename): + """waybar-sway-mode -> sway/mode (submodule heading for aggregated pages).""" + return basename.removeprefix("waybar-").replace("-", "/", 1) + + +def build_page(man_dir, sources, page, extras_dir): + parts = [] + banner = ("") + parts.append(banner) + aggregated = len(sources) > 1 + for src in sources: + scd = os.path.join(man_dir, src + ".5.scd") + body = strip_sections(man_to_gfm(scd)) + if aggregated: + parts.append(f"\n# {pretty(src)}\n\n" + demote(body)) + else: + parts.append("\n" + body) + # Optional hand-maintained appendix (screenshots, showcase snippets) that + # has no man-page equivalent: .github/wiki/extras/.md, appended verbatim. + extra = os.path.join(extras_dir, page + ".md") + if os.path.exists(extra): + parts.append("\n" + open(extra).read().strip("\n")) + return "\n".join(parts).rstrip() + "\n" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--man-dir", default="man") + ap.add_argument("--out-dir") + ap.add_argument("--mapping", default=os.path.join(os.path.dirname(__file__), "mapping.json")) + ap.add_argument("--check", action="store_true", + help="Only validate the mapping vs man/, write nothing.") + args = ap.parse_args() + + mapping = {k: v for k, v in json.load(open(args.mapping)).items() + if not k.startswith("_")} + + # Validate: every man page mapped exactly once; every source exists. + mapped, errors = {}, [] + for page, sources in mapping.items(): + for s in sources: + if not os.path.exists(os.path.join(args.man_dir, s + ".5.scd")): + errors.append(f"{page}: man/{s}.5.scd does not exist") + if s in mapped: + errors.append(f"{s} mapped to both {mapped[s]} and {page}") + mapped[s] = page + on_disk = {f[:-6] for f in os.listdir(args.man_dir) if f.endswith(".5.scd")} + for s in sorted(on_disk - set(mapped)): + errors.append(f"man/{s}.5.scd is not referenced in mapping.json") + if errors: + print("Mapping errors:\n " + "\n ".join(errors), file=sys.stderr) + return 1 + print(f"Mapping OK: {len(on_disk)} man pages -> {len(mapping)} wiki pages") + if args.check: + return 0 + if not args.out_dir: + print("--out-dir is required unless --check", file=sys.stderr) + return 2 + + extras_dir = os.path.join(os.path.dirname(args.mapping), "extras") + os.makedirs(args.out_dir, exist_ok=True) + for page, sources in mapping.items(): + out = os.path.join(args.out_dir, page + ".md") + open(out, "w").write(build_page(args.man_dir, sources, page, extras_dir)) + print(f" wrote {page}.md ({', '.join(sources)})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/wiki/mapping.json b/.github/wiki/mapping.json new file mode 100644 index 00000000..1bee1bc3 --- /dev/null +++ b/.github/wiki/mapping.json @@ -0,0 +1,49 @@ +{ + "_comment": "Maps each wiki page to the ordered list of man-page basenames that compose it. Edit this when adding a module or man page. The generator (generate.py) turns each man/.5.scd into the wiki page named by the key. Keys use the exact wiki page title (spaces become dashes in the file name, ':' is kept).", + + "Module:-Backlight": ["waybar-backlight"], + "Module:-Backlight-Slider": ["waybar-backlight-slider"], + "Module:-Battery": ["waybar-battery"], + "Module:-Bluetooth": ["waybar-bluetooth"], + "Module:-Cava": ["waybar-cava"], + "Module:-CFFI": ["waybar-cffi"], + "Module:-Clock": ["waybar-clock"], + "Module:-CPU": ["waybar-cpu", "waybar-cpu-graph"], + "Module:-Custom": ["waybar-custom", "waybar-custom-graph"], + "Module:-Disk": ["waybar-disk"], + "Module:-Dwl": ["waybar-dwl-tags", "waybar-dwl-window"], + "Module:-Gamemode": ["waybar-gamemode"], + "Module:-GPS": ["waybar-gps"], + "Module:-Hyprland": ["waybar-hyprland-window", "waybar-hyprland-workspaces", "waybar-hyprland-submap", "waybar-hyprland-windowcount"], + "Module:-Idle-Inhibitor": ["waybar-idle-inhibitor"], + "Module:-Image": ["waybar-image"], + "Module:-Inhibitor": ["waybar-inhibitor"], + "Module:-JACK": ["waybar-jack"], + "Module:-Keyboard-State": ["waybar-keyboard-state"], + "Module:-Language": ["waybar-sway-language", "waybar-hyprland-language", "waybar-niri-language", "waybar-mango-language"], + "Module:-Mango": ["waybar-mango-window", "waybar-mango-workspaces", "waybar-mango-keymode", "waybar-mango-layout"], + "Module:-Memory": ["waybar-memory"], + "Module:-Menu": ["waybar-menu"], + "Module:-MPD": ["waybar-mpd"], + "Module:-MPRIS": ["waybar-mpris"], + "Module:-Network": ["waybar-network"], + "Module:-Niri": ["waybar-niri-window", "waybar-niri-workspaces"], + "Module:-PowerProfilesDaemon": ["waybar-power-profiles-daemon"], + "Module:-Privacy": ["waybar-privacy"], + "Module:-PulseAudio": ["waybar-pulseaudio"], + "Module:-PulseAudio-Slider": ["waybar-pulseaudio-slider"], + "Module:-River": ["waybar-river-tags", "waybar-river-mode", "waybar-river-window", "waybar-river-layout"], + "Module:-Sndio": ["waybar-sndio"], + "Module:-Sway": ["waybar-sway-workspaces", "waybar-sway-window", "waybar-sway-mode", "waybar-sway-scratchpad"], + "Module:-Systemd-failed-units": ["waybar-systemd-failed-units"], + "Module:-Taskbar": ["waybar-wlr-taskbar"], + "Module:-Temperature": ["waybar-temperature"], + "Module:-Tray": ["waybar-tray"], + "Module:-UPower": ["waybar-upower"], + "Module:-User": ["waybar-user"], + "Module:-Wayfire": ["waybar-wayfire-window", "waybar-wayfire-workspaces"], + "Module:-WirePlumber": ["waybar-wireplumber"], + "Module:-Workspaces": ["waybar-ext-workspaces"], + "Module:-WWAN": ["waybar-wwan"], + "States": ["waybar-states"] +} diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml new file mode 100644 index 00000000..8875ca81 --- /dev/null +++ b/.github/workflows/wiki.yml @@ -0,0 +1,56 @@ +name: Sync wiki from man pages + +# The scdoc man pages under man/ are the single source of truth for module +# documentation. This workflow regenerates the corresponding GitHub wiki pages +# whenever a man page (or the sync tooling) changes on the default branch. +# +# Only the pages listed in .github/wiki/mapping.json are (re)written; every +# other wiki page (Home, Installation, user showcases, ...) is left untouched. + +on: + push: + branches: [master] + paths: + - 'man/**' + - '.github/wiki/**' + - '.github/workflows/wiki.yml' + workflow_dispatch: {} + +permissions: + contents: write + +concurrency: + group: wiki-sync + cancel-in-progress: true + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install scdoc and pandoc + run: sudo apt-get update && sudo apt-get install -y scdoc pandoc + + - name: Clone the wiki + run: | + git clone \ + "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.wiki.git" \ + wiki + + - name: Generate wiki pages from man pages + run: python3 .github/wiki/generate.py --man-dir man --out-dir wiki + + - name: Commit and push if changed + working-directory: wiki + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --staged --quiet; then + echo "Wiki already up to date." + else + git commit -m "docs: sync module pages from man (${{ github.sha }})" + git push + fi From 4e76d7339f6bf0d7fd05219841962a3df66f087e Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:07:53 +0200 Subject: [PATCH 306/416] ci(wiki): auto-insert new module pages into the sidebar generate.py now keeps _Sidebar.md in sync: any Module:-* page from the mapping that is not yet linked is inserted alphabetically into the Modules list. Existing entries (custom labels, nested sub-entries, hand-written non-module links) are left untouched, so adding a module no longer requires editing the sidebar by hand. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/wiki/README.md | 6 +++++ .github/wiki/generate.py | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/.github/wiki/README.md b/.github/wiki/README.md index 6588bec4..06c53b87 100644 --- a/.github/wiki/README.md +++ b/.github/wiki/README.md @@ -18,6 +18,12 @@ touches `man/**` or this tooling. - `extras/.md` — optional hand-maintained appendix (screenshots, showcase snippets that have no man equivalent), appended verbatim after the generated body. +The generator also keeps `_Sidebar.md` in sync: any `Module:-*` page in the mapping +that is not yet linked in the sidebar is inserted alphabetically into the `Modules:` +list. Existing sidebar entries (custom labels, nested sub-entries, hand-written +non-module links) are never modified — so a new module auto-appears in navigation +without disturbing the curated structure. + ## What it touches Only the pages listed in `mapping.json` are (re)written. Every other wiki page diff --git a/.github/wiki/generate.py b/.github/wiki/generate.py index 7e84e0d9..f915bec1 100755 --- a/.github/wiki/generate.py +++ b/.github/wiki/generate.py @@ -90,6 +90,54 @@ def build_page(man_dir, sources, page, extras_dir): return "\n".join(parts).rstrip() + "\n" +ENTRY_RE = re.compile(r"^ - \[([^\]]+)\]\(\./Module:-") +LINK_RE = re.compile(r"\]\(\./(Module:-[^)]+)\)") + + +def sync_sidebar(out_dir, mapping): + """Non-destructively add any mapped Module page missing from _Sidebar.md. + + Only inserts links for pages absent from the sidebar; existing entries + (custom labels, nested sub-entries, hand-written non-module links) are + never modified. Insertion is alphabetical within the top-level module list. + """ + path = os.path.join(out_dir, "_Sidebar.md") + if not os.path.exists(path): + print(" no _Sidebar.md; skipping sidebar sync") + return + lines = open(path).read().splitlines() + linked = set(LINK_RE.findall("\n".join(lines))) + missing = sorted((p for p in mapping + if p.startswith("Module:-") and p not in linked), + key=str.lower) + if not missing: + print(" sidebar up to date") + return + + def entries(): + return [i for i, l in enumerate(lines) if ENTRY_RE.match(l)] + + if not entries(): + print(" could not locate Modules section; skipping sidebar sync") + return + for page in missing: + label = page[len("Module:-"):].replace("-", " ") + new_line = f" - [{label}](./{page})" + pos = None + for i in entries(): + if ENTRY_RE.match(lines[i]).group(1).lower() > label.lower(): + pos = i + break + if pos is None: # after the last module entry and its sub-entries + j = entries()[-1] + 1 + while j < len(lines) and lines[j].startswith(" "): + j += 1 + pos = j + lines.insert(pos, new_line) + print(f" sidebar += {label}") + open(path, "w").write("\n".join(lines) + "\n") + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--man-dir", default="man") @@ -130,6 +178,7 @@ def main(): out = os.path.join(args.out_dir, page + ".md") open(out, "w").write(build_page(args.man_dir, sources, page, extras_dir)) print(f" wrote {page}.md ({', '.join(sources)})") + sync_sidebar(args.out_dir, mapping) return 0 From 1d86e8dfa9d5926fa72374174c63c620218aa896 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:09:20 +0200 Subject: [PATCH 307/416] fix(custom/graph): guard signal config with isInt() to avoid SIGABRT refresh() called config_["signal"].asInt() unconditionally on every RT signal; a non-integer "signal" value throws Json::LogicError and aborts Waybar. Matches the guard already present in custom/image/idle_inhibitor. Fixes #3514. --- src/modules/custom_graph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index 0f236304..8b0c70af 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -139,7 +139,7 @@ void waybar::modules::CustomGraph::waitingWorker() { } void waybar::modules::CustomGraph::refresh(int sig) { - if (sig == SIGRTMIN + config_["signal"].asInt()) { + if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); } } From 4eb2513cc0d99d6ea0e326c230fa945810bd2e12 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:09:20 +0200 Subject: [PATCH 308/416] fix(hyprland/language): pass named args in format- branches The format-[-] override branches only passed a positional arg, so a format using {short}/{long}/{variant} threw 'argument not found', which disabled the whole module. Now supply the same named args as the fallback/tooltip branches. Fixes #5120. --- src/modules/hyprland/language.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 9c082d3a..cc9c6c59 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -38,10 +38,18 @@ auto Language::update() -> void { std::string layoutName = std::string{}; if (config_.isMember("format-" + layout_.short_description + "-" + layout_.variant)) { const auto propName = "format-" + layout_.short_description + "-" + layout_.variant; - layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString()); + layoutName = trim(fmt::format(fmt::runtime(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("format-" + layout_.short_description)) { const auto propName = "format-" + layout_.short_description; - layoutName = fmt::format(fmt::runtime(format_), config_[propName].asString()); + layoutName = trim(fmt::format(fmt::runtime(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 { layoutName = trim(fmt::format(fmt::runtime(format_), fmt::arg("long", layout_.full_name), fmt::arg("short", layout_.short_name), From b5842d4f53d3aa06d4e07b86706da403b3d7ae2f Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:09:20 +0200 Subject: [PATCH 309/416] fix(core): don't floor an explicit interval:0 to 1ms std::max(1L, interval*1000) turned a user's explicit "interval": 0 into a 1ms periodic refresh, i.e. a ~1000x/s busy loop that starves the GTK main loop and leaks memory (mpris RSS growth, missing tooltips, frozen updates). An explicit 0 now stays the 'no periodic refresh' sentinel. Fixes #4987, #4842; helps #4864, #4917, #4998, #5145. --- src/ALabel.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index f7a98329..5db03457 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -25,8 +25,14 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st ? std::chrono::milliseconds::max() : std::chrono::milliseconds( (config_["interval"].isNumeric() - ? std::max(1L, // Minimum 1ms due to millisecond precision - static_cast(config_["interval"].asDouble() * 1000)) + ? (config_["interval"].asDouble() > 0 + // Minimum 1ms due to millisecond precision + ? std::max(1L, static_cast( + config_["interval"].asDouble() * 1000)) + // An explicit interval of 0 means "no periodic refresh" + // (event-driven only). Flooring it to 1ms busy-loops the + // main thread; keep it as the 0 sentinel (see custom.cpp). + : 0L) : 1000 * (long)interval))), default_format_(format_) { label_.set_name(name); From bfe38bd1e2f7f76cb86cc30e626ff33e12f2d091 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:09:20 +0200 Subject: [PATCH 310/416] fix(custom): validate JSON output UTF-8 and stop restart-interval:0 busy loop - parseOutputJson() passed script text/alt/tooltip straight to Pango/GTK; an invalid-UTF-8 byte aborted the bar in g_utf8_collate. Validate/make_valid like parseOutputRaw already does. Fixes #2829. - restart-interval:0 was floored to 1ms, respawning the script ~1000x/s and starving the main loop; a non-positive restart-interval now stops instead. Part of #4842. --- src/modules/custom.cpp | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 01781036..e27802d2 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -105,7 +105,8 @@ void waybar::modules::Custom::continuousWorker() { dp.emit(); spdlog::error("{} stopped unexpectedly, is it endless?", name_); } - if (config_["restart-interval"].isNumeric()) { + if (config_["restart-interval"].isNumeric() && + config_["restart-interval"].asDouble() > 0) { pid_ = -1; thread_.sleep_for(std::chrono::milliseconds( std::max(1L, // Minimum 1ms due to millisecond precision @@ -115,6 +116,8 @@ void waybar::modules::Custom::continuousWorker() { throw std::runtime_error("Unable to open " + cmd); } } else { + // A non-positive restart-interval must not busy-respawn the script + // (that starves the GTK main loop); treat it as "do not restart". thread_.stop(); return; } @@ -316,22 +319,32 @@ void waybar::modules::Custom::parseOutputJson() { std::istringstream output(output_.out); std::string line; class_.clear(); + // A script can emit invalid UTF-8; passing it unchecked to Pango/GTK aborts + // the whole bar in g_utf8_* (see parseOutputRaw, which validates the same way). + auto sanitize = [](const std::string& s) -> Glib::ustring { + Glib::ustring value = s; + if (!value.validate()) { + value = value.make_valid(); + } + return value; + }; while (getline(output, line)) { auto parsed = parser_.parse(line); - if (config_["escape"].isBool() && config_["escape"].asBool()) { - text_ = Glib::Markup::escape_text(parsed["text"].asString()); + const bool escape = config_["escape"].isBool() && config_["escape"].asBool(); + if (escape) { + text_ = Glib::Markup::escape_text(sanitize(parsed["text"].asString())); } else { - text_ = parsed["text"].asString(); + text_ = sanitize(parsed["text"].asString()); } - if (config_["escape"].isBool() && config_["escape"].asBool()) { - alt_ = Glib::Markup::escape_text(parsed["alt"].asString()); + if (escape) { + alt_ = Glib::Markup::escape_text(sanitize(parsed["alt"].asString())); } else { - alt_ = parsed["alt"].asString(); + alt_ = sanitize(parsed["alt"].asString()); } - if (config_["escape"].isBool() && config_["escape"].asBool()) { - tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString()); + if (escape) { + tooltip_ = Glib::Markup::escape_text(sanitize(parsed["tooltip"].asString())); } else { - tooltip_ = parsed["tooltip"].asString(); + tooltip_ = sanitize(parsed["tooltip"].asString()); } if (parsed["class"].isString()) { class_.push_back(parsed["class"].asString()); From d130ce6a664f79c4256b712049570e6bfd4a94af Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:10:55 +0200 Subject: [PATCH 311/416] fix(clock): disambiguate DST transitions to stop tooltip crash Building a zoned_time/zoned_seconds from a local_time throws ambiguous_local_time during the DST fall-back hour and nonexistent_local_time across the spring-forward gap. update() runs this every minute with the tooltip enabled by default and has no try/catch, so Waybar aborts every minute during a DST transition. Pass choose::earliest at each construction to resolve deterministically instead of throwing. Fixes #2615; resolves the recurring DST-crash duplicates #5006, #5018, #5063, #5096, #3024. --- src/modules/clock.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 016d1d75..bd1a17d1 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -190,8 +190,13 @@ auto waybar::modules::Clock::update() -> void { if (tooltipEnabled()) { const year_month_day today{floor(now.get_local_time())}; const auto shiftedDay{today + cldCurrShift_}; + // choose::earliest disambiguates the DST fall-back hour (ambiguous local + // time) and skips forward over the spring-forward gap (nonexistent local + // time); without it this constructor throws and aborts Waybar every minute + // during a DST transition. Fixes #2615 (and its many duplicates). const zoned_time shiftedNow{ - tz, local_days(shiftedDay) + (now.get_local_time() - floor(now.get_local_time()))}; + tz, local_days(shiftedDay) + (now.get_local_time() - floor(now.get_local_time())), + choose::earliest}; if (tzInTooltip_) tzText_ = getTZtext(now.get_sys_time()); if (cldInTooltip_) cldText_ = get_calendar(today, shiftedDay, tz); @@ -441,9 +446,11 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea fmt_lib::make_format_args( (line == 2) ? static_cast( - zoned_seconds{tz, local_days{ymTmp / 1}}) - : static_cast(zoned_seconds{ - tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}}))) + zoned_seconds{tz, local_days{ymTmp / 1}, choose::earliest}) + : static_cast( + zoned_seconds{tz, + local_days{cldGetWeekForLine(ymTmp, firstdow, line)}, + choose::earliest}))) << ' '; } else { os << pads; @@ -482,11 +489,11 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea << fmt_lib::vformat( m_locale_, fmtMap_[4], fmt_lib::make_format_args( - (line == 2) ? static_cast( - zoned_seconds{tz, local_days{ymTmp / 1}}) - : static_cast( - zoned_seconds{tz, local_days{cldGetWeekForLine( - ymTmp, firstdow, line)}}))); + (line == 2) ? static_cast(zoned_seconds{ + tz, local_days{ymTmp / 1}, choose::earliest}) + : static_cast(zoned_seconds{ + tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}, + choose::earliest}))); else os << pads; } From c29ed5f97214d022322345f5de11dac2cad6cf8b Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:13:30 +0200 Subject: [PATCH 312/416] fix(client): don't exit when the desktop portal fails to start The Portal constructor synchronously auto-starts org.freedesktop.portal.Desktop via a Gio::DBus::Proxy. If that service fails or crashes on start it throws a Glib::Error, which was previously uncaught and terminated Waybar. Wrap the construction in a try/catch, log a warning and leave portal as nullptr on failure, and null-guard every dereference so a missing portal simply disables light/dark appearance detection instead of crashing. Fixes #3140, #3601. --- src/client.cpp | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 57e0fe5c..511e35af 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -211,7 +211,7 @@ const std::string waybar::Client::getStyle(const std::string& style, if (style.empty()) { std::vector search_files; - switch (appearance.value_or(portal->getAppearance())) { + switch (appearance.value_or(portal ? portal->getAppearance() : waybar::Appearance::UNKNOWN)) { case waybar::Appearance::LIGHT: search_files.emplace_back("style-light.css"); gtk_settings->property_gtk_application_prefer_dark_theme() = false; @@ -344,16 +344,26 @@ int waybar::Client::main(int argc, char* argv[]) { wl_display = gdk_wayland_display_get_wl_display(gdk_display->gobj()); config.load(config_opt); if (!portal) { - portal = std::make_unique(); + try { + portal = std::make_unique(); + } catch (const Glib::Error& e) { + spdlog::warn( + "Failed to connect to the desktop portal, light/dark theme detection disabled: {}", + std::string(e.what())); + } catch (...) { + spdlog::warn("Failed to connect to the desktop portal, light/dark theme detection disabled"); + } } m_cssFile = getStyle(style_opt); setupCss(m_cssFile); m_cssReloadHelper = std::make_unique(m_cssFile, [&](const std::string& css_file) { setupCss(css_file); }); - portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) { - auto css_file = getStyle(style_opt, appearance); - m_cssReloadHelper->changeCssFile(css_file); - setupCss(css_file); - }); + if (portal) { + portal->signal_appearance_changed().connect([&](waybar::Appearance appearance) { + auto css_file = getStyle(style_opt, appearance); + m_cssReloadHelper->changeCssFile(css_file); + setupCss(css_file); + }); + } auto m_config = config.getConfig(); if (m_config.isObject() && m_config["reload_style_on_change"].asBool()) { @@ -378,5 +388,7 @@ int waybar::Client::main(int argc, char* argv[]) { void waybar::Client::reset() { gtk_app->quit(); // delete signal handler for css changes - portal->signal_appearance_changed().clear(); + if (portal) { + portal->signal_appearance_changed().clear(); + } } From c0a26104a59a37ebd9c11fe9233f975f09c7376e Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:13:20 +0200 Subject: [PATCH 313/416] fix(sway/language): apply CSS classes on the main thread to stop SIGSEGV set_current_layout() mutated label_'s GTK style context (remove_class/ add_class) while being called from the sway IPC worker thread via onEvent(). Off-main-thread GTK widget mutation caused a SIGSEGV. Record only the target layout in set_current_layout() and apply the matching CSS class in update(), which the dispatcher runs on the GTK main thread. A new applied_class_ member tracks the currently applied class so update() can swap it. The shared layout_/applied_class_ state is guarded by the existing mutex_. Fixes #3702. --- include/modules/sway/language.hpp | 3 +++ src/modules/sway/language.cpp | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/include/modules/sway/language.hpp b/include/modules/sway/language.hpp index 470b9879..6ababa4f 100644 --- a/include/modules/sway/language.hpp +++ b/include/modules/sway/language.hpp @@ -54,6 +54,9 @@ class Language : public ALabel, public sigc::trackable { const static std::string XKB_ACTIVE_LAYOUT_NAME_KEY; Layout layout_; + // CSS class currently applied to label_. Tracked so update() (main thread) can swap classes + // instead of set_current_layout() mutating the widget from the IPC worker thread (#3702). + std::string applied_class_; std::string tooltip_format_ = ""; std::map layouts_map_; bool hide_single_; diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index 7113d9ef..c284e53c 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -116,6 +116,17 @@ void Language::onEvent(const struct Ipc::ipc_response& res) { auto Language::update() -> void { std::lock_guard lock(mutex_); + // Apply the CSS class here, on the GTK main thread. set_current_layout() runs on the IPC worker + // thread, so mutating label_'s style context there would crash (#3702). + if (layout_.short_name != applied_class_) { + if (!applied_class_.empty()) { + label_.get_style_context()->remove_class(applied_class_); + } + if (!layout_.short_name.empty()) { + label_.get_style_context()->add_class(layout_.short_name); + } + applied_class_ = layout_.short_name; + } if (hide_single_ && layouts_map_.size() <= 1) { event_box_.hide(); return; @@ -145,6 +156,10 @@ auto Language::update() -> void { } auto Language::set_current_layout(const std::string& current_layout) -> void { + // Runs on the IPC worker thread (via onEvent) as well as the main thread (via onCmd), so it must + // not touch GTK widgets - off-main-thread widget mutation caused SIGSEGV (#3702). Only record the + // target layout here; update() applies the matching CSS class on the main thread. + // // Guard against unknown / empty layout names: transient virtual keyboards (e.g. wtype) and // hot-plugged devices whose layouts haven't made it into the map yet would otherwise blank out // layout_ via map::operator[]'s default-construct-on-miss. @@ -152,9 +167,7 @@ auto Language::set_current_layout(const std::string& current_layout) -> void { if (it == layouts_map_.end()) { return; } - label_.get_style_context()->remove_class(layout_.short_name); layout_ = it->second; - label_.get_style_context()->add_class(layout_.short_name); } auto Language::init_layouts_map(const std::vector& used_layouts) -> void { From 4229dc8ac262d145b4aa4af3eb8d08f814dfd90a Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:12:46 +0200 Subject: [PATCH 314/416] fix(sway/window): resolve app icon on the main thread to stop icon-theme race onCmd() runs on the sway IPC worker thread and called updateAppIconName(), which touches the global Gtk::IconTheme cache. Concurrent access with the main thread's draw (propagate_draw -> gtk_icon_theme_has_icon -> g_hash_table_lookup) races and can segfault, notably on multi-monitor and focus changes. Move the icon-theme lookup into Window::update(), which runs on the main thread via dp.emit(), and only store app_id_/app_class_ in onCmd(). Fixes #4108. --- src/modules/sway/window.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index 2b62da14..f005bd14 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -43,7 +43,9 @@ void Window::onCmd(const struct Ipc::ipc_response& res) { auto output = payload["output"].isString() ? payload["output"].asString() : ""; std::tie(app_nb_, floating_count_, windowId_, window_, app_id_, app_class_, shell_, layout_, marks_) = getFocusedNode(payload["nodes"], output); - updateAppIconName(app_id_, app_class_); + // Do not resolve the app icon here: onCmd runs on the sway IPC worker thread and + // updateAppIconName() touches the global Gtk::IconTheme cache, which is not thread-safe. + // The icon is resolved in update() on the main thread instead (triggered by dp.emit()). dp.emit(); } catch (const std::exception& e) { spdlog::error("Window: {}", e.what()); @@ -102,6 +104,9 @@ auto Window::update() -> void { setTooltipMarkup(window_); } + // Resolve the app icon on the main thread to avoid racing with GTK draw on the + // global Gtk::IconTheme cache (see onCmd). + updateAppIconName(app_id_, app_class_); updateAppIcon(); // Call parent update From d3bfec13cc9a1880221db14b9d8e9f43fbaf3c02 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:13:16 +0200 Subject: [PATCH 315/416] fix(keyboard-state): make device removal idempotent to stop libinput abort On device unplug the inotify IN_DELETE handler removed the libinput device and unref'd it before erasing the entry from libinput_devices_. A repeated IN_DELETE event for the same path (observed as the "has been removed" log line printed twice) could reach an already-unlinked device and trigger a libinput list_remove assertion abort. Erase the map entry first (under devices_mutex_) so a second delete for the same path is a no-op, then call libinput_path_remove_device() and libinput_device_unref() exactly once per device pointer. Fixes #5143, #4443, #4566. --- src/modules/keyboard_state.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 7e43c74c..458cf0af 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -268,10 +268,16 @@ waybar::modules::KeyboardState::KeyboardState(const std::string& id, const Bar& std::lock_guard lock(devices_mutex_); auto it = libinput_devices_.find(dev_path); if (it != libinput_devices_.end()) { - spdlog::info("Keyboard {} has been removed.", dev_path); - libinput_path_remove_device(it->second); - libinput_device_unref(it->second); + struct libinput_device* device = it->second; + // Erase from the map first so that a second IN_DELETE event for the + // same path becomes a no-op. This keeps removal idempotent and + // ensures libinput_path_remove_device()/libinput_device_unref() are + // called exactly once per device, avoiding a libinput list_remove + // assertion abort on double removal. libinput_devices_.erase(it); + spdlog::info("Keyboard {} has been removed.", dev_path); + libinput_path_remove_device(device); + libinput_device_unref(device); } } i += sizeof(struct inotify_event) + event->len; From 3831524ba81416ac5cbb25f0dca6eff5786943ac Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:13:37 +0200 Subject: [PATCH 316/416] fix(audio_backend): never throw across the PulseAudio callback boundary connectContext() throws std::runtime_error when pa_context_connect() fails. It was called directly from contextStateCb (the libpulse mainloop thread, running pure-C callback frames) on the PA_CONTEXT_FAILED reconnect path, so on a pipewire/pulse restart the exception unwound across the C callback boundary and triggered std::terminate/SIGABRT. Add reconnectContext() noexcept which wraps connectContext() and logs failures instead of throwing, and use it from the callback. Guard against the FAILED -> connect -> FAILED recursion/busy loop with a reentrancy flag. The constructor-time connectContext() still throws as before. Fixes #5141. --- include/util/audio_backend.hpp | 7 +++++++ src/util/audio_backend.cpp | 35 +++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/include/util/audio_backend.hpp b/include/util/audio_backend.hpp index ec732e1e..2086e868 100644 --- a/include/util/audio_backend.hpp +++ b/include/util/audio_backend.hpp @@ -29,10 +29,17 @@ class AudioBackend { static void volumeModifyCb(pa_context*, int, void*); static void sourceVolumeModifyCb(pa_context*, int, void*); void connectContext(); + // Non-throwing reconnect used from the PulseAudio callback thread. Throwing + // across the libpulse C callback boundary calls std::terminate, so this + // swallows any failure and reports it via the return value instead. + bool reconnectContext() noexcept; pa_threaded_mainloop* mainloop_; pa_mainloop_api* mainloop_api_; pa_context* context_; + // Guards against the FAILED -> connect -> FAILED recursion / busy loop when a + // reconnect attempt fails synchronously inside pa_context_connect(). + bool reconnecting_{false}; pa_cvolume pa_volume_; pa_cvolume pa_source_volume_; diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 482d7886..25ccde37 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -74,6 +74,22 @@ void AudioBackend::connectContext() { } } +// Reconnect the context without ever throwing. This is safe to call from within +// a PulseAudio state callback (which runs in pure C libpulse frames), where an +// escaping C++ exception cannot be unwound and would abort the process. +bool AudioBackend::reconnectContext() noexcept { + try { + connectContext(); + return true; + } catch (const std::exception& e) { + spdlog::error("PulseAudio reconnect failed: {}", e.what()); + return false; + } catch (...) { + spdlog::error("PulseAudio reconnect failed: unknown error"); + return false; + } +} + void AudioBackend::contextStateCb(pa_context* c, void* data) { auto* backend = static_cast(data); switch (pa_context_get_state(c)) { @@ -104,12 +120,29 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { // When pulseaudio server restarts, the connection is "failed". Try to reconnect. // pa_threaded_mainloop_lock is already acquired in callback threads. // So there is no need to lock it again. + // + // Guard against re-entrancy: pa_context_connect() can fire this callback + // synchronously with PA_CONTEXT_FAILED again, which would otherwise + // recurse (FAILED -> connect -> FAILED -> ...) and busy-loop. + if (backend->reconnecting_) { + break; + } if (backend->context_ != nullptr) { pa_context_disconnect(backend->context_); pa_context_unref(backend->context_); backend->context_ = nullptr; } - backend->connectContext(); + backend->reconnecting_ = true; + // Never throw across the libpulse C callback boundary: a failed reconnect + // is logged and left for a later PA event to retry instead of aborting. + if (!backend->reconnectContext()) { + spdlog::warn("PulseAudio context reconnect failed; will retry on next event"); + if (backend->context_ != nullptr) { + pa_context_unref(backend->context_); + backend->context_ = nullptr; + } + } + backend->reconnecting_ = false; } break; case PA_CONTEXT_CONNECTING: From 6672e924df67ed08160033c8dc836d79850ccc8e Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:13:42 +0200 Subject: [PATCH 317/416] fix(niri): reconnect IPC and stop per-event throttling to prevent freeze The niri IPC worker slept 1ms per event and never reconnected. Under an event burst the per-event cap back-pressures the socket, niri fills its send buffer and drops the stream; read_line then returns false, the detached thread exits and the module freezes permanently. Remove the per-event sleep so events drain as fast as they arrive, and wrap the socket setup and read loop in a reconnect loop that backs off and re-establishes the stream on drop. A running_ flag lets the thread exit cleanly on teardown. Fixes #5117. --- include/modules/niri/backend.hpp | 4 ++ src/modules/niri/backend.cpp | 86 ++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/include/modules/niri/backend.hpp b/include/modules/niri/backend.hpp index 07be039a..acbbf52e 100644 --- a/include/modules/niri/backend.hpp +++ b/include/modules/niri/backend.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -18,6 +19,7 @@ class EventHandler { class IPC { public: IPC(); + ~IPC(); void registerForIPC(const std::string& ev, EventHandler* ev_handler); void unregisterForIPC(EventHandler* handler); @@ -45,6 +47,8 @@ class IPC { util::JsonParser parser_; std::mutex callbackMutex_; std::list> callbacks_; + + std::atomic running_{true}; }; inline std::unique_ptr gIPC; diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 0900e092..245bf2de 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -23,6 +23,8 @@ namespace waybar::modules::niri { IPC::IPC() { startIPC(); } +IPC::~IPC() { running_ = false; } + int IPC::connectToSocket() { const char* socket_path = getenv("NIRI_SOCKET"); @@ -55,40 +57,60 @@ int IPC::connectToSocket() { void IPC::startIPC() { // will start IPC and relay events to parseIPC - int socketfd = connectToSocket(); - - std::thread([this, socketfd]() { + std::thread([this]() { spdlog::info("Niri IPC starting"); - auto unix_istream = Gio::UnixInputStream::create(socketfd, true); - auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false); - auto istream = Gio::DataInputStream::create(unix_istream); - auto ostream = Gio::DataOutputStream::create(unix_ostream); - - if (!ostream->put_string("\"EventStream\"\n") || !ostream->flush()) { - spdlog::error("Niri IPC: failed to start event stream"); - return; - } - - std::string line; - if (!istream->read_line(line) || line != R"({"Ok":"Handled"})") { - spdlog::error("Niri IPC: failed to start event stream"); - return; - } - - while (istream->read_line(line)) { - spdlog::debug("Niri IPC: received {}", line); - + // Reconnect loop: if the event stream drops we back off briefly and + // re-establish the socket instead of leaving the module frozen forever. + while (running_) { + int socketfd; try { - parseIPC(line); + socketfd = connectToSocket(); } catch (std::exception& e) { - spdlog::warn("Failed to parse IPC message: {}, reason: {}", line, e.what()); - } catch (...) { - throw; + spdlog::error("Niri IPC: failed to connect: {}", e.what()); + std::this_thread::sleep_for(std::chrono::seconds(2)); + continue; } - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + auto unix_istream = Gio::UnixInputStream::create(socketfd, true); + auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false); + auto istream = Gio::DataInputStream::create(unix_istream); + auto ostream = Gio::DataOutputStream::create(unix_ostream); + + if (!ostream->put_string("\"EventStream\"\n") || !ostream->flush()) { + spdlog::error("Niri IPC: failed to start event stream"); + std::this_thread::sleep_for(std::chrono::seconds(2)); + continue; + } + + std::string line; + if (!istream->read_line(line) || line != R"({"Ok":"Handled"})") { + spdlog::error("Niri IPC: failed to start event stream"); + std::this_thread::sleep_for(std::chrono::seconds(2)); + continue; + } + + // Drain events as fast as they arrive; throttling here back-pressures the + // socket, fills niri's send buffer and makes niri drop the stream. + while (running_ && istream->read_line(line)) { + spdlog::debug("Niri IPC: received {}", line); + + try { + parseIPC(line); + } catch (std::exception& e) { + spdlog::warn("Failed to parse IPC message: {}, reason: {}", line, e.what()); + } catch (...) { + throw; + } + } + + if (!running_) break; + + spdlog::warn("Niri IPC: event stream closed, reconnecting"); + std::this_thread::sleep_for(std::chrono::seconds(2)); } + + spdlog::info("Niri IPC stopping"); }).detach(); } @@ -196,12 +218,12 @@ void IPC::parseIPC(const std::string& line) { for (auto& win : windows_) { win["is_focused"] = focused && win["id"].asUInt64() == id; } - } else if (const auto &payload = ev["WindowLayoutsChanged"]) { - const auto &values = payload["changes"]; - for (const auto &changed : values) { + } else if (const auto& payload = ev["WindowLayoutsChanged"]) { + const auto& values = payload["changes"]; + for (const auto& changed : values) { const auto id = changed[0].asUInt64(); - const auto &change = changed[1]; - for (auto &win : windows_) { + const auto& change = changed[1]; + for (auto& win : windows_) { if (win["id"].asUInt64() == id) { win["layout"] = change; break; From 5c06233340b0d3a5d3e5882e8d19e0216fa8a20f Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 03:17:33 +0200 Subject: [PATCH 318/416] style: clang-format the crash-fix changes --- src/modules/clock.cpp | 18 +++++++++--------- src/modules/custom.cpp | 3 +-- src/modules/hyprland/language.cpp | 20 ++++++++++---------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index bd1a17d1..095ac403 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -447,10 +447,9 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea (line == 2) ? static_cast( zoned_seconds{tz, local_days{ymTmp / 1}, choose::earliest}) - : static_cast( - zoned_seconds{tz, - local_days{cldGetWeekForLine(ymTmp, firstdow, line)}, - choose::earliest}))) + : static_cast(zoned_seconds{ + tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}, + choose::earliest}))) << ' '; } else { os << pads; @@ -489,11 +488,12 @@ auto waybar::modules::Clock::get_calendar(const year_month_day& today, const yea << fmt_lib::vformat( m_locale_, fmtMap_[4], fmt_lib::make_format_args( - (line == 2) ? static_cast(zoned_seconds{ - tz, local_days{ymTmp / 1}, choose::earliest}) - : static_cast(zoned_seconds{ - tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}, - choose::earliest}))); + (line == 2) + ? static_cast( + zoned_seconds{tz, local_days{ymTmp / 1}, choose::earliest}) + : static_cast(zoned_seconds{ + tz, local_days{cldGetWeekForLine(ymTmp, firstdow, line)}, + choose::earliest}))); else os << pads; } diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index e27802d2..83f36797 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -105,8 +105,7 @@ void waybar::modules::Custom::continuousWorker() { dp.emit(); spdlog::error("{} stopped unexpectedly, is it endless?", name_); } - if (config_["restart-interval"].isNumeric() && - config_["restart-interval"].asDouble() > 0) { + if (config_["restart-interval"].isNumeric() && config_["restart-interval"].asDouble() > 0) { pid_ = -1; thread_.sleep_for(std::chrono::milliseconds( std::max(1L, // Minimum 1ms due to millisecond precision diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index cc9c6c59..85adaee0 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -38,18 +38,18 @@ auto Language::update() -> void { std::string layoutName = std::string{}; if (config_.isMember("format-" + layout_.short_description + "-" + layout_.variant)) { const auto propName = "format-" + layout_.short_description + "-" + layout_.variant; - layoutName = trim(fmt::format(fmt::runtime(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))); + layoutName = + trim(fmt::format(fmt::runtime(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("format-" + layout_.short_description)) { const auto propName = "format-" + layout_.short_description; - layoutName = trim(fmt::format(fmt::runtime(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))); + layoutName = + trim(fmt::format(fmt::runtime(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 { layoutName = trim(fmt::format(fmt::runtime(format_), fmt::arg("long", layout_.full_name), fmt::arg("short", layout_.short_name), From 45ab8a1211948b7dbee1e11e82d182ac24ce2a49 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 08:35:11 +0200 Subject: [PATCH 319/416] fix(core): fall back to default interval for periodic modules on interval:0 Addresses review: a zero interval_ must stay reserved for modules whose default interval is already 0 (event-driven). Periodic modules (clock, simpleclock, pollers) would otherwise do % interval_ (modulo by zero) or sleep_for(0) in a tight loop. interval:0 on a periodic module now falls back to its default interval. --- src/ALabel.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 5db03457..84ac2052 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -29,10 +29,11 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st // Minimum 1ms due to millisecond precision ? std::max(1L, static_cast( config_["interval"].asDouble() * 1000)) - // An explicit interval of 0 means "no periodic refresh" - // (event-driven only). Flooring it to 1ms busy-loops the - // main thread; keep it as the 0 sentinel (see custom.cpp). - : 0L) + // Only modules with no periodic default use 0 as an + // event-driven sentinel. Periodic modules fall back to their + // default interval so interval:0 cannot busy-loop or hit + // modulo-by-zero clock code. + : (interval == 0 ? 0L : 1000L * static_cast(interval))) : 1000 * (long)interval))), default_format_(format_) { label_.set_name(name); From 23d63d2b84cd432012e5e86e3b6a83f6795d3949 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 08:46:32 +0200 Subject: [PATCH 320/416] fix(clock): degrade gracefully on an invalid format specifier An unsupported specifier (e.g. the %-I / %OI no-leading-zero padding modifiers, which the date/std::chrono formatter does not implement) threw out of update() and the whole clock module failed to load. Catch it, warn once, and fall back to {:%H:%M} so the bar still comes up. Addresses #1469. --- src/modules/clock.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 095ac403..38dec4b2 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -185,7 +185,26 @@ auto waybar::modules::Clock::update() -> void { const auto* tz = tzList_[tzCurrIdx_] != nullptr ? tzList_[tzCurrIdx_] : local_zone(); const zoned_time now{tz, floor(system_clock::now())}; - setLabelMarkup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now))); + try { + setLabelMarkup(fmt_lib::vformat(m_locale_, format_, fmt_lib::make_format_args(now))); + } catch (const std::exception& e) { + // An unsupported/invalid specifier (e.g. the %-I / %OI padding modifiers, which the + // date/std::chrono formatter does not implement) must not take the whole module down. + // Warn once and fall back to a safe default so the bar still loads. + static bool warned = false; + if (!warned) { + spdlog::warn( + "Clock: could not format \"{}\": {}. Falling back to a default; check your format " + "specifiers.", + format_, e.what()); + warned = true; + } + try { + setLabelMarkup(fmt_lib::vformat(m_locale_, "{:%H:%M}", fmt_lib::make_format_args(now))); + } catch (...) { + setLabelMarkup(""); + } + } if (tooltipEnabled()) { const year_month_day today{floor(now.get_local_time())}; From 69b9a14b960526d08e145b37fcad6b236ec8ec45 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 08:57:15 +0200 Subject: [PATCH 321/416] fix(wireplumber): show correct volume on Bluetooth sinks The mixer-api is configured with the linear scale (0), so volume_ holds the raw linear gain. The perceptual "cubic" value shown by wpctl and exposed as {volume} is cbrt(linear), but update() computed pow(volume_, 3) instead. Cubing under-reads every volume below max and collapses small linear gains to 0% -- which is why the default Bluetooth sink (whose normal levels map to low linear gains, e.g. wpctl 0.55 -> linear 0.166) displayed 0% while wpctl reported a normal, unmuted volume. Replace the inverted conversions with the correct cube-root/cube pair in the display path, the scroll-scale conversions (cubic / cubic_percent), and the max-volume ceiling mapping so scrolling and the cap stay consistent with the corrected {volume}. Fixes #5159. --- src/modules/wireplumber.cpp | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 876c199d..74563c5d 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -547,8 +547,11 @@ auto waybar::modules::Wireplumber::update() -> void { label_.get_style_context()->remove_class("source-muted"); } - double vol_cube = pow(volume_, 3); - double source_vol_cube = pow(source_volume_, 3); + // mixer-api is configured with the linear scale, so volume_ holds the raw linear gain. The + // perceptual "cubic" value shown by wpctl and used for {volume} is cbrt(linear), not linear^3. + // Cubing here drives small linear gains (typical for Bluetooth sinks) to 0%. + double vol_cube = cbrt(volume_); + double source_vol_cube = cbrt(source_volume_); int vol = round(vol_cube * 100.0); int source_vol = round(source_vol_cube * 100.0); @@ -622,10 +625,10 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { step = config_["scroll-step"].asDouble(); } if (config_["max-volume"].isDouble()) { - // {volume} is displayed as cubic-percent (pow(volume_, 3) * 100), while volume_/newVol are + // {volume} is displayed as cubic-percent (cbrt(volume_) * 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); + // operates in, restoring the 0.15.0 cap semantics (e.g. 130 -> pow(1.3, 3) linear -> 130%). + maxVolume = pow(config_["max-volume"].asDouble() / 100.0, 3); } double vol = volume_; @@ -635,11 +638,11 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { } if (scale == "cubic") { - vol = pow(vol, 3); + vol = cbrt(vol); } else if (scale == "db") { vol = log10(vol) * 20.0; } else if (scale == "cubic_percent") { - vol = pow(vol, 3) * 100.0; + vol = cbrt(vol) * 100.0; } double newVol = vol; @@ -650,11 +653,11 @@ bool waybar::modules::Wireplumber::handleScroll(GdkEventScroll* e) { } if (scale == "cubic") { - newVol = cbrt(newVol); + newVol = pow(newVol, 3); } else if (scale == "db") { newVol = exp10(newVol / 20.0); } else if (scale == "cubic_percent") { - newVol = cbrt(newVol / 100.0); + newVol = pow(newVol / 100.0, 3); } if (dir == SCROLL_DIR::UP) { From 2650f062b5778571a1592de33b05f78198cec068 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 08:57:36 +0200 Subject: [PATCH 322/416] fix(wireplumber): apply source-muted for Audio/Source modules For a module configured node-type "Audio/Source", node_id_ and source_node_id_ resolve to the same source node, so its primary mute state is stored in muted_. update() unconditionally mapped muted_ to the muted/sink-muted classes and source-muted only to the secondary source_muted_ flag, so a source module could never receive source-muted -- only the sink classes. Gate the mute-class selection on the configured node-type: a source-type module drives source-muted from its primary mute state, while a sink-type module keeps muted/sink-muted for its sink and source-muted for the secondary default source it tracks for {format_source}. The primary node still feeds {volume} via updateVolume, so source-widget volume rendering is unaffected. Fixes #4523. --- src/modules/wireplumber.cpp | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 74563c5d..b3167381 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -527,24 +527,39 @@ auto waybar::modules::Wireplumber::update() -> void { label_.get_style_context()->remove_class("bluetooth"); } - // Handle sink mute state + // A module configured with node-type "Audio/Source" tracks a source as its primary node, so its + // primary mute state (muted_) must drive the source-muted class rather than the sink classes. + const bool is_source_type = g_strcmp0(type_, "Audio/Source") == 0; + + // Handle primary node mute state if (muted_) { // Check muted bluetooth format exists, otherwise fall back to default muted format. if (format_name != "format" && !config_[format_name + "-muted"].isString()) format_name = "format"; format_name += "-muted"; - label_.get_style_context()->add_class("muted"); - label_.get_style_context()->add_class("sink-muted"); + if (is_source_type) { + label_.get_style_context()->add_class("source-muted"); + } else { + label_.get_style_context()->add_class("muted"); + label_.get_style_context()->add_class("sink-muted"); + } } else { - label_.get_style_context()->remove_class("muted"); - label_.get_style_context()->remove_class("sink-muted"); + if (is_source_type) { + label_.get_style_context()->remove_class("source-muted"); + } else { + label_.get_style_context()->remove_class("muted"); + label_.get_style_context()->remove_class("sink-muted"); + } } - // Handle source mute state - if (source_muted_) { - label_.get_style_context()->add_class("source-muted"); - } else { - label_.get_style_context()->remove_class("source-muted"); + // Handle the secondary source mute state (only relevant for sink modules, which additionally + // track the default source for {format_source}). A source module already owns source-muted above. + if (!is_source_type) { + if (source_muted_) { + label_.get_style_context()->add_class("source-muted"); + } else { + label_.get_style_context()->remove_class("source-muted"); + } } // mixer-api is configured with the linear scale, so volume_ holds the raw linear gain. The From b2d15d1e3ab5508cf558b26369eed335424a5643 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:36:32 +0200 Subject: [PATCH 323/416] fix(keyboard-state): read lock LEDs from all devices, not just the first When no explicit device-path is configured, a multi-node keyboard can expose several event devices where only one actually toggles the lock LEDs. Previously update() read state from a single arbitrary device (libinput_devices_.begin()), which often picked a node whose EV_LED values never change, leaving the indicator stuck. Iterate all devices and OR their NUML/CAPSL/SCROLLL values together so a lock is reported on if any device reports it on. The single-device path is preserved when device-path is set. Fixes #2215. --- src/modules/keyboard_state.cpp | 55 ++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 458cf0af..87a843c4 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -297,31 +297,40 @@ auto waybar::modules::KeyboardState::update() -> void { sleep(0); // Wait for keyboard status change int numl = 0, capsl = 0, scrolll = 0; - try { - std::string dev_path; - { - std::lock_guard lock(devices_mutex_); - if (libinput_devices_.empty()) { - return; - } - if (config_["device-path"].isString() && - libinput_devices_.find(config_["device-path"].asString()) != libinput_devices_.end()) { - dev_path = config_["device-path"].asString(); - } else { - dev_path = libinput_devices_.begin()->first; + std::vector dev_paths; + { + std::lock_guard lock(devices_mutex_); + if (libinput_devices_.empty()) { + return; + } + if (config_["device-path"].isString() && + libinput_devices_.find(config_["device-path"].asString()) != libinput_devices_.end()) { + // An explicit device was configured: read lock state from just that device. + dev_paths.push_back(config_["device-path"].asString()); + } else { + // No explicit device: a multi-node keyboard may expose several event + // devices where only one of them actually toggles the lock LEDs. OR the + // LED values across all devices so a lock is reported on if any device + // reports it on. + for (const auto& [dev_path, _] : libinput_devices_) { + dev_paths.push_back(dev_path); } } - int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); - auto dev = openDevice(fd); - numl = libevdev_get_event_value(dev, EV_LED, LED_NUML); - capsl = libevdev_get_event_value(dev, EV_LED, LED_CAPSL); - scrolll = libevdev_get_event_value(dev, EV_LED, LED_SCROLLL); - libevdev_free(dev); - closeFile(fd); - } catch (const errno_error& e) { - // ENOTTY just means the device isn't an evdev device, skip it - if (e.code != ENOTTY) { - spdlog::warn(e.what()); + } + for (const auto& dev_path : dev_paths) { + try { + int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); + auto dev = openDevice(fd); + numl |= libevdev_get_event_value(dev, EV_LED, LED_NUML); + capsl |= libevdev_get_event_value(dev, EV_LED, LED_CAPSL); + scrolll |= libevdev_get_event_value(dev, EV_LED, LED_SCROLLL); + libevdev_free(dev); + closeFile(fd); + } catch (const errno_error& e) { + // ENOTTY just means the device isn't an evdev device, skip it + if (e.code != ENOTTY) { + spdlog::warn(e.what()); + } } } From 33223d5534284c5518dd035e086586ff3cec438c Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:49:30 +0200 Subject: [PATCH 324/416] ci(wiki): render option tables, pin pandoc, add visible auto-gen note Improve the generated wiki pages: - normalize definition-style option blocks (*name*: / typeof: / default:) into scdoc tables, so every module renders options as a clean table like the table-style pages (Bluetooth, Network, ... no longer a wall of text) - drop pandoc's empty leading table-header row so the real header shows - pin pandoc to 3.5 in CI (the distro 2.x man reader mangled lists into blockquotes); matches local output - prepend a visible "auto-generated from master, do not edit here" note to every page (was an invisible HTML comment) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/wiki/generate.py | 108 ++++++++++++++++++++++++++++++++++--- .github/workflows/wiki.yml | 11 +++- 2 files changed, 112 insertions(+), 7 deletions(-) diff --git a/.github/wiki/generate.py b/.github/wiki/generate.py index f915bec1..2db1e801 100755 --- a/.github/wiki/generate.py +++ b/.github/wiki/generate.py @@ -35,9 +35,74 @@ def sh(cmd, stdin=None): ).stdout +DEF_START = re.compile(r"^\s*\*(.+?)\*:\s*(?:\+\+)?\s*$") +INDENT = re.compile(r"^(?:\t| {2,})") +FIELD = re.compile(r"^(typeof|default)\s*:\s*(.*)$", re.IGNORECASE) + + +def normalize_option_blocks(text): + """Rewrite definition-style option blocks into scdoc table syntax. + + Some man pages document options as: + + *name*: ++ + typeof: string ++ + default: foo ++ + Description... + + which pandoc renders as a flat wall of text. Convert consecutive such + blocks into the same scdoc table syntax the table-style pages already use + (Option / Typeof / Default / Description), so every page renders as a + clean table. Table-style pages are left untouched (they never match). + Nested (indented) sub-option blocks become their own table. Fenced code + blocks are passed through verbatim. + """ + lines = text.split("\n") + out, i, n, in_code = [], 0, len(lines), False + while i < n: + if lines[i].lstrip().startswith("```"): + in_code = not in_code + out.append(lines[i]) + i += 1 + continue + # A block starts with `*name*:` and its first indented line is `typeof:`. + m = DEF_START.match(lines[i]) + nxt = lines[i + 1] if i + 1 < n else "" + if in_code or not (m and INDENT.match(nxt) + and nxt.strip().lower().startswith("typeof")): + out.append(lines[i]) + i += 1 + continue + rows = [] + while i < n and DEF_START.match(lines[i]): + name = DEF_START.match(lines[i]).group(1) + i += 1 + typ = default = "" + desc = [] + while i < n and INDENT.match(lines[i]): + cell = re.sub(r"\s*\+\+\s*$", "", lines[i].strip()) + f = FIELD.match(cell) + if f and f.group(1).lower() == "typeof": + typ = f.group(2).strip() + elif f and f.group(1).lower() == "default": + default = f.group(2).strip() + elif cell: + desc.append(cell) + i += 1 + rows.append((name, typ, default, " ".join(desc))) + if i < n and lines[i].strip() == "" and i + 1 < n and DEF_START.match(lines[i + 1]): + i += 1 # swallow blank line between two blocks, continue the run + out += ["[- *Option*", ":- *Typeof*", ":- *Default*", ":- *Description*"] + for name, typ, default, desc in rows: + out += [f"|[ *{name}*", f":[ {typ}", f":[ {default}", f":[ {desc}"] + out.append("") + return "\n".join(out) + + def man_to_gfm(scd_path): """scdoc -> roff -> pandoc gfm, as raw markdown text.""" - roff = sh(["scdoc"], stdin=open(scd_path, "rb").read()) + src = normalize_option_blocks(open(scd_path, encoding="utf-8").read()) + roff = sh(["scdoc"], stdin=src.encode("utf-8")) gfm = sh(["pandoc", "-f", "man", "-t", "gfm", "--wrap=none"], stdin=roff) return gfm.decode("utf-8") @@ -63,21 +128,52 @@ def demote(md, levels=1): return "\n".join(out) +EMPTY_ROW = re.compile(r"^\|(?:\s*\|)+\s*$") +SEP_ROW = re.compile(r"^\|[\s:\-|]+\|\s*$") + + +def fix_tables(md): + """Drop pandoc's empty leading header row so the real first row is the header. + + scdoc's `[-` header markers make pandoc emit an empty header row followed by + the actual header as the first body row. Detect `| | |` + separator and + remove them, promoting the next row to the header with the same alignment. + """ + lines = md.splitlines() + out, i = [], 0 + while i < len(lines): + if (EMPTY_ROW.match(lines[i]) and i + 2 < len(lines) + and SEP_ROW.match(lines[i + 1]) and lines[i + 2].startswith("|")): + out.append(lines[i + 2]) # real header + out.append(lines[i + 1]) # reuse the separator (keeps alignment) + i += 3 + else: + out.append(lines[i]) + i += 1 + return "\n".join(out) + + def pretty(basename): """waybar-sway-mode -> sway/mode (submodule heading for aggregated pages).""" return basename.removeprefix("waybar-").replace("-", "/", 1) +REPO = "https://github.com/Alexays/Waybar" + + def build_page(man_dir, sources, page, extras_dir): parts = [] - banner = ("") - parts.append(banner) + srclinks = ", ".join(f"[`man/{s}.5.scd`]({REPO}/blob/master/man/{s}.5.scd)" + for s in sources) + note = ("> [!NOTE]\n" + f"> This page is **auto-generated from {srclinks}** on the `master` branch.\n" + "> Do not edit it here — changes will be overwritten on the next sync.\n" + "> To update it, edit the man page(s) and open a PR.") + parts.append(note) aggregated = len(sources) > 1 for src in sources: scd = os.path.join(man_dir, src + ".5.scd") - body = strip_sections(man_to_gfm(scd)) + body = fix_tables(strip_sections(man_to_gfm(scd))) if aggregated: parts.append(f"\n# {pretty(src)}\n\n" + demote(body)) else: diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index 8875ca81..74320027 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -31,7 +31,16 @@ jobs: uses: actions/checkout@v4 - name: Install scdoc and pandoc - run: sudo apt-get update && sudo apt-get install -y scdoc pandoc + # pandoc is pinned: the distro package can be an old 2.x whose man reader + # mangles lists into blockquotes. Keep this in sync with local tooling. + env: + PANDOC_VERSION: "3.5" + run: | + sudo apt-get update && sudo apt-get install -y scdoc + curl -fsSL -o /tmp/pandoc.deb \ + "https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-1-amd64.deb" + sudo dpkg -i /tmp/pandoc.deb + pandoc --version | head -1 - name: Clone the wiki run: | From fae2b311de9c3318e9ba2cebf933a11eb975cf6e Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:46:30 +0200 Subject: [PATCH 325/416] fix(tray): set an accel group on the dbusmenu client to stop Gtk-CRITICAL crash When a tray item exports menu accelerators (e.g. Mattermost), libdbusmenu-gtk calls gtk_widget_set_accel_path() with a NULL accel group because the DbusmenuGtkClient never had one assigned. This raises a Gtk-CRITICAL that corrupts menu state, and aborts Waybar when running under G_DEBUG=fatal-criticals. Assign a fresh GtkAccelGroup to the client right after the menu is created, before it is populated or shown. Fixes #5142. --- src/modules/sni/item.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index 1c28d7af..843f386b 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -577,6 +577,15 @@ void Item::makeMenu() { if (dbus_menu != nullptr) { g_object_ref_sink(G_OBJECT(dbus_menu)); g_object_weak_ref(G_OBJECT(dbus_menu), (GWeakNotify)onMenuDestroyed, this); + // Provide an accel group to the dbusmenu client. Without one, items that export menu + // accelerators (e.g. Mattermost) trigger gtk_widget_set_accel_path() with a NULL accel group, + // which raises a Gtk-CRITICAL and corrupts menu state (or aborts under fatal-criticals). + DbusmenuGtkClient* client = dbusmenu_gtkmenu_get_client(DBUSMENU_GTKMENU(dbus_menu)); + if (client != nullptr) { + GtkAccelGroup* accel_group = gtk_accel_group_new(); + dbusmenu_gtkclient_set_accel_group(client, accel_group); + g_object_unref(accel_group); + } gtk_menu = Glib::wrap(GTK_MENU(dbus_menu)); gtk_menu->attach_to_widget(event_box); } From b7a6fa8c91e3529648cd796ad5e97a62dea9e341 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:49:01 +0200 Subject: [PATCH 326/416] fix(wlr/taskbar): dispatch built-in click actions via doAction, not the shell wlr/taskbar reads on-click* config values (close, minimize, maximize, fullscreen, minimize-raise, activate) directly as internal actions in Task::handle_clicked, but never adopted the eventActionMap_/doAction mechanism. As a result AModule::handleUserEvent additionally forkExec-ed the same value as a shell command, e.g. on-click-middle: "close" ran the action and then failed with "sh: line 1: close: command not found". Register the taskbar built-in action names in eventActionMap_ so they are recognized as module actions, and skip the shell forkExec in handleUserEvent when the configured value is a recognized module action. Non-action values are still run as user shell commands. Fixes #3284. --- include/AModule.hpp | 8 +++++++- src/AModule.cpp | 16 ++++++++++++---- src/modules/wlr/taskbar.cpp | 15 +++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/include/AModule.hpp b/include/AModule.hpp index e99bec9c..3fea52f6 100644 --- a/include/AModule.hpp +++ b/include/AModule.hpp @@ -98,6 +98,13 @@ class AModule : public IModule { bool disable_on_sleep_{false}; GObject* menu_ = nullptr; + // Maps a configured event name (e.g. "on-click-middle") to a built-in module + // action name. Populated from the `actions` config section, and by modules + // that interpret on-click* config values as internal actions (e.g. + // wlr/taskbar). Entries here are dispatched through doAction() instead of + // being run as shell commands. + std::map eventActionMap_; + private: bool handleUserEvent(GdkEventButton* const& ev); const bool isTooltip; @@ -106,7 +113,6 @@ class AModule : public IModule { gdouble distance_scrolled_y_; gdouble distance_scrolled_x_; sigc::connection cursor_timeout_conn_; - std::map eventActionMap_; static const inline std::map, std::string> eventMap_{ {std::make_pair(1, GdkEventType::GDK_BUTTON_PRESS), "on-click"}, {std::make_pair(1, GdkEventType::GDK_BUTTON_RELEASE), "on-click-release"}, diff --git a/src/AModule.cpp b/src/AModule.cpp index b53f6cee..83bafbcf 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -118,7 +118,7 @@ auto AModule::update() -> void { if (config_["on-update"].isString()) { pid_children_.push_back(util::command::forkExec(config_["on-update"].asString())); } - signal_updated.emit(this); + signal_updated.emit(this); } // Get mapping between event name and module action name // Then call overridden doAction in order to call appropriate module action @@ -222,10 +222,18 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { } // Second call user scripts if (!format.empty()) { - if (config_[format].isString()) - format = config_[format].asString(); - else + // If the configured value for this event is a recognized built-in module + // action (registered in eventActionMap_), it has already been dispatched + // via doAction() above / handled by the module itself. Don't additionally + // run it as a shell command (issue #3284). Any other value is still treated + // as a user shell command. + const auto actionIt = eventActionMap_.find(format); + const bool isModuleAction = actionIt != eventActionMap_.cend() && config_[format].isString() && + config_[format].asString() == actionIt->second; + if (isModuleAction || !config_[format].isString()) format.clear(); + else + format = config_[format].asString(); } if (!format.empty()) { const int width = gdk_window_get_width(e->window); diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 62371c15..4517aec4 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -696,6 +696,21 @@ Taskbar::Taskbar(const std::string& id, const waybar::Bar& bar, const Json::Valu box_.get_style_context()->add_class("empty"); event_box_.add(box_); + // wlr/taskbar interprets on-click* config values as built-in actions, handled + // per-task in Task::handle_clicked. Register the recognized action names so + // AModule dispatches them via doAction() instead of also running them as shell + // commands (issue #3284). Values that are not built-in actions are left alone + // and still run as user shell commands. + const auto is_builtin_action = [](const std::string& v) { + return v == "activate" || v == "minimize" || v == "minimize-raise" || v == "maximize" || + v == "fullscreen" || v == "close"; + }; + for (const auto* event : {"on-click", "on-click-middle", "on-click-right"}) { + if (config_[event].isString() && is_builtin_action(config_[event].asString())) { + eventActionMap_.insert({event, config_[event].asString()}); + } + } + // Make task buttons distribute evenly across the available width. if (config_["homogeneous"].isBool() && config_["homogeneous"].asBool()) { box_.set_homogeneous(true); From cb968c93692537b3c4291209092881e70feb1206 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:49:47 +0200 Subject: [PATCH 327/416] fix(mpd): bound the connect timeout so an unreachable server can't freeze the bar The MPD state machine drives all connection attempts from Glib::signal_timeout callbacks, which run on the GTK main loop. tryConnect() called mpd_connection_new() with the user-facing timeout_ (up to 30s by default), so an unreachable server blocked the whole bar for the full connect timeout. Bound the connect attempt to a short fixed timeout (2000 ms) so a dead server fails fast, then restore the configured timeout_ for subsequent command reads so slow-but-alive servers are unaffected. Fixes #1186. --- src/modules/mpd/mpd.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 106c54eb..8598628d 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -294,8 +294,19 @@ void waybar::modules::MPD::tryConnect() { return; } - connection_ = - detail::unique_connection(mpd_connection_new(server_, port_, timeout_), &mpd_connection_free); + // tryConnect() runs on the GTK main thread (via Glib::signal_timeout), so a + // blocking connect freezes the whole bar. Bound the connect attempt to a + // short timeout so an unreachable MPD server fails fast instead of hanging + // the loop for the full user-facing `timeout_` (up to 30s by default, #1186). + // The third argument to mpd_connection_new() is also the default command + // read timeout, so restore `timeout_` once connected to avoid shortening + // reads for slow-but-alive servers. + static constexpr unsigned kConnectTimeoutMs = 2'000; + unsigned connect_timeout = + (timeout_ != 0 && timeout_ < kConnectTimeoutMs) ? timeout_ : kConnectTimeoutMs; + + connection_ = detail::unique_connection(mpd_connection_new(server_, port_, connect_timeout), + &mpd_connection_free); if (connection_ == nullptr) { spdlog::error("{}: Failed to connect to MPD", module_name_); @@ -303,6 +314,11 @@ void waybar::modules::MPD::tryConnect() { return; } + // Restore the user-configured timeout for subsequent command reads. + if (timeout_ != 0) { + mpd_connection_set_timeout(connection_.get(), timeout_); + } + try { checkErrors(connection_.get()); spdlog::debug("{}: Connected to MPD", module_name_); From 5180dec43eae153f4361949098cdd7edc1dc2706 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:53:05 +0200 Subject: [PATCH 328/416] ci(wiki): fix STYLE lists rendering as blockquotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some pandoc versions (incl. the pinned 3.5) render scdoc bullet lists as a series of `> ·` blockquotes instead of a Markdown list, which made the STYLE sections look broken. Post-process the generated Markdown to collapse that artifact back into real `- item` bullets, independent of the pandoc version. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/wiki/generate.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/wiki/generate.py b/.github/wiki/generate.py index 2db1e801..280ae34f 100755 --- a/.github/wiki/generate.py +++ b/.github/wiki/generate.py @@ -153,6 +153,35 @@ def fix_tables(md): return "\n".join(out) +BQ_BULLET = re.compile(r"^>\s*[·•‣∙⋅]\s*$") + + +def fix_blockquote_bullets(md): + """Turn pandoc's blockquote-wrapped bullet artifact into a real list. + + Some pandoc versions render a scdoc bullet list (`- *#clock*`) as a series + of blockquotes, each `> ·` / `>` / `> item`, instead of a Markdown list. + Collapse every such block back into a `- item` bullet. A no-op when pandoc + already produced a clean list. + """ + lines = md.split("\n") + out, i, n = [], 0, len(lines) + while i < n: + if BQ_BULLET.match(lines[i]): + j, content = i + 1, [] + while j < n and lines[j].startswith(">"): + cell = lines[j][1:].strip() + if cell: + content.append(cell) + j += 1 + out.append(f"- {' '.join(content)}" if content else "-") + i = j + else: + out.append(lines[i]) + i += 1 + return "\n".join(out) + + def pretty(basename): """waybar-sway-mode -> sway/mode (submodule heading for aggregated pages).""" return basename.removeprefix("waybar-").replace("-", "/", 1) @@ -173,7 +202,7 @@ def build_page(man_dir, sources, page, extras_dir): aggregated = len(sources) > 1 for src in sources: scd = os.path.join(man_dir, src + ".5.scd") - body = fix_tables(strip_sections(man_to_gfm(scd))) + body = fix_blockquote_bullets(fix_tables(strip_sections(man_to_gfm(scd)))) if aggregated: parts.append(f"\n# {pretty(src)}\n\n" + demote(body)) else: From 268d8590435d3005307e1629eefa7424a5683ca4 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:04:02 +0200 Subject: [PATCH 329/416] fix(reload): reset pending bar-batch state on rebind On reload the GApplication is recreated but the default main context (and any queued PRIORITY_HIGH_IDLE createBarsBatch source) survives. pending_outputs_ was left holding dangling waybar_output* into the just-cleared outputs_ list; createBarsBatch filters by address, which can mis-match once a freed slot is reused. Clear pending_outputs_ and reset bars_scheduled_ in bindInterfaces so the next run batches from a clean state. Mitigates the dangling-pointer path of #4129 (the cross-process app-id race in #4117 is separate). --- src/client.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client.cpp b/src/client.cpp index 511e35af..d09de5bd 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -286,6 +286,15 @@ void waybar::Client::bindInterfaces() { // Clear stale outputs from previous run outputs_.clear(); + // Also drop any batch state that was left pending from the previous run. On + // reload the GApplication is swapped but the default main context (and its + // queued PRIORITY_HIGH_IDLE createBarsBatch source) survives; pending_outputs_ + // would then hold dangling waybar_output* into the just-cleared outputs_ list, + // which createBarsBatch's address comparison can mis-match if the freed slot is + // reused. Reset so the next run schedules its batch from a clean state (#4129). + pending_outputs_.clear(); + bars_scheduled_ = false; + // add existing outputs and subscribe to updates for (auto i = 0; i < gdk_display->get_n_monitors(); ++i) { auto monitor = gdk_display->get_monitor(i); From 8664d9a9635b0ffc61f7480fa0566cd950d8d59a Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:56:57 +0200 Subject: [PATCH 330/416] fix(wireplumber): guard async load callbacks against use-after-free on teardown The WirePlumber module registers three async callbacks (onDefaultNodesApiLoaded, onMixerApiLoaded, onPluginActivated) that receive a raw self pointer with a NULL GCancellable. WirePlumber cannot withdraw an in-flight callback, so if the module is destroyed before a queued callback fires (e.g. a temporary output/bar is removed while a component load is still pending, or during an audio route transition), the callback dereferences the freed self, causing heap corruption / a crash. Guard each of these callbacks with isModuleAlive(), which checks the existing static modules registry. The destructor already removes this from the registry before any teardown, so a missing entry means self is dangling and the callback bails out without touching it. A GCancellable cannot fix this cleanly here: every callback dereferences self on its first line, and wp_core_load_component completes via a WpTransition (not a GTask), so the cancellable is not recoverable from the GAsyncResult either. The liveness check must not touch self at all. Fixes #3974. --- include/modules/wireplumber.hpp | 3 +++ src/modules/wireplumber.cpp | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index 66900501..f1650e92 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -36,6 +36,9 @@ class Wireplumber : public ALabel { std::vector getWPIcon(); static std::list modules; + // Returns true while `self` is still a live module. Async load/activation callbacks use this to + // avoid dereferencing a `self` that was destroyed before the callback fired (see #3974). + static bool isModuleAlive(waybar::modules::Wireplumber* self); uint32_t resolvePhysicalSink(uint32_t start_id); uint32_t findPlaybackNodeId(const gchar* description); diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index b3167381..a2391071 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -10,6 +10,17 @@ bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; } std::list waybar::modules::Wireplumber::modules; +// Async load/activation callbacks (onDefaultNodesApiLoaded, onMixerApiLoaded, onPluginActivated) +// are handed a raw `self` pointer with no GCancellable, and WirePlumber has no way to withdraw an +// in-flight callback. If the module is destroyed before such a callback fires (e.g. an output/bar +// is removed while a component load is still pending, or during an audio route transition), the +// callback would dereference a freed `self`. The destructor removes `this` from this registry +// before any teardown, so a missing entry means `self` is dangling and the callback must bail out +// without touching it. See https://github.com/Alexays/Waybar/issues/3974. +bool waybar::modules::Wireplumber::isModuleAlive(waybar::modules::Wireplumber* self) { + return std::find(modules.begin(), modules.end(), self) != modules.end(); +} + waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Value& config) : ALabel(config, "wireplumber", id, "{volume}%"), wp_core_(nullptr), @@ -387,6 +398,10 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self) { + if (!isModuleAlive(self)) { + return; + } + const auto* pluginName = wp_plugin_get_name(WP_PLUGIN(p)); spdlog::debug("[{}]: onPluginActivated: {}", self->name_, pluginName); g_autoptr(GError) error = nullptr; @@ -432,6 +447,10 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self) { + if (!isModuleAlive(self)) { + return; + } + gboolean success = FALSE; g_autoptr(GError) error = nullptr; @@ -453,6 +472,10 @@ void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncRe void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self) { + if (!isModuleAlive(self)) { + return; + } + gboolean success = FALSE; g_autoptr(GError) error = nullptr; From c19abf373bceb3eb264690f94d367407d7133568 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 12:58:45 +0200 Subject: [PATCH 331/416] fix(mpris): defer widget visibility to update() to stop resume SIGSEGV The name-vanished playerctl callback called event_box_.set_visible(false) directly. On resume from suspend this runs in a re-entrant / torn-down state and crashes in Gtk::Widget::set_visible. Only clear the player and dp.emit() from the callback; update() (on the main thread) computes visibility from player state and hides the module when there is no player, matching the other hardened handlers. Fixes #5124. --- src/modules/mpris/mpris.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 7eb11594..387e0597 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -406,8 +406,11 @@ auto Mpris::onPlayerNameVanished(PlayerctlPlayerManager* manager, PlayerctlPlaye if (mpris->player_ == "playerctld") { mpris->dp.emit(); } else if (mpris->player_ == player_name->name) { + // Don't touch GTK widgets directly from the playerctl callback: on resume + // from suspend this can run in a re-entrant / torn-down state and crash in + // Gtk::Widget::set_visible. Only update state + emit; update() (on the main + // thread) hides the module when there is no player. See #5124. mpris->player = nullptr; - mpris->event_box_.set_visible(false); mpris->dp.emit(); } } From 54db6642ce132ef07ff95f4cfb2a32950677bf51 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:06:45 +0200 Subject: [PATCH 332/416] docs: revamp README and add contributor docs - README: quick-links bar, compositor-support table, module list grouped by theme with links to the wiki, getting-started section, collapsible dependency blocks, updated badges (CI, release), and a note that module docs live in man/ (auto-synced to the wiki) - add CONTRIBUTING.md (dev build, code style, docs workflow, PR checklist) - add issue templates (bug report, feature request) and a PR template Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/ISSUE_TEMPLATE/bug_report.md | 37 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 19 ++ .github/PULL_REQUEST_TEMPLATE.md | 13 ++ CONTRIBUTING.md | 45 +++++ README.md | 225 ++++++++++------------ 5 files changed, 216 insertions(+), 123 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..f7778d54 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Report a problem with Waybar +title: '' +labels: bug +assignees: '' +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Steps to reproduce the behaviour, including a minimal config that triggers it: + +```jsonc +// minimal config +``` + +**Expected behaviour** +What you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain the problem. + +**Environment** +- Waybar version (`waybar --version`): +- Installed from (distro package / source / other): +- Compositor and version (Sway, Hyprland, River, …): + +**Debug output** +Run `waybar -l debug` and paste any relevant output: + +``` +``` + +**Additional context** +Anything else that might help. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..677d643a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea or improvement for Waybar +title: '' +labels: enhancement +assignees: '' +--- + +**Is your feature request related to a problem?** +A clear and concise description of the problem, e.g. "I'm frustrated when […]". + +**Describe the solution you'd like** +What you want to happen. + +**Describe alternatives you've considered** +Any alternative solutions or features you've thought about. + +**Additional context** +Which module does this concern? Add any other context or mockups here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..ac4dec71 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ + + +**What does this PR do?** +A short description of the change and the motivation behind it. + +**Related issues** +Closes # + +**Checklist** +- [ ] Code is formatted with `clang-format` +- [ ] Builds locally (`ninja -C build`) +- [ ] Man page updated for any new/changed user-facing option (`man/`) +- [ ] Tested against the affected module(s) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..fc6fd972 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing to Waybar + +Thanks for helping improve Waybar! This guide covers the essentials. + +## Building for development + +```bash +meson setup build +ninja -C build +./build/waybar # run your build directly +``` + +Enable all optional modules while developing: + +```bash +meson setup build -Dexperimental=true +``` + +## Code style + +Waybar follows [Google's C++ style guide](https://google.github.io/styleguide/cppguide.html). +Format your changes before committing: + +```bash +clang-format -i +``` + +CI runs `clang-format` and a full build on Linux and FreeBSD — please make sure +both pass. + +## Documentation + +Module documentation lives in [`man/`](man) as scdoc man pages, **not** in the +wiki. Editing a man page and merging to `master` regenerates the matching wiki +page automatically (see [`.github/wiki`](.github/wiki)). When you add a module, +add its man page and a line in [`.github/wiki/mapping.json`](.github/wiki/mapping.json). + +## Pull requests + +- Branch from `master` and keep each PR focused on one change. +- Describe what changed and why; link any related issues. +- Add or update the man page for every user-facing option you introduce. +- Build and test against the module(s) you touched. + +Have fun :) diff --git a/README.md b/README.md index c92f842b..bcd53ae1 100644 --- a/README.md +++ b/README.md @@ -1,161 +1,140 @@ -# Waybar [![Licence](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Paypal Donate](https://img.shields.io/badge/Donate-Paypal-2244dd.svg)](https://paypal.me/ARouillard)
![Waybar](https://raw.githubusercontent.com/alexays/waybar/master/preview-2.png) +# Waybar -> Highly customizable Wayland bar for Sway and Wlroots based compositors.
-> Available in [all major distributions](https://github.com/Alexays/Waybar/wiki/Installation)
-> *Waybar [examples](https://github.com/Alexays/Waybar/wiki/Examples)* +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Donate](https://img.shields.io/badge/Donate-Paypal-2244dd.svg)](https://paypal.me/ARouillard) +[![CI](https://github.com/Alexays/Waybar/actions/workflows/linux.yml/badge.svg)](https://github.com/Alexays/Waybar/actions/workflows/linux.yml) +[![Release](https://img.shields.io/github/v/release/Alexays/Waybar)](https://github.com/Alexays/Waybar/releases) -#### Current features -- Sway (Workspaces, Binding mode, Focused window name) -- River (Mapping mode, Tags, Focused window name, Layout) -- Hyprland (Window Icons, Workspaces, Focused window name) -- Niri (Workspaces, Focused window name, Language) -- Mango (Workspaces, Focused window name, Language, Keymode) -- DWL (Tags, Focused window name) [requires dwl ipc patch](https://codeberg.org/dwl/dwl-patches/src/branch/main/patches/ipc) -- Tray [#21](https://github.com/Alexays/Waybar/issues/21) -- Local time -- Battery -- UPower -- Power profiles daemon -- Network -- Bluetooth -- Pulseaudio -- Privacy Info -- Wireplumber -- Disk -- Memory -- Cpu load average -- Temperature -- MPD -- Custom scripts -- Custom image -- Multiple output configuration -- And many more customizations +![Waybar](https://raw.githubusercontent.com/alexays/waybar/master/preview-2.png) -#### Configuration and Styling +> Highly customizable Wayland bar for Sway and wlroots-based compositors.
+> Available in [all major distributions](https://github.com/Alexays/Waybar/wiki/Installation). -[See the wiki for more details](https://github.com/Alexays/Waybar/wiki). +**[Installation](#installation) · [Wiki](https://github.com/Alexays/Waybar/wiki) · [Configuration](https://github.com/Alexays/Waybar/wiki/Configuration) · [Styling](https://github.com/Alexays/Waybar/wiki/Styling) · [Examples](https://github.com/Alexays/Waybar/wiki/Examples) · [FAQ](https://github.com/Alexays/Waybar/wiki/FAQ)** -### Installation +## Features -Waybar is available from a number of Linux distributions: +### Compositor integration + +| Compositor | Workspaces / Tags | Window | Layout | Language | Mode | +| --- | :---: | :---: | :---: | :---: | :---: | +| [Sway](https://github.com/Alexays/Waybar/wiki/Module:-Sway) | ✅ | ✅ | | ✅ | ✅ | +| [River](https://github.com/Alexays/Waybar/wiki/Module:-River) | ✅ | ✅ | ✅ | | ✅ | +| [Hyprland](https://github.com/Alexays/Waybar/wiki/Module:-Hyprland) | ✅ | ✅ | | ✅ | ✅ | +| [Niri](https://github.com/Alexays/Waybar/wiki/Module:-Niri) | ✅ | ✅ | | ✅ | | +| [Mango](https://github.com/Alexays/Waybar/wiki/Module:-Mango) | ✅ | ✅ | ✅ | ✅ | ✅ | +| [DWL](https://github.com/Alexays/Waybar/wiki/Module:-Dwl) | ✅ | ✅ | | | | +| [Wayfire](https://github.com/Alexays/Waybar/wiki/Module:-Wayfire) | ✅ | ✅ | | | | + +> DWL requires the [dwl IPC patch](https://codeberg.org/dwl/dwl-patches/src/branch/main/patches/ipc). + +### Modules + +- **Power & hardware** — [Battery](https://github.com/Alexays/Waybar/wiki/Module:-Battery), [UPower](https://github.com/Alexays/Waybar/wiki/Module:-UPower), [Power profiles daemon](https://github.com/Alexays/Waybar/wiki/Module:-PowerProfilesDaemon), [Backlight](https://github.com/Alexays/Waybar/wiki/Module:-Backlight), [CPU](https://github.com/Alexays/Waybar/wiki/Module:-CPU), [Memory](https://github.com/Alexays/Waybar/wiki/Module:-Memory), [Disk](https://github.com/Alexays/Waybar/wiki/Module:-Disk), [Temperature](https://github.com/Alexays/Waybar/wiki/Module:-Temperature) +- **Connectivity** — [Network](https://github.com/Alexays/Waybar/wiki/Module:-Network), [Bluetooth](https://github.com/Alexays/Waybar/wiki/Module:-Bluetooth), [GPS](https://github.com/Alexays/Waybar/wiki/Module:-GPS), [WWAN](https://github.com/Alexays/Waybar/wiki/Module:-WWAN) +- **Audio & media** — [PulseAudio](https://github.com/Alexays/Waybar/wiki/Module:-PulseAudio), [WirePlumber](https://github.com/Alexays/Waybar/wiki/Module:-WirePlumber), [JACK](https://github.com/Alexays/Waybar/wiki/Module:-JACK), [sndio](https://github.com/Alexays/Waybar/wiki/Module:-Sndio), [Cava](https://github.com/Alexays/Waybar/wiki/Module:-Cava), [MPD](https://github.com/Alexays/Waybar/wiki/Module:-MPD), [MPRIS](https://github.com/Alexays/Waybar/wiki/Module:-MPRIS) +- **Desktop** — [Clock & calendar](https://github.com/Alexays/Waybar/wiki/Module:-Clock), [System tray](https://github.com/Alexays/Waybar/wiki/Module:-Tray), [Idle inhibitor](https://github.com/Alexays/Waybar/wiki/Module:-Idle-Inhibitor), [Keyboard state](https://github.com/Alexays/Waybar/wiki/Module:-Keyboard-State), [Privacy](https://github.com/Alexays/Waybar/wiki/Module:-Privacy), [Gamemode](https://github.com/Alexays/Waybar/wiki/Module:-Gamemode), [Systemd failed units](https://github.com/Alexays/Waybar/wiki/Module:-Systemd-failed-units), [Image](https://github.com/Alexays/Waybar/wiki/Module:-Image), [Custom scripts](https://github.com/Alexays/Waybar/wiki/Module:-Custom) + +…and more. Every module is documented on the [wiki](https://github.com/Alexays/Waybar/wiki) (see the *Modules* sidebar). + +## Getting started + +```bash +git clone https://github.com/Alexays/Waybar +cd Waybar +meson setup build +ninja -C build +./build/waybar # run without installing +``` + +Waybar launches with a sensible [default config](resources/config.jsonc). To make +it yours, copy the default config and stylesheet into `~/.config/waybar/` and edit +them. The [Configuration](https://github.com/Alexays/Waybar/wiki/Configuration) +and [Styling](https://github.com/Alexays/Waybar/wiki/Styling) guides cover every +option, and [Examples](https://github.com/Alexays/Waybar/wiki/Examples) has +ready-to-use community setups. + +## Installation + +Waybar is packaged by most distributions: [![Packaging status](https://repology.org/badge/vertical-allrepos/waybar.svg?columns=3&header=Waybar%20Downstream%20Packaging)](https://repology.org/project/waybar/versions) -An Ubuntu PPA with more recent versions is available -[here](https://launchpad.net/~nschloe/+archive/ubuntu/waybar). +An Ubuntu PPA with more recent versions is available [here](https://launchpad.net/~nschloe/+archive/ubuntu/waybar). - -#### Building from source +### Building from source ```bash -$ git clone https://github.com/Alexays/Waybar -$ cd Waybar -$ meson setup build -$ ninja -C build -$ ./build/waybar -# If you want to install it -$ ninja -C build install -$ waybar +git clone https://github.com/Alexays/Waybar +cd Waybar +meson setup build +ninja -C build +ninja -C build install # optional ``` -**Dependencies** +
+Runtime dependencies ``` -gtkmm3 -jsoncpp -libsigc++ -fmt -wayland -chrono-date -spdlog -libgtk-3-dev [gtk-layer-shell] -gobject-introspection [gtk-layer-shell] -libgirepository1.0-dev [gtk-layer-shell] -libpulse [Pulseaudio module] -libnl [Network module] +gtkmm3 jsoncpp libsigc++ fmt wayland +chrono-date spdlog xkbregistry libgtk-3-dev upower + +libpulse [Pulseaudio module] +libnl [Network module] libappindicator-gtk3 [Tray module] -libdbusmenu-gtk3 [Tray module] -libmpdclient [MPD module] -libsndio [sndio module] -libevdev [KeyboardState module] -xkbregistry -upower [UPower battery module] +libdbusmenu-gtk3 [Tray module] +libmpdclient [MPD module] +libsndio [sndio module] +libevdev [KeyboardState module] ``` +
-**Build dependencies** +
+Build dependencies ``` -cmake -meson -scdoc -wayland-protocols +cmake meson scdoc wayland-protocols ``` +
-On Ubuntu, you can install all the relevant dependencies using this command (tested with 19.10 and 20.04): +
+Install dependencies — Ubuntu -``` +```bash sudo apt install \ - clang-tidy \ - gobject-introspection \ - libdbusmenu-gtk3-dev \ - libevdev-dev \ - libfmt-dev \ - libgirepository1.0-dev \ - libgtk-3-dev \ - libgtkmm-3.0-dev \ - libinput-dev \ - libjsoncpp-dev \ - libmpdclient-dev \ - libnl-3-dev \ - libnl-genl-3-dev \ - libpulse-dev \ - libsigc++-2.0-dev \ - libspdlog-dev \ - libwayland-dev \ - scdoc \ - upower \ + clang-tidy gobject-introspection libdbusmenu-gtk3-dev libevdev-dev \ + libfmt-dev libgirepository1.0-dev libgtk-3-dev libgtkmm-3.0-dev \ + libinput-dev libjsoncpp-dev libmpdclient-dev libnl-3-dev libnl-genl-3-dev \ + libpulse-dev libsigc++-2.0-dev libspdlog-dev libwayland-dev scdoc upower \ libxkbregistry-dev ``` +
-On Arch, you can use this command: +
+Install dependencies — Arch -``` +```bash pacman -S --asdeps \ - gtkmm3 \ - jsoncpp \ - libsigc++ \ - fmt \ - wayland \ - chrono-date \ - spdlog \ - gtk3 \ - gobject-introspection \ - libgirepository \ - libpulse \ - libnl \ - libappindicator-gtk3 \ - libdbusmenu-gtk3 \ - libmpdclient \ - sndio \ - libevdev \ - libxkbcommon \ - upower \ - meson \ - cmake \ - scdoc \ - wayland-protocols \ - glib2-devel + gtkmm3 jsoncpp libsigc++ fmt wayland chrono-date spdlog gtk3 \ + gobject-introspection libgirepository libpulse libnl libappindicator-gtk3 \ + libdbusmenu-gtk3 libmpdclient sndio libevdev libxkbcommon upower meson \ + cmake scdoc wayland-protocols glib2-devel ``` +
+## Contributing -Contributions welcome!
-Have fun :)
-The style guidelines are [Google's](https://google.github.io/styleguide/cppguide.html) +Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). The style +guidelines are [Google's C++ style](https://google.github.io/styleguide/cppguide.html). + +> **Docs live in the man pages.** Module documentation is written in +> [`man/`](man) (scdoc) and auto-synced to the wiki. Edit the man page, not the +> wiki — see [`.github/wiki`](.github/wiki). > [!CAUTION] -> Distributions of Waybar are only released on the [official GitHub page](https://github.com/Alexays/Waybar).
-> Waybar does **not** have an official website. Do not trust any sites that claim to be official. +> Distributions of Waybar are only released on the [official GitHub page](https://github.com/Alexays/Waybar).
+> Waybar does **not** have an official website. Do not trust any site claiming to be official. ## License -Waybar is licensed under the MIT license. [See LICENSE for more information](https://github.com/Alexays/Waybar/blob/master/LICENSE). +Waybar is licensed under the MIT license. [See LICENSE for details](https://github.com/Alexays/Waybar/blob/master/LICENSE). From cf2f120ad77ca81e4ad145ac457bfb947044b6ac Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:15:05 +0200 Subject: [PATCH 333/416] ci: add headless smoke + screenshot test for the bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Launch the real Waybar binary inside a headless, software-rendered sway compositor and verify it actually runs and renders — something the unit tests and build jobs never do. - test/smoke/run.sh: boots sway (WLR_BACKENDS=headless, pixman), starts waybar, asserts it stays alive with no fatal log, optionally grabs a screenshot with grim - level 1: real modules (clock/cpu/memory/disk) load without crashing - level 2: deterministic config is screenshotted and checked to be non-blank - level 3: screenshot compared to test/smoke/reference.png (fuzz 8%, 800px tolerance); screenshot + diff uploaded as artifacts - .github/workflows/smoke.yml runs it on push/PR The reference image must be blessed from a CI artifact (see test/smoke/README.md). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/smoke.yml | 80 +++++++++++++++++++++++++++++++++ test/smoke/README.md | 34 ++++++++++++++ test/smoke/config.jsonc | 26 +++++++++++ test/smoke/modules-config.jsonc | 19 ++++++++ test/smoke/run.sh | 77 +++++++++++++++++++++++++++++++ test/smoke/style.css | 27 +++++++++++ 6 files changed, 263 insertions(+) create mode 100644 .github/workflows/smoke.yml create mode 100644 test/smoke/README.md create mode 100644 test/smoke/config.jsonc create mode 100644 test/smoke/modules-config.jsonc create mode 100755 test/smoke/run.sh create mode 100644 test/smoke/style.css diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 00000000..439d8aae --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,80 @@ +name: smoke + +# Launch the real Waybar binary inside a headless sway compositor and verify it +# starts, renders, and matches a reference screenshot. Complements the unit +# tests and build jobs, which never actually run the bar. + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + sway grim imagemagick fonts-dejavu-core \ + meson ninja-build g++ pkg-config scdoc wayland-protocols \ + gobject-introspection libdbusmenu-gtk3-dev libevdev-dev libfmt-dev \ + libgirepository1.0-dev libgtk-3-dev libgtkmm-3.0-dev libinput-dev \ + libjsoncpp-dev libmpdclient-dev libnl-3-dev libnl-genl-3-dev \ + libpulse-dev libsigc++-2.0-dev libspdlog-dev libwayland-dev \ + upower libxkbregistry-dev libxkbcommon-dev + + - name: Build & install Waybar + run: | + meson setup build -Dman-pages=disabled + ninja -C build + sudo ninja -C build install + + # --- Level 1: smoke with real modules --- + - name: Smoke — real modules load without crashing + run: ./test/smoke/run.sh test/smoke/modules-config.jsonc + + # --- Level 1 + 2: launch deterministic config and screenshot --- + - name: Launch + screenshot (deterministic config) + run: ./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css bar.png + + # --- Level 2: the bar actually rendered something --- + - name: Check the bar is not blank + run: | + sd=$(convert bar.png -crop 1920x30+0+0 +repage -colorspace Gray \ + -format '%[fx:standard_deviation]' info:) + echo "bar strip stddev = $sd" + awk "BEGIN{ exit !($sd > 0.01) }" || { + echo "::error::the top bar strip looks blank — Waybar rendered nothing" + exit 1 + } + + # --- Level 3: golden comparison against the committed reference --- + - name: Compare against reference screenshot + run: | + if [ -f test/smoke/reference.png ]; then + ae=$(compare -metric AE -fuzz 8% test/smoke/reference.png bar.png diff.png 2>&1 || true) + ae=$(printf '%s' "$ae" | grep -oE '^[0-9]+' || echo 0) + echo "differing pixels (fuzz 8%): $ae" + if [ "$ae" -gt 800 ]; then + echo "::error::visual regression: $ae pixels differ from the reference (threshold 800). See the 'diff.png' artifact." + exit 1 + fi + echo "✓ within tolerance" + else + echo "::warning::No reference image yet. Download the 'waybar-screenshot' artifact from this run and commit bar.png as test/smoke/reference.png to enable golden comparison." + fi + + - name: Upload screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + name: waybar-screenshot + path: | + bar.png + diff.png + if-no-files-found: warn diff --git a/test/smoke/README.md b/test/smoke/README.md new file mode 100644 index 00000000..f181e793 --- /dev/null +++ b/test/smoke/README.md @@ -0,0 +1,34 @@ +# Smoke / screenshot test + +The [`smoke`](../../.github/workflows/smoke.yml) workflow launches the real +Waybar binary inside a **headless sway** compositor (software-rendered, no GPU) +and checks three things: + +1. **It starts and stays alive** — with real modules (`clock`, `cpu`, `memory`, + `disk`) loaded, Waybar must not crash or log a fatal error. + ([`modules-config.jsonc`](modules-config.jsonc)) +2. **It renders** — a deterministic config ([`config.jsonc`](config.jsonc) + + [`style.css`](style.css)) is captured with `grim`; the bar strip must not be + blank. +3. **It looks right** — the screenshot is compared to `reference.png` with an 8% + fuzz and an 800-pixel tolerance. Regressions fail the job, and the screenshot + and diff are uploaded as artifacts. + +## Updating the reference image + +`reference.png` must be generated by the CI environment (local rendering differs +from Ubuntu's). To (re)bless it: + +1. Push your change and open the run of the `smoke` workflow. +2. Download the `waybar-screenshot` artifact. +3. Commit its `bar.png` as `test/smoke/reference.png`. + +Do this whenever an **intended** visual change makes the golden comparison fail. + +## Run locally + +On a machine with `sway`, `grim` and `waybar`: + +```sh +./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css /tmp/bar.png +``` diff --git a/test/smoke/config.jsonc b/test/smoke/config.jsonc new file mode 100644 index 00000000..433b7007 --- /dev/null +++ b/test/smoke/config.jsonc @@ -0,0 +1,26 @@ +// Deterministic Waybar config for the CI smoke/screenshot test. +// Uses only static custom modules (interval "once") so the rendered bar is +// reproducible and can be compared against a reference image. +{ + "layer": "top", + "position": "top", + "height": 30, + "modules-left": ["custom/left"], + "modules-center": ["custom/center"], + "modules-right": ["custom/right"], + "custom/left": { + "format": "Waybar", + "exec": "echo", + "interval": "once" + }, + "custom/center": { + "format": "CI smoke test", + "exec": "echo", + "interval": "once" + }, + "custom/right": { + "format": "12:34", + "exec": "echo", + "interval": "once" + } +} diff --git a/test/smoke/modules-config.jsonc b/test/smoke/modules-config.jsonc new file mode 100644 index 00000000..d6a7b9a8 --- /dev/null +++ b/test/smoke/modules-config.jsonc @@ -0,0 +1,19 @@ +// Waybar config for the CI smoke test: exercises real modules that work in a +// headless environment (no hardware/daemon dependency). Its content is dynamic +// (time, cpu, ...) so it is NOT screenshot-compared — it only checks that these +// modules load and render without crashing the bar. +{ + "layer": "top", + "position": "top", + "height": 30, + "modules-center": ["clock", "cpu", "memory", "disk", "custom/hello"], + "clock": {}, + "cpu": {}, + "memory": {}, + "disk": {}, + "custom/hello": { + "format": "ok", + "exec": "echo", + "interval": "once" + } +} diff --git a/test/smoke/run.sh b/test/smoke/run.sh new file mode 100755 index 00000000..0b26790d --- /dev/null +++ b/test/smoke/run.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Launch Waybar inside a headless sway compositor and verify it starts and +# renders. Used by the `smoke` CI workflow. +# +# Usage: run.sh [style] [screenshot.png] +# - always: assert waybar launches, stays alive, logs no fatal error +# - if given: capture the bar with grim +# +# Requires: sway, grim, waybar (on PATH). Software-rendered, no GPU needed. +set -euo pipefail + +CONFIG="$1" +STYLE="${2:-}" +SHOT="${3:-}" + +export XDG_RUNTIME_DIR="$(mktemp -d)" +chmod 700 "$XDG_RUNTIME_DIR" +export WLR_BACKENDS=headless +export WLR_RENDERER=pixman +export WLR_LIBINPUT_NO_DEVICES=1 +export LIBGL_ALWAYS_SOFTWARE=1 + +LOG="$(mktemp)" +SWAYCFG="$(mktemp)" + +wb="waybar -c $CONFIG" +[ -n "$STYLE" ] && wb="$wb -s $STYLE" + +cat > "$SWAYCFG" < $LOG 2>&1" +EOF + +echo "::group::Starting sway (headless) + waybar" +sway -c "$SWAYCFG" & +SWAY_PID=$! + +cleanup() { + swaymsg -q exit 2>/dev/null || kill "$SWAY_PID" 2>/dev/null || true +} +trap cleanup EXIT + +# Wait for the Wayland socket to appear. +sock="" +for _ in $(seq 1 40); do + sock="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'wayland-*' ! -name '*.lock' 2>/dev/null | head -1 || true)" + [ -n "$sock" ] && break + sleep 0.5 +done +[ -n "$sock" ] || { echo "::error::compositor did not create a Wayland socket"; exit 1; } +export WAYLAND_DISPLAY="$(basename "$sock")" +echo "compositor up on WAYLAND_DISPLAY=$WAYLAND_DISPLAY" +echo "::endgroup::" + +# Give modules time to poll/settle. +sleep 6 + +echo "::group::Waybar log" +cat "$LOG" || true +echo "::endgroup::" + +# --- Level 1: smoke --- +if ! pgrep -x waybar >/dev/null; then + echo "::error::waybar is not running — it crashed on startup (config: $CONFIG)" + exit 1 +fi +if grep -iqE 'critical|segfault|\bfatal\b|terminate called' "$LOG"; then + echo "::error::waybar logged a fatal error (config: $CONFIG)" + exit 1 +fi +echo "✓ waybar launched and stayed alive under headless sway" + +# --- Level 2: screenshot --- +if [ -n "$SHOT" ]; then + grim "$SHOT" + echo "✓ captured screenshot -> $SHOT" +fi diff --git a/test/smoke/style.css b/test/smoke/style.css new file mode 100644 index 00000000..bf842ae1 --- /dev/null +++ b/test/smoke/style.css @@ -0,0 +1,27 @@ +/* Deterministic stylesheet for the CI smoke/screenshot test. + * A pinned font (DejaVu Sans, installed in CI) and fixed colors keep the + * rendered bar reproducible across runs for the golden comparison. */ +* { + font-family: "DejaVu Sans"; + font-size: 14px; + border: none; + border-radius: 0; + min-height: 0; +} + +window#waybar { + background-color: #1e1e2e; + color: #cdd6f4; +} + +#custom-left, +#custom-center, +#custom-right { + padding: 0 16px; + color: #cdd6f4; +} + +#custom-left { + font-weight: bold; + color: #89b4fa; +} From e8638e761e40a7496cba90c27413810a6d53dbb7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:22:08 +0200 Subject: [PATCH 334/416] ci(smoke): make custom modules emit text so the bar is not empty The static custom modules used `exec: echo` (empty output), so the modules rendered nothing and the bar strip was blank. Have exec print the label text and use the default `{}` format. --- test/smoke/config.jsonc | 9 +++------ test/smoke/modules-config.jsonc | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/smoke/config.jsonc b/test/smoke/config.jsonc index 433b7007..461b04d8 100644 --- a/test/smoke/config.jsonc +++ b/test/smoke/config.jsonc @@ -9,18 +9,15 @@ "modules-center": ["custom/center"], "modules-right": ["custom/right"], "custom/left": { - "format": "Waybar", - "exec": "echo", + "exec": "echo Waybar", "interval": "once" }, "custom/center": { - "format": "CI smoke test", - "exec": "echo", + "exec": "echo 'CI smoke test'", "interval": "once" }, "custom/right": { - "format": "12:34", - "exec": "echo", + "exec": "echo 12:34", "interval": "once" } } diff --git a/test/smoke/modules-config.jsonc b/test/smoke/modules-config.jsonc index d6a7b9a8..108c51fc 100644 --- a/test/smoke/modules-config.jsonc +++ b/test/smoke/modules-config.jsonc @@ -12,8 +12,7 @@ "memory": {}, "disk": {}, "custom/hello": { - "format": "ok", - "exec": "echo", + "exec": "echo ok", "interval": "once" } } From 0c059db0fd670837ba87d97d04501caa5425333c Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:28:48 +0200 Subject: [PATCH 335/416] ci(smoke): add reference screenshot to enable golden comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blessed from the CI artifact of the passing headless run (Waybar / CI smoke test / 12:34 on a pinned DejaVu Sans bar). Regenerate from a new artifact when an intended visual change trips the comparison — see test/smoke/README.md. --- test/smoke/reference.png | Bin 0 -> 9817 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/smoke/reference.png diff --git a/test/smoke/reference.png b/test/smoke/reference.png new file mode 100644 index 0000000000000000000000000000000000000000..8d3165e9a2c9865825da41c764c3decf80a450b0 GIT binary patch literal 9817 zcmeI2`8S(s|Hsp5i!MrA%d}H5X4*2f)xI;;MN!%y6eaduQcEnUNT;QWX|>c&w4$Q6 z*keyojIDM-hz?>8A|xb{e6{EK2fjZ%KRxar?)!bNb3XUyT(9fg@9TP>bH_d~HxfK~ z@gx8M5H!Af%K`x4e+mE`(-ipWV8pZBS_A<26<~bphE-VB+H`m@c(jRs6S*#U;^t2$ zl)EkoJkwN?`N>D#1a!=Iv^h@GS#(-sQGfB4KM8p0pgjQaZAQKZ zEB!-$CL03;zpKwssi%D3g%e)*&!gW(nZ3aKBj3fi{IP-WPr#bkPhLMnNIWm~NU+>S z*{U>zHPE@34R?#U-+vqcs5z$wd`xN(LN^f1MFD_j(4;w9(szVc{sDVQWHdt4c_g{A z$5Kl5ADxZ`^a{}LVI z$$igk@y~ScDC&t@xR<@nQh`NS)k@_tFDY>s`%owvToGrozHA#Qo4yadXy7&hPUS!s z6yV)F7cBtbNlbw=UL__w(9O}&u_tT1N{4S+|3l#L+lO;U`Gj=JAsY&H^2(Sv3gjMw zy*$&bU*u=s7uZPsyc&};USdY?NMDXsi&l{)Y;9I<^xEf4Ze?kg+;3)Tv^5a-9*?AB zW=hC=vFm<-Y&pnvjBfY?OjW5o$+H(-E%l{gjr74$#PB5*4}Lxztb_Gv!JcW;AbYMBtcQG#2hwMLsII5gxxUaDoR7g zhm(#1p6dN-Qj}Q(ZHcnsF}j#WXC_#8S8pOV8ho}a2}neMZLTfTQc;hcLTaU?zJ}D7 zybDIQ%$J|%K5VSG<}<>0S=rybwT^(Gxu56ZyuaTM>trMMgnSp|iuJr~r0Xk>1m6`~ z#62{q0zH4h4p|Y&RFhTJahrJos)zuad6?5*vpe&VS>5YnxyHJAg(eFqw1A+UVB^jv zFnmHw>%s-vElM1iPVPqS#72Mgrha7&zt?ByrEnP0IzU;|7s6_Kn_D)`#4AmR4jdX8 zOwNyQXL!RoFcxrHqXBw*qSup*qSaAWEu0%%t)m~3oe5$2HmkcY5>RxBr!jd=Yyc=3yn;Gs8M{`t$NuzF9}H(@0kaPB`+`}GT#Q|^T$ zw%c(1q<7v}{Cdz>GFF0warX$M<|dFMW}5?s3C*3_W3|L&jYq?t%NU~Wrn$43+~U(D zMLpMnvl5x3B?|j4-L?GO%}K1NJyp_pL(652(zJT#U%c!6Gg3;-I#U#DDDe*I6N`1O z!zzngz4a4G9suATz0z~lfHpoayt2Le_O!j8gn|;iZqcvDlLhM-8 z9ZKc%P1saF?sXJ2yof9$G4|2SZ!7CnxQ`Wqbiqc_X8RwtNT=U-ylUCm=1d`By*J}`?@xxHi~ zpdG43rN3Wprm)&H1y?eD1I!3$!|CboP2o!S*@=bqW_Iuu5g!c+Z_ac2YXs$ArQg@T zE=9J0+hDA4TynAsx@oSEV%wxxhuH1s`&#?>k+brx9ia>Q3oH7jTuqPNyj<<@=XV}O z%t@4=6>qL82#Jr+!|O8YR~6T;)6qSWY_HqDxdm&_twSPUU6PXZoaXlA9lH6?A?*@2 zews`M-o|fm$%g0Jo3H_^rKqSXYiCze3rEyb9;nO}Q4YT1o69@OE@~8CSq=4%aP~BJ z8wlFKnMXh1%ty5(oGy&y zc#Le`nyn@tpUTeNLI3uYF-v`n1mv1c{%ZK8z@m**0yR-btCFc*37wQh-fNH*A= z^ypxljd)UyL1Jz9Zrf{59FqKo*!74Y`*Q=*7k;HwZ!LE3=5{ z`gV-lo;1qVPSntEIJ}xpcC6CA(8?O(Fo_%|LPvQGyQ=t?I9HNH5IttTF`zP~*fd)B z+-S|gz8rzSJrmJQyb{l4SaZADHo`x6a_7@kux6RtEmO(hUlQJl%5ef+4`$F1;!o{< z7M+}F*HP_DI31oYm|J#r5`Qx@tOc@IJRN))#A(l}kgB6rx(-|CA1vBeB~0X~Q~PcX zWPfPNY?Ll99|pF220*uT6e5w7JDoN}tc)#HP*r{D7xE)C6;jGlX5JFLrpe z6BFfv?!&+={UBqrF{Y3K+$lu&7D&XfP?<+LE-g`x8XI(A$rUHrXqbrfX0rM;&aFT0 zHS>|RCJLaB02n8`5?d*1JESTYFtetQnD1wlyP2ICb8lm)$difYNAlv5zG^L2Lmanz zJ%17M9}T>kWUuym(3)2x4OT+=IFpr{60~+p_L7Lu3~Le7-f%`Py;E#`%8^R7@%KHE zv)(%n0B}0v)Yc=p)4#vA|})S977IF|KGw{!I|qG0#Z@$wl4sjU~jMGD0|zv zcb+#l(DW^`(oC6<3F_?g^Fc1J5eR!zX*x!awnsQMMkt(VL<|=8hizk7S&hTuS3wO2 zWpYJOY6nEYw#p-@(xGxUSqB#OnDQ1+*5qv7DHk@pE7m7p&_x|AqG-U!yKY_kGMl-u zB?u-xY_WAloh4d zy**ag&}=YEE%tY2tVGy9Z;F&t7OGZB2ChYa`Is0)ROijO%PX%$TkTYQf+hQ_@vN?4 zU<63>SkZ5KMaJfGCf0r70U-goS0`y{2WMgjiz0*FeZ3fR_J8Hk+JB zmFhfwY5ZHi_9Qiy$0t zFe64<6uT&RF(8n1d~PGVygek=s&6(R^i|xQ*FdCe?Y|2~`K1mT-cz;F_$(#X><*K_ zNXX6LGqVrXD>Pc0HtOu3U|l7wR1B%vvLz<#Y+Hr|)qbggh}JYk;ojL;J)xWcd`9IJ zI9965Uu+*{`U|%YWrX%p%cMY#RuApclzkgX5I6M++98dsfWIjqWmV#6(O7>i z>dhd17KC+g)MqvS0m(OP>V2Ku^`Ig5alqgiyakM;aC|@tq>j1+zJ_-cu>e=brH;H1HmvZPc$VnKPAPw5B8PUY zpf!BFzrT~vM?e|!^0V)+Bx=!yZO~?dde}WNl3L;wScLu}wok}U*k@}o(qcO~=h*1* zMkIFrWwQ}ea8%aR02wlSYOdQ-Dq{Hi61g8#%!E=ZBsp$N<(}jSlVLtuA^3JBwvo5F zkq?zdo{`E^w;G#(Ep4{gX!xf&HXsIjL!pRouX5eIIR1+1g;}(JUOp)*)uv7w#dusL z%4Kj?btPnf|I1tP*9cBl{UB#iaevvAR5Zyr*J{&qDg<%uKWB~(i$ z-W-kJXG{Mm-#s%qm+Mia(te Date: Sat, 4 Jul 2026 13:41:29 +0200 Subject: [PATCH 336/416] fix(wireplumber): reconnect when PipeWire/WirePlumber restarts Previously the wireplumber module connected to PipeWire once in its constructor and had no handling for the connection being lost. When PipeWire or the wireplumber service restarted (or crashed), the module went stale/blank and never recovered until Waybar itself was restarted. Connect to the WpCore "disconnected" signal and, on disconnect, schedule a bounded main-loop retry (Glib::signal_timeout) that tears down the now invalid core/object-manager/mixer-api references and rebuilds the whole connection from scratch, re-running the async API and object-manager setup. Connection setup/teardown is factored into setupConnection() and teardownConnection() so startup and reconnect share one code path. The reconnect timer is cancelled in the destructor and the existing isModuleAlive() registry guard still protects in-flight async callbacks, so teardown during a pending reconnect stays safe. Fixes #2882. --- include/modules/wireplumber.hpp | 8 +++ src/modules/wireplumber.cpp | 92 +++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index f1650e92..d32fbeec 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -17,6 +17,11 @@ class Wireplumber : public ALabel { auto update() -> void override; private: + bool setupConnection(); + void teardownConnection(); + void scheduleReconnect(); + bool onReconnectTimeout(); + static void onCoreDisconnected(waybar::modules::Wireplumber* self); void asyncLoadRequiredApiModules(); void prepare(waybar::modules::Wireplumber* self); void activatePlugins(); @@ -66,6 +71,9 @@ class Wireplumber : public ALabel { bool only_physical_; bool resolved_physical_; std::string form_factor_; + // Timer used to retry connecting to PipeWire after it goes away; disconnected in the destructor + // so a pending attempt can't outlive the module. See #2882. + sigc::connection reconnect_timer_; }; } // namespace waybar::modules diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index a2391071..11c2c60e 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -10,6 +10,11 @@ bool isValidNodeId(uint32_t id) { return id > 0 && id < G_MAXUINT32; } std::list waybar::modules::Wireplumber::modules; +// Interval between reconnect attempts after PipeWire/WirePlumber goes away. Fixed (rather than +// growing) so the module recovers promptly whenever the service comes back, while still being +// a bounded, main-loop-friendly poll rather than a busy loop. +static constexpr unsigned kReconnectIntervalMs = 2000; + // Async load/activation callbacks (onDefaultNodesApiLoaded, onMixerApiLoaded, onPluginActivated) // are handed a raw `self` pointer with no GCancellable, and WirePlumber has no way to withdraw an // in-flight callback. If the module is destroyed before such a callback fires (e.g. an output/bar @@ -46,21 +51,37 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val waybar::modules::Wireplumber::modules.push_back(this); wp_init(WP_INIT_PIPEWIRE); - wp_core_ = wp_core_new(nullptr, nullptr, nullptr); - apis_ = g_ptr_array_new_with_free_func(g_object_unref); - om_ = wp_object_manager_new(); type_ = g_strdup(config_["node-type"].isString() ? config_["node-type"].asString().c_str() : "Audio/Sink"); only_physical_ = config_["only-physical"].isBool() ? config_["only-physical"].asBool() : false; + if (!setupConnection()) { + spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_); + throw std::runtime_error("Could not connect to PipeWire\n"); + } +} + +// Creates a fresh WpCore/object manager, connects to PipeWire and kicks off async API loading. +// Used both at startup and when reconnecting after a PipeWire/WirePlumber restart, so all +// connection-scoped state is (re)built here. Returns false if the connection could not be +// initiated. See https://github.com/Alexays/Waybar/issues/2882. +bool waybar::modules::Wireplumber::setupConnection() { + wp_core_ = wp_core_new(nullptr, nullptr, nullptr); + apis_ = g_ptr_array_new_with_free_func(g_object_unref); + om_ = wp_object_manager_new(); + pending_plugins_ = 0; + prepare(this); + // Recover when PipeWire/WirePlumber goes away (service restart, crash). The "disconnected" + // signal fires on the GTK main loop; from there we schedule a reconnect attempt. + g_signal_connect_swapped(wp_core_, "disconnected", (GCallback)onCoreDisconnected, this); + spdlog::debug("[{}]: connecting to pipewire: '{}'...", name_, type_); if (wp_core_connect(wp_core_) == 0) { - spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_); - throw std::runtime_error("Could not connect to PipeWire\n"); + return false; } spdlog::debug("[{}]: {} connected!", name_, type_); @@ -68,10 +89,13 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val g_signal_connect_swapped(om_, "installed", (GCallback)onObjectManagerInstalled, this); asyncLoadRequiredApiModules(); + return true; } -waybar::modules::Wireplumber::~Wireplumber() { - waybar::modules::Wireplumber::modules.remove(this); +// Disconnects signal handlers and releases all connection-scoped WirePlumber objects. Safe to call +// when already partially/fully torn down (every pointer is null-checked and cleared), so it doubles +// as the reconnect reset and the destructor's cleanup. +void waybar::modules::Wireplumber::teardownConnection() { if (mixer_api_ != nullptr) { g_signal_handlers_disconnect_by_data(mixer_api_, this); } @@ -81,12 +105,62 @@ waybar::modules::Wireplumber::~Wireplumber() { if (om_ != nullptr) { g_signal_handlers_disconnect_by_data(om_, this); } - wp_core_disconnect(wp_core_); + if (wp_core_ != nullptr) { + g_signal_handlers_disconnect_by_data(wp_core_, this); + wp_core_disconnect(wp_core_); + } g_clear_pointer(&apis_, g_ptr_array_unref); g_clear_object(&om_); - g_clear_object(&wp_core_); g_clear_object(&mixer_api_); g_clear_object(&def_nodes_api_); + g_clear_object(&wp_core_); + // onObjectManagerInstalled re-populates these via out-params (which don't free the previous + // value), so clear them here to avoid leaking the old strings across a reconnect. + g_clear_pointer(&default_node_name_, g_free); + g_clear_pointer(&default_source_name_, g_free); + pending_plugins_ = 0; +} + +// "disconnected" signal handler on wp_core_. Runs during the core's own signal emission, so it must +// not tear down the core here; it only schedules a reconnect, which performs the teardown/rebuild +// once control has returned to the main loop. +void waybar::modules::Wireplumber::onCoreDisconnected(waybar::modules::Wireplumber* self) { + if (!isModuleAlive(self)) { + return; + } + spdlog::warn("[{}]: PipeWire connection lost; will attempt to reconnect", self->name_); + self->scheduleReconnect(); +} + +void waybar::modules::Wireplumber::scheduleReconnect() { + if (reconnect_timer_.connected()) { + return; // a reconnect attempt is already pending + } + reconnect_timer_ = Glib::signal_timeout().connect( + sigc::mem_fun(*this, &Wireplumber::onReconnectTimeout), kReconnectIntervalMs); +} + +// Runs on the GTK main loop. Rebuilds the connection from scratch; returns true to keep retrying at +// the fixed interval until PipeWire is back, or false to stop once reconnected (a future +// "disconnected" signal will re-arm the timer if needed). +bool waybar::modules::Wireplumber::onReconnectTimeout() { + teardownConnection(); + spdlog::info("[{}]: attempting to reconnect to PipeWire...", name_); + if (setupConnection()) { + spdlog::info("[{}]: reconnected to PipeWire", name_); + return false; + } + teardownConnection(); + spdlog::debug("[{}]: reconnect failed; retrying in {} ms", name_, kReconnectIntervalMs); + return true; +} + +waybar::modules::Wireplumber::~Wireplumber() { + // Remove from the live-module registry first so any in-flight async callback bails out (#3974), + // then cancel a pending reconnect so onReconnectTimeout can't fire on a half-destroyed module. + waybar::modules::Wireplumber::modules.remove(this); + reconnect_timer_.disconnect(); + teardownConnection(); g_free(default_node_name_); g_free(default_source_name_); g_free(type_); From 8ff8ceeca271dc50c2bd3343638726e868572da5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 4 Jul 2026 13:36:31 +0200 Subject: [PATCH 337/416] 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 338/416] 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 339/416] 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"; } From 2355486cb3c4afc47676671e49c27a49b3231002 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:30:14 -0500 Subject: [PATCH 340/416] feat(util): add GLib command stream for nonblocking line reads Add a small GLib-backed helper for command stdout that integrates with the main loop instead of blocking on getline() in a worker thread. The helper keeps the existing child setup semantics used by Waybar commands, including process groups, parent-death signaling, and WAYBAR_OUTPUT_NAME propagation. This is the foundation for moving long-running custom commands away from manual poll/read logic in the module itself. Signed-off-by: Austin Horstman --- include/util/command_line_stream.hpp | 40 +++++ meson.build | 3 +- src/util/command_line_stream.cpp | 209 +++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 include/util/command_line_stream.hpp create mode 100644 src/util/command_line_stream.cpp diff --git a/include/util/command_line_stream.hpp b/include/util/command_line_stream.hpp new file mode 100644 index 00000000..05c82fc4 --- /dev/null +++ b/include/util/command_line_stream.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include +#include + +namespace waybar::util::command { + +class LineStream { + public: + using OutputCallback = std::function; + using ExitCallback = std::function; + + LineStream(std::string output_name, OutputCallback on_output, ExitCallback on_exit); + ~LineStream(); + + void start(const std::string& cmd); + void stop(); + bool running() const; + + private: + bool handleStdout(Glib::IOCondition condition); + void handleExit(Glib::Pid pid, int status); + void closeStdout(); + void drainStdout(bool flush_trailing_line); + static int statusToExitCode(int status); + + std::string output_name_; + OutputCallback on_output_; + ExitCallback on_exit_; + std::string buffer_; + Glib::Pid pid_; + int stdout_fd_; + sigc::connection stdout_connection_; + sigc::connection child_connection_; +}; + +} // namespace waybar::util::command diff --git a/meson.build b/meson.build index f96139a0..e3839cf5 100644 --- a/meson.build +++ b/meson.build @@ -196,7 +196,8 @@ src_files = files( 'src/util/regex_collection.cpp', 'src/util/css_reload_helper.cpp', 'src/util/transform_8bit_to_rgba.cpp', - 'src/util/utf8_string.cpp' + 'src/util/utf8_string.cpp', + 'src/util/command_line_stream.cpp' ) man_files = files( diff --git a/src/util/command_line_stream.cpp b/src/util/command_line_stream.cpp new file mode 100644 index 00000000..b13cc19b --- /dev/null +++ b/src/util/command_line_stream.cpp @@ -0,0 +1,209 @@ +#include "util/command_line_stream.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#endif +#ifdef __FreeBSD__ +#include +#endif + +#include "util/command.hpp" + +namespace { + +void prepareChild(const std::string& output_name) { + sigset_t mask; + sigfillset(&mask); + + const auto err = pthread_sigmask(SIG_UNBLOCK, &mask, nullptr); + if (err != 0) { + spdlog::error("pthread_sigmask in LineStream failed: {}", std::strerror(err)); + } + + int deathsig = SIGTERM; +#ifdef __linux__ + if (prctl(PR_SET_PDEATHSIG, deathsig) != 0) { + spdlog::error("prctl(PR_SET_PDEATHSIG) in LineStream failed: {}", std::strerror(errno)); + } +#endif +#ifdef __FreeBSD__ + if (procctl(P_PID, 0, PROC_PDEATHSIG_CTL, reinterpret_cast(&deathsig)) == -1) { + spdlog::error("procctl(PROC_PDEATHSIG_CTL) in LineStream failed: {}", std::strerror(errno)); + } +#endif + + if (setpgid(0, 0) != 0) { + spdlog::error("setpgid in LineStream failed: {}", std::strerror(errno)); + } + if (!output_name.empty()) { + setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); + } +} + +void emitBufferedLines(std::string& buffer, + const waybar::util::command::LineStream::OutputCallback& on_output, + bool flush_trailing_line) { + for (auto newline_pos = buffer.find('\n'); newline_pos != std::string::npos; + newline_pos = buffer.find('\n')) { + auto line = buffer.substr(0, newline_pos); + buffer.erase(0, newline_pos + 1); + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + on_output(line); + } + + if (flush_trailing_line && !buffer.empty()) { + auto line = std::move(buffer); + buffer.clear(); + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + on_output(line); + } +} + +} // namespace + +waybar::util::command::LineStream::LineStream(std::string output_name, OutputCallback on_output, + ExitCallback on_exit) + : output_name_(std::move(output_name)), + on_output_(std::move(on_output)), + on_exit_(std::move(on_exit)), + pid_(0), + stdout_fd_(-1) {} + +waybar::util::command::LineStream::~LineStream() { stop(); } + +void waybar::util::command::LineStream::start(const std::string& cmd) { + stop(); + + std::vector argv{"/bin/sh", "-c", cmd}; + Glib::spawn_async_with_pipes( + "", argv, Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, + sigc::bind(sigc::ptr_fun(&prepareChild), output_name_), &pid_, nullptr, &stdout_fd_, nullptr); + + const auto flags = fcntl(stdout_fd_, F_GETFL, 0); + if (flags == -1 || fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK) == -1) { + const auto saved_errno = errno; + stop(); + throw std::runtime_error("Unable to configure child stdout: " + + std::string(std::strerror(saved_errno))); + } + + stdout_connection_ = + Glib::signal_io().connect(sigc::mem_fun(*this, &LineStream::handleStdout), stdout_fd_, + Glib::IO_IN | Glib::IO_HUP | Glib::IO_ERR | Glib::IO_NVAL); + child_connection_ = + Glib::signal_child_watch().connect(sigc::mem_fun(*this, &LineStream::handleExit), pid_); +} + +void waybar::util::command::LineStream::stop() { + stdout_connection_.disconnect(); + child_connection_.disconnect(); + + if (pid_ != 0) { + killpg(pid_, SIGTERM); + waitpid(pid_, nullptr, 0); + Glib::spawn_close_pid(pid_); + pid_ = 0; + } + + closeStdout(); + buffer_.clear(); +} + +bool waybar::util::command::LineStream::running() const { return pid_ != 0; } + +bool waybar::util::command::LineStream::handleStdout(Glib::IOCondition condition) { + const auto should_flush = + static_cast(condition & (Glib::IO_HUP | Glib::IO_ERR | Glib::IO_NVAL)); + drainStdout(should_flush); + + if (!running() || should_flush) { + closeStdout(); + return false; + } + + return true; +} + +void waybar::util::command::LineStream::handleExit(Glib::Pid pid, int status) { + child_connection_.disconnect(); + + if (stdout_fd_ != -1) { + drainStdout(true); + stdout_connection_.disconnect(); + closeStdout(); + } + + if (pid_ == pid) { + Glib::spawn_close_pid(pid_); + pid_ = 0; + } else { + Glib::spawn_close_pid(pid); + } + + on_exit_(statusToExitCode(status)); +} + +void waybar::util::command::LineStream::closeStdout() { + if (stdout_fd_ != -1) { + ::close(stdout_fd_); + stdout_fd_ = -1; + } +} + +void waybar::util::command::LineStream::drainStdout(bool flush_trailing_line) { + if (stdout_fd_ == -1) { + return; + } + + std::array chunk = {}; + while (true) { + const auto bytes_read = ::read(stdout_fd_, chunk.data(), chunk.size()); + if (bytes_read > 0) { + buffer_.append(chunk.data(), static_cast(bytes_read)); + emitBufferedLines(buffer_, on_output_, false); + continue; + } + + if (bytes_read == 0) { + emitBufferedLines(buffer_, on_output_, flush_trailing_line); + return; + } + + if (errno == EINTR) { + continue; + } + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return; + } + + spdlog::error("Reading command stdout failed: {}", std::strerror(errno)); + emitBufferedLines(buffer_, on_output_, flush_trailing_line); + return; + } +} + +int waybar::util::command::LineStream::statusToExitCode(int status) { + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + if (WIFSIGNALED(status)) { + return 128 + WTERMSIG(status); + } + return 1; +} From 560f02509b0b3bd09824d0aabe5ea29763ffbc7f Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:30:14 -0500 Subject: [PATCH 341/416] test(util): cover command stream line delivery and EOF flushing Add focused coverage for the new GLib command stream helper. These tests verify that complete lines are emitted as they arrive and that EOF flushes a final unterminated line without duplicating a newline-terminated one. That behavior is the contract the custom module will rely on when its continuous command handling moves onto this helper. Signed-off-by: Austin Horstman --- test/utils/command_line_stream.cpp | 69 ++++++++++++++++++++++++ test/utils/fixtures/GlibTestsFixture.hpp | 11 +++- test/utils/meson.build | 2 + 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 test/utils/command_line_stream.cpp diff --git a/test/utils/command_line_stream.cpp b/test/utils/command_line_stream.cpp new file mode 100644 index 00000000..8c1639e3 --- /dev/null +++ b/test/utils/command_line_stream.cpp @@ -0,0 +1,69 @@ +#if __has_include() +#include +#else +#include +#endif + +#include + +#include +#include +#include + +#include "util/command_line_stream.hpp" + +namespace { + +struct StreamResult { + std::vector lines; + std::optional exit_code; + bool timed_out = false; +}; + +auto run_stream_command(const std::string& cmd) -> StreamResult { + StreamResult result; + auto loop = Glib::MainLoop::create(); + + auto timeout = Glib::signal_timeout().connect( + [&]() { + result.timed_out = true; + loop->quit(); + return false; + }, + 3000); + + waybar::util::command::LineStream stream( + "", + [&](const std::string& line) { result.lines.push_back(line); }, + [&](int exit_code) { + result.exit_code = exit_code; + loop->quit(); + }); + + stream.start(cmd); + loop->run(); + timeout.disconnect(); + return result; +} + +} // namespace + +TEST_CASE("command::LineStream emits complete lines and flushes trailing output", + "[util][command_line_stream]") { + const auto result = run_stream_command("printf 'first\\nsecond'"); + + REQUIRE_FALSE(result.timed_out); + REQUIRE(result.exit_code.has_value()); + REQUIRE(*result.exit_code == 0); + REQUIRE(result.lines == std::vector{"first", "second"}); +} + +TEST_CASE("command::LineStream does not emit an extra line after newline-terminated output", + "[util][command_line_stream]") { + const auto result = run_stream_command("printf 'first\\nsecond\\n'"); + + REQUIRE_FALSE(result.timed_out); + REQUIRE(result.exit_code.has_value()); + REQUIRE(*result.exit_code == 0); + REQUIRE(result.lines == std::vector{"first", "second"}); +} diff --git a/test/utils/fixtures/GlibTestsFixture.hpp b/test/utils/fixtures/GlibTestsFixture.hpp index a21c8e07..95ce7f95 100644 --- a/test/utils/fixtures/GlibTestsFixture.hpp +++ b/test/utils/fixtures/GlibTestsFixture.hpp @@ -6,10 +6,16 @@ class GlibTestsFixture : public sigc::trackable { public: GlibTestsFixture() : main_loop_{Glib::MainLoop::create()} {} + ~GlibTestsFixture() { timeout_.disconnect(); } void setTimeout(int timeout) { - Glib::signal_timeout().connect_once([]() { throw std::runtime_error("Test timed out"); }, - timeout); + timeout_.disconnect(); + timeout_ = Glib::signal_timeout().connect( + []() { + throw std::runtime_error("Test timed out"); + return false; + }, + timeout); } void run(std::function fn) { @@ -21,4 +27,5 @@ class GlibTestsFixture : public sigc::trackable { protected: Glib::RefPtr main_loop_; + sigc::connection timeout_; }; diff --git a/test/utils/meson.build b/test/utils/meson.build index e8dd37fa..0d9130de 100644 --- a/test/utils/meson.build +++ b/test/utils/meson.build @@ -15,8 +15,10 @@ test_src = files( 'SafeSignal.cpp', 'sleeper_thread.cpp', 'command.cpp', + 'command_line_stream.cpp', 'css_reload_helper.cpp', '../../src/util/css_reload_helper.cpp', + '../../src/util/command_line_stream.cpp', ) if tz_dep.found() From 5f4b96ad1d8a34a819581a117b733a6ea097e288 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:32:15 -0500 Subject: [PATCH 342/416] refactor(custom): move continuous exec onto GLib command stream Replace the custom module's continuous getline() worker with the new GLib-backed command stream helper. This moves line delivery, child exit handling, and restart scheduling onto the main loop so continuous commands no longer depend on a blocking FILE* read inside SleeperThread. The behavior is kept aligned with the old module semantics: stdout lines still emit updates, non-zero exits still surface as errors, and restart-interval still respawns the command. Signed-off-by: Austin Horstman --- include/modules/custom.hpp | 9 ++- src/modules/custom.cpp | 117 +++++++++++++++++++------------------ 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index 442b1c37..dbdcb698 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -3,10 +3,12 @@ #include #include +#include #include #include "AIconLabel.hpp" #include "util/command.hpp" +#include "util/command_line_stream.hpp" #include "util/json.hpp" #include "util/sleeper_thread.hpp" @@ -22,6 +24,9 @@ class Custom : public AIconLabel { private: void delayWorker(); void continuousWorker(); + void startContinuousProcess(bool throw_on_failure); + void handleContinuousProcessExit(int exit_code); + void scheduleContinuousRestart(); void waitingWorker(); void parseOutputRaw(); void parseOutputJson(); @@ -42,10 +47,10 @@ class Custom : public AIconLabel { const bool tooltip_format_enabled_; std::vector class_; int percentage_; - FILE* fp_; - int pid_; util::command::res output_; util::JsonParser parser_; + std::unique_ptr continuous_stream_; + sigc::connection restart_connection_; util::SleeperThread thread_; }; diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 83f36797..54fda760 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -2,10 +2,9 @@ #include +#include #include -#include "util/scope_guard.hpp" - waybar::modules::Custom::Custom(const std::string& name, const std::string& id, const Json::Value& config, const std::string& output_name) : AIconLabel(config, "custom-" + name, id, "{}"), @@ -13,9 +12,7 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, output_name_(output_name), id_(id), tooltip_format_enabled_{config_["tooltip-format"].isString()}, - percentage_(0), - fp_(nullptr), - pid_(-1) { + percentage_(0) { if (config.isNull()) { spdlog::warn("There is no configuration for 'custom/{}', element will be hidden", name); } @@ -40,10 +37,9 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, } waybar::modules::Custom::~Custom() { - if (pid_ != -1) { - killpg(pid_, SIGTERM); - waitpid(pid_, NULL, 0); - pid_ = -1; + restart_connection_.disconnect(); + if (continuous_stream_) { + continuous_stream_->stop(); } } @@ -80,57 +76,64 @@ void waybar::modules::Custom::delayWorker() { } void waybar::modules::Custom::continuousWorker() { - auto cmd = config_["exec"].asString(); - pid_ = -1; - fp_ = util::command::open(cmd, pid_, output_name_); - if (!fp_) { - throw std::runtime_error("Unable to open " + cmd); - } - thread_ = [this, cmd] { - char* buff = nullptr; - waybar::util::ScopeGuard buff_deleter([&buff]() { - if (buff) { - free(buff); - } - }); - size_t len = 0; - if (getline(&buff, &len, fp_) == -1) { - int exit_code = 1; - if (fp_) { - exit_code = WEXITSTATUS(util::command::close(fp_, pid_)); - fp_ = nullptr; - } - if (exit_code != 0) { - output_ = {exit_code, ""}; + continuous_stream_ = std::make_unique( + output_name_, + [this](const std::string& output) { + output_ = {0, output}; dp.emit(); - spdlog::error("{} stopped unexpectedly, is it endless?", name_); - } - if (config_["restart-interval"].isNumeric() && config_["restart-interval"].asDouble() > 0) { - pid_ = -1; - thread_.sleep_for(std::chrono::milliseconds( - std::max(1L, // Minimum 1ms due to millisecond precision - static_cast(config_["restart-interval"].asDouble() * 1000)))); - fp_ = util::command::open(cmd, pid_, output_name_); - if (!fp_) { - throw std::runtime_error("Unable to open " + cmd); - } - } else { - // A non-positive restart-interval must not busy-respawn the script - // (that starves the GTK main loop); treat it as "do not restart". - thread_.stop(); - return; - } - } else { - std::string output = buff; + }, + [this](int exit_code) { handleContinuousProcessExit(exit_code); }); + startContinuousProcess(true); +} - // Remove last newline - if (!output.empty() && output[output.length() - 1] == '\n') { - output.erase(output.length() - 1); - } - output_ = {0, output}; - dp.emit(); +void waybar::modules::Custom::startContinuousProcess(bool throw_on_failure) { + const auto cmd = config_["exec"].asString(); + + try { + continuous_stream_->start(cmd); + } catch (const Glib::SpawnError& e) { + if (throw_on_failure) { + throw std::runtime_error("Unable to open " + cmd + ": " + e.what().raw()); } - }; + output_ = {1, ""}; + dp.emit(); + spdlog::error("Unable to restart {}: {}", name_, e.what().raw()); + scheduleContinuousRestart(); + } catch (const std::exception& e) { + if (throw_on_failure) { + throw; + } + output_ = {1, ""}; + dp.emit(); + spdlog::error("Unable to restart {}: {}", name_, e.what()); + scheduleContinuousRestart(); + } +} + +void waybar::modules::Custom::handleContinuousProcessExit(int exit_code) { + if (exit_code != 0) { + output_ = {exit_code, ""}; + dp.emit(); + spdlog::error("{} stopped unexpectedly, is it endless?", name_); + } + + scheduleContinuousRestart(); +} + +void waybar::modules::Custom::scheduleContinuousRestart() { + restart_connection_.disconnect(); + if (!config_["restart-interval"].isNumeric() || config_["restart-interval"].asDouble() <= 0) { + // A non-positive restart-interval must not busy-respawn the script + // (that starves the GTK main loop); treat it as "do not restart". + return; + } + + restart_connection_ = Glib::signal_timeout().connect( + [this] { + startContinuousProcess(false); + return false; + }, + std::max(1U, static_cast(config_["restart-interval"].asDouble() * 1000))); } void waybar::modules::Custom::waitingWorker() { From fc01c03a202ed84688ed900ed78048db57bf20ef Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:33:12 -0500 Subject: [PATCH 343/416] fix(custom): avoid blocking child reaps in interval worker Stop the interval worker from waiting synchronously on every pid in pid_children_ before it refreshes the module. Switching this reap pass to waitpid(..., WNOHANG) keeps the worker responsive when an older event-triggered child is still running, while still removing children that have already exited. Signed-off-by: Austin Horstman --- src/modules/custom.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 54fda760..f010209e 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -50,13 +51,21 @@ void waybar::modules::Custom::delayWorker() { } thread_ = [this] { - for (int i : this->pid_children_) { - int status; - waitpid(i, &status, 0); + for (auto it = this->pid_children_.begin(); it != this->pid_children_.end();) { + int status = 0; + const auto pid = static_cast(*it); + const auto waited = waitpid(pid, &status, WNOHANG); + if (waited == 0) { + ++it; + continue; + } + if (waited == -1 && errno != ECHILD) { + ++it; + continue; + } + it = this->pid_children_.erase(it); } - this->pid_children_.clear(); - bool can_update = true; if (config_["exec-if"].isString()) { output_ = util::command::execNoRead(config_["exec-if"].asString()); From f987c24144272afc5976e9385b81fbb19f59b928 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:36:15 -0500 Subject: [PATCH 344/416] style(custom): apply format and tidy cleanups Run clang-format on the changed C++ files and fix the clang-tidy findings introduced by the custom command migration. The only codegen-relevant change here is switching the new res assignments in custom.cpp to designated initializers. The rest is formatting only. Signed-off-by: Austin Horstman --- src/modules/custom.cpp | 8 ++++---- src/util/command_line_stream.cpp | 6 +++--- test/utils/command_line_stream.cpp | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index f010209e..64577509 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -88,7 +88,7 @@ void waybar::modules::Custom::continuousWorker() { continuous_stream_ = std::make_unique( output_name_, [this](const std::string& output) { - output_ = {0, output}; + output_ = {.exit_code = 0, .out = output}; dp.emit(); }, [this](int exit_code) { handleContinuousProcessExit(exit_code); }); @@ -104,7 +104,7 @@ void waybar::modules::Custom::startContinuousProcess(bool throw_on_failure) { if (throw_on_failure) { throw std::runtime_error("Unable to open " + cmd + ": " + e.what().raw()); } - output_ = {1, ""}; + output_ = {.exit_code = 1, .out = ""}; dp.emit(); spdlog::error("Unable to restart {}: {}", name_, e.what().raw()); scheduleContinuousRestart(); @@ -112,7 +112,7 @@ void waybar::modules::Custom::startContinuousProcess(bool throw_on_failure) { if (throw_on_failure) { throw; } - output_ = {1, ""}; + output_ = {.exit_code = 1, .out = ""}; dp.emit(); spdlog::error("Unable to restart {}: {}", name_, e.what()); scheduleContinuousRestart(); @@ -121,7 +121,7 @@ void waybar::modules::Custom::startContinuousProcess(bool throw_on_failure) { void waybar::modules::Custom::handleContinuousProcessExit(int exit_code) { if (exit_code != 0) { - output_ = {exit_code, ""}; + output_ = {.exit_code = exit_code, .out = ""}; dp.emit(); spdlog::error("{} stopped unexpectedly, is it endless?", name_); } diff --git a/src/util/command_line_stream.cpp b/src/util/command_line_stream.cpp index b13cc19b..aeeedfaf 100644 --- a/src/util/command_line_stream.cpp +++ b/src/util/command_line_stream.cpp @@ -91,9 +91,9 @@ void waybar::util::command::LineStream::start(const std::string& cmd) { stop(); std::vector argv{"/bin/sh", "-c", cmd}; - Glib::spawn_async_with_pipes( - "", argv, Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, - sigc::bind(sigc::ptr_fun(&prepareChild), output_name_), &pid_, nullptr, &stdout_fd_, nullptr); + Glib::spawn_async_with_pipes("", argv, Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, + sigc::bind(sigc::ptr_fun(&prepareChild), output_name_), &pid_, + nullptr, &stdout_fd_, nullptr); const auto flags = fcntl(stdout_fd_, F_GETFL, 0); if (flags == -1 || fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK) == -1) { diff --git a/test/utils/command_line_stream.cpp b/test/utils/command_line_stream.cpp index 8c1639e3..b852a6e8 100644 --- a/test/utils/command_line_stream.cpp +++ b/test/utils/command_line_stream.cpp @@ -33,8 +33,7 @@ auto run_stream_command(const std::string& cmd) -> StreamResult { 3000); waybar::util::command::LineStream stream( - "", - [&](const std::string& line) { result.lines.push_back(line); }, + "", [&](const std::string& line) { result.lines.push_back(line); }, [&](int exit_code) { result.exit_code = exit_code; loop->quit(); From 1b6173ddda9b2571ec0a754534fb8564d79aa1d1 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Mar 2026 08:00:27 -0500 Subject: [PATCH 345/416] fix(util): keep GLib child setup fork-safe Move WAYBAR_OUTPUT_NAME injection into the parent-provided spawn environment and strip logging and setenv() out of the GLib child-setup hook. That keeps the helper's post-fork path limited to the process setup it actually needs, which is a safer fit for sanitizer-heavy platforms such as FreeBSD. Signed-off-by: Austin Horstman --- src/util/command_line_stream.cpp | 50 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/util/command_line_stream.cpp b/src/util/command_line_stream.cpp index aeeedfaf..7abb79b8 100644 --- a/src/util/command_line_stream.cpp +++ b/src/util/command_line_stream.cpp @@ -23,33 +23,42 @@ namespace { -void prepareChild(const std::string& output_name) { +auto buildChildEnvironment(const std::string& output_name) -> std::vector { + auto names = Glib::listenv(); + std::vector envp; + envp.reserve(names.size() + 1); + + for (const auto& name : names) { + bool found = false; + auto value = Glib::getenv(name, found); + if (!found || name == "WAYBAR_OUTPUT_NAME") { + continue; + } + envp.push_back(name + "=" + value); + } + + if (!output_name.empty()) { + envp.push_back("WAYBAR_OUTPUT_NAME=" + output_name); + } + + return envp; +} + +void prepareChild() { sigset_t mask; sigfillset(&mask); - const auto err = pthread_sigmask(SIG_UNBLOCK, &mask, nullptr); - if (err != 0) { - spdlog::error("pthread_sigmask in LineStream failed: {}", std::strerror(err)); - } + (void)pthread_sigmask(SIG_UNBLOCK, &mask, nullptr); int deathsig = SIGTERM; #ifdef __linux__ - if (prctl(PR_SET_PDEATHSIG, deathsig) != 0) { - spdlog::error("prctl(PR_SET_PDEATHSIG) in LineStream failed: {}", std::strerror(errno)); - } + (void)prctl(PR_SET_PDEATHSIG, deathsig); #endif #ifdef __FreeBSD__ - if (procctl(P_PID, 0, PROC_PDEATHSIG_CTL, reinterpret_cast(&deathsig)) == -1) { - spdlog::error("procctl(PROC_PDEATHSIG_CTL) in LineStream failed: {}", std::strerror(errno)); - } + (void)procctl(P_PID, 0, PROC_PDEATHSIG_CTL, reinterpret_cast(&deathsig)); #endif - if (setpgid(0, 0) != 0) { - spdlog::error("setpgid in LineStream failed: {}", std::strerror(errno)); - } - if (!output_name.empty()) { - setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); - } + (void)setpgid(0, 0); } void emitBufferedLines(std::string& buffer, @@ -91,9 +100,10 @@ void waybar::util::command::LineStream::start(const std::string& cmd) { stop(); std::vector argv{"/bin/sh", "-c", cmd}; - Glib::spawn_async_with_pipes("", argv, Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, - sigc::bind(sigc::ptr_fun(&prepareChild), output_name_), &pid_, - nullptr, &stdout_fd_, nullptr); + auto envp = buildChildEnvironment(output_name_); + Glib::spawn_async_with_pipes("", argv, envp, + Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, + sigc::ptr_fun(&prepareChild), &pid_, nullptr, &stdout_fd_, nullptr); const auto flags = fcntl(stdout_fd_, F_GETFL, 0); if (flags == -1 || fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK) == -1) { From a3dc55f9eed8233d3f97163d44ea81fc129ddd20 Mon Sep 17 00:00:00 2001 From: Guillaume Maudoux Date: Sat, 4 Jul 2026 23:08:42 +0200 Subject: [PATCH 346/416] feat(format): expand pow_format modifiers Extend the pow_format spec parser beyond alignment so configs can shape the rendered number, not just pad it. A forced scale (#, k, M, G, T, P) pins the SI prefix instead of auto-selecting it and, since the author then knows both scale and unit, hides the prefix and unit by default; U brings the unit back, u hides it in auto mode, and i forces integer display. b and B override the decimal or binary base independently of the call site, and a trailing width now fixes the coefficient field when a scale is forced, overflowing to '#' when it does not fit. The old '>' and '<' branches rendered via fmt::format("{}", s), which discarded every new field on the recursive call; they now build from a single render path that reads the current formatter, so modifiers survive alignment. The auto path is byte-identical to before, verified against the previous implementation. Adds a Catch2 suite covering each modifier alone and combined, plus the backward-compatible alignment cases, and documents the modifiers in the network and disk man pages. --- include/util/format.hpp | 160 ++++++++++++++++++++++++++++----------- man/waybar-disk.5.scd | 23 ++++++ man/waybar-network.5.scd | 28 +++++++ test/utils/format.cpp | 105 +++++++++++++++++++++++++ test/utils/meson.build | 1 + 5 files changed, 273 insertions(+), 44 deletions(-) create mode 100644 test/utils/format.cpp diff --git a/include/util/format.hpp b/include/util/format.hpp index 14bf3ef3..7addb045 100644 --- a/include/util/format.hpp +++ b/include/util/format.hpp @@ -23,30 +23,51 @@ class pow_format { namespace fmt { template <> struct formatter { - char spec = 0; - int width = 0; + char spec = 0; // alignment: '>', '<', '=' (0 = none) + int width = 0; // width digits; enforced only when scale_spec != 0 + char scale_spec = 0; // forced scale: 0 = auto, else one of '#','k','M','G','T','P' + char unit_pref = 0; // unit tri-state: 0 = default, 'u' = hide, 'U' = show + char base_pref = 0; // base tri-state: 0 = call-site, 'b' = decimal, 'B' = binary + bool force_int = false; // 'i': force integer display template constexpr auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { auto it = ctx.begin(), end = ctx.end(); if (it != end && *it == ':') ++it; - if (it && (*it == '>' || *it == '<' || *it == '=')) { + if (it != end && (*it == '>' || *it == '<' || *it == '=')) { spec = *it; ++it; } - if (it == end || *it == '}') return it; - if ('0' <= *it && *it <= '9') { - // We ignore it for now, but keep it for compatibility with - // existing configs where the format for pow_format'ed numbers was - // 'string' and specifications such as {:>9} were valid. - // The rationale for ignoring it is that the only reason to specify - // an alignment and a with is to get a fixed width bar, and ">" is - // sufficient in this implementation. + // Consume scale/flag modifiers and the width in any order, until '}' or end. + // The width digits (parsed but only enforced when a scale is forced — see + // format()) may appear anywhere among the modifiers, so both {:=#3} and the + // more natural {:=3#} are accepted. On an unrecognised char we stop and let + // fmt raise its usual error. + while (it != end && *it != '}') { + char c = *it; + if (c == '#' || c == 'k' || c == 'M' || c == 'G' || c == 'T' || c == 'P') { + scale_spec = c; + ++it; + } else if (c == 'u' || c == 'U') { + unit_pref = c; + ++it; + } else if (c == 'b' || c == 'B') { + base_pref = c; + ++it; + } else if (c == 'i') { + force_int = true; + ++it; + } else if ('0' <= c && c <= '9') { + // Width kept for compatibility with existing configs such as {:>9}; only + // enforced (fixed field + '#' overflow) when a scale is forced. #if FMT_VERSION < 80000 - width = parse_nonnegative_int(it, end, ctx); + width = parse_nonnegative_int(it, end, ctx); #else - width = detail::parse_nonnegative_int(it, end, -1); + width = detail::parse_nonnegative_int(it, end, -1); #endif + } else { + break; + } } return it; } @@ -54,49 +75,100 @@ struct formatter { template auto format(const pow_format& s, FormatContext& ctx) const -> decltype(ctx.out()) { const char* units[] = {"", "k", "M", "G", "T", "P", nullptr}; + const int max_pow = 5; // last valid index in units[] - auto base = s.binary_ ? 1024ull : 1000ll; + // Effective base: 'b'/'B' override the call-site binary_. + bool binary = base_pref == 'B' ? true : base_pref == 'b' ? false : s.binary_; + auto base = binary ? 1024ull : 1000ll; auto div = 1ll; auto fraction = (double)s.val_; int pow; - for (pow = 0; units[pow + 1] != nullptr && fraction / base >= 1; ++pow) { - fraction /= base; - div *= base; + if (scale_spec != 0) { + // Forced scale: map the char to a fixed index into units[]. + switch (scale_spec) { + case 'k': + pow = 1; + break; + case 'M': + pow = 2; + break; + case 'G': + pow = 3; + break; + case 'T': + pow = 4; + break; + case 'P': + pow = 5; + break; + default: + pow = 0; + break; // '#' -> base scale + } + if (pow > max_pow) pow = max_pow; + for (int i = 0; i < pow; ++i) div *= base; + fraction /= div; + } else { + for (pow = 0; units[pow + 1] != nullptr && fraction / base >= 1; ++pow) { + fraction /= base; + div *= base; + } } - auto fixed_precision = (s.skip_decimal_ && ((s.val_ % div) == 0)) ? 0 : 1; - auto number_width = 3 + fixed_precision // coeff in {:.{fixed_precision}f} format - + (fixed_precision != 0) // float dot - + s.binary_; // potential digit before the decimal point - auto max_width = number_width + 1 // prefix from units array - + s.binary_ // for the 'i' in GiB. - + s.unit_.length(); + // Precision: 'i' forces 0; otherwise 1 (or 0 when skip_decimal_ divides + // evenly). min_pow_for_decimal_ keeps its default-branch-only effect. + int precision = force_int ? 0 : (s.skip_decimal_ && ((s.val_ % div) == 0)) ? 0 : 1; + if (!force_int && scale_spec == 0 && pow < s.min_pow_for_decimal_) precision = 0; + + // Unit visibility: default on for auto scale, off for a forced scale; 'u'/'U' + // override. The binary 'i' is part of the scale prefix, so the unit is just + // unit_. + bool hide_unit = unit_pref == 'u' || (unit_pref == 0 && scale_spec != 0); + + // Scale prefix (letter + binary 'i'), suppressed entirely when a scale is + // forced. + std::string prefix = + scale_spec != 0 ? "" : std::string(units[pow]) + ((binary && pow) ? "i" : ""); + std::string unit = hide_unit ? "" : s.unit_; + + auto number_width = 3 + precision // coeff in {:.{precision}f} format + + (precision != 0) // float dot + + binary; // potential digit before the decimal point + // In auto mode the prefix column is always reserved (letter + optional 'i'), + // matching the historical fixed max_width even at base scale (the '=' padding + // fills the gap). A forced scale drops the prefix column entirely. + auto prefix_col = scale_spec != 0 ? 0 : 1 + binary; + auto max_width = number_width + prefix_col + unit.length(); + + // The numeric coefficient string. When a scale is forced with a width and the + // number does not fit, it overflows to '#' (spreadsheet-style). + bool fixed_num = scale_spec != 0 && width > 0; + std::string number = fmt::format("{:.{}f}", fraction, precision); + if (fixed_num && (int)number.length() > width) number = std::string(width, '#'); + + // Base-scale compensation for the '=' column-align: only in auto mode, where + // the absent prefix (and binary 'i') would otherwise shift the unit column. + const char* padding = (scale_spec == 0 && pow == 0) ? (binary ? " " : " ") : ""; - const char* format; - std::string string; switch (spec) { - case '>': - return fmt::format_to(ctx.out(), "{:>{}}", fmt::format("{}", s), max_width); - case '<': - return fmt::format_to(ctx.out(), "{:<{}}", fmt::format("{}", s), max_width); case '=': - format = "{coefficient:<{number_width}.{fixed_precision}f}{padding}{prefix}{unit}"; - break; + // Column-align: left-justify the coefficient within its column, then pad + // so the prefix/unit line up across values of different magnitude. + return fmt::format_to(ctx.out(), "{:<{}}{}{}{}", number, fixed_num ? width : number_width, + padding, prefix, unit); + case '>': + case '<': case 0: - default: - format = pow < s.min_pow_for_decimal_ ? "{coefficient:.0f}{prefix}{unit}" - : "{coefficient:.{fixed_precision}f}{prefix}{unit}"; - break; + default: { + // Right-justify the numeric field to the fixed width when forced. + std::string body = + (fixed_num ? fmt::format("{:>{}}", number, width) : number) + prefix + unit; + if (spec == '>') return fmt::format_to(ctx.out(), "{:>{}}", body, max_width); + if (spec == '<') return fmt::format_to(ctx.out(), "{:<{}}", body, max_width); + return fmt::format_to(ctx.out(), "{}", body); + } } - return fmt::format_to( - ctx.out(), fmt::runtime(format), fmt::arg("coefficient", fraction), - fmt::arg("fixed_precision", fixed_precision), fmt::arg("number_width", number_width), - fmt::arg("prefix", std::string() + units[pow] + ((s.binary_ && pow) ? "i" : "")), - fmt::arg("unit", s.unit_), - fmt::arg("padding", pow ? "" - : s.binary_ ? " " - : " ")); } }; diff --git a/man/waybar-disk.5.scd b/man/waybar-disk.5.scd index 16e0d360..d36fb608 100644 --- a/man/waybar-disk.5.scd +++ b/man/waybar-disk.5.scd @@ -146,6 +146,29 @@ Addressed by *disk* *{specific_free}*: Amount of available disk space for normal users in a specific unit. Defaults to bytes. +# NUMBER FORMAT MODIFIERS + +*{total}*, *{used}* and *{free}* auto-scale with a binary prefix (KiB, GiB, …). +Their rendering can be tuned with fmt-style modifiers, e.g. *"{free:>}"* or +*"{free:G}"*, combined in any order: + +*<*, *=*, *>*: Alignment/padding (left, column-align, right). + +*u* / *U*: Hide (*u*) or show (*U*) the unit suffix. Shown by default with an auto +scale, hidden by default when a scale is forced. + +*#*, *k*, *M*, *G*, *T*, *P*: Force a fixed scale instead of auto-selecting +(*#* = base scale). Forcing a scale hides the scale prefix and, by default, the +unit. E.g. *"{free:G}"* always shows gibibytes. + +*i*: Force integer display (no decimals). + +*b* / *B*: Force decimal base 1000 (*b*) or binary base 1024 (*B*); disk values +are binary by default. + +A trailing number is a fixed width for the coefficient when a scale is forced; +overflow is shown as *#* characters (e.g. *"{free:=3#}"* → *###*). + # EXAMPLES ``` diff --git a/man/waybar-network.5.scd b/man/waybar-network.5.scd index efeed0ce..b950d431 100644 --- a/man/waybar-network.5.scd +++ b/man/waybar-network.5.scd @@ -214,6 +214,34 @@ Addressed by *network* *{icon}*: Icon, as defined in *format-icons*. +# NUMBER FORMAT MODIFIERS + +The bandwidth/bitrate replacements above (*{bandwidth\*}*, *{txBitrate}*, +*{rxBitrate}*, *{linkSpeed}*) auto-scale the number with an SI prefix (k, M, G, +…). Their rendering can be tuned with fmt-style modifiers, e.g. +*"{bandwidthDownBits:>}"* or *"{bandwidthDownBits:M}"*. Modifiers may be combined +in any order: + +*<*, *=*, *>*: Alignment/padding (left, column-align, right) — as before. + +*u* / *U*: Hide (*u*) or show (*U*) the unit suffix. The unit is shown by default +with an auto scale and hidden by default when a scale is forced (see below). + +*#*, *k*, *M*, *G*, *T*, *P*: Force a fixed scale instead of auto-selecting one +(*#* = base scale, no prefix). Forcing a scale hides the scale prefix and, by +default, the unit (re-enable it with *U*). E.g. *"{bandwidthDownBits:M}"* always +shows the value in megabits. + +*i*: Force integer display (no decimals). + +*b* / *B*: Force decimal base 1000 (*b*) or binary base 1024 with an *i* marker +(*B*), overriding the module default. + +A trailing number is a fixed width for the coefficient when a scale is forced; if +the value does not fit, it is shown as *#* characters (e.g. *"{bandwidthDownBits:=3#}"* +→ *###*). Without a forced scale the number is ignored (kept for config +compatibility). + # EXAMPLES ``` diff --git a/test/utils/format.cpp b/test/utils/format.cpp new file mode 100644 index 00000000..63dc4eaa --- /dev/null +++ b/test/utils/format.cpp @@ -0,0 +1,105 @@ +#include "util/format.hpp" + +#if __has_include() +#include +#else +#include +#endif + +#include + +// Helpers to build the two representative sample values used throughout the +// pow_format modifier tests. +static std::string fmtA(const char* spec) { + // 1536 bytes, binary (base 1024) -> auto "1.5kiB" + return fmt::format(fmt::runtime(std::string("{:") + spec + "}"), pow_format(1536, "B", true)); +} +static std::string fmtB(const char* spec) { + // 1500000 b/s, decimal (base 1000) -> auto "1.5Mb/s" + return fmt::format(fmt::runtime(std::string("{:") + spec + "}"), pow_format(1500000, "b/s")); +} + +TEST_CASE("pow_format default/auto rendering", "[format][pow_format]") { + REQUIRE(fmt::format("{}", pow_format(1536, "B", true)) == "1.5kiB"); + REQUIRE(fmt::format("{}", pow_format(1500000, "b/s")) == "1.5Mb/s"); + // Sub-scale values and unit-only rendering. + REQUIRE(fmt::format("{}", pow_format(500, "B")) == "500.0B"); +} + +TEST_CASE("pow_format hide/show unit (u/U)", "[format][pow_format]") { + REQUIRE(fmtA("u") == "1.5ki"); // unit hidden, scale prefix (ki) kept + REQUIRE(fmtA("U") == "1.5kiB"); // explicit show = auto default + REQUIRE(fmtB("u") == "1.5M"); + REQUIRE(fmtB("U") == "1.5Mb/s"); +} + +TEST_CASE("pow_format force integer (i)", "[format][pow_format]") { + REQUIRE(fmtA("i") == "2kiB"); // 1.5 -> 2 + REQUIRE(fmtA("iu") == "2ki"); + REQUIRE(fmtB("i") == "2Mb/s"); +} + +TEST_CASE("pow_format forced scale hides prefix and unit", "[format][pow_format]") { + // Base scale (#): no prefix; unit off by default, back on with U. + REQUIRE(fmtA("#") == "1536.0"); + REQUIRE(fmtA("#U") == "1536.0B"); + REQUIRE(fmtA("#i") == "1536"); + REQUIRE(fmtB("#") == "1500000.0"); + REQUIRE(fmtB("#U") == "1500000.0b/s"); + + // Force kilo: whole prefix (ki) suppressed, unit off by default. + REQUIRE(fmtA("k") == "1.5"); + REQUIRE(fmtA("kU") == "1.5B"); // unit shown, no 'i' (it lives on the prefix) + REQUIRE(fmtA("ki") == "2"); + REQUIRE(fmtB("k") == "1500.0"); + REQUIRE(fmtB("kU") == "1500.0b/s"); + REQUIRE(fmtB("ki") == "1500"); + + // Force a scale far above the value's magnitude -> underflow to 0, no bump. + REQUIRE(fmtA("M") == "0.0"); + REQUIRE(fmtA("MU") == "0.0B"); + REQUIRE(fmtB("G") == "0.0"); +} + +TEST_CASE("pow_format force base (b/B)", "[format][pow_format]") { + // Force decimal on a binary call-site value. + REQUIRE(fmtA("b") == "1.5kB"); // base 1000, prefix 'k', no 'i' + REQUIRE(fmtA("bu") == "1.5k"); + REQUIRE(fmtA("b#") == "1536.0"); + + // Force binary on a decimal call-site value. + REQUIRE(fmtB("B") == "1.4Mib/s"); // base 1024, prefix 'Mi' + REQUIRE(fmtB("Bu") == "1.4Mi"); + REQUIRE(fmtB("Bk") == "1464.8"); + REQUIRE(fmtB("BkU") == "1464.8b/s"); +} + +TEST_CASE("pow_format fixed width with # overflow", "[format][pow_format]") { + // Base scale, numeric field width 3: 1536.0 (6 chars) overflows -> ###. + REQUIRE(fmtA("=3#") == "###"); + REQUIRE(fmtA("=6#") == "1536.0"); + REQUIRE(fmtA("=3#U") == "###B"); + REQUIRE(fmtA("=6#U") == "1536.0B"); + REQUIRE(fmtB("=4k#") == "####"); + // Width digit position is flexible: {:=3#} and {:=#3} are equivalent. + REQUIRE(fmtA("=#3") == "###"); + // Force kilo, width 6, coefficient "1500" fits, shown with unit; the '=' + // column left-justifies the coefficient in its field. + REQUIRE(fmtB("=6kiU") == "1500 b/s"); +} + +TEST_CASE("pow_format modifiers compose with alignment", "[format][pow_format]") { + REQUIRE(fmtA(">u") == " 1.5ki"); // right-align, unit hidden (prefix kept) + REQUIRE(fmtB(">ki") == "1500"); // force kilo+int (unit off), no scale width + REQUIRE(fmtB("Bu") == "1.4Mi"); // force binary, unit hidden +} + +TEST_CASE("pow_format backward compatible alignment", "[format][pow_format]") { + // These specs predate the new modifiers and must render as before. + REQUIRE(fmt::format("{:>}", pow_format(1536, "B", true)) == " 1.5kiB"); + REQUIRE(fmt::format("{:<}", pow_format(1536, "B", true)) == "1.5kiB "); + REQUIRE(fmt::format("{:=}", pow_format(1536, "B", true)) == "1.5 kiB"); + // Width without a forced scale is still ignored (only used for compat). + REQUIRE(fmt::format("{:>9}", pow_format(1536, "B", true)) == " 1.5kiB"); + REQUIRE(fmt::format("{}", pow_format(1500000, "b/s")) == "1.5Mb/s"); +} diff --git a/test/utils/meson.build b/test/utils/meson.build index 0d9130de..fb4ad60e 100644 --- a/test/utils/meson.build +++ b/test/utils/meson.build @@ -13,6 +13,7 @@ test_src = files( '../../src/config.cpp', 'JsonParser.cpp', 'SafeSignal.cpp', + 'format.cpp', 'sleeper_thread.cpp', 'command.cpp', 'command_line_stream.cpp', From 64f70caa46359f772a9cd059200685dbfa4c18bc Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 5 Jul 2026 00:29:27 -0500 Subject: [PATCH 347/416] fix(niri): keep the initial IPC connect synchronous #5158 (6672e924) moved connectToSocket() off the constructing thread and into the detached IPC worker's own try/catch, so a missing NIRI_SOCKET no longer throws out of IPC::IPC(). That was needed to fix #5117 (the worker should reconnect instead of dying when an established stream drops), but it also meant the very first connection attempt can never fail anymore. Factory::makeModule()/Bar::getModules() rely on that constructor throwing to disable a module it can't construct. With niri/workspaces and niri/window always constructing successfully now, they get added to every bar regardless of which compositor is actually running, showing up as a permanently-empty widget next to the real workspace modules under Hyprland/Sway. Restore the old semantics for the first connection: connectToSocket() runs synchronously in IPC::IPC() again, so a missing socket still throws and the module gets disabled as before. Only a drop *after* that succeeds falls into the retrying reconnect loop, preserving the #5117 fix. --- include/modules/niri/backend.hpp | 2 +- src/modules/niri/backend.cpp | 36 ++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/include/modules/niri/backend.hpp b/include/modules/niri/backend.hpp index acbbf52e..94b6d66f 100644 --- a/include/modules/niri/backend.hpp +++ b/include/modules/niri/backend.hpp @@ -34,7 +34,7 @@ class IPC { unsigned keyboardLayoutCurrent() const { return keyboardLayoutCurrent_; } private: - void startIPC(); + void startIPC(int initial_socketfd); static int connectToSocket(); void parseIPC(const std::string&); diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 245bf2de..460754cc 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -21,7 +21,13 @@ namespace waybar::modules::niri { -IPC::IPC() { startIPC(); } +IPC::IPC() { + // Connect synchronously so a missing socket (this WM isn't the active + // compositor) throws here, same as before the reconnect loop below existed. + // That lets the module constructor fail and Factory disable the module, + // instead of the module always attaching with a permanently empty widget. + startIPC(connectToSocket()); +} IPC::~IPC() { running_ = false; } @@ -54,22 +60,30 @@ int IPC::connectToSocket() { return socketfd.release(); } -void IPC::startIPC() { +void IPC::startIPC(int initial_socketfd) { // will start IPC and relay events to parseIPC - std::thread([this]() { + std::thread([this, initial_socketfd]() { spdlog::info("Niri IPC starting"); - // Reconnect loop: if the event stream drops we back off briefly and - // re-establish the socket instead of leaving the module frozen forever. + bool have_initial_fd = true; + + // Reconnect loop: if the event stream drops *after* the initial connect + // above succeeded, we back off briefly and re-establish the socket + // instead of leaving the module frozen forever. while (running_) { int socketfd; - try { - socketfd = connectToSocket(); - } catch (std::exception& e) { - spdlog::error("Niri IPC: failed to connect: {}", e.what()); - std::this_thread::sleep_for(std::chrono::seconds(2)); - continue; + if (have_initial_fd) { + socketfd = initial_socketfd; + have_initial_fd = false; + } else { + try { + socketfd = connectToSocket(); + } catch (std::exception& e) { + spdlog::error("Niri IPC: failed to connect: {}", e.what()); + std::this_thread::sleep_for(std::chrono::seconds(2)); + continue; + } } auto unix_istream = Gio::UnixInputStream::create(socketfd, true); From a51bddab9efa44c8398eb05ed61dc4cca83850e8 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 5 Jul 2026 01:18:26 -0500 Subject: [PATCH 348/416] fix(label): compare markup cache by raw bytes, not collation 2190871a (perf(label): skip redundant markup updates) caches the last label/tooltip markup as Glib::ustring and skips set_markup() when the new markup compares equal. Glib::ustring::operator== goes through g_utf8_collate(), and under the UTF-8 locale GTK sets at startup, Unicode private-use codepoints carry no collation weight. All nerd-font icons live in the PUA, so two labels that differ only in their icon glyph collate as equal and the visual update is silently dropped. idle_inhibitor is the visible victim (#5169): clicking toggles the state class (highlight changes) but the {icon} glyph never switches between the activated/deactivated icons, while plain-text icons like "YES"/"NO" work. Any ALabel module whose consecutive updates differ only by a PUA glyph is affected. The module only started routing through this cache when db4941ef migrated it onto the shared setLabelMarkup()/setTooltipMarkup() helpers, which is why it broke in the latest batch of refactors. Store the cache as raw UTF-8 bytes (std::string via ustring::raw()) and compare those instead, so the skip only triggers on byte-identical markup. Reproduced and verified under a nested niri session: before the fix RTMIN+n toggled the state class but left the sleep glyph unchanged; after it the glyph flips as expected, and byte-identical updates are still skipped. --- include/ALabel.hpp | 7 +++++-- src/ALabel.cpp | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/include/ALabel.hpp b/include/ALabel.hpp index 201d640c..3e3690b1 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -87,8 +87,11 @@ class ALabel : public AModule { static void handleGtkMenuEvent(GtkMenuItem* menuitem, gpointer data); private: - std::optional last_label_markup_; - std::optional last_tooltip_markup_; + // Raw UTF-8 bytes, not Glib::ustring: ustring::operator== collates with + // g_utf8_collate(), which gives private-use codepoints (nerd-font icons) + // no collation weight, so two different icons compare equal. + std::optional last_label_markup_; + std::optional last_tooltip_markup_; }; } // namespace waybar diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 84ac2052..752c80bd 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -147,22 +147,22 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st auto ALabel::update() -> void { AModule::update(); } bool ALabel::setLabelMarkup(const Glib::ustring& markup) { - if (last_label_markup_ == markup) { + if (last_label_markup_ == markup.raw()) { return false; } label_.set_markup(markup); - last_label_markup_ = markup; + last_label_markup_ = markup.raw(); return true; } bool ALabel::setTooltipMarkup(const Glib::ustring& markup) { - if (last_tooltip_markup_ == markup) { + if (last_tooltip_markup_ == markup.raw()) { return false; } label_.set_tooltip_markup(markup); - last_tooltip_markup_ = markup; + last_tooltip_markup_ = markup.raw(); return true; } From 0729823c133e5b1c1c50d05a0923281c3ce0dd41 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 09:47:47 +0200 Subject: [PATCH 349/416] fix(image): don't crash at startup when the widget isn't realized yet The HiDPI code path builds a cairo surface from the pixbuf via Gdk::Cairo::create_surface_from_pixbuf(pixbuf, scale, image_.get_window()), which requires a realized Gtk::Image. During startup an image module can run its first update() before the widget is realized, so get_window() returns a null Gdk::Window and that path aborts before anything is logged. The more image modules are configured, the more likely at least one updates before realization, which is why >2 images reliably kills startup. Guard on get_window(): only take the surface path when a window is available, otherwise fall back to image_.set(pixbuf) (the pre-HiDPI behavior). This keeps HiDPI crispness once realized and never crashes at startup. Fixes #5051. --- src/modules/image.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/modules/image.cpp b/src/modules/image.cpp index 7c733880..d762f1eb 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -265,9 +265,19 @@ void SingleImageStrategy::update() { } if (pixbuf) { - auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), - image_.get_window()); - image_.set(surface); + // Building a HiDPI-aware cairo surface requires a realized widget: it reads the + // GdkWindow and its scale factor. During startup update() can run before the + // Gtk::Image is realized, in which case get_window() is null; feeding that path a + // null window aborts startup. Fall back to setting the pixbuf directly while the + // widget is unrealized, and use the crisp surface path once a window is available. + auto window = image_.get_window(); + if (window) { + auto surface = + Gdk::Cairo::create_surface_from_pixbuf(pixbuf, image_.get_scale_factor(), window); + image_.set(surface); + } else { + image_.set(pixbuf); + } image_.show(); if (hasTooltip_ && !tooltip_.empty()) { From acc7060be6f78f36a30a9baeb147815efb80cdd8 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 09:53:11 +0200 Subject: [PATCH 350/416] fix(wireplumber): guard async callbacks by connection generation; wire scroll once Addresses review on #5168: - Generational aliasing (blocking): setupConnection()/onReconnectTimeout() rebuild wp_core_/om_/pending_plugins_ in place on the same self, but the async load/activate callbacks carried no generation, and isModuleAlive() only proves self still exists. If PipeWire dropped again while a previous connection's async chain was still in flight, a stale completion would run against the rebuilt connection (a stray --pending_plugins_, an out-of-order install_object_manager), re-creating #2882's stale/blank state. Each async call now carries an AsyncCall{self, generation}; connection_generation_ is bumped in setupConnection(), and every callback drops out when its generation no longer matches (checked after isModuleAlive short-circuits). - Duplicate scroll handlers: onMixerApiLoaded re-runs on every reconnect and connected a new scroll handler each time (dead but accumulating). Moved the one-time wiring to the constructor; handleScroll no-ops while mixer_api_ is null, so wiring it before the first connect is safe. --- include/modules/wireplumber.hpp | 11 ++++--- src/modules/wireplumber.cpp | 52 ++++++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/include/modules/wireplumber.hpp b/include/modules/wireplumber.hpp index d32fbeec..dc96b520 100644 --- a/include/modules/wireplumber.hpp +++ b/include/modules/wireplumber.hpp @@ -29,10 +29,9 @@ class Wireplumber : public ALabel { static void updateNodeName(waybar::modules::Wireplumber* self, uint32_t id); static void updateSourceVolume(waybar::modules::Wireplumber* self, uint32_t id); static void updateSourceName(waybar::modules::Wireplumber* self, uint32_t id); // NEW - static void onPluginActivated(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); - static void onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, - waybar::modules::Wireplumber* self); - static void onMixerApiLoaded(WpObject* p, GAsyncResult* res, waybar::modules::Wireplumber* self); + static void onPluginActivated(WpObject* p, GAsyncResult* res, gpointer data); + static void onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, gpointer data); + static void onMixerApiLoaded(WpObject* p, GAsyncResult* res, gpointer data); static void onObjectManagerInstalled(waybar::modules::Wireplumber* self); static void onMixerChanged(waybar::modules::Wireplumber* self, uint32_t id); static void onDefaultNodesApiChanged(waybar::modules::Wireplumber* self); @@ -57,6 +56,10 @@ class Wireplumber : public ALabel { WpPlugin* def_nodes_api_; gchar* default_node_name_; uint32_t pending_plugins_; + // Bumped on every (re)connection. The async load/activate callbacks capture the generation they + // were scheduled under (via their user_data) and no-op if it no longer matches, so a completion + // from a connection that was already torn down cannot corrupt the new generation's state (#2882). + uint32_t connection_generation_{0}; bool muted_; double volume_; double min_step_; diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 11c2c60e..8f9bd961 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -26,6 +27,17 @@ bool waybar::modules::Wireplumber::isModuleAlive(waybar::modules::Wireplumber* s return std::find(modules.begin(), modules.end(), self) != modules.end(); } +namespace { +// user_data for the async load/activate callbacks. Pairs the module with the connection generation +// the call was scheduled under so a completion belonging to a torn-down connection can be dropped +// (see Wireplumber::connection_generation_ and #2882). Heap-allocated per call; the callback takes +// ownership and frees it. +struct AsyncCall { + waybar::modules::Wireplumber* self; + uint32_t generation; +}; +} // namespace + waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Value& config) : ALabel(config, "wireplumber", id, "{volume}%"), wp_core_(nullptr), @@ -56,6 +68,12 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val : "Audio/Sink"); only_physical_ = config_["only-physical"].isBool() ? config_["only-physical"].asBool() : false; + // Wire the scroll handler once, here, rather than in onMixerApiLoaded: the latter now re-runs on + // every reconnect and would accumulate duplicate handlers. handleScroll no-ops while mixer_api_ + // is null, so wiring it before the first successful connect is safe. + event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); + event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Wireplumber::handleScroll)); + if (!setupConnection()) { spdlog::error("[{}]: Could not connect to PipeWire: '{}'", name_, type_); throw std::runtime_error("Could not connect to PipeWire\n"); @@ -67,6 +85,10 @@ waybar::modules::Wireplumber::Wireplumber(const std::string& id, const Json::Val // connection-scoped state is (re)built here. Returns false if the connection could not be // initiated. See https://github.com/Alexays/Waybar/issues/2882. bool waybar::modules::Wireplumber::setupConnection() { + // New connection generation: any async load/activate callback still in flight from a previous + // connection will see a mismatched generation and bail out instead of mutating this one's state. + ++connection_generation_; + wp_core_ = wp_core_new(nullptr, nullptr, nullptr); apis_ = g_ptr_array_new_with_free_func(g_object_unref); om_ = wp_object_manager_new(); @@ -471,8 +493,10 @@ void waybar::modules::Wireplumber::onObjectManagerInstalled(waybar::modules::Wir } void waybar::modules::Wireplumber::onPluginActivated(WpObject* p, GAsyncResult* res, - waybar::modules::Wireplumber* self) { - if (!isModuleAlive(self)) { + gpointer data) { + std::unique_ptr call(static_cast(data)); + auto* self = call->self; + if (!isModuleAlive(self) || call->generation != self->connection_generation_) { return; } @@ -496,7 +520,8 @@ void waybar::modules::Wireplumber::activatePlugins() { WpPlugin* plugin = static_cast(g_ptr_array_index(apis_, i)); pending_plugins_++; wp_object_activate(WP_OBJECT(plugin), WP_PLUGIN_FEATURE_ENABLED, nullptr, - (GAsyncReadyCallback)onPluginActivated, this); + (GAsyncReadyCallback)onPluginActivated, + new AsyncCall{this, connection_generation_}); } } @@ -520,8 +545,10 @@ void waybar::modules::Wireplumber::prepare(waybar::modules::Wireplumber* self) { } void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncResult* res, - waybar::modules::Wireplumber* self) { - if (!isModuleAlive(self)) { + gpointer data) { + std::unique_ptr call(static_cast(data)); + auto* self = call->self; + if (!isModuleAlive(self) || call->generation != self->connection_generation_) { return; } @@ -541,12 +568,14 @@ void waybar::modules::Wireplumber::onDefaultNodesApiLoaded(WpObject* p, GAsyncRe spdlog::debug("[{}]: loading mixer api module", self->name_); wp_core_load_component(self->wp_core_, "libwireplumber-module-mixer-api", "module", nullptr, - "mixer-api", nullptr, (GAsyncReadyCallback)onMixerApiLoaded, self); + "mixer-api", nullptr, (GAsyncReadyCallback)onMixerApiLoaded, + new AsyncCall{self, call->generation}); } -void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* res, - waybar::modules::Wireplumber* self) { - if (!isModuleAlive(self)) { +void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* res, gpointer data) { + std::unique_ptr call(static_cast(data)); + auto* self = call->self; + if (!isModuleAlive(self) || call->generation != self->connection_generation_) { return; } @@ -570,16 +599,13 @@ void waybar::modules::Wireplumber::onMixerApiLoaded(WpObject* p, GAsyncResult* r self->activatePlugins(); self->dp.emit(); - - self->event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); - self->event_box_.signal_scroll_event().connect(sigc::mem_fun(*self, &Wireplumber::handleScroll)); } void waybar::modules::Wireplumber::asyncLoadRequiredApiModules() { spdlog::debug("[{}]: loading default nodes api module", name_); wp_core_load_component(wp_core_, "libwireplumber-module-default-nodes-api", "module", nullptr, "default-nodes-api", nullptr, (GAsyncReadyCallback)onDefaultNodesApiLoaded, - this); + new AsyncCall{this, connection_generation_}); } static const std::array ports = { From fc297a7df3632fdf62213322d619f89b6a59b845 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:04 +0200 Subject: [PATCH 351/416] ci: only trigger on push to master to avoid duplicate PR runs These workflows triggered on both push and pull_request with no branch filter, so every push to a PR branch ran each workflow twice. Restrict the push trigger to master; pull_request already covers PR branches. --- .github/workflows/clang-format.yml | 5 ++++- .github/workflows/freebsd.yml | 5 ++++- .github/workflows/linux.yml | 5 ++++- .github/workflows/nix-tests.yml | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/clang-format.yml b/.github/workflows/clang-format.yml index 25f62c8e..690bbde5 100644 --- a/.github/workflows/clang-format.yml +++ b/.github/workflows/clang-format.yml @@ -1,6 +1,9 @@ name: clang-format -on: [push, pull_request] +on: + push: + branches: [master] + pull_request: concurrency: group: ${{ github.workflow }}-format-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index f6c96f95..977e514c 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -1,6 +1,9 @@ name: freebsd -on: [push, pull_request] +on: + push: + branches: [master] + pull_request: concurrency: group: ${{ github.workflow }}-freebsd-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index fb4ab451..87775d5a 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -1,6 +1,9 @@ name: linux -on: [push, pull_request] +on: + push: + branches: [master] + pull_request: concurrency: group: ${{ github.workflow }}-linux-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/nix-tests.yml b/.github/workflows/nix-tests.yml index c2193b99..cd6ea1e1 100644 --- a/.github/workflows/nix-tests.yml +++ b/.github/workflows/nix-tests.yml @@ -2,6 +2,7 @@ name: "Nix-Tests" on: pull_request: push: + branches: [master] concurrency: group: ${{ github.workflow }}-nix-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true From afa6ab1fd81220b8f4fa5c138c6b9e3c1987abc5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:07:12 +0200 Subject: [PATCH 352/416] fix(AModule): don't crash on click/scroll commands with literal braces handleUserEvent ran the configured command through fmt::format(fmt::runtime(...)) to substitute {x}/{y}. Commands containing literal braces that aren't {x}/{y} (e.g. `echo ${HOME}`, `awk '{print $1}'`, brace expansions) made libfmt throw fmt::format_error. Uncaught inside a GTK signal handler this aborts the whole bar. Only format when {x}/{y} is present and fall back to the raw command on failure. Fixes bar abort/std::terminate on on-click/on-scroll commands containing braces. --- src/AModule.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/AModule.cpp b/src/AModule.cpp index 83bafbcf..65ffb433 100644 --- a/src/AModule.cpp +++ b/src/AModule.cpp @@ -238,9 +238,23 @@ bool AModule::handleUserEvent(GdkEventButton* const& e) { if (!format.empty()) { const int width = gdk_window_get_width(e->window); const int height = gdk_window_get_height(e->window); - const std::string cmd = - fmt::format(fmt::runtime(format), fmt::arg("x", (int)round(100. * e->x / width)), - fmt::arg("y", (int)round(100. * e->y / height))); + // Substitute {x}/{y} with the click position. The configured command is + // arbitrary user input that may contain literal braces which are not {x}/{y} + // (e.g. `echo ${HOME}`, `awk '{print $1}'`, brace expansions). Those make + // libfmt throw fmt::format_error; since we run inside a GTK signal handler an + // uncaught exception aborts the whole bar. Only format when a placeholder is + // actually present, and fall back to the raw command if formatting throws. + std::string cmd = format; + if (format.find("{x}") != std::string::npos || format.find("{y}") != std::string::npos) { + try { + cmd = fmt::format(fmt::runtime(format), fmt::arg("x", (int)round(100. * e->x / width)), + fmt::arg("y", (int)round(100. * e->y / height))); + } catch (const fmt::format_error& err) { + spdlog::warn("Failed to format command '{}': {}. Running it unformatted.", format, + err.what()); + cmd = format; + } + } pid_children_.push_back(util::command::forkExec(cmd)); } dp.emit(); From 48db4aa36e85fe1e0d67ac4b4c9ef8e576d71e70 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:07:21 +0200 Subject: [PATCH 353/416] fix(bar): make disable-on-sleep DPMS suspend actually reach modules toggleSuspend dynamic_cast-ed the children of the left/center/right Gtk::Box. But modules are packed via AModule::operator Gtk::Widget&(), which returns the member event_box_, so every box child is a Gtk::EventBox and the cast is always null -- suspend()/resume() never ran, making disable-on-sleep a silent no-op. Iterate modules_all_ (the real module pointers) instead. Fixes disable-on-sleep DPMS suspend/resume never firing. --- src/bar.cpp | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/bar.cpp b/src/bar.cpp index cf284e0d..1eb5ce41 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -331,12 +331,14 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) * returned to the main loop, when any late initial configure has been dispatched and widgets have * had a chance to allocate/draw. */ - Glib::signal_idle().connect(sigc::track_obj([this] { - window.queue_resize(); - window.queue_draw(); - forceLayerCommit(); - return false; - }, *this)); + Glib::signal_idle().connect(sigc::track_obj( + [this] { + window.queue_resize(); + window.queue_draw(); + forceLayerCommit(); + return false; + }, + *this)); if (spdlog::should_log(spdlog::level::debug)) { // Unfortunately, this function isn't in the C++ bindings, so we have to call the C version. @@ -750,20 +752,18 @@ void waybar::Bar::onOutputGeometryChanged() { } void waybar::Bar::toggleSuspend(bool suspend) { - auto process_modules = [suspend](Gtk::Box& module_box) { - for (auto* widget : module_box.get_children()) { - auto* module = dynamic_cast(widget); - if (module && module->shouldSuspend()) { - if (suspend) { - module->suspend(); - } else { - module->resume(); - } + // Iterate the actual module objects. Modules are packed into the Gtk::Box via + // AModule::operator Gtk::Widget&(), which returns the member event_box_, so the + // box children are Gtk::EventBox, never AModule -- a dynamic_cast over them is + // always null and suspend()/resume() would never fire. modules_all_ holds the + // real module pointers (including group children), so use it instead. + for (auto const& module : modules_all_) { + if (module && module->shouldSuspend()) { + if (suspend) { + module->suspend(); + } else { + module->resume(); } } - }; - - process_modules(left_); - process_modules(center_); - process_modules(right_); + } } From 77734e9b02856b9195ee55e38eca627e6bcf0bc0 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:07:29 +0200 Subject: [PATCH 354/416] custom-graph: fix worker crash, JSON UTF-8 validation, SIGRTMIN guard Bring src/modules/custom_graph.cpp in line with the hardened custom.cpp: - continuousWorker: on the restart path, an open() failure threw std::runtime_error out of the SleeperThread lambda, which escaped the thread and called std::terminate, killing all of Waybar. Log the error and stop the worker gracefully instead of throwing. - parseOutputJson: validate/make_valid the text/alt/tooltip JSON string fields before they reach fmt markup / set_tooltip_markup. Invalid UTF-8 from a script otherwise aborts the bar in g_utf8_* (parseOutputRaw already validated the same way). - refresh: wrap the SIGRTMIN-based signal check in #ifdef SIGRTMIN so the module builds on platforms without SIGRTMIN (e.g. some BSDs). Fixes a std::terminate crash on continuous-exec restart failure, an invalid-UTF-8 bar abort via JSON output, and a build break on platforms lacking SIGRTMIN. --- src/modules/custom_graph.cpp | 38 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index 8b0c70af..06e45d06 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -99,7 +99,13 @@ void waybar::modules::CustomGraph::continuousWorker() { thread_.sleep_for(std::chrono::seconds(config_["restart-interval"].asUInt())); fp_ = util::command::open(cmd, pid_, output_name_); if (!fp_) { - throw std::runtime_error("Unable to open " + cmd); + // Letting this exception escape the SleeperThread would call + // std::terminate and kill all of Waybar. Degrade gracefully instead. + output_ = {1, ""}; + dp.emit(); + spdlog::error("Unable to restart {}: unable to open {}", name_, cmd); + thread_.stop(); + return; } } else { thread_.stop(); @@ -139,9 +145,11 @@ void waybar::modules::CustomGraph::waitingWorker() { } void waybar::modules::CustomGraph::refresh(int sig) { +#ifdef SIGRTMIN if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { thread_.wake_up(); } +#endif } void waybar::modules::CustomGraph::handleEvent() { @@ -268,22 +276,32 @@ void waybar::modules::CustomGraph::parseOutputJson() { std::istringstream output(output_.out); std::string line; class_.clear(); + // A script can emit invalid UTF-8; passing it unchecked to Pango/GTK aborts + // the whole bar in g_utf8_* (see parseOutputRaw, which validates the same way). + auto sanitize = [](const std::string& s) -> Glib::ustring { + Glib::ustring value = s; + if (!value.validate()) { + value = value.make_valid(); + } + return value; + }; while (getline(output, line)) { auto parsed = parser_.parse(line); - if (config_["escape"].isBool() && config_["escape"].asBool()) { - text_ = Glib::Markup::escape_text(parsed["text"].asString()); + const bool escape = config_["escape"].isBool() && config_["escape"].asBool(); + if (escape) { + text_ = Glib::Markup::escape_text(sanitize(parsed["text"].asString())); } else { - text_ = parsed["text"].asString(); + text_ = sanitize(parsed["text"].asString()); } - if (config_["escape"].isBool() && config_["escape"].asBool()) { - alt_ = Glib::Markup::escape_text(parsed["alt"].asString()); + if (escape) { + alt_ = Glib::Markup::escape_text(sanitize(parsed["alt"].asString())); } else { - alt_ = parsed["alt"].asString(); + alt_ = sanitize(parsed["alt"].asString()); } - if (config_["escape"].isBool() && config_["escape"].asBool()) { - tooltip_ = Glib::Markup::escape_text(parsed["tooltip"].asString()); + if (escape) { + tooltip_ = Glib::Markup::escape_text(sanitize(parsed["tooltip"].asString())); } else { - tooltip_ = parsed["tooltip"].asString(); + tooltip_ = sanitize(parsed["tooltip"].asString()); } if (parsed["class"].isString()) { class_.push_back(parsed["class"].asString()); From c16e7efa135e6516cd7321783a81370ee5cbe9cf Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:06:55 +0200 Subject: [PATCH 355/416] fix(niri): close the IPC socket fd once (ScopedFd owns it), not twice IPC::send() wrapped the socket fd in a util::ScopedFd, which closes the fd in its destructor. The input stream was created with close_fd=true, so the stream also closed the same fd, resulting in a double-close. In multithreaded Waybar another thread can open a new fd with the same number between the two close() calls, which the second close() then wrongly closes. Pass close_fd=false so ScopedFd is the sole owner and the fd is closed exactly once. The streams are declared after socketfd, so they flush and destruct while the fd is still open, then ScopedFd closes it. --- src/modules/niri/backend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/niri/backend.cpp b/src/modules/niri/backend.cpp index 460754cc..19eeada0 100644 --- a/src/modules/niri/backend.cpp +++ b/src/modules/niri/backend.cpp @@ -285,7 +285,7 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) { Json::Value IPC::send(const Json::Value& request) { util::ScopedFd socketfd(connectToSocket()); - auto unix_istream = Gio::UnixInputStream::create(socketfd, true); + auto unix_istream = Gio::UnixInputStream::create(socketfd, false); auto unix_ostream = Gio::UnixOutputStream::create(socketfd, false); auto istream = Gio::DataInputStream::create(unix_istream); auto ostream = Gio::DataOutputStream::create(unix_ostream); From 4764a62afc34d6662f77fcf122cc0b256e17d223 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:07:10 +0200 Subject: [PATCH 356/416] fix(wwan): null members after unref and guard destructor to avoid double-free The destructor unconditionally unref'd current_modem, manager and connection, but the constructor can leave them NULL or already-unref'd: - On the mm_manager_new_sync failure path the ctor unref'd connection without nulling it, so the dtor unref'd it a second time -> double-free. - On the g_bus_get_sync failure path all three stay NULL, and in the common no-WWAN-hardware case current_modem is NULL, so the dtor ran g_object_unref(NULL) -> G_IS_OBJECT assertion criticals. Use g_clear_object() in the failing ctor path (unref + null) and in the destructor (NULL-safe unref + null). Teardown is now safe for every ctor outcome (bus fail, MM fail, no modem, normal), and a normal run still unrefs each owned ref exactly once. --- src/modules/wwan.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/wwan.cpp b/src/modules/wwan.cpp index 86e68624..c741405d 100644 --- a/src/modules/wwan.cpp +++ b/src/modules/wwan.cpp @@ -43,7 +43,7 @@ waybar::modules::Wwan::Wwan(const std::string& id, const Json::Value& config) if (error) { spdlog::error("Failed to create ModemManager proxy: " + std::string(error->message)); g_error_free(error); - g_object_unref(connection); + g_clear_object(&connection); return; } @@ -302,7 +302,7 @@ auto waybar::modules::Wwan::update() -> void { } waybar::modules::Wwan::~Wwan() { - g_object_unref(current_modem); - g_object_unref(manager); - g_object_unref(connection); + g_clear_object(¤t_modem); + g_clear_object(&manager); + g_clear_object(&connection); } From 30dcd7a7ca70deb405a40a9d025c38d7f4b6d99e Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:09:35 +0200 Subject: [PATCH 357/416] fix(hyprland/workspaces): own debounce timer on main thread, fix UAF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The debounce timer added for flicker prevention was armed from the IPC listener thread via Glib::signal_timeout().connect(), while its timeout lambda and the m_updatePending flag ran on the GTK main thread — an unsynchronized cross-thread data race on GLib timer/main-loop state. Additionally ~Workspaces() never disconnected the timer, so a pending timeout could fire on a freed 'this' (use-after-free). Restore the pre-refactor threading model: onEvent now only mutates state under m_mutex on the IPC thread and calls dp.emit() (Glib::Dispatcher is thread-safe). The debounce timer is owned entirely by the main-thread update() path, which arms/re-arms it on each dispatch and coalesces bursts into a single refresh. ~Workspaces() disconnects the timer (guarded) so none outlives the object. Debounce behavior is preserved. --- include/modules/hyprland/workspaces.hpp | 3 +- src/modules/hyprland/workspaces.cpp | 43 ++++++++++++++----------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 4be7fd44..1237ef24 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -227,8 +227,9 @@ class Workspaces : public AModule, public EventHandler { sigc::connection m_scrollEventConnection_; IPC& m_ipc; + // Coalesces bursts of Hyprland events into a single UI refresh. Armed and + // disconnected only on the GTK main thread (see Workspaces::update). sigc::connection m_debounceTimer; - bool m_updatePending = false; }; } // namespace waybar::modules::hyprland diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 4cd0d2ae..202b892b 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -37,6 +37,11 @@ Workspaces::~Workspaces() { if (m_scrollEventConnection_.connected()) { m_scrollEventConnection_.disconnect(); } + // Cancel any pending debounce timeout so it cannot fire on a freed `this`. + // Runs on the main thread, same as where the timer is armed. + if (m_debounceTimer.connected()) { + m_debounceTimer.disconnect(); + } m_ipc.unregisterForIPC(this); // wait for possible event handler to finish std::lock_guard lg(m_mutex); @@ -332,23 +337,11 @@ void Workspaces::onEvent(const std::string& ev) { } } - if (m_debounceTimer.connected()) { - m_debounceTimer.disconnect(); - m_updatePending = false; - } - - m_updatePending = true; - m_debounceTimer = Glib::signal_timeout().connect( - [this]() { - if (!m_updatePending) return false; - std::lock_guard lock(m_mutex); - if (m_updatePending) { - dp.emit(); - m_updatePending = false; - } - return false; - }, - 7); + // Notify the main thread. dp (Glib::Dispatcher) is the only thread-safe way to + // hand off to the GTK main loop; GLib timer state must never be touched from the + // IPC listener thread. The debounce timer is owned entirely by the main-thread + // update() path (see Workspaces::update). + dp.emit(); } void Workspaces::onWorkspaceActivated(std::string const& payload) { @@ -1041,8 +1034,20 @@ void Workspaces::setUrgentWorkspace(std::string const& windowaddress) { } auto Workspaces::update() -> void { - doUpdate(); - AModule::update(); + // Debounce rapid events (e.g. out-of-order create/destroy workspace events from + // Hyprland) to prevent workspace button flicker. This runs on the GTK main thread + // (invoked via the dp dispatcher), so arming/disconnecting the GLib timer here is + // thread-safe. Each event re-arms the timer, coalescing bursts into one refresh. + if (m_debounceTimer.connected()) { + m_debounceTimer.disconnect(); + } + m_debounceTimer = Glib::signal_timeout().connect( + [this]() { + doUpdate(); + AModule::update(); + return false; + }, + 7); } void Workspaces::updateWindowCount() { From 74cf45d53017da6ab9ccf54b6740d488d2bfa977 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:11:59 +0200 Subject: [PATCH 358/416] fix(hyprland): detect Lua protocol without side-effecting dispatch isLuaProtocol() probed the protocol by sending a real "dispatch workspace __waybar_probe__". On Hyprland < 0.54 "workspace" is a valid dispatcher, so the probe actually switched the user to a junk workspace named __waybar_probe__ on the first workspace click/scroll. Detect the protocol with the read-only "version" IPC query instead: parse the numeric "version" field (falling back to the always-present "tag" field) and treat Hyprland >= 0.54 as Lua. This has no side effects. On any parse/query failure we log and fall back to the legacy protocol, preserving prior behavior for older versions. --- src/modules/hyprland/backend.cpp | 39 ++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/modules/hyprland/backend.cpp b/src/modules/hyprland/backend.cpp index f38f8216..891bed1a 100644 --- a/src/modules/hyprland/backend.cpp +++ b/src/modules/hyprland/backend.cpp @@ -297,11 +297,40 @@ bool IPC::isLuaProtocol() { return *s_luaProtocolDetected_; } - // Probe: send a harmless old-style dispatch and check the error. - // In Lua-based Hyprland (>= 0.54) the error contains "hl.dispatch". - // In older versions it returns "ok" or a different error. - auto reply = getSocket1Reply("dispatch workspace __waybar_probe__"); - bool luaProto = reply.find("hl.dispatch") != std::string::npos; + // Detect the Lua-based dispatch protocol (Hyprland >= 0.54) via the read-only + // "version" query. This MUST have no side effects: an earlier probe issued a real + // "dispatch workspace __waybar_probe__", which on Hyprland < 0.54 actually switched + // the user to a junk workspace named __waybar_probe__ on the first click/scroll. + bool luaProto = false; + try { + util::JsonParser parser; + const Json::Value ver = parser.parse(getSocket1Reply("j/version")); + + // Prefer the numeric "version" field ("0.54.0"); fall back to the "tag" field + // ("v0.54.0" or "v0.54.0-16-gdeadbee"), which is present on all releases. + std::string versionStr = ver["version"].asString(); + if (versionStr.empty()) { + versionStr = ver["tag"].asString(); + } + + const size_t firstDigit = versionStr.find_first_of("0123456789"); + if (firstDigit != std::string::npos) { + // std::stoi parses the leading integer and stops at the first non-digit, so it + // tolerates the trailing ".patch-commits-ghash" suffix on the tag. + const int major = std::stoi(versionStr.substr(firstDigit)); + int minor = 0; + const size_t dot = versionStr.find('.', firstDigit); + if (dot != std::string::npos && dot + 1 < versionStr.size()) { + minor = std::stoi(versionStr.substr(dot + 1)); + } + luaProto = major > 0 || (major == 0 && minor >= 54); + } else { + spdlog::warn("Hyprland IPC: could not parse version '{}', assuming legacy protocol", + versionStr); + } + } catch (const std::exception& e) { + spdlog::warn("Hyprland IPC: version detection failed ({}), assuming legacy protocol", e.what()); + } if (luaProto) { spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol (Hyprland >= 0.54)"); From 66139e4440b626e0d92c4b902185ced10ce78df7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:11:59 +0200 Subject: [PATCH 359/416] fix(tray): stop reorderItems from re-adding items (iterator UAF + double add) The item-ordering feature made Host::reorderItems() re-run the full remove/add path over items_ via std::ranges::for_each(on_remove_/on_add_). This caused two confirmed bugs: BUG 1 (iterator invalidation / UAF): on_add_ (Tray::onAdd) calls Host::checkIgnoreList, which erases from items_ while for_each is still iterating items_, invalidating iterators/pointers. Triggered by a non-empty ignore-list matching an item with >=2 items present. BUG 2 (double add): reorderItems runs while an item's Id is resolved in proxyReady, i.e. before setReady(). It added the not-yet-ready item (re-parenting its event_box, pushing into Tray::items_, connecting signal_show/hide), then setReady() -> itemReady -> onAdd added it again: GTK 'widget already has a parent' critical, duplicate Item* and signal handlers that accumulated unbounded. Fixes: - reorderItems() now only reorders already-added GTK box children via a dedicated on_reorder_ callback (Tray::reorderBox), never re-adding or removing. reorderBox stable-sorts items_ by order_ and repositions children with gtk_box_reorder_child (honouring reverse-direction). - Tray::onAdd is idempotent (guards against an already-added item) and positions the new widget via reorderBox before the ignore-list check. - signal_show/signal_hide connections are stored per item and disconnected in Tray::onRemove; onRemove is a no-op for items that were never added. --- include/modules/sni/host.hpp | 7 +++++- include/modules/sni/tray.hpp | 10 ++++++++ src/modules/sni/host.cpp | 17 ++++++++----- src/modules/sni/tray.cpp | 49 +++++++++++++++++++++++++++++++++--- 4 files changed, 72 insertions(+), 11 deletions(-) diff --git a/include/modules/sni/host.hpp b/include/modules/sni/host.hpp index 49771fac..4dad1f94 100644 --- a/include/modules/sni/host.hpp +++ b/include/modules/sni/host.hpp @@ -16,7 +16,8 @@ class Host { public: Host(std::size_t id, const Json::Value&, const Bar&, const std::vector&, const std::function&)>&, - const std::function&)>&, const std::function&); + const std::function&)>&, const std::function&, + const std::function&); ~Host(); void checkIgnoreList(const std::vector& ignore_list, @@ -55,6 +56,10 @@ class Host { const std::vector ignore_list_; const std::function&)> on_add_; const std::function&)> on_remove_; + // Re-applies the configured ordering to the already-added tray widgets. This + // must NOT re-run the add path (which would re-parent widgets and reconnect + // signals); it only reorders existing children. + const std::function on_reorder_; ItemOrderMap orders_; const std::function on_update_; diff --git a/include/modules/sni/tray.hpp b/include/modules/sni/tray.hpp index a2fd08eb..c3ac4e34 100644 --- a/include/modules/sni/tray.hpp +++ b/include/modules/sni/tray.hpp @@ -1,6 +1,10 @@ #pragma once #include +#include + +#include +#include #include "AModule.hpp" #include "bar.hpp" @@ -19,6 +23,9 @@ class Tray : public AModule { private: void onAdd(std::unique_ptr& item); void onRemove(std::unique_ptr& item); + // Reorders the already-added tray widgets by their configured order. Does not + // add or remove any widget. + void reorderBox(); void checkIgnoreList(std::unique_ptr* item); std::vector parseIgnoreList(const Json::Value& config); void queueUpdate(); @@ -29,6 +36,9 @@ class Tray : public AModule { std::vector ignore_list_; SNI::Host host_; std::vector items_; + // signal_show/signal_hide connections owned per added item, so they can be + // disconnected on removal instead of leaking and accumulating. + std::unordered_map> item_connections_; }; } // namespace waybar::modules::SNI diff --git a/src/modules/sni/host.cpp b/src/modules/sni/host.cpp index 272e05aa..c55c8f2e 100644 --- a/src/modules/sni/host.cpp +++ b/src/modules/sni/host.cpp @@ -16,7 +16,7 @@ Host::Host(std::size_t id, const Json::Value& config, const Bar& bar, const std::vector& ignore_list, const std::function&)>& on_add, const std::function&)>& on_remove, - const std::function& on_update) + const std::function& on_reorder, const std::function& on_update) : bus_name_("org.kde.StatusNotifierHost-" + std::to_string(getpid()) + "-" + std::to_string(id)), object_path_("/StatusNotifierHost/" + std::to_string(id)), @@ -27,6 +27,7 @@ Host::Host(std::size_t id, const Json::Value& config, const Bar& bar, ignore_list_(ignore_list), on_add_(on_add), on_remove_(on_remove), + on_reorder_(on_reorder), on_update_(on_update) { auto orders = config["orders"]; if (!orders.isNull()) { @@ -292,11 +293,15 @@ void Host::addRegisteredItem(const std::string& service) { } void Host::reorderItems() { - std::ranges::for_each(items_, on_remove_); - std::ranges::sort(items_, [](std::unique_ptr& item1, std::unique_ptr& item2) { - return item1->order_ < item2->order_; - }); - std::ranges::for_each(items_, on_add_); + // Re-apply the configured ordering to the tray. This is invoked while an + // item's Id/order is first resolved (from Item::setCustomIcon), which happens + // *before* the item is marked ready and added. It must therefore only reorder + // the widgets that have already been added; re-running the full add path here + // would (a) re-parent widgets and reconnect signals for every item and (b) + // mutate items_ from within checkIgnoreList while it is being iterated, + // invalidating iterators/pointers. Delegating to on_reorder_ keeps this to a + // pure reordering of existing children. + on_reorder_(); } } // namespace waybar::modules::SNI diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index f3e413a8..285c103f 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -38,7 +38,7 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) host_((initIconsConfig(config), nb_hosts_), config, bar, ignore_list_, std::bind(&Tray::onAdd, this, std::placeholders::_1), std::bind(&Tray::onRemove, this, std::placeholders::_1), - std::bind(&Tray::queueUpdate, this)) { + std::bind(&Tray::reorderBox, this), std::bind(&Tray::queueUpdate, this)) { box_.set_name("tray"); event_box_.add(box_); if (!id.empty()) { @@ -63,6 +63,16 @@ void Tray::onAdd(std::unique_ptr& item) { spdlog::info("Tray::onAdd - item bus_name='{}', category='{}', icon_name='{}', title='{}'", item->bus_name, item->category, item->icon_name, item->title); + // Idempotency guard: onAdd can be reached more than once for the same item + // (e.g. an item is processed while its Id/order is resolved and then again + // when it becomes ready). Re-adding would re-parent the event_box (GTK + // "widget already has a parent" critical), push a duplicate pointer into + // items_ and leak extra signal connections. + if (std::find(items_.begin(), items_.end(), item.get()) != items_.end()) { + spdlog::debug("Tray::onAdd - item already added, skipping"); + return; + } + if (config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool()) { box_.pack_end(item->event_box); } else { @@ -70,8 +80,13 @@ void Tray::onAdd(std::unique_ptr& item) { } items_.push_back(item.get()); - item->event_box.signal_show().connect([this] { dp.emit(); }); - item->event_box.signal_hide().connect([this] { dp.emit(); }); + auto show_conn = item->event_box.signal_show().connect([this] { dp.emit(); }); + auto hide_conn = item->event_box.signal_hide().connect([this] { dp.emit(); }); + item_connections_[item.get()] = {show_conn, hide_conn}; + + // Position the freshly added widget according to the configured order. This + // must happen before the ignore-list check below, which may erase `item`. + reorderBox(); // After this point `item` may be erased/invalidated by the ignore-list check; // do not touch it again below. @@ -82,11 +97,37 @@ void Tray::onAdd(std::unique_ptr& item) { } void Tray::onRemove(std::unique_ptr& item) { + // May be called for items that were never added (e.g. the ignore-list check + // runs over items that are not yet ready). Only touch state we actually own. + auto it = std::find(items_.begin(), items_.end(), item.get()); + if (it == items_.end()) { + return; + } + + auto conn_it = item_connections_.find(item.get()); + if (conn_it != item_connections_.end()) { + conn_it->second.first.disconnect(); + conn_it->second.second.disconnect(); + item_connections_.erase(conn_it); + } + box_.remove(item->event_box); - items_.erase(std::remove(items_.begin(), items_.end(), item.get()), items_.end()); + items_.erase(it); dp.emit(); } +void Tray::reorderBox() { + const bool reverse = + config_["reverse-direction"].isBool() && config_["reverse-direction"].asBool(); + // Stable sort keeps insertion order among items sharing the same order value. + std::stable_sort(items_.begin(), items_.end(), + [](const Item* a, const Item* b) { return a->order_ < b->order_; }); + for (std::size_t i = 0; i < items_.size(); ++i) { + const int pos = reverse ? static_cast(items_.size() - 1 - i) : static_cast(i); + box_.reorder_child(items_[i]->event_box, pos); + } +} + auto Tray::update() -> void { // Check if any items should be ignored now that properties have loaded if (!ignore_list_.empty()) { From 8db89cc289a84405c420bc7b066e46911c5cb5c2 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:07:45 +0200 Subject: [PATCH 360/416] fix(mango): use xkb brief for shortDescription in language module Remove leftover unconditional assignment that clobbered the value computed from rxkb_layout_get_brief() with short_name, which made short_description always equal short_name and defeated format- / {shortDescription}. --- src/modules/mango/language.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/mango/language.cpp b/src/modules/mango/language.cpp index 9415002a..549d22ed 100644 --- a/src/modules/mango/language.cpp +++ b/src/modules/mango/language.cpp @@ -110,8 +110,6 @@ Language::Layout Language::getLayout(const std::string& fullName) { short_description = short_name; } - short_description = short_name; - Layout info{desc, short_name, variant, short_description}; return info; } From 2f2479ca35eae795e5f2e5f721efa455164d7767 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:09:26 +0200 Subject: [PATCH 361/416] fix(mango): reconnect IPC event thread on disconnect The IPC event thread had no reconnect: on POLLHUP/POLLERR/POLLNVAL or read()==0/error it broke out of the loop and the thread exited permanently, freezing every mango module with stale content until Waybar was restarted. Wrap the connect + poll/read loop in a reconnect loop with a bounded 2s backoff, re-establishing the socket and resuming on disconnect, modeled on the niri backend. Add an atomic running_ flag so the worker exits cleanly on teardown; the destructor now sets it false before closing the socket so the worker breaks out and joins, and leaves the final close to the destructor to avoid a double close. --- include/modules/mango/backend.hpp | 2 + src/modules/mango/backend.cpp | 131 +++++++++++++++++++++--------- 2 files changed, 93 insertions(+), 40 deletions(-) diff --git a/include/modules/mango/backend.hpp b/include/modules/mango/backend.hpp index a1b0392e..7e4313f3 100644 --- a/include/modules/mango/backend.hpp +++ b/include/modules/mango/backend.hpp @@ -1,6 +1,7 @@ // include/modules/mango/backend.hpp #pragma once +#include #include #include #include @@ -52,6 +53,7 @@ class IPC { static Json::Value sendCommand(const std::string& cmd); + std::atomic running_ = true; int sockfd_ = -1; std::thread ipc_thread_; mutable std::mutex data_mutex_; diff --git a/src/modules/mango/backend.cpp b/src/modules/mango/backend.cpp index 5083a7fc..63aecb7b 100644 --- a/src/modules/mango/backend.cpp +++ b/src/modules/mango/backend.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -100,71 +101,121 @@ void IPC::sendAsync(const Json::Value& request) { IPC::IPC() : sockfd_(-1), active_client_(Json::nullValue) { startIPC(); } IPC::~IPC() { + running_ = false; if (sockfd_ != -1) close(sockfd_); if (ipc_thread_.joinable()) ipc_thread_.join(); } void IPC::startIPC() { + // Connect synchronously so a missing socket (this WM isn't the active + // compositor) throws here and lets the module constructor fail, instead of + // the module always attaching with a permanently empty widget. sockfd_ = IPC::connectToSocket(); ipc_thread_ = std::thread([this]() { spdlog::info("Mango IPC thread started"); - struct pollfd pfd; - pfd.fd = sockfd_; - pfd.events = POLLIN; - - const std::vector subs = {"watch all-monitors"}; - for (const auto& cmd : subs) { - if (write(sockfd_, cmd.c_str(), cmd.size()) != (ssize_t)cmd.size() || - write(sockfd_, "\n", 1) != 1) { - spdlog::error("Failed to subscribe to {}", cmd); - return; - } - } - char buf[4096]; std::string buffer; - while (true) { - int ret = poll(&pfd, 1, 1000); - if (ret == 0) continue; - if (ret < 0) { - if (errno == EINTR) continue; - spdlog::error("IPC poll error: {}", strerror(errno)); - break; - } + bool have_initial_fd = true; - if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { - spdlog::info("Mango IPC socket closed or invalid"); - break; + // Reconnect loop: if the event stream drops (POLLHUP/POLLERR, read()==0 or + // an error) we back off briefly and re-establish the socket instead of + // leaving every mango module frozen forever with stale content. + while (running_) { + if (!have_initial_fd) { + try { + sockfd_ = IPC::connectToSocket(); + } catch (const std::exception& e) { + spdlog::error("Mango IPC: failed to reconnect: {}", e.what()); + std::this_thread::sleep_for(std::chrono::seconds(2)); + continue; + } } + have_initial_fd = false; - if (pfd.revents & POLLIN) { - ssize_t n = read(sockfd_, buf, sizeof(buf)); - if (n == 0) { - spdlog::info("Mango IPC connection closed"); + bool subscribed = true; + const std::vector subs = {"watch all-monitors"}; + for (const auto& cmd : subs) { + if (write(sockfd_, cmd.c_str(), cmd.size()) != (ssize_t)cmd.size() || + write(sockfd_, "\n", 1) != 1) { + spdlog::error("Failed to subscribe to {}", cmd); + subscribed = false; break; } - if (n < 0) { + } + if (!subscribed) { + if (sockfd_ != -1) { + close(sockfd_); + sockfd_ = -1; + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + continue; + } + + struct pollfd pfd; + pfd.fd = sockfd_; + pfd.events = POLLIN; + buffer.clear(); + + bool connected = true; + while (running_ && connected) { + int ret = poll(&pfd, 1, 1000); + if (ret == 0) continue; + if (ret < 0) { if (errno == EINTR) continue; - spdlog::error("IPC read error: {}", strerror(errno)); + spdlog::error("IPC poll error: {}", strerror(errno)); + connected = false; break; } - buffer.append(buf, n); - size_t pos; - while ((pos = buffer.find('\n')) != std::string::npos) { - std::string line = buffer.substr(0, pos); - buffer.erase(0, pos + 1); - if (line.empty()) continue; - try { - parseIPC(line); - } catch (const std::exception& e) { - spdlog::warn("Failed to parse IPC line: {} - {}", line, e.what()); + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) { + spdlog::info("Mango IPC socket closed or invalid"); + connected = false; + break; + } + + if (pfd.revents & POLLIN) { + ssize_t n = read(sockfd_, buf, sizeof(buf)); + if (n == 0) { + spdlog::info("Mango IPC connection closed"); + connected = false; + break; + } + if (n < 0) { + if (errno == EINTR) continue; + spdlog::error("IPC read error: {}", strerror(errno)); + connected = false; + break; + } + buffer.append(buf, n); + + size_t pos; + while ((pos = buffer.find('\n')) != std::string::npos) { + std::string line = buffer.substr(0, pos); + buffer.erase(0, pos + 1); + if (line.empty()) continue; + try { + parseIPC(line); + } catch (const std::exception& e) { + spdlog::warn("Failed to parse IPC line: {} - {}", line, e.what()); + } } } } + + // On shutdown leave the socket for the destructor to close (avoids a + // double close); on a genuine disconnect close it before reconnecting. + if (!running_) break; + if (sockfd_ != -1) { + close(sockfd_); + sockfd_ = -1; + } + spdlog::warn("Mango IPC: event stream closed, reconnecting"); + std::this_thread::sleep_for(std::chrono::seconds(2)); } + + spdlog::info("Mango IPC thread stopping"); }); } From 892ab479ba3c4897ed3f0532413c76f30a7b91a1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:10 +0200 Subject: [PATCH 362/416] group: disconnect pending reveal timeout in destructor to fix UAF With reveal-delay set, handleMouseEnter arms a Glib::signal_timeout that captures 'this'. sigc::connection's destructor does not remove the GLib source, so a Group destroyed with a pending reveal timer would fire the timeout on freed memory. Add a destructor that disconnects reveal_timeout_. --- include/group.hpp | 2 +- src/group.cpp | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/include/group.hpp b/include/group.hpp index 0e981d0d..1e2a5d55 100644 --- a/include/group.hpp +++ b/include/group.hpp @@ -14,7 +14,7 @@ class Group : public AModule { public: Group(const std::string&, const std::string&, const Json::Value&, bool); - ~Group() override = default; + ~Group() override; auto update() -> void override; operator Gtk::Widget&() override; diff --git a/src/group.cpp b/src/group.cpp index 9f11661f..5fa04884 100644 --- a/src/group.cpp +++ b/src/group.cpp @@ -105,6 +105,14 @@ Group::Group(const std::string& name, const std::string& id, const Json::Value& event_box_.add(box); } +Group::~Group() { + // Disconnect any pending reveal timeout so it cannot fire on a destroyed + // instance (the GLib source is not removed by sigc::connection's destructor). + if (reveal_timeout_.connected()) { + reveal_timeout_.disconnect(); + } +} + void Group::show_group() { box.set_state_flags(Gtk::StateFlags::STATE_FLAG_PRELIGHT); revealer.set_reveal_child(true); From 8441d5e124658ff226cfed6ac640832d82f816a5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:10 +0200 Subject: [PATCH 363/416] config: harden output-dimensions parsing against malformed values The parser assumed each entry was ' '. An entry with no space caused str.substr((size_t)-1) to throw out_of_range, and a non-integer value made std::stoi throw invalid_argument, failing the whole bar on that output. Validate spaces with find()!=npos and wrap stoi in try/catch; log a warning and skip malformed entries instead of throwing. --- src/config.cpp | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index ad136339..800b6b02 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -240,12 +240,33 @@ bool isValidOutput(const Json::Value& config, const std::string& name, continue; } std::string str = config_output_dimension.asString(); - int i = str.find(" "); - std::string dimension = str.substr(0, i); - str = str.substr(i + 1); - i = str.find(" "); - std::string comparator = str.substr(0, i); - int value = std::stoi(str.substr(i)); + auto first_space = str.find(' '); + if (first_space == std::string::npos) { + spdlog::warn( + "Ignoring malformed 'output-dimensions' entry (expected ' " + "'): '{}'", + str); + continue; + } + std::string dimension = str.substr(0, first_space); + str = str.substr(first_space + 1); + auto second_space = str.find(' '); + if (second_space == std::string::npos) { + spdlog::warn( + "Ignoring malformed 'output-dimensions' entry (expected ' " + "'): '{}'", + config_output_dimension.asString()); + continue; + } + std::string comparator = str.substr(0, second_space); + int value; + try { + value = std::stoi(str.substr(second_space + 1)); + } catch (const std::exception& e) { + spdlog::warn("Ignoring 'output-dimensions' entry with non-integer value: '{}'", + config_output_dimension.asString()); + continue; + } int comparison_value; if (dimension == "height") { From 91ca603b04cdb95a330ae054f049dc32578ab325 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:10 +0200 Subject: [PATCH 364/416] keyboard_state: close fd when openDevice throws to fix fd leak openDevice() throws without closing the fd if libevdev_new_from_fd fails. In both update() and tryAddDevice() the outer catch only logged, so closeFile(fd) was never reached and a descriptor leaked on every failing tick. Guard openDevice with a try/catch that closes the fd before rethrowing. --- src/modules/keyboard_state.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/modules/keyboard_state.cpp b/src/modules/keyboard_state.cpp index 87a843c4..fa80ef64 100644 --- a/src/modules/keyboard_state.cpp +++ b/src/modules/keyboard_state.cpp @@ -320,7 +320,14 @@ auto waybar::modules::KeyboardState::update() -> void { for (const auto& dev_path : dev_paths) { try { int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); - auto dev = openDevice(fd); + libevdev* dev; + try { + dev = openDevice(fd); + } catch (...) { + // openDevice does not close the fd if libevdev_new_from_fd fails. + closeFile(fd); + throw; + } numl |= libevdev_get_event_value(dev, EV_LED, LED_NUML); capsl |= libevdev_get_event_value(dev, EV_LED, LED_CAPSL); scrolll |= libevdev_get_event_value(dev, EV_LED, LED_SCROLLL); @@ -376,7 +383,14 @@ auto waybar::modules::KeyboardState::update() -> void { auto waybar::modules ::KeyboardState::tryAddDevice(const std::string& dev_path) -> void { try { int fd = openFile(dev_path, O_NONBLOCK | O_CLOEXEC | O_RDONLY); - auto dev = openDevice(fd); + libevdev* dev; + try { + dev = openDevice(fd); + } catch (...) { + // openDevice does not close the fd if libevdev_new_from_fd fails. + closeFile(fd); + throw; + } if (supportsLockStates(dev)) { spdlog::info("Found device {} at '{}'", libevdev_get_name(dev), dev_path); std::lock_guard lock(devices_mutex_); From c9012c4107204301f473620606a0931e7e1d95a1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:10 +0200 Subject: [PATCH 365/416] sway/window: escape window title in tooltip markup setTooltipMarkup uses set_tooltip_markup without escaping. Raw window titles routinely contain &, < and >, which break Pango markup parsing and the tooltip. Escape the title with Glib::Markup::escape_text before passing it. --- src/modules/sway/window.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/sway/window.cpp b/src/modules/sway/window.cpp index f005bd14..4faa7d77 100644 --- a/src/modules/sway/window.cpp +++ b/src/modules/sway/window.cpp @@ -101,7 +101,7 @@ auto Window::update() -> void { fmt::arg("shell", shell_), fmt::arg("marks", marks_)), config_["rewrite"])); if (tooltipEnabled()) { - setTooltipMarkup(window_); + setTooltipMarkup(Glib::Markup::escape_text(window_)); } // Resolve the app icon on the main thread to avoid racing with GTK draw on the From b17743ff6a5b54b7b6c4cc38218289deb8bdb202 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:10 +0200 Subject: [PATCH 366/416] network: treat negative/failed link speed as 0 /sys/class/net//speed reports -1 with no carrier. Reading it into a uint32_t wrapped to 4294967295 (without setting failbit), so {linkSpeed} showed an absurd value. Read into int64_t, check fail(), and treat negative or failed reads as 0. --- src/modules/network.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 7b4dc15f..72a367fc 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -90,13 +90,16 @@ uint32_t waybar::modules::Network::readLinkSpeed() const { if (!sysfs_speed) return 0; - uint32_t speed; + // Read into a signed type: /sys/class/net//speed reports -1 when there is + // no carrier. Extracting -1 into an unsigned type would wrap to a huge value + // (and would not set failbit), so use a signed type and validate the result. + int64_t speed = 0; sysfs_speed >> speed; - if (sysfs_speed.bad()) // read fails on incompatible devices + if (sysfs_speed.fail() || speed < 0) // read fails on incompatible devices return 0; - return speed; + return static_cast(speed); } waybar::modules::Network::Network(const std::string& id, const Json::Value& config) From 88064137ca8f5259dda800d784ccb9e2bb2f65b7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:26 +0200 Subject: [PATCH 367/416] fix(mpris): clear stale GError in playerctld selection loop When playerctl_player_new_from_name() fails for a candidate player, the loop continued without clearing the GError. The stale non-NULL error then leaked into the next GLib call (GLib-CRITICAL assertion) and made the post-loop 'if (error) goto errorexit' fire even when a valid playing player had been selected, blanking the whole module. Clear the error at the discard point with g_clear_error(). --- src/modules/mpris/mpris.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index 387e0597..d70f5902 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -491,7 +491,13 @@ auto Mpris::getPlayerInfo() -> std::optional { continue; } auto* tmp = playerctl_player_new_from_name(pn, &error); - if (error || !tmp) continue; + if (error || !tmp) { + // Discard any error from this candidate so it doesn't leak into the next + // playerctl_player_new_from_name() call or the post-loop metadata calls, which + // assert that the passed GError is NULL (otherwise: GLib-CRITICAL / spurious errorexit). + g_clear_error(&error); + continue; + } if (!first_valid_player) { first_valid_player = tmp; first_valid_name = name; From 34522b4ccdc60bd91bff543505ac44b5c4bff816 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:33 +0200 Subject: [PATCH 368/416] fix(clock): guard tooltip formatting against unsupported specifiers Only the label vformat was wrapped in try/catch. An unsupported specifier (e.g. %-I / %OI) in tooltip-format or the calendar format still threw out of update() every tick via the calendar/tooltip vformat calls. Wrap the tooltip-building section in try/catch that warns once and skips the tooltip for that tick instead of letting the exception escape update(). --- src/modules/clock.cpp | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/modules/clock.cpp b/src/modules/clock.cpp index 38dec4b2..bd0c828d 100644 --- a/src/modules/clock.cpp +++ b/src/modules/clock.cpp @@ -220,20 +220,35 @@ auto waybar::modules::Clock::update() -> void { if (tzInTooltip_) tzText_ = getTZtext(now.get_sys_time()); if (cldInTooltip_) cldText_ = get_calendar(today, shiftedDay, tz); if (ordInTooltip_) ordText_ = get_ordinal_date(shiftedDay); - if (tzInTooltip_ || cldInTooltip_ || ordInTooltip_) { - // std::vformat doesn't support named arguments. - m_tlpText_ = - std::regex_replace(m_tlpFmt_, std::regex("\\{" + kTZPlaceholder + "\\}"), tzText_); - m_tlpText_ = std::regex_replace( - m_tlpText_, std::regex("\\{" + kCldPlaceholder + "\\}"), - fmt_lib::vformat(m_locale_, cldText_, fmt_lib::make_format_args(shiftedNow))); - m_tlpText_ = - std::regex_replace(m_tlpText_, std::regex("\\{" + kOrdPlaceholder + "\\}"), ordText_); - } else { - m_tlpText_ = m_tlpFmt_; - } + try { + if (tzInTooltip_ || cldInTooltip_ || ordInTooltip_) { + // std::vformat doesn't support named arguments. + m_tlpText_ = + std::regex_replace(m_tlpFmt_, std::regex("\\{" + kTZPlaceholder + "\\}"), tzText_); + m_tlpText_ = std::regex_replace( + m_tlpText_, std::regex("\\{" + kCldPlaceholder + "\\}"), + fmt_lib::vformat(m_locale_, cldText_, fmt_lib::make_format_args(shiftedNow))); + m_tlpText_ = + std::regex_replace(m_tlpText_, std::regex("\\{" + kOrdPlaceholder + "\\}"), ordText_); + } else { + m_tlpText_ = m_tlpFmt_; + } - m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(now)); + m_tlpText_ = fmt_lib::vformat(m_locale_, m_tlpText_, fmt_lib::make_format_args(now)); + } catch (const std::exception& e) { + // An unsupported/invalid specifier (e.g. %-I / %OI) in the tooltip-format or the + // calendar format must not take the whole module down every tick. Warn once and skip + // the tooltip for this update so the bar keeps working. + static bool tlpWarned = false; + if (!tlpWarned) { + spdlog::warn( + "Clock: could not format tooltip \"{}\": {}. Skipping tooltip; check your " + "tooltip-format/calendar format specifiers.", + m_tlpFmt_, e.what()); + tlpWarned = true; + } + m_tlpText_.clear(); + } // Pango doesn't support CSS classes but to continue using it while staying // backwards compatible this approach uses post-posting to replace fake From fc6a567974b61481334d034c8dbcc4b0a23702e7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:10:38 +0200 Subject: [PATCH 369/416] fix(systemd-failed-units): guard label/tooltip format against bad config A malformed user format or tooltip-format (unknown {placeholder}) made fmt::format throw fmt::format_error out of update(). Wrap the label and tooltip format calls in try/catch that warn once and fall back to a safe label / skip the tooltip instead of taking the module down. --- src/modules/systemd_failed_units.cpp | 43 +++++++++++++++++++++------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index faa1ca7e..3804cbe2 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -281,21 +281,42 @@ auto SystemdFailedUnits::update() -> void { last_status_ = overall_state_; - setLabelMarkup(fmt::format( - fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_), - fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_), - fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_), - fmt::arg("overall_state", overall_state_))); + // A malformed user format/tooltip-format (e.g. an unknown {placeholder}) makes fmt throw a + // fmt::format_error; catch it so a bad config warns once instead of taking update() down. + try { + setLabelMarkup(fmt::format( + fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_), + fmt::arg("nr_failed_system", nr_failed_system_), + fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), + fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_))); + } catch (const std::exception& e) { + static bool labelWarned = false; + if (!labelWarned) { + spdlog::warn("systemd-failed-units: invalid format, using fallback: {}", e.what()); + labelWarned = true; + } + setLabelMarkup(fmt::format("{} failed", nr_failed_)); + } if (tooltipEnabled()) { std::string failed_list = BuildTooltipFailedList(); auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_; if (!tooltip_template.empty()) { - setTooltipMarkup(fmt::format( - fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_), - fmt::arg("nr_failed_system", nr_failed_system_), - fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), - fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_), - fmt::arg("failed_units_list", failed_list))); + try { + setTooltipMarkup(fmt::format( + fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_), + fmt::arg("nr_failed_system", nr_failed_system_), + fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_), + fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_), + fmt::arg("failed_units_list", failed_list))); + } catch (const std::exception& e) { + static bool tooltipWarned = false; + if (!tooltipWarned) { + spdlog::warn("systemd-failed-units: invalid tooltip-format, skipping tooltip: {}", + e.what()); + tooltipWarned = true; + } + setTooltipMarkup(""); + } } else { setTooltipMarkup(""); } From 9b093c53e90cd194c3da8fe89d4105042bb98fad Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:19:51 +0200 Subject: [PATCH 370/416] fix(wireplumber): read gboolean into a gboolean, not a 1-byte bool (OOB write) g_variant_lookup with the "b" format writes a gboolean (gint, 4 bytes), but muted_ and source_muted_ are C++ bool members (1 byte). Passing their addresses caused a 3-byte out-of-bounds write past the member (undefined behavior). Read into a gboolean temporary and assign back to the bool, preserving the prior value when "mute" is absent. --- src/modules/wireplumber.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/modules/wireplumber.cpp b/src/modules/wireplumber.cpp index 8f9bd961..ddcdda6e 100644 --- a/src/modules/wireplumber.cpp +++ b/src/modules/wireplumber.cpp @@ -304,7 +304,12 @@ void waybar::modules::Wireplumber::updateVolume(waybar::modules::Wireplumber* se g_variant_lookup(variant, "volume", "d", &self->volume_); g_variant_lookup(variant, "step", "d", &self->min_step_); - g_variant_lookup(variant, "mute", "b", &self->muted_); + // GVariant "b" writes a gboolean (4 bytes); reading directly into the 1-byte bool member is an + // out-of-bounds write. Read into a gboolean temporary and assign back. + gboolean mute = FALSE; + if (g_variant_lookup(variant, "mute", "b", &mute)) { + self->muted_ = mute; + } g_clear_pointer(&variant, g_variant_unref); self->dp.emit(); @@ -329,7 +334,11 @@ void waybar::modules::Wireplumber::updateSourceVolume(waybar::modules::Wireplumb } g_variant_lookup(variant, "volume", "d", &self->source_volume_); - g_variant_lookup(variant, "mute", "b", &self->source_muted_); + // See updateVolume: GVariant "b" writes a gboolean (4 bytes), not a 1-byte bool. + gboolean mute = FALSE; + if (g_variant_lookup(variant, "mute", "b", &mute)) { + self->source_muted_ = mute; + } g_clear_pointer(&variant, g_variant_unref); self->dp.emit(); From 3f77c078750ba37bc589b167c903ae2e0deee33d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:20:41 +0200 Subject: [PATCH 371/416] fix(image): don't block the main loop with exec in the multiple-image path MultipleImageStrategy::update() and handleClick() ran util::command::exec (a blocking fork+exec+read) on the GTK main thread, so the whole bar froze for the script's duration on every interval and on every click. Move the exec into a new IStrategy::fetch() hook that the SleeperThread worker runs before dp.emit(); update() now only parses the cached output and draws on the main thread (mirroring how custom.cpp separates exec from formatting). handleClick() uses forkExec() so clicks fire-and-forget instead of blocking on the command's output. The entries and single-image paths are unchanged. --- include/modules/image.hpp | 6 ++++++ src/modules/image.cpp | 23 +++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/include/modules/image.hpp b/include/modules/image.hpp index 035dc4df..5b4ac818 100644 --- a/include/modules/image.hpp +++ b/include/modules/image.hpp @@ -19,6 +19,9 @@ namespace image { class IStrategy { public: virtual ~IStrategy() = default; + // Runs on the worker thread before update(). Use it for blocking work (e.g. + // spawning a user script) so the GTK main loop isn't stalled. Default no-op. + virtual void fetch() {} virtual void update() = 0; }; @@ -46,6 +49,7 @@ class MultipleImageStrategy : public IStrategy { public: MultipleImageStrategy(const std::string&, const Json::Value&, const std::string&, Gtk::EventBox&); ~MultipleImageStrategy() override = default; + void fetch() override; void update() override; private: @@ -67,6 +71,8 @@ class MultipleImageStrategy : public IStrategy { int size_; Gtk::Box box_; std::vector images_data_; + // stdout captured by fetch() on the worker thread and consumed by update() + std::string exec_output_; }; } // namespace image diff --git a/src/modules/image.cpp b/src/modules/image.cpp index d762f1eb..8c8132d7 100644 --- a/src/modules/image.cpp +++ b/src/modules/image.cpp @@ -45,6 +45,9 @@ auto waybar::modules::Image::getStrategy(const std::string& id, const Json::Valu void waybar::modules::Image::delayWorker() { thread_ = [this] { + // Do the blocking work (e.g. running a user script) here on the worker + // thread; update() then only parses the result and draws on the main thread. + strategy_->fetch(); dp.emit(); thread_.sleep_for(interval_); }; @@ -82,6 +85,15 @@ MultipleImageStrategy::MultipleImageStrategy(const std::string& id, const Json:: } } +void MultipleImageStrategy::fetch() { + // Run the (blocking) user script off the GTK main thread so the bar doesn't + // freeze for the script's duration on every interval. update() consumes the + // captured output. The static "entries" path takes priority and needs no exec. + if (config_["entries"].empty() && !config_["exec"].empty()) { + exec_output_ = util::command::exec(config_["exec"].asString(), "").out; + } +} + void MultipleImageStrategy::update() { // spdlog::info("update function run"); @@ -94,12 +106,12 @@ void MultipleImageStrategy::update() { if (!config_["entries"].empty()) { setImagesData(config_["entries"]); } else if (!config_["exec"].empty()) { - auto exec = util::command::exec(config_["exec"].asString(), ""); + // exec output was captured by fetch() on the worker thread Json::Value as_json; Json::Reader reader; - if (!reader.parse(exec.out, as_json)) { - spdlog::error("invalid json from exec {}", exec.out); + if (!reader.parse(exec_output_, as_json)) { + spdlog::error("invalid json from exec {}", exec_output_); return; } @@ -211,9 +223,8 @@ void MultipleImageStrategy::resetBoxAndMemory() { } void MultipleImageStrategy::handleClick(const Glib::ustring& data) { - auto msg = std::string(data); - - auto exec = util::command::exec(data, ""); + // Fire-and-forget: don't block the main loop waiting on the command's output. + util::command::forkExec(data); } SingleImageStrategy::SingleImageStrategy(const std::string& id, const Json::Value& config, From eb10a511f0d4266e7a3e81930169c81a3e8b0de5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 09:48:22 +0200 Subject: [PATCH 372/416] ci(smoke): sanitizer build + module/interaction/a11y/layout coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turn the smoke test into a real runtime safety net. Waybar is now built with AddressSanitizer and exercised end to end in a headless compositor: - Tier 0: run everything under ASan; fail on ASan reports and Gtk/GLib criticals in the log (lib.sh assert_clean) - Tier 1: per-module render matrix (modules.sh) — each headless-safe module rendered in isolation - Tier 2: pointer interaction (interact.sh) — inject clicks via sway, assert on-click side effect and format-alt toggle; AT-SPI assertions (a11y.sh/.py) check module labels semantically instead of by pixels - Tier 3: layout matrix (positions.sh) — top/bottom/left + HiDPI scale 2; second compositor run under labwc Shared helpers extracted to lib.sh. Adds a workflow_dispatch `bless` input to regenerate the golden reference in one click. Trigger push only on master to avoid duplicate PR runs. AT-SPI and labwc steps are continue-on-error. --- .github/workflows/smoke.yml | 97 ++++++++++++++++------- test/smoke/README.md | 55 ++++++++----- test/smoke/a11y.py | 58 ++++++++++++++ test/smoke/a11y.sh | 18 +++++ test/smoke/interact.sh | 81 +++++++++++++++++++ test/smoke/lib.sh | 136 ++++++++++++++++++++++++++++++++ test/smoke/modules-config.jsonc | 18 ----- test/smoke/modules.sh | 59 ++++++++++++++ test/smoke/positions.sh | 54 +++++++++++++ test/smoke/run.sh | 83 ++++--------------- 10 files changed, 524 insertions(+), 135 deletions(-) create mode 100644 test/smoke/a11y.py create mode 100755 test/smoke/a11y.sh create mode 100755 test/smoke/interact.sh create mode 100755 test/smoke/lib.sh delete mode 100644 test/smoke/modules-config.jsonc create mode 100755 test/smoke/modules.sh create mode 100755 test/smoke/positions.sh diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 439d8aae..8de5ffe6 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -1,15 +1,31 @@ name: smoke -# Launch the real Waybar binary inside a headless sway compositor and verify it -# starts, renders, and matches a reference screenshot. Complements the unit +# Launch the real Waybar binary (built with AddressSanitizer) inside a headless +# compositor and exercise it: render, per-module matrix, pointer interaction, +# accessibility-tree assertions, and a layout matrix. Complements the unit # tests and build jobs, which never actually run the bar. -on: [push, pull_request] +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + inputs: + bless: + description: "Regenerate and commit the golden reference screenshot" + type: boolean + default: false + +permissions: + contents: write concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + WAYBAR_BIN: ${{ github.workspace }}/build/waybar + jobs: smoke: runs-on: ubuntu-latest @@ -20,7 +36,8 @@ jobs: run: | sudo apt-get update sudo apt-get install --no-install-recommends -y \ - sway grim imagemagick fonts-dejavu-core \ + sway labwc grim imagemagick fonts-dejavu-core jq \ + at-spi2-core python3-pyatspi dbus-x11 \ meson ninja-build g++ pkg-config scdoc wayland-protocols \ gobject-introspection libdbusmenu-gtk3-dev libevdev-dev libfmt-dev \ libgirepository1.0-dev libgtk-3-dev libgtkmm-3.0-dev libinput-dev \ @@ -28,53 +45,77 @@ jobs: libpulse-dev libsigc++-2.0-dev libspdlog-dev libwayland-dev \ upower libxkbregistry-dev libxkbcommon-dev - - name: Build & install Waybar + - name: Build Waybar with AddressSanitizer run: | - meson setup build -Dman-pages=disabled + meson setup build -Dman-pages=disabled -Db_sanitize=address ninja -C build - sudo ninja -C build install - # --- Level 1: smoke with real modules --- - - name: Smoke — real modules load without crashing - run: ./test/smoke/run.sh test/smoke/modules-config.jsonc - - # --- Level 1 + 2: launch deterministic config and screenshot --- - - name: Launch + screenshot (deterministic config) + # --- Tier 0 + 2: render + sanitizer/critical gate --- + - name: Render deterministic config run: ./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css bar.png - # --- Level 2: the bar actually rendered something --- - name: Check the bar is not blank run: | sd=$(convert bar.png -crop 1920x30+0+0 +repage -colorspace Gray \ -format '%[fx:standard_deviation]' info:) echo "bar strip stddev = $sd" - awk "BEGIN{ exit !($sd > 0.01) }" || { - echo "::error::the top bar strip looks blank — Waybar rendered nothing" - exit 1 - } + awk "BEGIN{ exit !($sd > 0.01) }" || { echo "::error::bar looks blank"; exit 1; } - # --- Level 3: golden comparison against the committed reference --- - - name: Compare against reference screenshot + # --- Manual re-bless of the reference image --- + - name: Bless reference screenshot + if: github.event_name == 'workflow_dispatch' && inputs.bless + run: | + cp bar.png test/smoke/reference.png + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add test/smoke/reference.png + git commit -m "ci(smoke): re-bless reference screenshot [skip ci]" + git push + + - name: Golden comparison run: | if [ -f test/smoke/reference.png ]; then ae=$(compare -metric AE -fuzz 8% test/smoke/reference.png bar.png diff.png 2>&1 || true) ae=$(printf '%s' "$ae" | grep -oE '^[0-9]+' || echo 0) echo "differing pixels (fuzz 8%): $ae" - if [ "$ae" -gt 800 ]; then - echo "::error::visual regression: $ae pixels differ from the reference (threshold 800). See the 'diff.png' artifact." - exit 1 - fi - echo "✓ within tolerance" + [ "$ae" -gt 800 ] && { echo "::error::visual regression: $ae px (see diff.png artifact)"; exit 1; } || echo "✓ within tolerance" else - echo "::warning::No reference image yet. Download the 'waybar-screenshot' artifact from this run and commit bar.png as test/smoke/reference.png to enable golden comparison." + echo "::warning::No reference image; run this workflow via 'Run workflow' with bless=true to create it." fi - - name: Upload screenshots + # --- Tier 1: per-module matrix --- + - name: Per-module render matrix + run: ./test/smoke/modules.sh module-shots + + # --- Tier 2: pointer interaction (best-effort) --- + # Headless seats may not expose a pointer capability, so injected clicks + # can be dropped; keep this non-blocking while it is hardened. + - name: Interaction (click + format-alt) + continue-on-error: true + run: ./test/smoke/interact.sh interaction-shots + + # --- Tier 2: accessibility assertions --- + - name: Accessibility (AT-SPI) assertions + run: dbus-run-session -- ./test/smoke/a11y.sh + + # --- Tier 3: layout matrix (positions + HiDPI) --- + - name: Layout matrix (top/bottom/left + scale 2) + run: ./test/smoke/positions.sh layout-shots + + # --- Tier 3: second compositor --- + - name: Launch under labwc + run: COMPOSITOR=labwc ./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css labwc.png + + - name: Upload screenshots & diffs uses: actions/upload-artifact@v4 if: always() with: - name: waybar-screenshot + name: waybar-smoke path: | bar.png diff.png + labwc.png + module-shots/ + interaction-shots/ + layout-shots/ if-no-files-found: warn diff --git a/test/smoke/README.md b/test/smoke/README.md index f181e793..60800362 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -1,34 +1,49 @@ -# Smoke / screenshot test +# Smoke / render / interaction tests -The [`smoke`](../../.github/workflows/smoke.yml) workflow launches the real -Waybar binary inside a **headless sway** compositor (software-rendered, no GPU) -and checks three things: +The [`smoke`](../../.github/workflows/smoke.yml) workflow builds Waybar **with +AddressSanitizer** and runs the real binary inside a **headless** compositor +(software-rendered, no GPU) to exercise it end to end. All helpers share +[`lib.sh`](lib.sh). -1. **It starts and stays alive** — with real modules (`clock`, `cpu`, `memory`, - `disk`) loaded, Waybar must not crash or log a fatal error. - ([`modules-config.jsonc`](modules-config.jsonc)) -2. **It renders** — a deterministic config ([`config.jsonc`](config.jsonc) + - [`style.css`](style.css)) is captured with `grim`; the bar strip must not be - blank. -3. **It looks right** — the screenshot is compared to `reference.png` with an 8% - fuzz and an 800-pixel tolerance. Regressions fail the job, and the screenshot - and diff are uploaded as artifacts. +## What it checks + +| Tier | Check | Script | +| --- | --- | --- | +| 0 | Launches, stays alive, and logs **no ASan / `Gtk-CRITICAL` / crash** — for every launch below | [`lib.sh`](lib.sh) `assert_clean` | +| 1 | **Renders** — deterministic config screenshotted with `grim`, bar strip not blank | [`run.sh`](run.sh) | +| 1 | **Per-module matrix** — each headless-safe module rendered in isolation | [`modules.sh`](modules.sh) | +| 2 | **Golden** — screenshot vs [`reference.png`](reference.png) (fuzz 8%, 800px) | workflow | +| 2 | **Interaction** — inject pointer clicks via sway; assert `on-click` side effect + `format-alt` toggle | [`interact.sh`](interact.sh) | +| 2 | **Accessibility** — assert module labels via the AT-SPI tree (robust to fonts) | [`a11y.sh`](a11y.sh) / [`a11y.py`](a11y.py) | +| 3 | **Layout matrix** — top / bottom / left (vertical) + HiDPI (scale 2) | [`positions.sh`](positions.sh) | +| 3 | **Second compositor** — same launch under `labwc` | workflow (`COMPOSITOR=labwc`) | + +AddressSanitizer runs under *all* of the above, so any memory bug in the real +render path aborts the run. The interaction step is best-effort +(`continue-on-error`): a headless seat exposes no pointer capability, so injected +clicks may be dropped — hardening it needs a virtual pointer (uinput/wlr-virtual-pointer). ## Updating the reference image `reference.png` must be generated by the CI environment (local rendering differs -from Ubuntu's). To (re)bless it: +from Ubuntu's). Two ways: -1. Push your change and open the run of the `smoke` workflow. -2. Download the `waybar-screenshot` artifact. -3. Commit its `bar.png` as `test/smoke/reference.png`. +- **One click:** Actions → *smoke* → **Run workflow** with `bless = true`. The + job regenerates the screenshot and commits it to your branch. +- **Manual:** download the `waybar-smoke` artifact from a run and commit its + `bar.png` as `test/smoke/reference.png`. -Do this whenever an **intended** visual change makes the golden comparison fail. +Do this whenever an **intended** visual change trips the golden comparison. ## Run locally -On a machine with `sway`, `grim` and `waybar`: +On a machine with `sway`, `grim`, `imagemagick` and Waybar built: ```sh -./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css /tmp/bar.png +export WAYBAR_BIN=$PWD/build/waybar +./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css /tmp/bar.png +./test/smoke/modules.sh /tmp/module-shots +./test/smoke/interact.sh /tmp/interaction-shots +./test/smoke/positions.sh /tmp/layout-shots +dbus-run-session -- ./test/smoke/a11y.sh ``` diff --git a/test/smoke/a11y.py b/test/smoke/a11y.py new file mode 100644 index 00000000..032e57bc --- /dev/null +++ b/test/smoke/a11y.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Tier 2 — assert on Waybar's accessibility (AT-SPI) tree instead of pixels. + +Walks the running Waybar's GTK accessibility tree and checks that the expected +module labels are exposed. This is robust to fonts/anti-aliasing (unlike the +golden image) and verifies actual widget content. +""" +import sys + +import pyatspi + +EXPECT = ["Waybar", "CI smoke test", "12:34"] + + +def collect(acc, out): + try: + name = acc.name or "" + except Exception: + name = "" + if name: + out.append(name) + try: + count = acc.childCount + except Exception: + count = 0 + for i in range(count): + try: + child = acc.getChildAtIndex(i) + except Exception: + child = None + if child is not None: + collect(child, out) + + +def main(): + desktop = pyatspi.Registry.getDesktop(0) + texts, found = [], False + for i in range(desktop.childCount): + app = desktop.getChildAtIndex(i) + if app is None: + continue + if (app.name or "").lower().startswith("waybar"): + found = True + collect(app, texts) + print("AT-SPI labels seen on waybar:", texts) + if not found: + print("ERROR: waybar did not appear on the AT-SPI bus") + return 1 + missing = [e for e in EXPECT if not any(e in t for t in texts)] + if missing: + print("ERROR: expected labels missing from AT-SPI tree:", missing) + return 1 + print("OK: all expected labels present via AT-SPI") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/smoke/a11y.sh b/test/smoke/a11y.sh new file mode 100755 index 00000000..19f66c76 --- /dev/null +++ b/test/smoke/a11y.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Tier 2 — accessibility assertions. Must run under a D-Bus session so the +# AT-SPI bus is available, e.g.: dbus-run-session -- test/smoke/a11y.sh +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" + +# Make sure GTK registers with the AT-SPI bridge. +unset NO_AT_BRIDGE +export GTK_A11Y=1 + +smoke::setup +smoke::start_compositor +trap smoke::stop EXIT +smoke::launch_waybar "$DIR/config.jsonc" "$DIR/style.css" 6 +smoke::assert_alive +python3 "$DIR/a11y.py" diff --git a/test/smoke/interact.sh b/test/smoke/interact.sh new file mode 100755 index 00000000..d73c3ad4 --- /dev/null +++ b/test/smoke/interact.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Tier 2 — interaction: inject real pointer input via sway and assert on the +# behaviour (on-click side effect, format-alt toggle), not on pixels alone. +# +# Usage: interact.sh [screenshot-dir] +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" + +SHOTDIR="${1:-interaction-shots}" +mkdir -p "$SHOTDIR" + +smoke::setup +smoke::start_compositor +trap smoke::stop EXIT + +SEAT="$(swaymsg -t get_seats 2>/dev/null | jq -r '.[0].name' 2>/dev/null || echo seat0)" +CX=$((WIDTH / 2)); CY=15 + +click_center() { + swaymsg "seat $SEAT cursor set $CX $CY" >/dev/null + swaymsg "seat $SEAT cursor press button1" >/dev/null + swaymsg "seat $SEAT cursor release button1" >/dev/null +} + +fail=0 + +# --- 1) on-click side effect --------------------------------------------- +echo "::group::on-click side effect" +SENTINEL="$(mktemp -u)" +cfg1="$(mktemp --suffix=.json)" +cat > "$cfg1" </dev/null || true; sleep 1 +echo "::endgroup::" + +# --- 2) format-alt toggle on click --------------------------------------- +echo "::group::format-alt toggle" +cfg2="$(mktemp --suffix=.json)" +cat > "$cfg2" <&1 || true)" +ae="$(printf '%s' "$ae" | grep -oE '^[0-9]+' || echo 0)" +echo "pixels changed after click: $ae" +if [ "$ae" -gt 100 ]; then + echo "✓ format-alt toggled the label on click" +else + echo "::error::format-alt did not change the bar on click ($ae px)"; fail=1 +fi +kill "$WAYBAR_PID" 2>/dev/null || true +echo "::endgroup::" + +[ "$fail" = 0 ] || exit 1 +echo "✓ interaction tests passed" diff --git a/test/smoke/lib.sh b/test/smoke/lib.sh new file mode 100755 index 00000000..bed35abb --- /dev/null +++ b/test/smoke/lib.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Shared helpers for the headless Waybar smoke/render/interaction tests. +# +# A test sources this file, then typically: +# smoke::setup +# smoke::start_compositor +# smoke::launch_waybar [style] +# smoke::assert_alive +# smoke::assert_clean # ASan/UBSan/Gtk-CRITICAL/segfault gate +# smoke::screenshot out.png +# smoke::stop +# +# Environment knobs (optional): WIDTH HEIGHT SCALE COMPOSITOR WAYBAR_BIN +set -uo pipefail + +WIDTH="${WIDTH:-1920}" +HEIGHT="${HEIGHT:-1080}" +SCALE="${SCALE:-1}" +COMPOSITOR="${COMPOSITOR:-sway}" +WAYBAR_BIN="${WAYBAR_BIN:-waybar}" + +SMOKE_LOG="" +COMP_PID="" +WAYBAR_PID="" + +smoke::setup() { + export XDG_RUNTIME_DIR="$(mktemp -d)" + chmod 700 "$XDG_RUNTIME_DIR" + export WLR_BACKENDS=headless + export WLR_RENDERER=pixman + export WLR_LIBINPUT_NO_DEVICES=1 + export LIBGL_ALWAYS_SOFTWARE=1 + # Keep ASan/UBSan noise low but fatal on real errors (leaks in GTK/glib are + # too noisy to gate on, so only memory-corruption and UB abort the run). + export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:detect_odr_violation=0" + export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" +} + +smoke::start_compositor() { + case "$COMPOSITOR" in + sway) + local cfg; cfg="$(mktemp)" + cat > "$cfg" </tmp/compositor.log 2>&1 & + COMP_PID=$! + ;; + labwc) + # labwc reads outputs from the wlroots headless backend directly. + labwc >/tmp/compositor.log 2>&1 & + COMP_PID=$! + ;; + *) + echo "::error::unknown compositor '$COMPOSITOR'"; return 1 ;; + esac + + local sock="" + for _ in $(seq 1 40); do + sock="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'wayland-*' ! -name '*.lock' 2>/dev/null | head -1 || true)" + [ -n "$sock" ] && break + sleep 0.5 + done + if [ -z "$sock" ]; then + echo "::error::$COMPOSITOR did not create a Wayland socket"; cat /tmp/compositor.log || true; return 1 + fi + export WAYLAND_DISPLAY="$(basename "$sock")" + + # sway IPC socket, so swaymsg (cursor injection, clean exit) can reach it. + if [ "$COMPOSITOR" = sway ]; then + for _ in $(seq 1 10); do + SWAYSOCK="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'sway-ipc.*.sock' 2>/dev/null | head -1 || true)" + [ -n "$SWAYSOCK" ] && break + sleep 0.3 + done + export SWAYSOCK + fi + echo "compositor '$COMPOSITOR' up on WAYLAND_DISPLAY=$WAYLAND_DISPLAY (${WIDTH}x${HEIGHT} scale ${SCALE})" +} + +# smoke::launch_waybar [style] [settle_seconds] +smoke::launch_waybar() { + local config="$1" style="${2:-}" settle="${3:-6}" + SMOKE_LOG="$(mktemp)" + local args=(-c "$config" -l debug) + [ -n "$style" ] && args+=(-s "$style") + "$WAYBAR_BIN" "${args[@]}" >"$SMOKE_LOG" 2>&1 & + WAYBAR_PID=$! + sleep "$settle" +} + +smoke::log() { cat "$SMOKE_LOG" 2>/dev/null || true; } + +smoke::assert_alive() { + if ! kill -0 "$WAYBAR_PID" 2>/dev/null; then + echo "::error::waybar exited early (crashed on startup)" + echo "::group::waybar log"; smoke::log; echo "::endgroup::" + return 1 + fi + echo "✓ waybar is alive" +} + +# Fail on sanitizer reports and GTK/GLib criticals in the log. +smoke::assert_clean() { + local bad + bad="$(grep -iE 'AddressSanitizer|runtime error:|LeakSanitizer|Gtk-CRITICAL|GLib-CRITICAL|GLib-GObject-CRITICAL|assertion .*failed|segfault|terminate called|SUMMARY: .*Sanitizer' "$SMOKE_LOG" || true)" + if [ -n "$bad" ]; then + echo "::error::waybar reported sanitizer/critical issues:" + echo "$bad" + echo "::group::full waybar log"; smoke::log; echo "::endgroup::" + return 1 + fi + echo "✓ no sanitizer/critical issues in log" +} + +smoke::screenshot() { + local out="$1" + grim "$out" + echo "✓ screenshot -> $out" +} + +# Assert the top bar strip actually rendered something (not a uniform color). +smoke::assert_not_blank() { + local img="$1" h="${2:-30}" + local sd + sd="$(convert "$img" -crop "${WIDTH}x${h}+0+0" +repage -colorspace Gray -format '%[fx:standard_deviation]' info:)" + echo "bar strip stddev = $sd" + awk "BEGIN{ exit !($sd > 0.01) }" || { echo "::error::bar strip is blank in $img"; return 1; } + echo "✓ bar rendered content" +} + +smoke::stop() { + kill "$WAYBAR_PID" 2>/dev/null || true + swaymsg -q exit 2>/dev/null || kill "$COMP_PID" 2>/dev/null || true + wait 2>/dev/null || true +} diff --git a/test/smoke/modules-config.jsonc b/test/smoke/modules-config.jsonc deleted file mode 100644 index 108c51fc..00000000 --- a/test/smoke/modules-config.jsonc +++ /dev/null @@ -1,18 +0,0 @@ -// Waybar config for the CI smoke test: exercises real modules that work in a -// headless environment (no hardware/daemon dependency). Its content is dynamic -// (time, cpu, ...) so it is NOT screenshot-compared — it only checks that these -// modules load and render without crashing the bar. -{ - "layer": "top", - "position": "top", - "height": 30, - "modules-center": ["clock", "cpu", "memory", "disk", "custom/hello"], - "clock": {}, - "cpu": {}, - "memory": {}, - "disk": {}, - "custom/hello": { - "exec": "echo ok", - "interval": "once" - } -} diff --git a/test/smoke/modules.sh b/test/smoke/modules.sh new file mode 100755 index 00000000..46f13c88 --- /dev/null +++ b/test/smoke/modules.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Tier 1 — per-module matrix: render each headless-safe module in isolation and +# assert it loads, logs no sanitizer/critical error, and draws something. +# +# Usage: modules.sh +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" + +SHOTDIR="${1:-module-shots}" +mkdir -p "$SHOTDIR" + +# module name -> its config options (JSON object). Only modules that work with +# no real hardware/daemon in a headless runner are included. +declare -A MODULES=( + [clock]='{}' + [cpu]='{}' + [memory]='{}' + [disk]='{}' + [idle_inhibitor]='{"format":"{status}"}' + [custom/echo]='{"exec":"echo hello","interval":"once"}' +) + +smoke::setup +smoke::start_compositor +trap smoke::stop EXIT + +fails=() +for mod in "${!MODULES[@]}"; do + safe="${mod//\//-}" + cfg="$(mktemp --suffix=.json)" + cat > "$cfg" </dev/null || true + sleep 1 + echo "::endgroup::" +done + +if [ "${#fails[@]}" -gt 0 ]; then + echo "::error::modules failed to render cleanly: ${fails[*]}" + exit 1 +fi +echo "✓ all ${#MODULES[@]} modules rendered cleanly" diff --git a/test/smoke/positions.sh b/test/smoke/positions.sh new file mode 100755 index 00000000..9573bb8a --- /dev/null +++ b/test/smoke/positions.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Tier 3 — layout matrix: bar at top/bottom (horizontal) and left (vertical), +# plus a HiDPI (scale 2) run. Each must launch and render without a crash or +# sanitizer/critical error. Screenshots are uploaded for visual inspection. +# +# Usage: positions.sh [screenshot-dir] +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" + +SHOTDIR="${1:-layout-shots}" +mkdir -p "$SHOTDIR" + +smoke::setup +trap smoke::stop EXIT + +fails=() + +# run_case +run_case() { + local name="$1" pos="$2" scale="$3" + echo "::group::layout: $name (position=$pos scale=$scale)" + SCALE="$scale" smoke::start_compositor || { fails+=("$name"); echo "::endgroup::"; return; } + local cfg; cfg="$(mktemp --suffix=.json)" + cat > "$cfg" < [style] [screenshot.png] -# - always: assert waybar launches, stays alive, logs no fatal error -# - if given: capture the bar with grim -# -# Requires: sway, grim, waybar (on PATH). Software-rendered, no GPU needed. set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" -CONFIG="$1" -STYLE="${2:-}" -SHOT="${3:-}" +CONFIG="$1"; STYLE="${2:-}"; SHOT="${3:-}" -export XDG_RUNTIME_DIR="$(mktemp -d)" -chmod 700 "$XDG_RUNTIME_DIR" -export WLR_BACKENDS=headless -export WLR_RENDERER=pixman -export WLR_LIBINPUT_NO_DEVICES=1 -export LIBGL_ALWAYS_SOFTWARE=1 +smoke::setup +smoke::start_compositor +trap smoke::stop EXIT +smoke::launch_waybar "$CONFIG" "$STYLE" -LOG="$(mktemp)" -SWAYCFG="$(mktemp)" +echo "::group::waybar log"; smoke::log; echo "::endgroup::" -wb="waybar -c $CONFIG" -[ -n "$STYLE" ] && wb="$wb -s $STYLE" - -cat > "$SWAYCFG" < $LOG 2>&1" -EOF - -echo "::group::Starting sway (headless) + waybar" -sway -c "$SWAYCFG" & -SWAY_PID=$! - -cleanup() { - swaymsg -q exit 2>/dev/null || kill "$SWAY_PID" 2>/dev/null || true -} -trap cleanup EXIT - -# Wait for the Wayland socket to appear. -sock="" -for _ in $(seq 1 40); do - sock="$(find "$XDG_RUNTIME_DIR" -maxdepth 1 -name 'wayland-*' ! -name '*.lock' 2>/dev/null | head -1 || true)" - [ -n "$sock" ] && break - sleep 0.5 -done -[ -n "$sock" ] || { echo "::error::compositor did not create a Wayland socket"; exit 1; } -export WAYLAND_DISPLAY="$(basename "$sock")" -echo "compositor up on WAYLAND_DISPLAY=$WAYLAND_DISPLAY" -echo "::endgroup::" - -# Give modules time to poll/settle. -sleep 6 - -echo "::group::Waybar log" -cat "$LOG" || true -echo "::endgroup::" - -# --- Level 1: smoke --- -if ! pgrep -x waybar >/dev/null; then - echo "::error::waybar is not running — it crashed on startup (config: $CONFIG)" - exit 1 -fi -if grep -iqE 'critical|segfault|\bfatal\b|terminate called' "$LOG"; then - echo "::error::waybar logged a fatal error (config: $CONFIG)" - exit 1 -fi -echo "✓ waybar launched and stayed alive under headless sway" - -# --- Level 2: screenshot --- -if [ -n "$SHOT" ]; then - grim "$SHOT" - echo "✓ captured screenshot -> $SHOT" -fi +smoke::assert_alive +smoke::assert_clean +[ -n "$SHOT" ] && smoke::screenshot "$SHOT" From b527376985e3dcdd304e389a389ffde118913ce4 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:24:37 +0200 Subject: [PATCH 373/416] fix(audio_backend): release leaked pa_operation handles Every pa_operation* returned by the PulseAudio context introspection, subscribe and volume/mute calls was discarded without pa_operation_unref, leaking one operation object per call. Over a long session the periodic subscription events accumulate an unbounded number of these handles. Capture each returned handle and unref it (guarded against NULL) at every discard site. Callback behavior is unchanged; these calls already run under the threaded-mainloop lock, where unref is safe. --- src/util/audio_backend.cpp | 93 +++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 25ccde37..19d8109f 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -104,16 +105,17 @@ void AudioBackend::contextStateCb(pa_context* c, void* data) { } break; case PA_CONTEXT_READY: - pa_context_get_server_info(c, serverInfoCb, data); + if (auto* o = pa_context_get_server_info(c, serverInfoCb, data)) pa_operation_unref(o); pa_context_set_subscribe_callback(c, subscribeCb, data); - pa_context_subscribe(c, - static_cast( - static_cast(PA_SUBSCRIPTION_MASK_SERVER) | - static_cast(PA_SUBSCRIPTION_MASK_SINK) | - static_cast(PA_SUBSCRIPTION_MASK_SINK_INPUT) | - static_cast(PA_SUBSCRIPTION_MASK_SOURCE) | - static_cast(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)), - nullptr, nullptr); + if (auto* o = pa_context_subscribe(c, + static_cast( + static_cast(PA_SUBSCRIPTION_MASK_SERVER) | + static_cast(PA_SUBSCRIPTION_MASK_SINK) | + static_cast(PA_SUBSCRIPTION_MASK_SINK_INPUT) | + static_cast(PA_SUBSCRIPTION_MASK_SOURCE) | + static_cast(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)), + nullptr, nullptr)) + pa_operation_unref(o); break; case PA_CONTEXT_FAILED: if (pa_context_errno(c) != PA_ERR_CONNECTIONREFUSED) { @@ -164,15 +166,18 @@ void AudioBackend::subscribeCb(pa_context* context, pa_subscription_event_type_t return; } if (facility == PA_SUBSCRIPTION_EVENT_SERVER) { - pa_context_get_server_info(context, serverInfoCb, data); + if (auto* o = pa_context_get_server_info(context, serverInfoCb, data)) pa_operation_unref(o); } else if (facility == PA_SUBSCRIPTION_EVENT_SINK) { - pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data); + if (auto* o = pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data)) + pa_operation_unref(o); } else if (facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT) { - pa_context_get_sink_info_list(context, sinkInfoCb, data); + if (auto* o = pa_context_get_sink_info_list(context, sinkInfoCb, data)) pa_operation_unref(o); } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE) { - pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data); + if (auto* o = pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data)) + pa_operation_unref(o); } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT) { - pa_context_get_source_info_list(context, sourceInfoCb, data); + if (auto* o = pa_context_get_source_info_list(context, sourceInfoCb, data)) + pa_operation_unref(o); } } @@ -184,7 +189,9 @@ void AudioBackend::volumeModifyCb(pa_context* c, int success, void* data) { if (success != 0) { if ((backend->context_ != nullptr) && pa_context_get_state(backend->context_) == PA_CONTEXT_READY) { - pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data); + if (auto* o = pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, + sinkInfoCb, data)) + pa_operation_unref(o); } } else { spdlog::debug("Volume modification failed"); @@ -196,8 +203,9 @@ void AudioBackend::sourceVolumeModifyCb(pa_context* c, int success, void* data) if (success != 0) { if ((backend->context_ != nullptr) && pa_context_get_state(backend->context_) == PA_CONTEXT_READY) { - pa_context_get_source_info_by_index(backend->context_, backend->source_idx_, sourceInfoCb, - data); + if (auto* o = pa_context_get_source_info_by_index(backend->context_, backend->source_idx_, + sourceInfoCb, data)) + pa_operation_unref(o); } } else { spdlog::debug("Source volume modification failed"); @@ -317,8 +325,8 @@ void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, vo backend->default_sink_name = i->default_sink_name ? i->default_sink_name : ""; backend->default_source_name_ = i->default_source_name ? i->default_source_name : ""; - pa_context_get_sink_info_list(context, sinkInfoCb, data); - pa_context_get_source_info_list(context, sourceInfoCb, data); + if (auto* o = pa_context_get_sink_info_list(context, sinkInfoCb, data)) pa_operation_unref(o); + if (auto* o = pa_context_get_source_info_list(context, sourceInfoCb, data)) pa_operation_unref(o); } uint16_t AudioBackend::getVolume(PulseaudioTarget target) const { @@ -368,10 +376,13 @@ void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t m // Apply the volume change pa_threaded_mainloop_lock(mainloop_); if (is_source) { - pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb, - this); + if (auto* o = pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, + sourceVolumeModifyCb, this)) + pa_operation_unref(o); } else { - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (auto* o = pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, + volumeModifyCb, this)) + pa_operation_unref(o); } pa_threaded_mainloop_unlock(mainloop_); } @@ -412,10 +423,13 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma // No need to continue with volume change if we had to create a new structure pa_threaded_mainloop_lock(mainloop_); if (is_source) { - pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb, - this); + if (auto* o = pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, + sourceVolumeModifyCb, this)) + pa_operation_unref(o); } else { - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (auto* o = pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, + volumeModifyCb, this)) + pa_operation_unref(o); } pa_threaded_mainloop_unlock(mainloop_); return; @@ -458,10 +472,13 @@ void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t ma // Apply the volume change pa_threaded_mainloop_lock(mainloop_); if (is_source) { - pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, sourceVolumeModifyCb, - this); + if (auto* o = pa_context_set_source_volume_by_index(context_, source_idx_, &pa_volume, + sourceVolumeModifyCb, this)) + pa_operation_unref(o); } else { - pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); + if (auto* o = pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, + volumeModifyCb, this)) + pa_operation_unref(o); } pa_threaded_mainloop_unlock(mainloop_); } @@ -478,8 +495,9 @@ void AudioBackend::toggleSinkMute() { if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = !muted_; pa_threaded_mainloop_lock(mainloop_); - pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, - nullptr); + if (auto* o = pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), + nullptr, nullptr)) + pa_operation_unref(o); pa_threaded_mainloop_unlock(mainloop_); } @@ -487,8 +505,9 @@ void AudioBackend::toggleSinkMute(bool mute) { if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; muted_ = mute; pa_threaded_mainloop_lock(mainloop_); - pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), nullptr, - nullptr); + if (auto* o = pa_context_set_sink_mute_by_index(context_, sink_idx_, static_cast(muted_), + nullptr, nullptr)) + pa_operation_unref(o); pa_threaded_mainloop_unlock(mainloop_); } @@ -496,8 +515,9 @@ void AudioBackend::toggleSourceMute() { if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; source_muted_ = !source_muted_; pa_threaded_mainloop_lock(mainloop_); - pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), - nullptr, nullptr); + if (auto* o = pa_context_set_source_mute_by_index( + context_, source_idx_, static_cast(source_muted_), nullptr, nullptr)) + pa_operation_unref(o); pa_threaded_mainloop_unlock(mainloop_); } @@ -505,8 +525,9 @@ void AudioBackend::toggleSourceMute(bool mute) { if (context_ == nullptr || pa_context_get_state(context_) != PA_CONTEXT_READY) return; source_muted_ = mute; pa_threaded_mainloop_lock(mainloop_); - pa_context_set_source_mute_by_index(context_, source_idx_, static_cast(source_muted_), - nullptr, nullptr); + if (auto* o = pa_context_set_source_mute_by_index( + context_, source_idx_, static_cast(source_muted_), nullptr, nullptr)) + pa_operation_unref(o); pa_threaded_mainloop_unlock(mainloop_); } From e8e12cdfa1e59aade0eca14ac177ba82323e4787 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:26:34 +0200 Subject: [PATCH 374/416] fix(audio_backend): make sink-mapping selection order-independent With a sink-mapping configured, sinkInfoCb could report the wrong sink's volume depending on the order in which PulseAudio enumerated sinks during a pa_context_get_sink_info_list sweep. The mapping override was keyed on the mutable current_sink_name_ and ran before the 'pick a running sink' fallback, which also mutated current_sink_name_. If the default sink was running while the mapped target was suspended, the fallback could reassign the selection to the default sink after the mapping had already matched, so the reported sink depended on enumeration order (and each sweep wrote the state twice, causing a flicker). Resolve the target up front: key the mapping on the stable default_sink_name and, when a mapping is in effect, treat the mapped target sink as the sole definitive selection - every other sink is ignored and the running-sink fallback is skipped. The default-sink + running-fallback behavior is unchanged when no mapping applies. Verified in isolation across all sink enumeration orders. --- src/util/audio_backend.cpp | 56 ++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/util/audio_backend.cpp b/src/util/audio_backend.cpp index 19d8109f..7933835d 100644 --- a/src/util/audio_backend.cpp +++ b/src/util/audio_backend.cpp @@ -239,29 +239,45 @@ void AudioBackend::sinkInfoCb(pa_context* /*context*/, const pa_sink_info* i, in } } - if (const auto mapping = backend->sink_mapping_.find(backend->current_sink_name_); + // Resolve which sink to report deterministically, independent of the order in + // which PulseAudio enumerates sinks during a sink_info_list sweep. + // + // When the default sink has an explicit mapping, the mapped target sink is the + // definitive selection ("the sink named by the value is considered to be the + // current sink instead of the one named by the key"): only that target sink may + // write the reported volume/mute state. The mapping is keyed on the stable + // default_sink_name (set once per server-info update) rather than on the mutable + // current_sink_name_. Previously the mapping override and the "pick a running + // sink" fallback both mutated current_sink_name_ mid-sweep, so the mapping could + // be overridden by whichever running sink happened to be enumerated, and the + // reported volume depended on enumeration order. + if (const auto mapping = backend->sink_mapping_.find(backend->default_sink_name); mapping != backend->sink_mapping_.end()) { - if (i->name == mapping->second) { - backend->current_sink_name_ = i->name; + if (i->name != mapping->second) { + // A mapping is in effect but this is not the mapped target sink; ignore it so + // it can never override the target's reported volume. + return; } - } - - backend->default_sink_running_ = backend->default_sink_name == i->name && - (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); - - if (i->name != backend->default_sink_name && i->name != backend->current_sink_name_ && - !backend->default_sink_running_) { - return; - } - - if (backend->current_sink_name_ == i->name) { - backend->current_sink_running_ = (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); - } - - if (!backend->current_sink_running_ && - (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE)) { backend->current_sink_name_ = i->name; - backend->current_sink_running_ = true; + backend->current_sink_running_ = (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); + } else { + backend->default_sink_running_ = backend->default_sink_name == i->name && + (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); + + if (i->name != backend->default_sink_name && i->name != backend->current_sink_name_ && + !backend->default_sink_running_) { + return; + } + + if (backend->current_sink_name_ == i->name) { + backend->current_sink_running_ = (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); + } + + if (!backend->current_sink_running_ && + (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE)) { + backend->current_sink_name_ = i->name; + backend->current_sink_running_ = true; + } } if (backend->current_sink_name_ == i->name) { From 9a1cb6183a061d7da21ea52adf742070301eccfd Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:20:14 +0200 Subject: [PATCH 375/416] fix(ALabel): free g_strdup'd menu action string via closure notify The per-menu-action string duplicated with g_strdup was never freed, leaking one string per action on every menu build and reload. Use g_signal_connect_data with (GClosureNotify)g_free so the copy is freed when the closure is destroyed. --- src/ALabel.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 752c80bd..3af6f5a3 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -123,8 +123,9 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st } submenus_[key] = GTK_MENU_ITEM(item); menuActionsMap_[key] = it->asString(); - g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), - (gpointer)g_strdup(menuActionsMap_[key].c_str())); + g_signal_connect_data(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), + g_strdup(menuActionsMap_[key].c_str()), (GClosureNotify)g_free, + (GConnectFlags)0); } g_object_unref(builder); } catch (std::runtime_error& e) { @@ -183,7 +184,9 @@ std::string ALabel::getIcon(uint16_t percentage, const std::string& alt, uint16_ if (!threshold.isObject() || !threshold["icon"].isString() || !threshold["max"].isUInt()) { static bool warned = false; if (!warned) { - spdlog::warn("format-icons: skipping invalid threshold object, expected {\"icon\": \"...\", \"max\": N}"); + spdlog::warn( + "format-icons: skipping invalid threshold object, expected {\"icon\": \"...\", " + "\"max\": N}"); warned = true; } continue; @@ -229,7 +232,9 @@ std::string ALabel::getIcon(uint16_t percentage, const std::vector& if (!threshold.isObject() || !threshold["icon"].isString() || !threshold["max"].isUInt()) { static bool warned = false; if (!warned) { - spdlog::warn("format-icons: skipping invalid threshold object, expected {\"icon\": \"...\", \"max\": N}"); + spdlog::warn( + "format-icons: skipping invalid threshold object, expected {\"icon\": \"...\", " + "\"max\": N}"); warned = true; } continue; From a600fba538bf3ffea4bbc49f2a6023d26a316028 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:20:18 +0200 Subject: [PATCH 376/416] fix(AGraph): unref transient GtkBuilder on all menu-build paths The GtkBuilder created for menu construction was never unref'd on any path (success or throw), leaking one builder per graph module with a menu. Unref on each throw and at the end, and take an explicit ref on menu_ so it survives dropping the builder (mirrors ALabel). --- src/AGraph.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/AGraph.cpp b/src/AGraph.cpp index ea1f754d..b0dd5f02 100644 --- a/src/AGraph.cpp +++ b/src/AGraph.cpp @@ -79,13 +79,17 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st // Make the GtkBuilder and check for errors in his parsing if (gtk_builder_add_from_string(builder, fileContent.str().c_str(), -1, nullptr) == 0U) { + g_object_unref(builder); throw std::runtime_error("Error found in the file " + menuFile); } menu_ = gtk_builder_get_object(builder, "menu"); if (menu_ == nullptr) { + g_object_unref(builder); throw std::runtime_error("Failed to get 'menu' object from GtkBuilder"); } + // Keep the menu alive after dropping the transient GtkBuilder. + g_object_ref(menu_); submenus_ = std::map(); menuActionsMap_ = std::map(); @@ -98,6 +102,7 @@ AGraph::AGraph(const Json::Value& config, const std::string& name, const std::st g_signal_connect(submenus_[key], "activate", G_CALLBACK(handleGtkMenuEvent), (gpointer)menuActionsMap_[key].c_str()); } + g_object_unref(builder); } catch (std::runtime_error& e) { spdlog::warn("Error while creating the menu : {}. Menu popup not activated.", e.what()); } From d2493906dfa7869ade65b9eba53e4aaacdeaaf87 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:43:57 +0200 Subject: [PATCH 377/416] ci(freebsd): install bzip2 so freetype2's pkg-config resolves The FreeBSD build broke on 'Package bzip2, required by freetype2, not found': freetype2.pc lists bzip2 as a (private) requirement, but no bzip2.pc was installed, so pkg-config could not generate cflags for gtkmm-3.0. Add bzip2 to the package list. --- .github/workflows/freebsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 977e514c..339394f6 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -29,7 +29,7 @@ jobs: sync_files: runner-to-vm run: | sudo pkg install -y git # subprojects/date - sudo pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ + sudo pkg install -y bzip2 catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \ pkgconf pipewire pulseaudio scdoc sndio spdlog wayland-protocols upower \ libinotify From 969855f99e2d43b8ba8c5a68d5c969c666275718 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 10:46:58 +0200 Subject: [PATCH 378/416] ci(freebsd): provide a bzip2.pc stub for freetype2 Installing the bzip2 package does not ship a pkg-config file, so freetype2.pc (a transitive requirement of gtkmm-3.0) still failed with 'Package bzip2 ... not found'. bzip2 lives in the FreeBSD base system; write a minimal bzip2.pc before meson setup so pkg-config can resolve it. --- .github/workflows/freebsd.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 339394f6..68ce22eb 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -29,10 +29,15 @@ jobs: sync_files: runner-to-vm run: | sudo pkg install -y git # subprojects/date - sudo pkg install -y bzip2 catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ + sudo pkg install -y catch evdev-proto gtk-layer-shell gtkmm30 jsoncpp \ libdbusmenu libevdev libfmt libmpdclient libudev-devd meson \ pkgconf pipewire pulseaudio scdoc sndio spdlog wayland-protocols upower \ libinotify + # freetype2.pc declares a bzip2 pkg-config requirement that no FreeBSD + # package ships (bzip2 lives in the base system), which breaks the + # gtkmm-3.0 lookup. Drop in a minimal bzip2.pc pointing at base libbz2. + printf 'Name: bzip2\nDescription: bzip2\nVersion: 1.0.8\nLibs: -lbz2\nCflags:\n' \ + | sudo tee /usr/local/libdata/pkgconfig/bzip2.pc >/dev/null meson setup build -Dman-pages=enabled ninja -C build meson test -C build --no-rebuild --print-errorlogs --suite waybar From 5711b1f4ef713b2c659120e00ce3ad9601012892 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 11:08:59 +0200 Subject: [PATCH 379/416] fix(backlight): stop per-tick filesystem re-enumeration / I/O flooding The backlight udev worker thread called enumerate_devices() on every epoll_wait timeout, i.e. once per polling interval. enumerate_devices() runs udev_enumerate_scan_devices(), which walks the entire /sys/class/backlight and /sys/class/leds trees and opens/closes the sysfs root and every device path. With no `interval` configured the module polls on its default cadence, so this full re-scan ran continuously even when brightness never changed, flooding the filesystem (observed via fatrace as constant open/close of `/`). Raising `interval` only lowered the cadence, which is why the reporter's `interval: 10` workaround reduced the flood. The full re-enumeration is redundant: the udev monitor already delivers change/add/remove events for the backlight and leds subsystems. On the timeout path, re-read only the sysfs attributes of the devices already tracked (via udev_device_new_from_subsystem_sysname) instead of re-scanning the whole tree. This keeps periodic refresh working for firmware backlights such as acpi_video that may not emit udev change events, while eliminating the tree-wide scan. Device discovery of new/removed devices continues through the udev monitor. Fixes #5020. --- src/util/backlight_backend.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/util/backlight_backend.cpp b/src/util/backlight_backend.cpp index bf669f15..9ccf7250 100644 --- a/src/util/backlight_backend.cpp +++ b/src/util/backlight_backend.cpp @@ -230,9 +230,21 @@ BacklightBackend::BacklightBackend(std::chrono::milliseconds interval, upsert_device(devices, dev.get()); } - // Refresh state if timed out + // Refresh state if timed out. Only re-read the sysfs attributes of the + // devices we already track instead of re-enumerating the whole udev tree + // (udev_enumerate_scan_devices), which walks all of /sys/class/backlight + // and /sys/class/leds and floods the filesystem with open/close syscalls + // on every polling tick (#5020). Device add/remove is already delivered + // by the udev monitor above, so a periodic full re-scan is redundant. if (event_count == 0) { - enumerate_devices(devices, udev.get()); + for (const auto& device : devices) { + std::unique_ptr dev{ + udev_device_new_from_subsystem_sysname(udev.get(), device.subsystem().c_str(), + device.name().c_str())}; + if (dev) { + upsert_device(devices, dev.get()); + } + } } { std::scoped_lock lock(udev_thread_mutex_); From d518110e6f51ccfb435b307eccef36343fdb5f9d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 11:07:59 +0200 Subject: [PATCH 380/416] fix(network): recover IP address after a link flap / router reboot The network module only populates the interface address from netlink events (RTM_NEWADDR) or an explicit address dump. The interval timer re-queries WiFi and bandwidth but never re-fetches the address, so the module relies entirely on receiving the RTM_NEWADDR event. Netlink multicast delivery is reliable unless the socket receive buffer overflows, in which case the kernel drops notifications and reports ENOBUFS. During a burst of link/address/route changes -- e.g. a router reboot or a PPPoE redial -- this can drop the RTM_NEWADDR carrying the interface's new IP (after the old one was removed by RTM_DELADDR). With no overrun handling and no periodic resync, the address field stays blank until Waybar is restarted (which re-dumps addresses). Handle the overrun: when nl_recvmsgs_default reports ENOBUFS/NLE_NOMEM, request a fresh link/address (and route, when auto-detecting) dump to resynchronise, instead of silently continuing with lost state. Also enlarge the event socket receive buffer to make overruns less likely in the first place. The fix stays within the event thread, so it adds no new locking or cross-thread socket access. Fixes #5122. --- src/modules/network.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 72a367fc..d3adfbe5 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -183,6 +183,12 @@ void waybar::modules::Network::createEventSocket() { if (nl_socket_set_nonblocking(ev_sock_)) { throw std::runtime_error("Can't set non-blocking on network socket"); } + // Enlarge the socket receive buffer so that a burst of link/address/route + // change notifications (e.g. a router reboot or a PPPoE redial) is less likely + // to overflow it and make the kernel drop messages (ENOBUFS). Overruns are + // still handled in worker() by resynchronising the state, but a larger buffer + // avoids most of them. The kernel caps the request at net.core.rmem_max. + nl_socket_set_buffer_size(ev_sock_, 1024 * 1024, 0); nl_socket_add_memberships(ev_sock_, RTNLGRP_LINK, RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR, 0); if (!config_["interface"].isString()) { nl_socket_add_memberships(ev_sock_, RTNLGRP_IPV4_ROUTE, RTNLGRP_IPV6_ROUTE, 0); @@ -277,6 +283,27 @@ void waybar::modules::Network::worker() { rc = 0; break; } + if (rc == -NLE_NOMEM || errno == ENOBUFS) { + // The kernel dropped multicast notifications because our receive + // buffer overflowed. This happens during a burst of + // link/address/route changes such as a router reboot or a PPPoE + // redial. We have lost track of the current state -- in + // particular the RTM_NEWADDR carrying the interface's new IP + // address may have been dropped -- so request a fresh dump to + // resynchronise. Without this the address (cleared by the + // preceding RTM_DELADDR) would stay blank until Waybar is + // restarted, because nothing else re-queries it (#5122). + spdlog::warn("network: netlink receive buffer overrun, resyncing state"); + want_link_dump_ = true; + want_addr_dump_ = true; + if (!config_["interface"].isString()) { + want_route_dump_ = true; + } + askForStateDump(); + // Keep draining; the next recv proceeds normally now that the + // overrun has been reported. + continue; + } } if (rc < 0) { spdlog::error("nl_recvmsgs_default error: {}", nl_geterror(-rc)); From cbad42bc9bb42fb7d40ff9a7e843ec88eb2ba320 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 5 Jul 2026 10:23:06 -0500 Subject: [PATCH 381/416] fix(wlr/taskbar): stop forcing tasks onto every output's bar hide_if_duplicate() unconditionally re-ran handle_output_enter() with the bar's own wl_output for every non-squashed task, so any app_id or title event made the task visible on all bars and "all-outputs": false was effectively ignored. The un-squash path in handle_closed() showed the replacement task unconditionally, with the same effect. Track whether the toplevel is actually on the bar's output from the protocol's output_enter/output_leave events, split the button show/hide logic out of the protocol handlers, and gate every synthetic re-show on all-outputs or the tracked output membership. Fixes #5178 --- include/modules/wlr/taskbar.hpp | 6 +++ src/modules/wlr/taskbar.cpp | 86 ++++++++++++++++++++------------- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/include/modules/wlr/taskbar.hpp b/include/modules/wlr/taskbar.hpp index c4e56b14..74269320 100644 --- a/include/modules/wlr/taskbar.hpp +++ b/include/modules/wlr/taskbar.hpp @@ -71,6 +71,10 @@ class Task { bool button_visible_ = false; bool ignored_ = false; bool squashed_ = false; + /* Whether the toplevel is on this bar's output, per the protocol's + * output_enter/output_leave events */ + bool on_bar_output_ = false; + bool size_allocate_connected_ = false; bool with_icon_ = false; bool with_name_ = false; @@ -96,6 +100,8 @@ class Task { void on_button_size_allocated(Gtk::Allocation& alloc); void hide_if_ignored(); void hide_if_duplicate(); + void show_button(); + void hide_button(); public: /* Getter functions */ diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 4517aec4..1bba7dd8 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -278,25 +278,18 @@ void Task::hide_if_duplicate() { // Squashes if the app is in the squash list and more than 1 instance is open if (contains_app && (tbar_->task_id_count(app_id_) > 1 || tbar_->task_title_count(title_) > 1)) { squashed_ = true; - if (button_visible_) { - auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - handle_output_leave(output); - } + hide_button(); } - if (!squashed_ && !ignored_) { - auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - handle_output_enter(output); + if (!squashed_ && !ignored_ && (tbar_->all_outputs() || on_bar_output_)) { + show_button(); } } void Task::hide_if_ignored() { if (tbar_->ignore_list().count(app_id_) || tbar_->ignore_list().count(title_)) { ignored_ = true; - if (button_visible_) { - auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - handle_output_leave(output); - } + hide_button(); } if (!ignored_ && !squashed_) { @@ -353,6 +346,12 @@ void Task::on_button_size_allocated(Gtk::Allocation& alloc) { } void Task::handle_output_enter(struct wl_output* output) { + spdlog::debug("{} entered output {}", repr(), (void*)output); + + if (tbar_->show_output(output)) { + on_bar_output_ = true; + } + if (ignored_) { spdlog::debug("{} is ignored", repr()); return; @@ -362,33 +361,52 @@ void Task::handle_output_enter(struct wl_output* output) { return; } - spdlog::debug("{} entered output {}", repr(), (void*)output); - - if (!button_visible_ && (tbar_->all_outputs() || tbar_->show_output(output))) { - /* The task entered the output of the current bar make the button visible */ - button.signal_size_allocate().connect_notify( - sigc::mem_fun(this, &Task::on_button_size_allocated)); - tbar_->add_button(button); - if (!config_["active-only"].asBool() || active()) { - button.show(); - } - button_visible_ = true; - spdlog::debug("{} now visible on {}", repr(), bar_.output->name); - tbar_->update_bar_css_classes(); + if (tbar_->all_outputs() || on_bar_output_) { + /* The task entered the output of the current bar, make the button visible */ + show_button(); } } void Task::handle_output_leave(struct wl_output* output) { spdlog::debug("{} left output {}", repr(), (void*)output); - if (button_visible_ && !tbar_->all_outputs() && tbar_->show_output(output)) { - /* The task left the output of the current bar, make the button invisible */ - tbar_->remove_button(button); - button.hide(); - button_visible_ = false; - spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); - tbar_->update_bar_css_classes(); + if (tbar_->show_output(output)) { + on_bar_output_ = false; } + + if (!tbar_->all_outputs() && !on_bar_output_) { + /* The task left the output of the current bar, make the button invisible */ + hide_button(); + } +} + +void Task::show_button() { + if (button_visible_) { + return; + } + if (!size_allocate_connected_) { + button.signal_size_allocate().connect_notify( + sigc::mem_fun(this, &Task::on_button_size_allocated)); + size_allocate_connected_ = true; + } + tbar_->add_button(button); + if (!config_["active-only"].asBool() || active()) { + button.show(); + } + button_visible_ = true; + spdlog::debug("{} now visible on {}", repr(), bar_.output->name); + tbar_->update_bar_css_classes(); +} + +void Task::hide_button() { + if (!button_visible_) { + return; + } + tbar_->remove_button(button); + button.hide(); + button_visible_ = false; + spdlog::debug("{} now invisible on {}", repr(), bar_.output->name); + tbar_->update_bar_css_classes(); } void Task::handle_state(struct wl_array* state) { @@ -474,9 +492,9 @@ void Task::handle_closed() { if (it != tasks.end() && !(*it).ignored_) { Task& task = *it; task.squashed_ = false; - tbar_->add_button(task.button); - task.button.show(); - task.button_visible_ = true; + if (tbar_->all_outputs() || task.on_bar_output_) { + task.show_button(); + } } } From ebbc7ea4dc8ab397704ec6dc25e3ebd6ce16a4bf Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 5 Jul 2026 10:23:58 -0500 Subject: [PATCH 382/416] fix(wlr/taskbar): re-show task when it no longer matches ignore-list The un-ignore branch in hide_if_ignored() computed is_was_ignored after the enclosing condition already required ignored_ to be false, so it never ran, and a task whose app_id/title changed away from an ignore-list entry stayed hidden forever. Reset ignored_ and show the button again, subject to the same all-outputs/output-membership check used everywhere else. --- src/modules/wlr/taskbar.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/wlr/taskbar.cpp b/src/modules/wlr/taskbar.cpp index 1bba7dd8..34960624 100644 --- a/src/modules/wlr/taskbar.cpp +++ b/src/modules/wlr/taskbar.cpp @@ -290,14 +290,14 @@ void Task::hide_if_ignored() { if (tbar_->ignore_list().count(app_id_) || tbar_->ignore_list().count(title_)) { ignored_ = true; hide_button(); + return; } - if (!ignored_ && !squashed_) { - bool is_was_ignored = ignored_; + if (ignored_) { + /* The app_id/title changed to a value that is no longer ignored */ ignored_ = false; - if (is_was_ignored) { - auto output = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); - handle_output_enter(output); + if (!squashed_ && (tbar_->all_outputs() || on_bar_output_)) { + show_button(); } } } From 9d8a8356b56df21a4b941a57ebe99f6915f3456b Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 22:02:17 +0200 Subject: [PATCH 383/416] fix(mpd): don't dereference a null song when the queue is cleared mpd_run_current_song() returns NULL when there is no current song (e.g. after `mpc clear`), leaving song_ null. setLabel() always evaluates the fmt::format() tag arguments -- even for format-stopped -- so getTag() and getFilename() would call mpd_song_get_tag()/mpd_song_get_uri() on a null song and segfault. Guard both against a null song_. Fixes #5183 --- src/modules/mpd/mpd.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/mpd/mpd.cpp b/src/modules/mpd/mpd.cpp index 8598628d..035183a0 100644 --- a/src/modules/mpd/mpd.cpp +++ b/src/modules/mpd/mpd.cpp @@ -62,7 +62,8 @@ auto waybar::modules::MPD::update() -> void { std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const { std::string result = config_["unknown-tag"].isString() ? config_["unknown-tag"].asString() : "N/A"; - const char* tag = mpd_song_get_tag(song_.get(), type, idx); + // song_ is null when there is no current song (e.g. after `mpc clear`) (#5183). + const char* tag = song_ ? mpd_song_get_tag(song_.get(), type, idx) : nullptr; // mpd_song_get_tag can return NULL, so make sure it's valid before setting if (tag) result = tag; @@ -71,6 +72,9 @@ std::string waybar::modules::MPD::getTag(mpd_tag_type type, unsigned idx) const } std::string waybar::modules::MPD::getFilename() const { + if (!song_) { + return ""; + } std::string path = mpd_song_get_uri(song_.get()); size_t position = path.find_last_of("/"); if (position == std::string::npos) { From 445e2aec1ff3f289a268711a41522d72e0ee7734 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 22:02:49 +0200 Subject: [PATCH 384/416] fix(bar): avoid use-after-free segfault on exit Members are destroyed in reverse declaration order, so modules_all_ (and the modules it owns) are gone before the GtkWindow. Tearing down a mapped window emits `unmap`, whose handler runs toggleSuspend() over the already freed modules. Disconnect the map/unmap handlers in ~Bar first. Fixes #5182 --- include/bar.hpp | 4 ++++ src/bar.cpp | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/include/bar.hpp b/include/bar.hpp index f3dfa781..2dc51379 100644 --- a/include/bar.hpp +++ b/include/bar.hpp @@ -131,6 +131,10 @@ class Bar : public sigc::trackable { waybar::util::KillSignalAction onSigusr1 = util::SIGNALACTION_DEFAULT_SIGUSR1; waybar::util::KillSignalAction onSigusr2 = util::SIGNALACTION_DEFAULT_SIGUSR2; + + /* Disconnected in ~Bar before the modules are destroyed (#5182). */ + sigc::connection map_conn_; + sigc::connection unmap_conn_; }; } // namespace waybar diff --git a/src/bar.cpp b/src/bar.cpp index 1eb5ce41..ec3189c6 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -269,12 +269,12 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) window.signal_map_event().connect_notify(sigc::mem_fun(*this, &Bar::onMap)); - window.signal_unmap().connect([this]() { + unmap_conn_ = window.signal_unmap().connect([this]() { spdlog::debug("Output {} unmapped (DPMS off), suspending modules", output->name); toggleSuspend(true); }); - window.signal_map().connect([this]() { + map_conn_ = window.signal_map().connect([this]() { spdlog::debug("Output {} mapped (DPMS on), resuming modules", output->name); toggleSuspend(false); }); @@ -352,7 +352,12 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) } /* Need to define it here because of forward declared members */ -waybar::Bar::~Bar() = default; +waybar::Bar::~Bar() { + /* Destroying the window emits `unmap`, whose handler runs toggleSuspend() over + * modules_all_ -- already freed by this point. Disconnect first (#5182). */ + unmap_conn_.disconnect(); + map_conn_.disconnect(); +} void waybar::Bar::setMode(const std::string& mode) { using namespace std::literals::string_literals; From 9a10949938b3bcf827345687f6a137d556bfdd0f Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 22:21:46 +0200 Subject: [PATCH 385/416] ci(smoke): cover teardown, state transitions and fake hardware The smoke test only exercised steady-state rendering and killed waybar with SIGTERM, so whole crash classes were invisible: exit-time use-after-free (#5182), module state transitions (#5183) and hardware-backed modules (#5179). Add tiers that hit those paths under ASan (+ _GLIBCXX_ASSERTIONS): - lib.sh: assert_clean_exit (SIGINT teardown -> checks segfault/abort + ASan report emitted during destruction), output hotplug helpers, signal/reload, WAYBAR_WRAP hook, opt-in leak detection. - lifecycle.sh (gating): clean exit, runtime output hotplug (Bar/module destroy), toggle/reload churn, fast-interval teardown race (#5182 class). - fuzz.sh (gating): pathological custom-backend output (empty, nonzero exit, invalid JSON, huge, non-UTF8, empty format). - coverage.sh (gating): every Factory module must be classified for smoke coverage; a new unlisted module fails the job (#5179 slipped through). - modules.sh: also render the whole matrix inside a group; SIGINT teardown per module instead of SIGTERM. - state.sh (best-effort): real mpd driven through an empty queue (#5183) + stop/clear; pulseaudio + slider on a null sink. - hardware.sh (best-effort): backlight, backlight/slider (#5179) and battery via umockdev. - leakcheck.sh (report-only): clean-exit run under LSan. --- .github/workflows/smoke.yml | 39 +++++++++++- test/smoke/README.md | 32 ++++++++-- test/smoke/coverage.sh | 48 +++++++++++++++ test/smoke/fuzz.sh | 43 ++++++++++++++ test/smoke/hardware.sh | 87 +++++++++++++++++++++++++++ test/smoke/leakcheck.sh | 34 +++++++++++ test/smoke/lib.sh | 66 ++++++++++++++++++++- test/smoke/lifecycle.sh | 80 +++++++++++++++++++++++++ test/smoke/modules.sh | 28 ++++++++- test/smoke/state.sh | 114 ++++++++++++++++++++++++++++++++++++ 10 files changed, 558 insertions(+), 13 deletions(-) create mode 100755 test/smoke/coverage.sh create mode 100755 test/smoke/fuzz.sh create mode 100755 test/smoke/hardware.sh create mode 100755 test/smoke/leakcheck.sh create mode 100755 test/smoke/lifecycle.sh create mode 100755 test/smoke/state.sh diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 8de5ffe6..001b7ecf 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -38,6 +38,7 @@ jobs: sudo apt-get install --no-install-recommends -y \ sway labwc grim imagemagick fonts-dejavu-core jq \ at-spi2-core python3-pyatspi dbus-x11 \ + mpd mpc pulseaudio pulseaudio-utils umockdev \ meson ninja-build g++ pkg-config scdoc wayland-protocols \ gobject-introspection libdbusmenu-gtk3-dev libevdev-dev libfmt-dev \ libgirepository1.0-dev libgtk-3-dev libgtkmm-3.0-dev libinput-dev \ @@ -45,9 +46,17 @@ jobs: libpulse-dev libsigc++-2.0-dev libspdlog-dev libwayland-dev \ upower libxkbregistry-dev libxkbcommon-dev + # Cheap static gate (source-only): every Factory module must be classified + # for smoke coverage. Runs before the build so it fails fast. + - name: Module coverage tripwire + run: ./test/smoke/coverage.sh + - name: Build Waybar with AddressSanitizer run: | - meson setup build -Dman-pages=disabled -Db_sanitize=address + # _GLIBCXX_ASSERTIONS turns libstdc++ container bounds/precondition + # violations (e.g. out-of-range vector access) into aborts under ASan. + meson setup build -Dman-pages=disabled -Db_sanitize=address \ + -Dcpp_args=-D_GLIBCXX_ASSERTIONS ninja -C build # --- Tier 0 + 2: render + sanitizer/critical gate --- @@ -83,10 +92,36 @@ jobs: echo "::warning::No reference image; run this workflow via 'Run workflow' with bless=true to create it." fi - # --- Tier 1: per-module matrix --- + # --- Tier 1: per-module matrix (standalone + grouped) --- - name: Per-module render matrix run: ./test/smoke/modules.sh module-shots + # --- Tier 4: lifecycle / teardown (exit, output hotplug, reload) --- + # Runs the C++ destructor path a plain kill never touches (#5182). + - name: Lifecycle / teardown + run: ./test/smoke/lifecycle.sh + + # --- Tier 4: custom-backend fuzz (pathological exec output) --- + - name: Custom-backend fuzz + run: ./test/smoke/fuzz.sh + + # --- Tier 5: state transitions with real daemons (mpd/pulseaudio) --- + # Best-effort: daemon setup varies by runner; keep non-blocking for now. + - name: State transitions (mpd / pulseaudio) + continue-on-error: true + run: ./test/smoke/state.sh + + # --- Tier 6: fake hardware via umockdev (backlight/slider, battery) --- + # Best-effort: the mock device descriptions may need per-distro tuning. + - name: Fake hardware (umockdev) + continue-on-error: true + run: ./test/smoke/hardware.sh + + # --- Leak check (report-only) — we fixed several leaks recently --- + - name: Leak check (LSan, report-only) + continue-on-error: true + run: ./test/smoke/leakcheck.sh + # --- Tier 2: pointer interaction (best-effort) --- # Headless seats may not expose a pointer capability, so injected clicks # can be dropped; keep this non-blocking while it is hardened. diff --git a/test/smoke/README.md b/test/smoke/README.md index 60800362..86c3dcae 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -10,18 +10,31 @@ AddressSanitizer** and runs the real binary inside a **headless** compositor | Tier | Check | Script | | --- | --- | --- | | 0 | Launches, stays alive, and logs **no ASan / `Gtk-CRITICAL` / crash** — for every launch below | [`lib.sh`](lib.sh) `assert_clean` | +| — | **Coverage tripwire** — every `Factory` module must be classified (source-only, runs before the build) | [`coverage.sh`](coverage.sh) | | 1 | **Renders** — deterministic config screenshotted with `grim`, bar strip not blank | [`run.sh`](run.sh) | -| 1 | **Per-module matrix** — each headless-safe module rendered in isolation | [`modules.sh`](modules.sh) | +| 1 | **Per-module matrix** — each headless-safe module rendered standalone **and inside a group**, each torn down via SIGINT | [`modules.sh`](modules.sh) | | 2 | **Golden** — screenshot vs [`reference.png`](reference.png) (fuzz 8%, 800px) | workflow | | 2 | **Interaction** — inject pointer clicks via sway; assert `on-click` side effect + `format-alt` toggle | [`interact.sh`](interact.sh) | | 2 | **Accessibility** — assert module labels via the AT-SPI tree (robust to fonts) | [`a11y.sh`](a11y.sh) / [`a11y.py`](a11y.py) | | 3 | **Layout matrix** — top / bottom / left (vertical) + HiDPI (scale 2) | [`positions.sh`](positions.sh) | | 3 | **Second compositor** — same launch under `labwc` | workflow (`COMPOSITOR=labwc`) | +| 4 | **Lifecycle / teardown** — clean SIGINT exit, runtime **output hotplug**, toggle + reload churn, fast-interval teardown (the C++ destructor path a `kill` never runs, #5182) | [`lifecycle.sh`](lifecycle.sh) | +| 4 | **Custom-backend fuzz** — empty / non-zero-exit / invalid-JSON / huge / non-UTF8 / empty-format exec output | [`fuzz.sh`](fuzz.sh) | +| 5 | **State transitions** *(best-effort)* — real `mpd` driven through an **empty queue** (#5183) + stop/clear, `pulseaudio` + slider on a null sink | [`state.sh`](state.sh) | +| 6 | **Fake hardware** *(best-effort)* — `backlight`, `backlight/slider` (#5179) and `battery` via **umockdev** | [`hardware.sh`](hardware.sh) | +| — | **Leak check** *(report-only)* — clean-exit run under LSan | [`leakcheck.sh`](leakcheck.sh) | -AddressSanitizer runs under *all* of the above, so any memory bug in the real -render path aborts the run. The interaction step is best-effort -(`continue-on-error`): a headless seat exposes no pointer capability, so injected -clicks may be dropped — hardening it needs a virtual pointer (uinput/wlr-virtual-pointer). +AddressSanitizer (plus `-D_GLIBCXX_ASSERTIONS`) runs under *all* of the above, so +any memory bug in the real render/teardown path aborts the run. Tiers 5–6 and the +leak check are best-effort (`continue-on-error`): they depend on daemon setup / +mock devices that vary by runner. The interaction step is likewise best-effort — a +headless seat exposes no pointer capability, so injected clicks may be dropped +(hardening it needs a virtual pointer, uinput/wlr-virtual-pointer). + +Because the teardown path (`~Bar` → window unmap → `toggleSuspend`) is where the +exit-time crashes live, tests assert on a **clean SIGINT exit** via +`smoke::assert_clean_exit` rather than killing with SIGTERM, and re-check the log +for sanitizer reports emitted *during* destruction. ## Updating the reference image @@ -37,13 +50,20 @@ Do this whenever an **intended** visual change trips the golden comparison. ## Run locally -On a machine with `sway`, `grim`, `imagemagick` and Waybar built: +On a machine with `sway`, `grim`, `imagemagick`, `jq` and Waybar built: ```sh export WAYBAR_BIN=$PWD/build/waybar +./test/smoke/coverage.sh # no compositor needed ./test/smoke/run.sh test/smoke/config.jsonc test/smoke/style.css /tmp/bar.png ./test/smoke/modules.sh /tmp/module-shots +./test/smoke/lifecycle.sh +./test/smoke/fuzz.sh ./test/smoke/interact.sh /tmp/interaction-shots ./test/smoke/positions.sh /tmp/layout-shots dbus-run-session -- ./test/smoke/a11y.sh +# best-effort (need extra daemons / tools): +./test/smoke/state.sh # needs mpd + mpc, pulseaudio +./test/smoke/hardware.sh # needs umockdev +./test/smoke/leakcheck.sh ``` diff --git a/test/smoke/coverage.sh b/test/smoke/coverage.sh new file mode 100755 index 00000000..9969d679 --- /dev/null +++ b/test/smoke/coverage.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Tier — coverage tripwire. Every module the Factory can build must be classified +# below: either smoke-tested headless (SAFE), or explicitly parked as needing real +# hardware/a daemon (HARDWARE, best-effort in state.sh / hardware.sh) or a specific +# compositor (COMPOSITOR). A newly added module that isn't listed fails this job, +# forcing a conscious "is it covered?" decision instead of silently escaping the +# smoke net -- which is how the slider/backlight crash class (#5179) slipped by. +# +# Usage: coverage.sh +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$DIR/../.." && pwd)" +FACTORY_SRC="$ROOT/src/factory.cpp" + +mapfile -t FACTORY < <(grep -oE 'ref == "[^"]+"' "$FACTORY_SRC" | sed -E 's/.*"(.*)"/\1/' | sort -u) +[ "${#FACTORY[@]}" -gt 0 ] || { echo "::error::could not parse module names from $FACTORY_SRC"; exit 1; } + +# Rendered headless with no hardware/daemon/compositor dependency. +SAFE=(clock cpu cpu_usage cpu_graph cpu_frequency load memory disk + idle_inhibitor image user inhibitor) + +# Need real hardware or a system daemon; covered best-effort by hardware.sh / state.sh. +HARDWARE=(backlight backlight/slider battery upower power-profiles-daemon bluetooth + network wwan gps pulseaudio pulseaudio/slider wireplumber jack sndio cava + mpd mpris temperature keyboard-state systemd-failed-units privacy tray gamemode) + +# Need a specific compositor / its IPC to produce output. +COMPOSITOR=(sway/language sway/mode sway/scratchpad sway/window sway/workspaces + hyprland/language hyprland/submap hyprland/window hyprland/windowcount hyprland/workspaces + river/layout river/mode river/tags river/window + niri/language niri/window niri/workspaces + dwl/tags dwl/window + mango/keymode mango/language mango/layout mango/window mango/workspaces + wayfire/window wayfire/workspaces ext/workspaces wlr/taskbar) + +declare -A known=() +for m in "${SAFE[@]}" "${HARDWARE[@]}" "${COMPOSITOR[@]}"; do known["$m"]=1; done + +missing=() +for m in "${FACTORY[@]}"; do [ -n "${known[$m]:-}" ] || missing+=("$m"); done + +if [ "${#missing[@]}" -gt 0 ]; then + echo "::error::modules not classified for smoke coverage: ${missing[*]}" + echo "Add each to SAFE / HARDWARE / COMPOSITOR in test/smoke/coverage.sh" + echo "(and, if it can run headless, to the matrix in modules.sh)." + exit 1 +fi +echo "✓ all ${#FACTORY[@]} factory modules are classified for smoke coverage" diff --git a/test/smoke/fuzz.sh b/test/smoke/fuzz.sh new file mode 100755 index 00000000..29f6a767 --- /dev/null +++ b/test/smoke/fuzz.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Tier — custom-backend fuzz. The custom module's exec output flows straight into +# formatting / sanitising / (optionally) JSON parsing, so feed it the pathological +# inputs that tend to segfault (empty, non-zero exit, invalid JSON, huge, non-UTF8, +# empty format) and assert no crash / critical, including on clean exit. +# +# Usage: fuzz.sh +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" + +smoke::setup +smoke::start_compositor +trap smoke::stop EXIT + +fail=0 + +# run_case +run_case() { + local name="$1" mod="$2" cfg + echo "::group::fuzz: $name" + cfg="$(mktemp --suffix=.json)" + cat > "$cfg" </dev/null; then + echo "(umockdev not installed, skipping hardware tier)"; exit 0 +fi + +smoke::setup +smoke::start_compositor +trap smoke::stop EXIT + +fail=0 + +BL_DEV="$(mktemp --suffix=.umockdev)" +cat > "$BL_DEV" <<'EOF' +P: /devices/platform/fake_backlight/backlight/intel_backlight +E: SUBSYSTEM=backlight +A: brightness=500 +A: max_brightness=1000 +A: actual_brightness=500 +A: bl_power=0 +EOF + +BAT_DEV="$(mktemp --suffix=.umockdev)" +cat > "$BAT_DEV" <<'EOF' +P: /devices/platform/fake_battery/power_supply/BAT0 +E: SUBSYSTEM=power_supply +E: POWER_SUPPLY_NAME=BAT0 +A: type=Battery +A: present=1 +A: status=Discharging +A: capacity=55 +A: energy_now=5500000 +A: energy_full=10000000 +A: power_now=1000000 +EOF + +# run_mock +run_mock() { + local name="$1" dev="$2" mod="$3" cfg + echo "::group::hardware: $name" + cfg="$(mktemp --suffix=.json)" + cat > "$cfg" < "$cfg" <<'EOF' +{ "layer": "top", "position": "top", "height": 30, + "modules-center": ["group/bl"], + "group/bl": { "modules": ["backlight/slider"] }, + "backlight/slider": { "min": 0, "max": 100, "orientation": "horizontal" } } +EOF +WAYBAR_WRAP="umockdev-run --device $BL_DEV --" smoke::launch_waybar "$cfg" "$DIR/style.css" 5 +smoke::assert_alive || fail=1 +smoke::assert_clean || fail=1 +smoke::stop; WAYBAR_PID="" +echo "::endgroup::" + +[ "$fail" = 0 ] || { echo "::error::hardware tier failed"; exit 1; } +echo "✓ fake-hardware tier passed" diff --git a/test/smoke/leakcheck.sh b/test/smoke/leakcheck.sh new file mode 100755 index 00000000..36f5d21e --- /dev/null +++ b/test/smoke/leakcheck.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Report-only leak check. Runs the deterministic config, exits cleanly (SIGINT, +# so LeakSanitizer's atexit hook actually runs -- a SIGTERM kill would skip it), +# and surfaces any leak report. Never fails the job: leaks in GTK/GLib are too +# noisy to gate on, but regressions in our own code are worth seeing. +# +# Usage: leakcheck.sh +set -uo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" + +export SMOKE_DETECT_LEAKS=1 +smoke::setup +smoke::start_compositor +trap smoke::stop EXIT + +smoke::launch_waybar "$DIR/config.jsonc" "$DIR/style.css" 5 +smoke::assert_alive || true + +# Clean exit so LSan runs. +kill -INT "$WAYBAR_PID" 2>/dev/null || true +for _ in $(seq 1 20); do kill -0 "$WAYBAR_PID" 2>/dev/null || break; sleep 0.5; done +kill -9 "$WAYBAR_PID" 2>/dev/null || true + +if grep -iqE 'LeakSanitizer|Direct leak|Indirect leak' "$SMOKE_LOG"; then + echo "::warning::LeakSanitizer reported leaks (report-only)" + echo "::group::leak report" + grep -iE 'leak|SUMMARY|#[0-9]+ 0x' "$SMOKE_LOG" || true + echo "::endgroup::" +else + echo "✓ no leaks reported" +fi +WAYBAR_PID="" diff --git a/test/smoke/lib.sh b/test/smoke/lib.sh index bed35abb..5ba02d5b 100755 --- a/test/smoke/lib.sh +++ b/test/smoke/lib.sh @@ -18,6 +18,9 @@ HEIGHT="${HEIGHT:-1080}" SCALE="${SCALE:-1}" COMPOSITOR="${COMPOSITOR:-sway}" WAYBAR_BIN="${WAYBAR_BIN:-waybar}" +# Optional command prefix for the waybar launch (e.g. `umockdev-run -d dev --`), +# used by the hardware tier to fake sysfs/udev devices. Empty by default. +WAYBAR_WRAP="${WAYBAR_WRAP:-}" SMOKE_LOG="" COMP_PID="" @@ -32,7 +35,9 @@ smoke::setup() { export LIBGL_ALWAYS_SOFTWARE=1 # Keep ASan/UBSan noise low but fatal on real errors (leaks in GTK/glib are # too noisy to gate on, so only memory-corruption and UB abort the run). - export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:detect_odr_violation=0" + # Leak detection is opt-in (leakcheck.sh sets SMOKE_DETECT_LEAKS=1) and needs + # a clean exit for LSan's atexit hook to run. + export ASAN_OPTIONS="detect_leaks=${SMOKE_DETECT_LEAKS:-0}:halt_on_error=1:abort_on_error=1:detect_odr_violation=0" export UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" } @@ -84,7 +89,10 @@ smoke::launch_waybar() { SMOKE_LOG="$(mktemp)" local args=(-c "$config" -l debug) [ -n "$style" ] && args+=(-s "$style") - "$WAYBAR_BIN" "${args[@]}" >"$SMOKE_LOG" 2>&1 & + local run=() + [ -n "$WAYBAR_WRAP" ] && read -ra run <<<"$WAYBAR_WRAP" + run+=("$WAYBAR_BIN" "${args[@]}") + "${run[@]}" >"$SMOKE_LOG" 2>&1 & WAYBAR_PID=$! sleep "$settle" } @@ -129,6 +137,60 @@ smoke::assert_not_blank() { echo "✓ bar rendered content" } +# Send a signal to waybar and give it a moment to act on it. +# smoke::signal [settle_seconds] +smoke::signal() { + kill -"$1" "$WAYBAR_PID" 2>/dev/null || true + sleep "${2:-2}" +} + +# Reload waybar's config in place (recreates bars + modules). +smoke::reload() { smoke::signal SIGUSR2 "${1:-3}"; } + +# Create a headless output at runtime (sway only). wlroots' headless backend +# lets sway hotplug outputs, which makes waybar build a new Bar + module set. +smoke::add_output() { + [ "$COMPOSITOR" = sway ] || { echo "(add_output: sway only, skipped)"; return 0; } + swaymsg create_output >/dev/null 2>&1 || echo "(create_output unsupported, skipped)" + sleep 2 +} + +# Remove every headless output except the primary HEADLESS-1. Destroying an +# output tears down its Bar -> ~Bar -> GtkWindow unmap -> toggleSuspend(), i.e. +# the exact runtime path that segfaulted in #5182 -- exercised here under ASan. +smoke::remove_output() { + [ "$COMPOSITOR" = sway ] || return 0 + local o + for o in $(swaymsg -t get_outputs 2>/dev/null | jq -r '.[].name' 2>/dev/null | grep -v '^HEADLESS-1$' || true); do + swaymsg output "$o" unplug >/dev/null 2>&1 || true + done + sleep 2 +} + +# Shut waybar down the clean way (SIGINT -> Client::reset -> gtk quit -> +# bars.clear -> ~Bar) and assert the destructor path neither crashed nor tripped +# a sanitizer report. A plain SIGTERM/kill never runs this path, which is why the +# exit-time use-after-free (#5182) went unnoticed. Call instead of smoke::stop's +# kill when you want the teardown itself checked. +smoke::assert_clean_exit() { + [ -n "$WAYBAR_PID" ] || { echo "(no waybar running)"; return 0; } + kill -INT "$WAYBAR_PID" 2>/dev/null || true + local _ ; for _ in $(seq 1 20); do kill -0 "$WAYBAR_PID" 2>/dev/null || break; sleep 0.5; done + if kill -0 "$WAYBAR_PID" 2>/dev/null; then + echo "::error::waybar did not exit on SIGINT (hung during teardown)" + echo "::group::waybar log"; smoke::log; echo "::endgroup::" + kill -9 "$WAYBAR_PID" 2>/dev/null || true; WAYBAR_PID=""; return 1 + fi + local st=0; wait "$WAYBAR_PID" 2>/dev/null || st=$? + WAYBAR_PID="" + if [ "$st" -ge 128 ]; then + echo "::error::waybar crashed on exit (killed by signal $((st - 128)))" + echo "::group::waybar log"; smoke::log; echo "::endgroup::"; return 1 + fi + smoke::assert_clean || return 1 + echo "✓ clean exit (status $st)" +} + smoke::stop() { kill "$WAYBAR_PID" 2>/dev/null || true swaymsg -q exit 2>/dev/null || kill "$COMP_PID" 2>/dev/null || true diff --git a/test/smoke/lifecycle.sh b/test/smoke/lifecycle.sh new file mode 100755 index 00000000..d8210b89 --- /dev/null +++ b/test/smoke/lifecycle.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Tier 4 — lifecycle / teardown safety net. +# +# Everything above renders in steady state and kills waybar with SIGTERM, which +# never runs the C++ destructors. The crash classes that hurt most (exit, +# DPMS/output unmap, reload) all live in that create/destroy path, so exercise it +# explicitly under ASan: +# 1. clean exit on SIGINT (the real Bar teardown path, #5182) +# 2. runtime output hotplug (bar + module destruction while running, #5182) +# 3. visibility toggle + config reload churn (SIGUSR1 / SIGUSR2) +# 4. fast-interval modules torn down mid-tick (thread vs destructor races) +# +# Usage: lifecycle.sh +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" + +CFG="$DIR/config.jsonc" +STYLE="$DIR/style.css" + +smoke::setup +smoke::start_compositor +trap smoke::stop EXIT + +fail=0 + +# --- 1) clean exit on SIGINT (destructor path, #5182) -------------------- +echo "::group::clean exit (SIGINT)" +smoke::launch_waybar "$CFG" "$STYLE" 5 +smoke::assert_alive || fail=1 +smoke::assert_clean_exit || fail=1 +echo "::endgroup::" + +# --- 2) output hotplug at runtime (bar/module destroy, #5182) ------------ +if [ "$COMPOSITOR" = sway ]; then + echo "::group::output hotplug" + smoke::launch_waybar "$CFG" "$STYLE" 5 + smoke::assert_alive || fail=1 + for _ in 1 2 3; do + smoke::add_output + smoke::assert_alive || { fail=1; break; } + smoke::remove_output # destroys that output's Bar -> ~Bar -> unmap + smoke::assert_alive || { fail=1; break; } + done + smoke::assert_clean || fail=1 + smoke::assert_clean_exit || fail=1 + echo "::endgroup::" +fi + +# --- 3) visibility toggle + reload churn --------------------------------- +echo "::group::signal churn (toggle + reload)" +smoke::launch_waybar "$CFG" "$STYLE" 5 +smoke::assert_alive || fail=1 +for _ in 1 2 3; do smoke::signal SIGUSR1 1; done # hide/show mode churn +smoke::reload # SIGUSR2: recreate bars/modules +smoke::assert_alive || fail=1 +smoke::assert_clean_exit || fail=1 +echo "::endgroup::" + +# --- 4) fast-interval modules torn down mid-tick ------------------------- +echo "::group::fast-interval teardown race" +cfg="$(mktemp --suffix=.json)" +cat > "$cfg" <<'EOF' +{ + "layer": "top", "position": "top", "height": 30, + "modules-center": ["clock", "cpu", "memory", "custom/tick"], + "clock": { "interval": 1 }, + "cpu": { "interval": 1 }, + "memory": { "interval": 1 }, + "custom/tick": { "exec": "date +%s", "interval": 1 } +} +EOF +smoke::launch_waybar "$cfg" "$STYLE" 8 +smoke::assert_alive || fail=1 +smoke::assert_clean_exit || fail=1 # SIGINT while worker threads are mid-tick +echo "::endgroup::" + +[ "$fail" = 0 ] || { echo "::error::lifecycle tier failed"; exit 1; } +echo "✓ lifecycle tier passed" diff --git a/test/smoke/modules.sh b/test/smoke/modules.sh index 46f13c88..b7e2541a 100755 --- a/test/smoke/modules.sh +++ b/test/smoke/modules.sh @@ -47,13 +47,35 @@ EOF smoke::assert_not_blank "$SHOTDIR/$safe.png" || ok=0 fi [ "$ok" = 1 ] || fails+=("$mod") - kill "$WAYBAR_PID" 2>/dev/null || true - sleep 1 + # SIGINT teardown (not SIGTERM) so each module's destructor path is ASan-checked. + smoke::assert_clean_exit || fails+=("$mod (exit)") echo "::endgroup::" done +# --- all modules inside a group ----------------------------------------- +# Group children have a different construct/destroy path than top-level modules +# (#5179 crashed inside a group), so render the whole matrix wrapped in one. +echo "::group::modules inside a group" +mods_json="$(printf '%s\n' "${!MODULES[@]}" | jq -R . | jq -s .)" +opts_json='{}' +for mod in "${!MODULES[@]}"; do + opts_json="$(jq --arg k "$mod" --argjson v "${MODULES[$mod]}" '. + {($k):$v}' <<<"$opts_json")" +done +gcfg="$(mktemp --suffix=.json)" +jq -n --argjson mods "$mods_json" --argjson opts "$opts_json" ' + {layer:"top",position:"top",height:30,"modules-center":["group/g"],"group/g":{modules:$mods}} + $opts' \ + > "$gcfg" +smoke::launch_waybar "$gcfg" "$DIR/style.css" 5 +ok=1 +smoke::assert_alive || ok=0 +smoke::assert_clean || ok=0 +[ "$ok" = 1 ] && { smoke::screenshot "$SHOTDIR/_group.png"; smoke::assert_not_blank "$SHOTDIR/_group.png" || ok=0; } +smoke::assert_clean_exit || ok=0 +[ "$ok" = 1 ] || fails+=("group") +echo "::endgroup::" + if [ "${#fails[@]}" -gt 0 ]; then echo "::error::modules failed to render cleanly: ${fails[*]}" exit 1 fi -echo "✓ all ${#MODULES[@]} modules rendered cleanly" +echo "✓ all ${#MODULES[@]} modules rendered cleanly (standalone + grouped)" diff --git a/test/smoke/state.sh b/test/smoke/state.sh new file mode 100755 index 00000000..80c1b04f --- /dev/null +++ b/test/smoke/state.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# Tier — state-transition coverage with real daemons. Steady-state rendering +# never hits the empty/edge states where modules crash. Drive an actual mpd and +# PulseAudio through those states under ASan: +# - mpd: connect with an EMPTY queue (no current song -> null song_, #5183), +# then clear/add/stop/play while waybar is live +# - pulseaudio + pulseaudio/slider against a null sink +# +# Best-effort: each sub-tier is skipped (not failed) if its daemon isn't +# installed. Run continue-on-error in CI while the fixtures settle. +# +# Usage: state.sh +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$DIR/lib.sh" + +smoke::setup +smoke::start_compositor +trap 'smoke::stop; [ -n "${MPD_PID:-}" ] && kill "$MPD_PID" 2>/dev/null; [ -n "${PA_STARTED:-}" ] && pulseaudio --kill 2>/dev/null; true' EXIT + +fail=0 +MPD_PID="" +PA_STARTED="" + +# ---------------------------------------------------------------- mpd ----- +mpd_tier() { + if ! command -v mpd >/dev/null || ! command -v mpc >/dev/null; then + echo "(mpd/mpc not installed, skipping mpd tier)"; return 0 + fi + echo "::group::mpd: empty queue + transitions (#5183)" + local d conf; d="$(mktemp -d)"; conf="$d/mpd.conf" + mkdir -p "$d/music" "$d/playlists" + cat > "$conf" </tmp/mpd.log 2>&1 & + MPD_PID=$! + local up=0 _ + for _ in $(seq 1 20); do + mpc -h 127.0.0.1 -p 6600 status >/dev/null 2>&1 && { up=1; break; } + sleep 0.5 + done + if [ "$up" != 1 ]; then + echo "::warning::mpd did not come up; skipping"; cat /tmp/mpd.log || true; echo "::endgroup::"; return 0 + fi + + mpc -h 127.0.0.1 -p 6600 clear >/dev/null 2>&1 || true # empty queue: the crash trigger + + local cfg; cfg="$(mktemp --suffix=.json)" + cat > "$cfg" <<'EOF' +{ + "layer": "top", "position": "top", "height": 30, + "modules-center": ["mpd"], + "mpd": { "server": "127.0.0.1", "port": 6600, "interval": 1, + "format": "{album} - {artist} - {title}", "format-stopped": "stopped" } +} +EOF + smoke::launch_waybar "$cfg" "$DIR/style.css" 5 + smoke::assert_alive || fail=1 # would have segfaulted pre-#5183 fix + smoke::assert_clean || fail=1 + # Churn state while live: clear again, then stop/play. + mpc -h 127.0.0.1 -p 6600 clear >/dev/null 2>&1 || true; sleep 2 + mpc -h 127.0.0.1 -p 6600 stop >/dev/null 2>&1 || true; sleep 2 + smoke::assert_alive || fail=1 + smoke::assert_clean_exit || fail=1 + echo "::endgroup::" +} + +# -------------------------------------------------------- pulseaudio ------ +pulse_tier() { + if ! command -v pulseaudio >/dev/null; then + echo "(pulseaudio not installed, skipping pulse tier)"; return 0 + fi + echo "::group::pulseaudio + pulseaudio/slider (null sink)" + export PULSE_RUNTIME_PATH="$XDG_RUNTIME_DIR/pulse" + if pulseaudio --start --exit-idle-time=-1 --log-target=stderr >/tmp/pa.log 2>&1; then + PA_STARTED=1 + else + echo "::warning::pulseaudio failed to start; skipping"; cat /tmp/pa.log || true; echo "::endgroup::"; return 0 + fi + pactl load-module module-null-sink sink_name=smoke_null >/dev/null 2>&1 || true + + local cfg; cfg="$(mktemp --suffix=.json)" + cat > "$cfg" <<'EOF' +{ + "layer": "top", "position": "top", "height": 30, + "modules-center": ["pulseaudio", "pulseaudio/slider"], + "pulseaudio": { "format": "{volume}%" }, + "pulseaudio/slider": { "min": 0, "max": 100, "orientation": "horizontal" } +} +EOF + smoke::launch_waybar "$cfg" "$DIR/style.css" 5 + smoke::assert_alive || fail=1 + smoke::assert_clean || fail=1 + pactl set-sink-volume @DEFAULT_SINK@ 40% >/dev/null 2>&1 || true; sleep 1 + pactl set-sink-mute @DEFAULT_SINK@ 1 >/dev/null 2>&1 || true; sleep 1 + smoke::assert_alive || fail=1 + smoke::assert_clean_exit || fail=1 + echo "::endgroup::" +} + +mpd_tier +pulse_tier + +[ "$fail" = 0 ] || { echo "::error::state tier failed"; exit 1; } +echo "✓ state-transition tier passed" From 74f0d3393a0b5cc13cd911ac8e2cb59f643a4191 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 22:39:35 +0200 Subject: [PATCH 386/416] ci(smoke): fix the mpd / hardware / leak tiers so they actually run The first run went green but the best-effort tiers silently didn't exercise anything: - state.sh: the mpd `audio_output { ... }` block was on one line, which mpd rejects ("Unknown tokens after '{'"), so mpd never started and the mpd sub-tier (the #5183 empty-queue repro) skipped. Split the block across lines. Also move teardown into a cleanup() with `|| true` so a dead-pid kill under `set -e` no longer makes the step exit 1. - hardware.sh: umockdev-run's LD_PRELOAD lands ahead of the linked-in ASan runtime, so ASan aborted before main() ("ASan runtime does not come first") and waybar never started -- the backlight/slider (#5179) path wasn't tested. Set verify_asan_link_order=0 so the instrumented binary runs under the preload. - leakcheck.sh: abort_on_error=1 turned LSan's exit report into a core dump; force abort_on_error=0 for this report-only tier. - lib.sh: assert_clean now also flags "ASan runtime does not come first" so a future preload/link-order regression fails loudly instead of passing. --- test/smoke/hardware.sh | 5 +++++ test/smoke/leakcheck.sh | 3 +++ test/smoke/lib.sh | 2 +- test/smoke/state.sh | 20 +++++++++++++++----- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/test/smoke/hardware.sh b/test/smoke/hardware.sh index 03101d23..4cd84028 100755 --- a/test/smoke/hardware.sh +++ b/test/smoke/hardware.sh @@ -19,6 +19,11 @@ if ! command -v umockdev-run >/dev/null; then fi smoke::setup +# waybar is linked with ASan, but umockdev-run injects its own LD_PRELOAD, which +# lands ahead of the ASan runtime and makes ASan abort before main() ("ASan +# runtime does not come first"). Disabling the link-order check lets the +# instrumented binary run under the preload. +export ASAN_OPTIONS="${ASAN_OPTIONS}:verify_asan_link_order=0" smoke::start_compositor trap smoke::stop EXIT diff --git a/test/smoke/leakcheck.sh b/test/smoke/leakcheck.sh index 36f5d21e..005f5740 100755 --- a/test/smoke/leakcheck.sh +++ b/test/smoke/leakcheck.sh @@ -12,6 +12,9 @@ source "$DIR/lib.sh" export SMOKE_DETECT_LEAKS=1 smoke::setup +# Report leaks without aborting: abort_on_error=1 (the default gate) would turn +# LSan's exit-time report into a core dump. This tier only reports. +export ASAN_OPTIONS="detect_leaks=1:halt_on_error=0:abort_on_error=0:detect_odr_violation=0" smoke::start_compositor trap smoke::stop EXIT diff --git a/test/smoke/lib.sh b/test/smoke/lib.sh index 5ba02d5b..e3848442 100755 --- a/test/smoke/lib.sh +++ b/test/smoke/lib.sh @@ -111,7 +111,7 @@ smoke::assert_alive() { # Fail on sanitizer reports and GTK/GLib criticals in the log. smoke::assert_clean() { local bad - bad="$(grep -iE 'AddressSanitizer|runtime error:|LeakSanitizer|Gtk-CRITICAL|GLib-CRITICAL|GLib-GObject-CRITICAL|assertion .*failed|segfault|terminate called|SUMMARY: .*Sanitizer' "$SMOKE_LOG" || true)" + bad="$(grep -iE 'AddressSanitizer|runtime error:|LeakSanitizer|Gtk-CRITICAL|GLib-CRITICAL|GLib-GObject-CRITICAL|assertion .*failed|segfault|terminate called|SUMMARY: .*Sanitizer|ASan runtime does not come first' "$SMOKE_LOG" || true)" if [ -n "$bad" ]; then echo "::error::waybar reported sanitizer/critical issues:" echo "$bad" diff --git a/test/smoke/state.sh b/test/smoke/state.sh index 80c1b04f..77fb31e2 100755 --- a/test/smoke/state.sh +++ b/test/smoke/state.sh @@ -15,14 +15,21 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib.sh source "$DIR/lib.sh" -smoke::setup -smoke::start_compositor -trap 'smoke::stop; [ -n "${MPD_PID:-}" ] && kill "$MPD_PID" 2>/dev/null; [ -n "${PA_STARTED:-}" ] && pulseaudio --kill 2>/dev/null; true' EXIT - fail=0 MPD_PID="" PA_STARTED="" +cleanup() { + smoke::stop + [ -n "$MPD_PID" ] && kill "$MPD_PID" 2>/dev/null || true + [ -n "$PA_STARTED" ] && pulseaudio --kill 2>/dev/null || true + return 0 +} + +smoke::setup +smoke::start_compositor +trap cleanup EXIT + # ---------------------------------------------------------------- mpd ----- mpd_tier() { if ! command -v mpd >/dev/null || ! command -v mpc >/dev/null; then @@ -39,7 +46,10 @@ state_file "$d/state" pid_file "$d/pid" bind_to_address "127.0.0.1" port "6600" -audio_output { type "null" name "null" } +audio_output { + type "null" + name "null" +} EOF mpd --no-daemon "$conf" >/tmp/mpd.log 2>&1 & MPD_PID=$! From a7b3a2c0db8001ebd94ff57fd5791edeee56fc79 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 23:00:33 +0200 Subject: [PATCH 387/416] ci(smoke): fix state tier hang from bare wait on a lingering daemon The state tier started actually running mpd in 74f0d33, which exposed a deadlock in teardown: state.sh launches `mpd --no-daemon &` and never kills it during the tier, and its cleanup() called smoke::stop *before* killing mpd. smoke::stop ended in a bare `wait`, which reaps *every* background job of the shell -- including the still-running mpd -- so it blocked until GitHub's 6h job timeout. continue-on-error doesn't help: it catches failures, not hangs. - lib.sh: smoke::stop now waits only on the PIDs it owns (waybar, compositor), so an unrelated daemon left running by a tier can't deadlock teardown. - state.sh: cleanup() tears down mpd/pulseaudio before smoke::stop. - smoke.yml: add timeout-minutes: 20 so a future hang fails fast instead of burning the default 6h runner budget. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/smoke.yml | 4 ++++ test/smoke/lib.sh | 9 ++++++++- test/smoke/state.sh | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 001b7ecf..e680aaea 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -29,6 +29,10 @@ env: jobs: smoke: runs-on: ubuntu-latest + # Safety net: the whole smoke job is minutes of work. Cap it so a hung tier + # (e.g. a daemon left running under a bare `wait`) fails fast instead of + # burning the default 6h runner budget. + timeout-minutes: 20 steps: - uses: actions/checkout@v6 diff --git a/test/smoke/lib.sh b/test/smoke/lib.sh index e3848442..82367206 100755 --- a/test/smoke/lib.sh +++ b/test/smoke/lib.sh @@ -194,5 +194,12 @@ smoke::assert_clean_exit() { smoke::stop() { kill "$WAYBAR_PID" 2>/dev/null || true swaymsg -q exit 2>/dev/null || kill "$COMP_PID" 2>/dev/null || true - wait 2>/dev/null || true + # Wait only on the processes we own. A bare `wait` reaps *every* background + # job of the caller's shell, so a tier that leaves an unrelated daemon + # running (state.sh's mpd, killed later in its own cleanup) would deadlock + # here until GitHub's 6h job timeout. + local p + for p in "$WAYBAR_PID" "$COMP_PID"; do + [ -n "$p" ] && wait "$p" 2>/dev/null || true + done } diff --git a/test/smoke/state.sh b/test/smoke/state.sh index 77fb31e2..697347f5 100755 --- a/test/smoke/state.sh +++ b/test/smoke/state.sh @@ -20,9 +20,11 @@ MPD_PID="" PA_STARTED="" cleanup() { - smoke::stop + # Tear our daemons down first: smoke::stop waits on the compositor/waybar, + # and a still-running mpd/pulseaudio must not be left for anything to block on. [ -n "$MPD_PID" ] && kill "$MPD_PID" 2>/dev/null || true [ -n "$PA_STARTED" ] && pulseaudio --kill 2>/dev/null || true + smoke::stop return 0 } From 699589b66a6652496e909fbc3688c3c2b296652b Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 23:04:58 +0200 Subject: [PATCH 388/416] fix(river/tags): parse hide-vacant once, not in the wl callbacks config_["hide-vacant"].asBool() was called from the river status listeners. When the option is given as a string ("true") jsoncpp's asBool() throws "Value is not convertible to bool", and that exception unwinding through libwayland's C dispatch aborts the process. Parse it once in the constructor into a bool member (accepting the string form) and read the cached value. Fixes #4078 --- include/modules/river/tags.hpp | 1 + src/modules/river/tags.cpp | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/include/modules/river/tags.hpp b/include/modules/river/tags.hpp index dca0873d..df73513d 100644 --- a/include/modules/river/tags.hpp +++ b/include/modules/river/tags.hpp @@ -38,6 +38,7 @@ class Tags : public waybar::AModule { std::vector buttons_; struct zriver_output_status_v1* output_status_; struct zriver_seat_status_v1* seat_status_; + bool hide_vacant_ = false; // parsed once; asBool() in a wl callback would throw (#4078) }; } /* namespace waybar::modules::river */ diff --git a/src/modules/river/tags.cpp b/src/modules/river/tags.cpp index d7f87cf8..67b33686 100644 --- a/src/modules/river/tags.cpp +++ b/src/modules/river/tags.cpp @@ -145,6 +145,12 @@ Tags::Tags(const std::string& id, const waybar::Bar& bar, const Json::Value& con // handle_show() to avoid leaking objects without listeners here. output_ = gdk_wayland_monitor_get_wl_output(bar_.output->monitor->gobj()); + // Parse hide-vacant once here, not in the wl callbacks: jsoncpp's asBool() + // throws on a string value ("true"), and an exception unwinding through + // libwayland's C dispatch aborts (#4078). Accept the string form too. + const auto& hv = config_["hide-vacant"]; + hide_vacant_ = hv.isString() ? hv.asString() == "true" : hv.asBool(); + box_.set_name("tags"); if (!id.empty()) { box_.get_style_context()->add_class(id); @@ -248,7 +254,7 @@ bool Tags::handle_button_press(GdkEventButton* event_button, uint32_t tag) { } void Tags::handle_focused_tags(uint32_t tags) { - auto hide_vacant = config_["hide-vacant"].asBool(); + const bool hide_vacant = hide_vacant_; for (size_t i = 0; i < buttons_.size(); ++i) { bool visible = buttons_[i].is_visible(); bool occupied = buttons_[i].get_style_context()->has_class("occupied"); @@ -274,7 +280,7 @@ void Tags::handle_view_tags(struct wl_array* view_tags) { for (; view_tag < end; ++view_tag) { tags |= *view_tag; } - auto hide_vacant = config_["hide-vacant"].asBool(); + const bool hide_vacant = hide_vacant_; for (size_t i = 0; i < buttons_.size(); ++i) { bool visible = buttons_[i].is_visible(); bool focused = buttons_[i].get_style_context()->has_class("focused"); @@ -294,7 +300,7 @@ void Tags::handle_view_tags(struct wl_array* view_tags) { } void Tags::handle_urgent_tags(uint32_t tags) { - auto hide_vacant = config_["hide-vacant"].asBool(); + const bool hide_vacant = hide_vacant_; for (size_t i = 0; i < buttons_.size(); ++i) { bool visible = buttons_[i].is_visible(); bool occupied = buttons_[i].get_style_context()->has_class("occupied"); From 6d5d5f076889033c36085509325c050c50815877 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 23:04:58 +0200 Subject: [PATCH 389/416] fix(cava): throw instead of exit() on a bad config loadConfig() called exit(EXIT_FAILURE) when the cava config failed to load or no input source was available, killing the whole bar. Throw std::runtime_error instead: the factory/bar catch it at construction and disable only the cava module. The read_thread_ also calls loadConfig() at runtime, so contain the throw there too, logging instead of terminating. Fixes #4456 --- src/modules/cava/cava_backend.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/modules/cava/cava_backend.cpp b/src/modules/cava/cava_backend.cpp index 6eb540c9..99f03e16 100644 --- a/src/modules/cava/cava_backend.cpp +++ b/src/modules/cava/cava_backend.cpp @@ -2,6 +2,8 @@ #include +#include + std::shared_ptr waybar::modules::cava::CavaBackend::inst( const Json::Value& config) { static auto* backend = new CavaBackend(config); @@ -20,7 +22,13 @@ waybar::modules::cava::CavaBackend::CavaBackend(const Json::Value& config) : con spdlog::warn("Cava backend. Read source error: {0}", e.what()); } read_thread_.sleep_for(fetch_input_delay_); - loadConfig(); + // loadConfig() now throws on failure; contain it so a runtime config error + // logs instead of terminating the process (#4456). + try { + loadConfig(); + } catch (const std::exception& e) { + spdlog::error("{}", e.what()); + } }; // Write outcoming data. Emit signals out_thread_ = [this] { @@ -175,8 +183,9 @@ void waybar::modules::cava::CavaBackend::loadConfig() { error_.length = 0; if (!load_config(cfgPath, &prm_, &error_)) { - spdlog::error("cava backend. Error loading config. {0}", error_.message); - exit(EXIT_FAILURE); + // Throw, don't exit(): a bad config must disable only the cava module, not + // kill the whole bar (#4456). Caught by the factory (ctor) / read_thread_. + throw std::runtime_error(std::string{"cava backend: error loading config: "} + error_.message); } // Override cava parameters by the user config @@ -252,8 +261,7 @@ void waybar::modules::cava::CavaBackend::loadConfig() { input_source_ = get_input(&audio_data_, &prm_); if (!input_source_) { - spdlog::error("cava backend API didn't provide input audio source method"); - exit(EXIT_FAILURE); + throw std::runtime_error("cava backend: API didn't provide an input audio source"); } prm_.output = ::cava::output_method::OUTPUT_RAW; From 6c1a0fa1851b8c837d4defa64073d0bbf5b880e9 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 23:04:58 +0200 Subject: [PATCH 390/416] fix(bluetooth): accept module-level placeholders in enumerate formats tooltip-format-enumerate-connected[-battery] only received the device_* args, so {status}, {num_connections} and {controller_*} threw fmt "argument not found". Pass those args to the enumerate fmt::format as well. Fixes #4384 --- src/modules/bluetooth.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 8adfff26..3e4203d9 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -349,7 +349,15 @@ auto waybar::modules::Bluetooth::update() -> void { fmt::arg("device_alias", dev.alias), fmt::arg("icon", enumerate_icon), fmt::arg("device_battery_percentage", dev.battery_percentage.value_or(0)), fmt::arg("device_battery_percentage_peripheral", - dev.battery_percentage_peripheral.value_or(0))); + dev.battery_percentage_peripheral.value_or(0)), + // Also accept the module-level placeholders so {status} etc. don't + // throw "argument not found" in an enumerate format (#4384). + fmt::arg("status", state_), + fmt::arg("num_connections", connected_devices_.size()), + fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"), + fmt::arg("controller_address_type", + cur_controller_ ? cur_controller_->address_type : "null"), + fmt::arg("controller_alias", cur_controller_ ? cur_controller_->alias : "null")); } } device_enumerate_ = ss.str(); From 6294ed25207cdfa28d2022bdde15cf87009e956b Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 23:04:58 +0200 Subject: [PATCH 391/416] fix(hyprland/language): apply the CSS class on the main thread onEvent() runs on the Hyprland IPC listener thread and mutated the label's style context (add/remove class) directly, racing the GTK main thread's drawing and corrupting the heap (double free / corrupted double-linked list). Follow the Submap pattern: onEvent only stores the layout under the mutex and emits the dispatcher; update() swaps the CSS class on the main thread, tracking the previously applied class. Fixes #4665 --- include/modules/hyprland/language.hpp | 3 +-- src/modules/hyprland/language.cpp | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/include/modules/hyprland/language.hpp b/include/modules/hyprland/language.hpp index 199356c1..3cb8e31d 100644 --- a/include/modules/hyprland/language.hpp +++ b/include/modules/hyprland/language.hpp @@ -30,8 +30,6 @@ class Language : public waybar::ALabel, public EventHandler { std::string short_description; }; - auto removeXkbLayoutCssClass() -> void; - auto addXkbLayoutCssClass() -> void; static auto getLayout(const std::string&) -> Layout; std::mutex mutex_; @@ -39,6 +37,7 @@ class Language : public waybar::ALabel, public EventHandler { util::JsonParser parser_; Layout layout_; + std::string prev_short_name_; // applied CSS class; touched only in update() (#4665) IPC& m_ipc; }; diff --git a/src/modules/hyprland/language.cpp b/src/modules/hyprland/language.cpp index 85adaee0..b0b1574f 100644 --- a/src/modules/hyprland/language.cpp +++ b/src/modules/hyprland/language.cpp @@ -30,6 +30,18 @@ Language::~Language() { auto Language::update() -> void { std::lock_guard lg(mutex_); + // Swap the layout CSS class here (main thread). onEvent() runs on the IPC + // thread and must never touch GTK -- that race corrupts the heap (#4665). + if (prev_short_name_ != layout_.short_name) { + if (!prev_short_name_.empty()) { + label_.get_style_context()->remove_class(prev_short_name_); + } + if (!layout_.short_name.empty()) { + label_.get_style_context()->add_class(layout_.short_name); + } + prev_short_name_ = layout_.short_name; + } + spdlog::debug("hyprland language update with full name {}", layout_.full_name); spdlog::debug("hyprland language update with short name {}", layout_.short_name); spdlog::debug("hyprland language update with short description {}", layout_.short_description); @@ -143,9 +155,8 @@ void Language::onEvent(const std::string& ev) { layoutName = waybar::util::sanitize_string(layoutName); - removeXkbLayoutCssClass(); + // CSS class swap happens in update() on the main thread (#4665). layout_ = getLayout(layoutName); - addXkbLayoutCssClass(); spdlog::debug("hyprland language onevent with {}", layoutName); @@ -167,7 +178,6 @@ void Language::initLanguage() { searcher = waybar::util::sanitize_string(searcher); layout_ = getLayout(searcher); - addXkbLayoutCssClass(); spdlog::debug("hyprland language initLanguage found {}", layout_.full_name); @@ -177,16 +187,6 @@ void Language::initLanguage() { } } -auto Language::removeXkbLayoutCssClass() -> void { - label_.get_style_context()->remove_class(layout_.short_name); - spdlog::debug("hyprland language try to remove currently short_name css class {}", - layout_.short_name); -} -auto Language::addXkbLayoutCssClass() -> void { - label_.get_style_context()->add_class(layout_.short_name); - spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name); -} - auto Language::getLayout(const std::string& fullName) -> Layout { auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES); rxkb_context_parse_default_ruleset(context); From 4222d5a213038bf261c2dc3dfffadd3b919ad996 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 23:00:33 +0200 Subject: [PATCH 392/416] ci(smoke): fix state tier hang from bare wait on a lingering daemon The state tier started actually running mpd in 74f0d33, which exposed a deadlock in teardown: state.sh launches `mpd --no-daemon &` and never kills it during the tier, and its cleanup() called smoke::stop *before* killing mpd. smoke::stop ended in a bare `wait`, which reaps *every* background job of the shell -- including the still-running mpd -- so it blocked until GitHub's 6h job timeout. continue-on-error doesn't help: it catches failures, not hangs. - lib.sh: smoke::stop now waits only on the PIDs it owns (waybar, compositor), so an unrelated daemon left running by a tier can't deadlock teardown. - state.sh: cleanup() tears down mpd/pulseaudio before smoke::stop. - smoke.yml: add timeout-minutes: 20 so a future hang fails fast instead of burning the default 6h runner budget. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/smoke.yml | 4 ++++ test/smoke/lib.sh | 9 ++++++++- test/smoke/state.sh | 4 +++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 001b7ecf..e680aaea 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -29,6 +29,10 @@ env: jobs: smoke: runs-on: ubuntu-latest + # Safety net: the whole smoke job is minutes of work. Cap it so a hung tier + # (e.g. a daemon left running under a bare `wait`) fails fast instead of + # burning the default 6h runner budget. + timeout-minutes: 20 steps: - uses: actions/checkout@v6 diff --git a/test/smoke/lib.sh b/test/smoke/lib.sh index e3848442..82367206 100755 --- a/test/smoke/lib.sh +++ b/test/smoke/lib.sh @@ -194,5 +194,12 @@ smoke::assert_clean_exit() { smoke::stop() { kill "$WAYBAR_PID" 2>/dev/null || true swaymsg -q exit 2>/dev/null || kill "$COMP_PID" 2>/dev/null || true - wait 2>/dev/null || true + # Wait only on the processes we own. A bare `wait` reaps *every* background + # job of the caller's shell, so a tier that leaves an unrelated daemon + # running (state.sh's mpd, killed later in its own cleanup) would deadlock + # here until GitHub's 6h job timeout. + local p + for p in "$WAYBAR_PID" "$COMP_PID"; do + [ -n "$p" ] && wait "$p" 2>/dev/null || true + done } diff --git a/test/smoke/state.sh b/test/smoke/state.sh index 77fb31e2..697347f5 100755 --- a/test/smoke/state.sh +++ b/test/smoke/state.sh @@ -20,9 +20,11 @@ MPD_PID="" PA_STARTED="" cleanup() { - smoke::stop + # Tear our daemons down first: smoke::stop waits on the compositor/waybar, + # and a still-running mpd/pulseaudio must not be left for anything to block on. [ -n "$MPD_PID" ] && kill "$MPD_PID" 2>/dev/null || true [ -n "$PA_STARTED" ] && pulseaudio --kill 2>/dev/null || true + smoke::stop return 0 } From eee2ab92fee15c4151314bbfc16d0ae5492b455d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 5 Jul 2026 23:12:36 +0200 Subject: [PATCH 393/416] style(bluetooth): clang-format the enumerate args --- src/modules/bluetooth.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 3e4203d9..58b1711f 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -352,8 +352,7 @@ auto waybar::modules::Bluetooth::update() -> void { dev.battery_percentage_peripheral.value_or(0)), // Also accept the module-level placeholders so {status} etc. don't // throw "argument not found" in an enumerate format (#4384). - fmt::arg("status", state_), - fmt::arg("num_connections", connected_devices_.size()), + fmt::arg("status", state_), fmt::arg("num_connections", connected_devices_.size()), fmt::arg("controller_address", cur_controller_ ? cur_controller_->address : "null"), fmt::arg("controller_address_type", cur_controller_ ? cur_controller_->address_type : "null"), From 624623215d11f017cc948502eec54dcec7ddec56 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 6 Jul 2026 02:08:06 +0200 Subject: [PATCH 394/416] ci(smoke): keep the compositor alive between hardware cases hardware.sh called smoke::stop between umockdev cases, but smoke::stop runs `swaymsg exit` and tears down the shared compositor -- so only the first case (backlight) had a display and the rest failed with "cannot open display". Kill only waybar between cases; leave compositor teardown to the EXIT trap. --- test/smoke/hardware.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/smoke/hardware.sh b/test/smoke/hardware.sh index 4cd84028..47e21ac8 100755 --- a/test/smoke/hardware.sh +++ b/test/smoke/hardware.sh @@ -18,6 +18,11 @@ if ! command -v umockdev-run >/dev/null; then echo "(umockdev not installed, skipping hardware tier)"; exit 0 fi +# Kill only waybar between cases -- smoke::stop would `swaymsg exit` the shared +# compositor, and the next case couldn't open the display (compositor teardown is +# the EXIT trap's job). +kill_waybar() { kill "$WAYBAR_PID" 2>/dev/null || true; sleep 1; WAYBAR_PID=""; } + smoke::setup # waybar is linked with ASan, but umockdev-run injects its own LD_PRELOAD, which # lands ahead of the ASan runtime and makes ASan abort before main() ("ASan @@ -65,7 +70,7 @@ EOF WAYBAR_WRAP="umockdev-run --device $dev --" smoke::launch_waybar "$cfg" "$DIR/style.css" 5 smoke::assert_alive || fail=1 smoke::assert_clean || fail=1 - smoke::stop; WAYBAR_PID="" + kill_waybar echo "::endgroup::" } @@ -85,7 +90,7 @@ EOF WAYBAR_WRAP="umockdev-run --device $BL_DEV --" smoke::launch_waybar "$cfg" "$DIR/style.css" 5 smoke::assert_alive || fail=1 smoke::assert_clean || fail=1 -smoke::stop; WAYBAR_PID="" +kill_waybar echo "::endgroup::" [ "$fail" = 0 ] || { echo "::error::hardware tier failed"; exit 1; } From b5abbdc696003c91653942ad486b9badc9ac97ef Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Mon, 6 Jul 2026 16:47:30 +0200 Subject: [PATCH 395/416] docs(man): add load --- .github/wiki/mapping.json | 1 + man/waybar-load.5.scd | 134 ++++++++++++++++++++++++++++++++++++++ man/waybar.5.scd.in | 1 + 3 files changed, 136 insertions(+) create mode 100644 man/waybar-load.5.scd diff --git a/.github/wiki/mapping.json b/.github/wiki/mapping.json index 1bee1bc3..d17338ed 100644 --- a/.github/wiki/mapping.json +++ b/.github/wiki/mapping.json @@ -21,6 +21,7 @@ "Module:-JACK": ["waybar-jack"], "Module:-Keyboard-State": ["waybar-keyboard-state"], "Module:-Language": ["waybar-sway-language", "waybar-hyprland-language", "waybar-niri-language", "waybar-mango-language"], + "Module:-Load": ["waybar-load"], "Module:-Mango": ["waybar-mango-window", "waybar-mango-workspaces", "waybar-mango-keymode", "waybar-mango-layout"], "Module:-Memory": ["waybar-memory"], "Module:-Menu": ["waybar-menu"], diff --git a/man/waybar-load.5.scd b/man/waybar-load.5.scd new file mode 100644 index 00000000..3a3ce5b9 --- /dev/null +++ b/man/waybar-load.5.scd @@ -0,0 +1,134 @@ +waybar-load(5) + +# NAME + +waybar - load module + +# DESCRIPTION + +The *load* module displays the current CPU load. + +# CONFIGURATION + +Addressed by *load* + +*interval*: ++ + typeof: integer++ + default: 10 ++ + The interval in which the information gets polled. + +*format*: ++ + typeof: string ++ + default: {load1} ++ + The format, how information should be displayed. + +*rotate*: ++ + typeof: integer ++ + Positive value to rotate the text label (in 90 degree increments). + +*states*: ++ + typeof: object ++ + A number of load states which get activated on certain thresholds, based on `load1`. See *waybar-states(5)*. + +*max-length*: ++ + typeof: integer ++ + The maximum length in character the module should display. + +*min-length*: ++ + typeof: integer ++ + The minimum length in characters the module should accept. + +*align*: ++ + typeof: float ++ + The alignment of the label within the module, where 0 is left-aligned and 1 is right-aligned. If the module is rotated, it will follow the flow of the text. + +*justify*: ++ + typeof: string ++ + The alignment of the text within the module's label, allowing options 'left', 'right', or 'center' to define the positioning. + +*on-click*: ++ + typeof: string ++ + Command to execute when clicked on the module. + +*on-click-middle*: ++ + typeof: string ++ + Command to execute when middle-clicked on the module using mousewheel. + +*on-click-right*: ++ + typeof: string ++ + Command to execute when you right-click on the module. + +*on-update*: ++ + typeof: string ++ + Command to execute when the module is updated. + +*on-scroll-up*: ++ + typeof: string ++ + Command to execute when scrolling up on the module. + +*on-scroll-down*: ++ + typeof: string ++ + Command to execute when scrolling down on the module. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-format*: ++ + typeof: string ++ + default: Load 1: {load1}\\nLoad 5: {load5}\\nLoad 15: {load15} ++ + The format of the tooltip shown on hover. Supports the same replacements as *format*. + +*menu*: ++ + typeof: string ++ + Action that popups the menu. + +*menu-file*: ++ + typeof: string ++ + Location of the menu descriptor file. There need to be an element of type + GtkMenu with id *menu* + +*menu-actions*: ++ + typeof: array ++ + The actions corresponding to the buttons of the menu. + +*expand*: ++ + typeof: bool ++ + default: false ++ + Enables this module to consume all left over space dynamically. + +# FORMAT REPLACEMENTS + +*{}*: Equivalent to `{load1}` + +*{load1}*: Current CPU load average over the last minute + +*{load5}*: Current CPU load average over the last 5 minutes + +*{load15}*: Current CPU load average over the last 15 minutes + +# EXAMPLES + +``` +"load": { + "interval": 10, + "format": "load: {load1}", + "max-length": 10 +} +``` + +``` +"load": { + "interval": 1, + "format": "load: {load1} {load5} {load15}" +} +``` + +# STYLE + +- *#load* diff --git a/man/waybar.5.scd.in b/man/waybar.5.scd.in index 78dc433e..ff2af1bc 100644 --- a/man/waybar.5.scd.in +++ b/man/waybar.5.scd.in @@ -474,6 +474,7 @@ To hide the power group if it is empty, apply to #power and all its children: - *waybar-inhibitor(5)* - *waybar-jack(5)* - *waybar-keyboard-state(5)* +- *waybar-load(5)* - *waybar-memory(5)* - *waybar-mpd(5)* - *waybar-mpris(5)* From 795047ba8b27edbadd105d1c8b63dacb25b9ff91 Mon Sep 17 00:00:00 2001 From: Volker Schlecht <47375452+VlkrS@users.noreply.github.com> Date: Sat, 11 Jul 2026 23:36:59 +0200 Subject: [PATCH 396/416] idle_inhibitor.cpp: Fix build for systems without SIGRTMIN/SIGRTMAX See https://github.com/Alexays/Waybar/pull/4992 for reference --- src/modules/idle_inhibitor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/idle_inhibitor.cpp b/src/modules/idle_inhibitor.cpp index 170701ce..05b30ca2 100644 --- a/src/modules/idle_inhibitor.cpp +++ b/src/modules/idle_inhibitor.cpp @@ -103,6 +103,7 @@ auto waybar::modules::IdleInhibitor::update() -> void { } auto waybar::modules::IdleInhibitor::refresh(int sig) -> void { +#ifdef SIGRTMIN if (config_["signal"].isInt() && sig == SIGRTMIN + config_["signal"].asInt()) { toggleStatus(); @@ -111,6 +112,7 @@ auto waybar::modules::IdleInhibitor::refresh(int sig) -> void { module->update(); } } +#endif } void waybar::modules::IdleInhibitor::toggleStatus(int force_status) { From 5c979152de965739e525eaafd66ca40839f08223 Mon Sep 17 00:00:00 2001 From: Viktar Lukashonak Date: Mon, 13 Jul 2026 19:50:09 +0300 Subject: [PATCH 397/416] refactor(cava): fix thread-safety, resource leaks, and style violations Comprehensive refactor of the cava module backend and frontends. Style & naming - Rename Cava -> CavaRaw; snake_case methods -> lowerCamelCase - Replace NULL with nullptr; replace C-style casts with static_cast - Add explicit standard-library includes (, , ) - Fix missing trailing underscores on member variables Architecture - Remove Gtk::GLArea multiple inheritance in CavaGLSL (composition) - Return std::unique_ptr from factory; add doAction() to GLSL variant - Encapsulate thread timing arithmetic in AdaptiveDelay struct Thread safety & correctness - Replace raw sigc::signal with SafeSignal for cross-thread marshalling - Fix data race between loadConfig() and out_thread_ (recursive_mutex) - Fix audio_raw shallow-copy use-after-free via deep-copy AudioRaw payload - Make loadConfig() exception-safe with CavaConfigGuard RAII - Fix blocking read_thread_ race on shutdown (condition_variable + timeout join) - Fix isSilent() data race (acquire pthread mutex) - Eliminate doUpdate() recursion (iteration instead) - Guard audio_raw_clean() against uninitialized state - Fix format-icons underflow and cava_config buffer overflow - Store config by value to prevent dangling references on reload - Cache frontend config and refresh on runtime changes - Fix signed-char icon lookup bug on x86 - Prevent Json::Value mutation bloat via const-ref lookups - Broaden exception catches in worker threads (std::exception) Resource management & GL robustness - Fix OpenGL resource leaks (persist VBO/IBO/VAO; explicit destructor cleanup) - Fix shader error handling crashes (valid infoLog allocation) - Cache uniform locations instead of querying per frame - Fix gradient color uninitialized stack memory (zero-init + clamped count) - Fix shader time uniform integer division bug (float arithmetic) - Add explicit VAO bind in onRender() - Handle runtime surface config changes independently of shader changes - Clamp negative gradient_count before GL upload Follow-up - Singleton API split (inst() + configure()) intentionally deferred to a dedicated PR because it changes the public constructor contract. --- include/modules/cava/cavaGLSL.hpp | 56 +++- include/modules/cava/cavaRaw.hpp | 26 +- include/modules/cava/cava_backend.hpp | 111 ++++++-- include/modules/cava/cava_frontend.hpp | 10 +- src/factory.cpp | 2 +- src/modules/cava/cavaGLSL.cpp | 363 +++++++++++++++++------- src/modules/cava/cavaRaw.cpp | 87 +++--- src/modules/cava/cava_backend.cpp | 376 +++++++++++++++---------- 8 files changed, 692 insertions(+), 339 deletions(-) diff --git a/include/modules/cava/cavaGLSL.hpp b/include/modules/cava/cavaGLSL.hpp index d2632aff..5372d054 100644 --- a/include/modules/cava/cavaGLSL.hpp +++ b/include/modules/cava/cavaGLSL.hpp @@ -1,36 +1,64 @@ #pragma once #include +#include +#include +#include +#include + +#include #include "AModule.hpp" #include "cava_backend.hpp" namespace waybar::modules::cava { -class CavaGLSL final : public AModule, public Gtk::GLArea { +class CavaGLSL final : public AModule { public: CavaGLSL(const std::string&, const Json::Value&); - ~CavaGLSL() = default; + ~CavaGLSL(); + auto doAction(const std::string& name) -> void override; private: + using Action = void (CavaGLSL::*)(); + + Gtk::GLArea gl_area_; std::shared_ptr backend_; - struct ::cava::config_params prm_; - int frame_counter{0}; + // Cached config params (deep-copied strings to avoid dangling char* on backend reload) + int sdl_width_{0}; + int sdl_height_{0}; + int bar_width_{0}; + int bar_spacing_{0}; + int gradient_count_{0}; + std::string vertex_shader_; + std::string fragment_shader_; + std::string bcolor_; + std::string color_; + std::array gradient_colors_; + int frame_counter_{0}; bool silence_{false}; bool hide_on_silence_{false}; + bool mapped_{false}; // Cava method - auto onUpdate(const ::cava::audio_raw& input) -> void; + void pauseResume(); + auto onUpdate(const CavaBackend::AudioRaw& input) -> void; auto onSilence() -> void; - // Member variable to store the shared pointer - std::shared_ptr<::cava::audio_raw> m_data_; - GLuint shaderProgram_; + auto onBackendConfigChanged() -> void; + void cacheConfigParams(const ::cava::config_params& src); + // Member variable to store audio data + CavaBackend::AudioRaw m_data_; + GLuint shaderProgram_{0}; // OpenGL variables - GLuint fbo_; - GLuint texture_; + GLuint fbo_{0}; + GLuint texture_{0}; + GLuint vbo_{0}; + GLuint ibo_{0}; + GLuint vao_{0}; GLint uniform_bars_; GLint uniform_previous_bars_; GLint uniform_bars_count_; GLint uniform_time_; + GLint uniform_input_texture_; // Methods void onRealize(); bool onRender(const Glib::RefPtr& context); @@ -39,5 +67,13 @@ class CavaGLSL final : public AModule, public Gtk::GLArea { void initSurface(); void initGLSL(); GLuint loadShader(const std::string& fileName, GLenum type); + void cleanupGL(); + + // ModuleActionMap + static const std::map actionMap_; + + sigc::connection audio_raw_update_conn_; + sigc::connection silence_conn_; + sigc::connection config_changed_conn_; }; } // namespace waybar::modules::cava diff --git a/include/modules/cava/cavaRaw.hpp b/include/modules/cava/cavaRaw.hpp index f4ceb603..76f88eff 100644 --- a/include/modules/cava/cavaRaw.hpp +++ b/include/modules/cava/cavaRaw.hpp @@ -1,30 +1,38 @@ #pragma once +#include +#include + +#include + #include "ALabel.hpp" #include "cava_backend.hpp" namespace waybar::modules::cava { -class Cava final : public ALabel, public sigc::trackable { +class CavaRaw final : public ALabel { public: - Cava(const std::string&, const Json::Value&); - ~Cava() = default; + CavaRaw(const std::string&, const Json::Value&); + ~CavaRaw(); auto doAction(const std::string& name) -> void override; private: + using Action = void (CavaRaw::*)(); + std::shared_ptr backend_; // Text to display - Glib::ustring label_text_{""}; + Glib::ustring label_text_; bool silence_{false}; bool hide_on_silence_{false}; - std::string format_silent_{""}; - int ascii_range_{0}; + std::string format_silent_; // Cava method - void pause_resume(); + void pauseResume(); auto onUpdate(const std::string& input) -> void; auto onSilence() -> void; // ModuleActionMap - static inline std::map - actionMap_{{"mode", &waybar::modules::cava::Cava::pause_resume}}; + static const std::map actionMap_; + + sigc::connection update_conn_; + sigc::connection silence_conn_; }; } // namespace waybar::modules::cava diff --git a/include/modules/cava/cava_backend.hpp b/include/modules/cava/cava_backend.hpp index 4022a2ff..bdbac4a1 100644 --- a/include/modules/cava/cava_backend.hpp +++ b/include/modules/cava/cava_backend.hpp @@ -1,8 +1,17 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include + #include #include +#include "util/SafeSignal.hpp" #include "util/sleeper_thread.hpp" namespace cava { @@ -21,7 +30,6 @@ extern "C" { } // namespace cava namespace waybar::modules::cava { -using namespace std::literals::chrono_literals; class CavaBackend final { public: @@ -29,19 +37,38 @@ class CavaBackend final { virtual ~CavaBackend(); // Methods - int getAsciiRange(); + int getAsciiRange() const; void doPauseResume(); - void Update(); - const struct ::cava::config_params* getPrm(); - std::chrono::milliseconds getFrameTimeMilsec(); + void update(); + const ::cava::config_params& getPrm() const; + std::chrono::milliseconds getFrameTimeMilsec() const; + + struct AudioRaw { + std::vector bars_raw; + std::vector previous_bars_raw; + int number_of_bars = 0; + + AudioRaw() = default; + explicit AudioRaw(const ::cava::audio_raw& raw) { + number_of_bars = raw.number_of_bars; + if (raw.bars_raw != nullptr && number_of_bars > 0) { + bars_raw.assign(raw.bars_raw, raw.bars_raw + number_of_bars); + } + if (raw.previous_bars_raw != nullptr && number_of_bars > 0) { + previous_bars_raw.assign(raw.previous_bars_raw, raw.previous_bars_raw + number_of_bars); + } + } + }; // Signal accessor - using type_signal_update = sigc::signal; - type_signal_update signal_update(); - using type_signal_audio_raw_update = sigc::signal; - type_signal_audio_raw_update signal_audio_raw_update(); - using type_signal_silence = sigc::signal; - type_signal_silence signal_silence(); + using SignalUpdate = SafeSignal; + SignalUpdate& signalUpdate(); + using SignalAudioRawUpdate = SafeSignal; + SignalAudioRawUpdate& signalAudioRawUpdate(); + using SignalSilence = SafeSignal<>; + SignalSilence& signalSilence(); + using SignalConfigChanged = SafeSignal<>; + SignalConfigChanged& signalConfigChanged(); private: CavaBackend(const Json::Value& config); @@ -49,36 +76,78 @@ class CavaBackend final { util::SleeperThread out_thread_; // Cava API to read audio source - ::cava::ptr input_source_{NULL}; + ::cava::ptr input_source_{nullptr}; struct ::cava::error_s error_{}; // cava errors struct ::cava::config_params prm_{}; // cava parameters struct ::cava::audio_raw audio_raw_{}; // cava handled raw audio data(is based on audio_data) struct ::cava::audio_data audio_data_{}; // cava audio data - struct ::cava::cava_plan* plan_{NULL}; //{new cava_plan{}}; + struct ::cava::cava_plan* plan_{nullptr}; //{new cava_plan{}}; std::chrono::seconds fetch_input_delay_{4}; - // Delay to handle audio source - std::chrono::milliseconds frame_time_milsec_{1s}; - const Json::Value& config_; + struct AdaptiveDelay { + std::chrono::milliseconds delay; + std::chrono::seconds delta{0}; + + explicit AdaptiveDelay(std::chrono::milliseconds initial = std::chrono::seconds(1)) + : delay(initial) {} + + bool increase() { + if (delta == std::chrono::seconds{0}) { + delta += std::chrono::seconds{1}; + delay += delta; + return true; + } + return false; + } + + bool decrease() { + if (delta > std::chrono::seconds{0}) { + delay -= delta; + delta -= std::chrono::seconds{1}; + return true; + } + return false; + } + + std::chrono::milliseconds current() const { return delay; } + + void reset(std::chrono::milliseconds new_delay) { + delay = new_delay; + delta = std::chrono::seconds{0}; + } + }; + + AdaptiveDelay adaptive_delay_; + + Json::Value config_; int re_paint_{0}; bool silence_{false}; bool silence_prev_{false}; - std::chrono::seconds suspend_silence_delay_{0}; int sleep_counter_{0}; std::string output_{}; // Methods void invoke(); void execute(); - bool isSilence(); + bool isSilent(); void doUpdate(bool force = false); void loadConfig(); void freeBackend(); // Signal - type_signal_update m_signal_update_; - type_signal_audio_raw_update m_signal_audio_raw_; - type_signal_silence m_signal_silence_; + SignalUpdate m_signal_update_; + SignalAudioRawUpdate m_signal_audio_raw_; + SignalSilence m_signal_silence_; + SignalConfigChanged m_signal_config_changed_; + + std::atomic shutdown_{false}; + bool audio_raw_initialized_{false}; + mutable std::recursive_mutex state_mutex_; + + // Synchronization for joining read_thread_ during destruction + bool read_thread_exited_{false}; + mutable std::mutex read_thread_exit_mutex_; + std::condition_variable read_thread_exit_cv_; }; } // namespace waybar::modules::cava diff --git a/include/modules/cava/cava_frontend.hpp b/include/modules/cava/cava_frontend.hpp index 402a9bad..4eefb0fe 100644 --- a/include/modules/cava/cava_frontend.hpp +++ b/include/modules/cava/cava_frontend.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #ifdef HAVE_LIBCAVA #include "cavaRaw.hpp" #include "cava_backend.hpp" @@ -9,16 +11,16 @@ #endif namespace waybar::modules::cava { -AModule* getModule(const std::string& id, const Json::Value& config) { +inline std::unique_ptr getModule(const std::string& id, const Json::Value& config) { #ifdef HAVE_LIBCAVA const std::shared_ptr backend_{waybar::modules::cava::CavaBackend::inst(config)}; - switch (backend_->getPrm()->output) { + switch (backend_->getPrm().output) { #ifdef HAVE_LIBCAVAGLSL case ::cava::output_method::OUTPUT_SDL_GLSL: - return new waybar::modules::cava::CavaGLSL(id, config); + return std::make_unique(id, config); #endif default: - return new waybar::modules::cava::Cava(id, config); + return std::make_unique(id, config); } #else throw std::runtime_error("Unknown module"); diff --git a/src/factory.cpp b/src/factory.cpp index c0d018b7..86abb43d 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -372,7 +372,7 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name, } #endif if (ref == "cava") { - return waybar::modules::cava::getModule(id, config_[name]); + return waybar::modules::cava::getModule(id, config_[name]).release(); } #ifdef HAVE_SYSTEMD_MONITOR if (ref == "systemd-failed-units") { diff --git a/src/modules/cava/cavaGLSL.cpp b/src/modules/cava/cavaGLSL.cpp index 4ab03bcd..755c6acd 100644 --- a/src/modules/cava/cavaGLSL.cpp +++ b/src/modules/cava/cavaGLSL.cpp @@ -2,88 +2,214 @@ #include +#include +#include +#include #include +#include +#include + +const std::map + waybar::modules::cava::CavaGLSL::actionMap_{{"mode", &CavaGLSL::pauseResume}}; waybar::modules::cava::CavaGLSL::CavaGLSL(const std::string& id, const Json::Value& config) : AModule(config, "cavaGLSL", id, false, false), backend_{waybar::modules::cava::CavaBackend::inst(config)} { - set_name(name_); + gl_area_.set_name(name_); if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool(); if (!id.empty()) { - get_style_context()->add_class(id); + gl_area_.get_style_context()->add_class(id); } - get_style_context()->add_class(MODULE_CLASS); + gl_area_.get_style_context()->add_class(MODULE_CLASS); - set_use_es(true); - // set_auto_render(true); - signal_realize().connect(sigc::mem_fun(*this, &CavaGLSL::onRealize)); - signal_render().connect(sigc::mem_fun(*this, &CavaGLSL::onRender), false); + gl_area_.set_use_es(true); + gl_area_.signal_realize().connect(sigc::mem_fun(*this, &CavaGLSL::onRealize)); + gl_area_.signal_render().connect(sigc::mem_fun(*this, &CavaGLSL::onRender), false); + gl_area_.signal_map().connect([this]() { mapped_ = true; }); + gl_area_.signal_unmap().connect([this]() { mapped_ = false; }); - // Get parameters_config struct from the backend - prm_ = *backend_->getPrm(); + cacheConfigParams(backend_->getPrm()); - // Set widget length int length{0}; if (config_["min-length"].isUInt()) length = config_["min-length"].asUInt(); else if (config_["max-length"].isUInt()) length = config_["max-length"].asUInt(); else - length = prm_.sdl_width; + length = sdl_width_; - set_size_request(length, prm_.sdl_height); + gl_area_.set_size_request(length, sdl_height_); - // Subscribe for changes - backend_->signal_audio_raw_update().connect(sigc::mem_fun(*this, &CavaGLSL::onUpdate)); - // Subscribe for silence - backend_->signal_silence().connect(sigc::mem_fun(*this, &CavaGLSL::onSilence)); - event_box_.add(*this); + audio_raw_update_conn_ = + backend_->signalAudioRawUpdate().connect(sigc::mem_fun(*this, &CavaGLSL::onUpdate)); + silence_conn_ = backend_->signalSilence().connect(sigc::mem_fun(*this, &CavaGLSL::onSilence)); + config_changed_conn_ = + backend_->signalConfigChanged().connect(sigc::mem_fun(*this, &CavaGLSL::onBackendConfigChanged)); + event_box_.add(gl_area_); } -auto waybar::modules::cava::CavaGLSL::onUpdate(const ::cava::audio_raw& input) -> void { - Glib::signal_idle().connect_once([this, input]() { - m_data_ = std::make_shared<::cava::audio_raw>(input); - if (silence_) { - get_style_context()->remove_class("silent"); - if (!get_style_context()->has_class("updated")) get_style_context()->add_class("updated"); - show(); - silence_ = false; - } +waybar::modules::cava::CavaGLSL::~CavaGLSL() { + audio_raw_update_conn_.disconnect(); + silence_conn_.disconnect(); + config_changed_conn_.disconnect(); - queue_render(); - }); + if (gl_area_.get_realized()) { + gl_area_.make_current(); + cleanupGL(); + } +} + +void waybar::modules::cava::CavaGLSL::cleanupGL() { + if (shaderProgram_ != 0) { + glDeleteProgram(shaderProgram_); + shaderProgram_ = 0; + } + if (fbo_ != 0) { + glDeleteFramebuffers(1, &fbo_); + fbo_ = 0; + } + if (texture_ != 0) { + glDeleteTextures(1, &texture_); + texture_ = 0; + } + if (vbo_ != 0) { + glDeleteBuffers(1, &vbo_); + vbo_ = 0; + } + if (ibo_ != 0) { + glDeleteBuffers(1, &ibo_); + ibo_ = 0; + } + if (vao_ != 0) { + glDeleteVertexArrays(1, &vao_); + vao_ = 0; + } +} + +auto waybar::modules::cava::CavaGLSL::doAction(const std::string& name) -> void { + auto it = actionMap_.find(name); + if (it != actionMap_.end() && it->second) { + (this->*it->second)(); + } else { + spdlog::error("CavaGLSL. Unsupported action \"{0}\"", name); + } +} + +void waybar::modules::cava::CavaGLSL::pauseResume() { backend_->doPauseResume(); } + +auto waybar::modules::cava::CavaGLSL::onUpdate(const CavaBackend::AudioRaw& input) -> void { + m_data_ = input; + if (silence_) { + gl_area_.get_style_context()->remove_class("silent"); + if (!gl_area_.get_style_context()->has_class("updated")) + gl_area_.get_style_context()->add_class("updated"); + gl_area_.show(); + silence_ = false; + } + + if (mapped_) { + gl_area_.queue_render(); + } } auto waybar::modules::cava::CavaGLSL::onSilence() -> void { - Glib::signal_idle().connect_once([this]() { - if (!silence_) { - if (get_style_context()->has_class("updated")) get_style_context()->remove_class("updated"); + if (!silence_) { + if (gl_area_.get_style_context()->has_class("updated")) + gl_area_.get_style_context()->remove_class("updated"); - if (hide_on_silence_) hide(); - silence_ = true; - get_style_context()->add_class("silent"); - // Set clear color to black - glClearColor(0.0f, 0.0f, 0.0f, 1.0f); - queue_render(); + if (hide_on_silence_) gl_area_.hide(); + silence_ = true; + gl_area_.get_style_context()->add_class("silent"); + } +} + +void waybar::modules::cava::CavaGLSL::cacheConfigParams(const ::cava::config_params& src) { + sdl_width_ = src.sdl_width; + sdl_height_ = src.sdl_height; + bar_width_ = src.bar_width; + bar_spacing_ = src.bar_spacing; + gradient_count_ = src.gradient_count; + vertex_shader_ = src.vertex_shader ? src.vertex_shader : ""; + fragment_shader_ = src.fragment_shader ? src.fragment_shader : ""; + bcolor_ = src.bcolor ? src.bcolor : ""; + color_ = src.color ? src.color : ""; + for (size_t i = 0; i < gradient_colors_.size(); ++i) { + gradient_colors_[i] = (src.gradient_colors[i] ? src.gradient_colors[i] : ""); + } +} + +auto waybar::modules::cava::CavaGLSL::onBackendConfigChanged() -> void { + auto new_prm = backend_->getPrm(); + + bool dimensions_changed = + (new_prm.sdl_width != sdl_width_) || (new_prm.sdl_height != sdl_height_); + + bool shaders_changed = false; + std::string new_vertex = new_prm.vertex_shader ? new_prm.vertex_shader : ""; + std::string new_fragment = new_prm.fragment_shader ? new_prm.fragment_shader : ""; + shaders_changed = (vertex_shader_ != new_vertex) || (fragment_shader_ != new_fragment); + + bool surface_changed = false; + if (new_prm.bar_width != bar_width_) surface_changed = true; + if (new_prm.bar_spacing != bar_spacing_) surface_changed = true; + if (new_prm.gradient_count != gradient_count_) surface_changed = true; + std::string new_bcolor = new_prm.bcolor ? new_prm.bcolor : ""; + if (bcolor_ != new_bcolor) surface_changed = true; + std::string new_color = new_prm.color ? new_prm.color : ""; + if (color_ != new_color) surface_changed = true; + for (size_t i = 0; i < gradient_colors_.size(); ++i) { + std::string new_grad = new_prm.gradient_colors[i] ? new_prm.gradient_colors[i] : ""; + if (gradient_colors_[i] != new_grad) { + surface_changed = true; + break; } - }); + } + + cacheConfigParams(new_prm); + + if ((dimensions_changed || shaders_changed) && gl_area_.get_realized()) { + gl_area_.make_current(); + cleanupGL(); + initShaders(); + if (shaderProgram_ != 0) { + initGLSL(); + initSurface(); + } + } else if (surface_changed && gl_area_.get_realized()) { + gl_area_.make_current(); + glUseProgram(shaderProgram_); + initSurface(); + } + + int length{0}; + if (config_["min-length"].isUInt()) + length = config_["min-length"].asUInt(); + else if (config_["max-length"].isUInt()) + length = config_["max-length"].asUInt(); + else + length = sdl_width_; + + gl_area_.set_size_request(length, sdl_height_); } bool waybar::modules::cava::CavaGLSL::onRender(const Glib::RefPtr& context) { - if (!m_data_) return true; + if (m_data_.bars_raw.empty() || shaderProgram_ == 0) return true; + + glUseProgram(shaderProgram_); + glBindVertexArray(vao_); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture_); - glUniform1i(glGetUniformLocation(shaderProgram_, "inputTexture"), 0); + glUniform1i(uniform_input_texture_, 0); - glUniform1fv(uniform_bars_, m_data_->number_of_bars, m_data_->bars_raw); - glUniform1fv(uniform_previous_bars_, m_data_->number_of_bars, m_data_->previous_bars_raw); - glUniform1i(uniform_bars_count_, m_data_->number_of_bars); - ++frame_counter; - glUniform1f(uniform_time_, (frame_counter / backend_->getFrameTimeMilsec().count()) / 1e3); + glUniform1fv(uniform_bars_, m_data_.number_of_bars, m_data_.bars_raw.data()); + glUniform1fv(uniform_previous_bars_, m_data_.number_of_bars, m_data_.previous_bars_raw.data()); + glUniform1i(uniform_bars_count_, m_data_.number_of_bars); + ++frame_counter_; + glUniform1f(uniform_time_, + static_cast(frame_counter_) * backend_->getFrameTimeMilsec().count() / 1000.0f); - // glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr); glBindFramebuffer(GL_FRAMEBUFFER, fbo_); @@ -94,21 +220,35 @@ bool waybar::modules::cava::CavaGLSL::onRender(const Glib::RefPtrR, &color->G, &color->B); +static void parse_color(const char* color_string, struct Colors* color) { + if (color_string == nullptr) { + return; + } + if (color_string[0] != '#') { + return; + } + if (std::strlen(color_string) < 7) { + spdlog::warn("Invalid color string '{}': expected #RRGGBB", color_string); + return; + } + if (std::sscanf(color_string + 1, "%02hx%02hx%02hx", &color->R, &color->G, &color->B) != 3) { + spdlog::warn("Failed to parse color string '{}'", color_string); } } @@ -123,49 +263,44 @@ void waybar::modules::cava::CavaGLSL::initGLSL() { GLfloat vertexData[]{-1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f}; GLint indexData[]{0, 1, 2, 3}; - GLuint gVBO{0}; - glGenBuffers(1, &gVBO); - glBindBuffer(GL_ARRAY_BUFFER, gVBO); + glGenBuffers(1, &vbo_); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); glBufferData(GL_ARRAY_BUFFER, 2 * 4 * sizeof(GLfloat), vertexData, GL_STATIC_DRAW); - GLuint gIBO{0}; - glGenBuffers(1, &gIBO); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gIBO); + glGenBuffers(1, &ibo_); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_); glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), indexData, GL_STATIC_DRAW); - GLuint gVAO{0}; - glGenVertexArrays(1, &gVAO); - glBindVertexArray(gVAO); + glGenVertexArrays(1, &vao_); + glBindVertexArray(vao_); glEnableVertexAttribArray(gVertexPos2DLocation); - glBindBuffer(GL_ARRAY_BUFFER, gVBO); + glBindBuffer(GL_ARRAY_BUFFER, vbo_); glVertexAttribPointer(gVertexPos2DLocation, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gIBO); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_); glGenFramebuffers(1, &fbo_); glBindFramebuffer(GL_FRAMEBUFFER, fbo_); - // Create a texture to attach the framebuffer glGenTextures(1, &texture_); glBindTexture(GL_TEXTURE_2D, texture_); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, prm_.sdl_width, prm_.sdl_height, 0, GL_RGBA, + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, sdl_width_, sdl_height_, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_, 0); - // Check is framebuffer is complete if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { spdlog::error("{0}. Framebuffer not complete", name_); } - // Unbind the framebuffer glBindFramebuffer(GL_FRAMEBUFFER, 0); uniform_bars_ = glGetUniformLocation(shaderProgram_, "bars"); uniform_previous_bars_ = glGetUniformLocation(shaderProgram_, "previous_bars"); uniform_bars_count_ = glGetUniformLocation(shaderProgram_, "bars_count"); uniform_time_ = glGetUniformLocation(shaderProgram_, "shader_time"); + uniform_input_texture_ = glGetUniformLocation(shaderProgram_, "inputTexture"); GLuint err{glGetError()}; if (err != 0) { @@ -174,32 +309,32 @@ void waybar::modules::cava::CavaGLSL::initGLSL() { } void waybar::modules::cava::CavaGLSL::initSurface() { - colors color = {0}; + Colors color = {0}; GLint uniform_bg_col{glGetUniformLocation(shaderProgram_, "bg_color")}; - parse_color(prm_.bcolor, &color); - glUniform3f(uniform_bg_col, (float)color.R / 255.0, (float)color.G / 255.0, - (float)color.B / 255.0); + parse_color(bcolor_.c_str(), &color); + glUniform3f(uniform_bg_col, static_cast(color.R) / 255.0f, static_cast(color.G) / 255.0f, + static_cast(color.B) / 255.0f); GLint uniform_fg_col{glGetUniformLocation(shaderProgram_, "fg_color")}; - parse_color(prm_.color, &color); - glUniform3f(uniform_fg_col, (float)color.R / 255.0, (float)color.G / 255.0, - (float)color.B / 255.0); + parse_color(color_.c_str(), &color); + glUniform3f(uniform_fg_col, static_cast(color.R) / 255.0f, static_cast(color.G) / 255.0f, + static_cast(color.B) / 255.0f); GLint uniform_res{glGetUniformLocation(shaderProgram_, "u_resolution")}; - glUniform3f(uniform_res, (float)prm_.sdl_width, (float)prm_.sdl_height, 0.0f); + glUniform3f(uniform_res, static_cast(sdl_width_), static_cast(sdl_height_), 0.0f); GLint uniform_bar_width{glGetUniformLocation(shaderProgram_, "bar_width")}; - glUniform1i(uniform_bar_width, prm_.bar_width); + glUniform1i(uniform_bar_width, bar_width_); GLint uniform_bar_spacing{glGetUniformLocation(shaderProgram_, "bar_spacing")}; - glUniform1i(uniform_bar_spacing, prm_.bar_spacing); + glUniform1i(uniform_bar_spacing, bar_spacing_); GLint uniform_gradient_count{glGetUniformLocation(shaderProgram_, "gradient_count")}; - glUniform1i(uniform_gradient_count, prm_.gradient_count); + glUniform1i(uniform_gradient_count, std::max(0, gradient_count_)); GLint uniform_gradient_colors{glGetUniformLocation(shaderProgram_, "gradient_colors")}; - GLfloat gradient_colors[8][3]; - for (int i{0}; i < prm_.gradient_count; ++i) { - parse_color(prm_.gradient_colors[i], &color); - gradient_colors[i][0] = (float)color.R / 255.0; - gradient_colors[i][1] = (float)color.G / 255.0; - gradient_colors[i][2] = (float)color.B / 255.0; + GLfloat gradient_colors[8][3] = {}; + for (int i{0}; i < gradient_count_; ++i) { + parse_color(gradient_colors_[i].c_str(), &color); + gradient_colors[i][0] = static_cast(color.R) / 255.0f; + gradient_colors[i][1] = static_cast(color.G) / 255.0f; + gradient_colors[i][2] = static_cast(color.B) / 255.0f; } - glUniform3fv(uniform_gradient_colors, 8, (const GLfloat*)gradient_colors); + glUniform3fv(uniform_gradient_colors, std::max(0, std::min(gradient_count_, 8)), &gradient_colors[0][0]); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glDrawElements(GL_TRIANGLE_FAN, 4, GL_UNSIGNED_INT, nullptr); @@ -207,9 +342,23 @@ void waybar::modules::cava::CavaGLSL::initSurface() { void waybar::modules::cava::CavaGLSL::initShaders() { shaderProgram_ = glCreateProgram(); + if (shaderProgram_ == 0) { + spdlog::error("{0}. Failed to create shader program", name_); + gl_area_.hide(); + return; + } - GLuint vertexShader{loadShader(prm_.vertex_shader, GL_VERTEX_SHADER)}; - GLuint fragmentShader{loadShader(prm_.fragment_shader, GL_FRAGMENT_SHADER)}; + GLuint vertexShader{loadShader(vertex_shader_, GL_VERTEX_SHADER)}; + GLuint fragmentShader{loadShader(fragment_shader_, GL_FRAGMENT_SHADER)}; + + if (vertexShader == 0 || fragmentShader == 0) { + if (vertexShader != 0) glDeleteShader(vertexShader); + if (fragmentShader != 0) glDeleteShader(fragmentShader); + glDeleteProgram(shaderProgram_); + shaderProgram_ = 0; + gl_area_.hide(); + return; + } glAttachShader(shaderProgram_, vertexShader); glAttachShader(shaderProgram_, fragmentShader); @@ -219,14 +368,18 @@ void waybar::modules::cava::CavaGLSL::initShaders() { glDeleteShader(vertexShader); glDeleteShader(fragmentShader); - // Check for linking errors - GLint success, len; + GLint success{0}; glGetProgramiv(shaderProgram_, GL_LINK_STATUS, &success); if (!success) { + GLint len{0}; glGetProgramiv(shaderProgram_, GL_INFO_LOG_LENGTH, &len); - GLchar* infoLog{(char*)'\0'}; - glGetProgramInfoLog(shaderProgram_, len, &len, infoLog); - spdlog::error("{0}. Shader linking error: {1}", name_, infoLog); + std::vector infoLog(len + 1); + glGetProgramInfoLog(shaderProgram_, len, nullptr, infoLog.data()); + spdlog::error("{0}. Shader linking error: {1}", name_, infoLog.data()); + glDeleteProgram(shaderProgram_); + shaderProgram_ = 0; + gl_area_.hide(); + return; } glReleaseShaderCompiler(); @@ -236,35 +389,37 @@ void waybar::modules::cava::CavaGLSL::initShaders() { GLuint waybar::modules::cava::CavaGLSL::loadShader(const std::string& fileName, GLenum type) { spdlog::debug("{0}. loadShader: {1}", name_, fileName); - // Read shader source code from the file std::ifstream shaderFile{fileName}; - if (!shaderFile.is_open()) { spdlog::error("{0}. Could not open shader file: {1}", name_, fileName); + return 0; } std::ostringstream buffer; - buffer << shaderFile.rdbuf(); // read file content into stringstream + buffer << shaderFile.rdbuf(); std::string str{buffer.str()}; - const char* shaderSource = str.c_str(); shaderFile.close(); GLuint shaderID{glCreateShader(type)}; - if (shaderID == 0) spdlog::error("{0}. Error creating shader type: {0}", type); + if (shaderID == 0) { + spdlog::error("{0}. Error creating shader type: {1}", name_, type); + return 0; + } + + const char* shaderSource = str.c_str(); glShaderSource(shaderID, 1, &shaderSource, nullptr); glCompileShader(shaderID); - // Check for compilation errors - GLint success, len; - + GLint success{0}; glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success); - if (!success) { + GLint len{0}; glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &len); - - GLchar* infoLog{(char*)'\0'}; - glGetShaderInfoLog(shaderID, len, nullptr, infoLog); - spdlog::error("{0}. Shader compilation error in {1}: {2}", name_, fileName, infoLog); + std::vector infoLog(len + 1); + glGetShaderInfoLog(shaderID, len, nullptr, infoLog.data()); + spdlog::error("{0}. Shader compilation error in {1}: {2}", name_, fileName, infoLog.data()); + glDeleteShader(shaderID); + return 0; } return shaderID; diff --git a/src/modules/cava/cavaRaw.cpp b/src/modules/cava/cavaRaw.cpp index 2b0aca72..65ce093d 100644 --- a/src/modules/cava/cavaRaw.cpp +++ b/src/modules/cava/cavaRaw.cpp @@ -2,59 +2,68 @@ #include -waybar::modules::cava::Cava::Cava(const std::string& id, const Json::Value& config) +const std::map + waybar::modules::cava::CavaRaw::actionMap_{{"mode", &CavaRaw::pauseResume}}; + +waybar::modules::cava::CavaRaw::CavaRaw(const std::string& id, const Json::Value& config) : ALabel(config, "cava", id, "{}", 60, false, false, false), backend_{waybar::modules::cava::CavaBackend::inst(config)} { if (config_["hide_on_silence"].isBool()) hide_on_silence_ = config_["hide_on_silence"].asBool(); if (config_["format_silent"].isString()) format_silent_ = config_["format_silent"].asString(); - ascii_range_ = backend_->getAsciiRange(); - backend_->signal_update().connect(sigc::mem_fun(*this, &Cava::onUpdate)); - backend_->signal_silence().connect(sigc::mem_fun(*this, &Cava::onSilence)); - backend_->Update(); + update_conn_ = backend_->signalUpdate().connect(sigc::mem_fun(*this, &CavaRaw::onUpdate)); + silence_conn_ = backend_->signalSilence().connect(sigc::mem_fun(*this, &CavaRaw::onSilence)); + backend_->update(); } -auto waybar::modules::cava::Cava::doAction(const std::string& name) -> void { - if ((actionMap_[name])) { - (this->*actionMap_[name])(); - } else +waybar::modules::cava::CavaRaw::~CavaRaw() { + update_conn_.disconnect(); + silence_conn_.disconnect(); +} + +auto waybar::modules::cava::CavaRaw::doAction(const std::string& name) -> void { + auto it = actionMap_.find(name); + if (it != actionMap_.end() && it->second) { + (this->*it->second)(); + } else { spdlog::error("Cava. Unsupported action \"{0}\"", name); + } } // Cava actions -void waybar::modules::cava::Cava::pause_resume() { backend_->doPauseResume(); } -auto waybar::modules::cava::Cava::onUpdate(const std::string& input) -> void { - Glib::signal_idle().connect_once([this, input]() { - if (silence_) { - silence_ = false; - label_.get_style_context()->remove_class("silent"); - if (!label_.get_style_context()->has_class("updated")) - label_.get_style_context()->add_class("updated"); - } - label_text_.clear(); - for (auto& ch : input) - label_text_.append(getIcon((ch > ascii_range_) ? ascii_range_ : ch, "", ascii_range_ + 1)); +void waybar::modules::cava::CavaRaw::pauseResume() { backend_->doPauseResume(); } +auto waybar::modules::cava::CavaRaw::onUpdate(const std::string& input) -> void { + if (silence_) { + silence_ = false; + label_.get_style_context()->remove_class("silent"); + if (!label_.get_style_context()->has_class("updated")) + label_.get_style_context()->add_class("updated"); + } + label_text_.clear(); + auto ascii_range = backend_->getAsciiRange(); + for (auto& ch : input) { + auto uch = static_cast(ch); + label_text_.append(getIcon((uch > ascii_range) ? ascii_range : uch, "", ascii_range + 1)); + } - label_.set_markup(label_text_); - label_.show(); - ALabel::update(); - }); + label_.set_markup(label_text_); + label_.show(); + ALabel::update(); } -auto waybar::modules::cava::Cava::onSilence() -> void { - Glib::signal_idle().connect_once([this]() { - if (!silence_) { - if (label_.get_style_context()->has_class("updated")) - label_.get_style_context()->remove_class("updated"); +auto waybar::modules::cava::CavaRaw::onSilence() -> void { + if (!silence_) { + if (label_.get_style_context()->has_class("updated")) + label_.get_style_context()->remove_class("updated"); - if (hide_on_silence_) { - // Clear the label markup before hiding to prevent GTK from rendering a NULL Pango layout - label_.set_markup(""); - label_.hide(); - } else if (config_["format_silent"].isString()) - label_.set_markup(format_silent_); - silence_ = true; - label_.get_style_context()->add_class("silent"); + if (hide_on_silence_) { + // Clear the label markup before hiding to prevent GTK from rendering a NULL Pango layout + label_.set_markup(""); + label_.hide(); + } else if (!format_silent_.empty()) { + label_.set_markup(format_silent_); } - }); + silence_ = true; + label_.get_style_context()->add_class("silent"); + } } diff --git a/src/modules/cava/cava_backend.cpp b/src/modules/cava/cava_backend.cpp index 99f03e16..00239131 100644 --- a/src/modules/cava/cava_backend.cpp +++ b/src/modules/cava/cava_backend.cpp @@ -2,8 +2,23 @@ #include +#include #include +namespace { +struct CavaConfigGuard { + ::cava::config_params* prm; + bool released = false; + explicit CavaConfigGuard(::cava::config_params* p) : prm(p) {} + ~CavaConfigGuard() { + if (!released && prm) { + free_config(prm); + } + } + void release() { released = true; } +}; +} // namespace + std::shared_ptr waybar::modules::cava::CavaBackend::inst( const Json::Value& config) { static auto* backend = new CavaBackend(config); @@ -12,69 +27,84 @@ std::shared_ptr waybar::modules::cava::CavaB } waybar::modules::cava::CavaBackend::CavaBackend(const Json::Value& config) : config_(config) { - // Load waybar module config loadConfig(); - // Read audio source trough cava API. Cava orginizes this process via infinity loop read_thread_ = [this] { - try { - input_source_(&audio_data_); - } catch (const std::runtime_error& e) { - spdlog::warn("Cava backend. Read source error: {0}", e.what()); + while (read_thread_.isRunning()) { + try { + if (input_source_) { + input_source_(&audio_data_); + } + } catch (const std::exception& e) { + spdlog::warn("Cava backend. Read source error: {0}", e.what()); + } + if (!read_thread_.isRunning()) break; + read_thread_.sleep_for(fetch_input_delay_); + if (!read_thread_.isRunning()) break; + try { + loadConfig(); + } catch (const std::exception& e) { + spdlog::error("{}", e.what()); + } } - read_thread_.sleep_for(fetch_input_delay_); - // loadConfig() now throws on failure; contain it so a runtime config error - // logs instead of terminating the process (#4456). - try { - loadConfig(); - } catch (const std::exception& e) { - spdlog::error("{}", e.what()); + { + std::lock_guard lk(read_thread_exit_mutex_); + read_thread_exited_ = true; } + read_thread_exit_cv_.notify_one(); }; - // Write outcoming data. Emit signals out_thread_ = [this] { - doUpdate(false); - out_thread_.sleep_for(frame_time_milsec_); + try { + doUpdate(false); + } catch (const std::exception& e) { + spdlog::error("Cava backend. Output thread error: {0}", e.what()); + } + out_thread_.sleep_for(adaptive_delay_.current()); }; } waybar::modules::cava::CavaBackend::~CavaBackend() { + { + std::lock_guard lock(state_mutex_); + shutdown_ = true; + } + + pthread_mutex_lock(&audio_data_.lock); + audio_data_.terminate = 1; + pthread_mutex_unlock(&audio_data_.lock); + out_thread_.stop(); read_thread_.stop(); + std::unique_lock lk(read_thread_exit_mutex_); + if (!read_thread_exit_cv_.wait_for(lk, std::chrono::milliseconds(100), + [this] { return read_thread_exited_; })) { + spdlog::debug( + "Cava backend: read thread still blocked in input_source() on shutdown. " + "Proceeding with cleanup."); + } + + std::lock_guard lock(state_mutex_); freeBackend(); } -static bool upThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { - if (delta == std::chrono::seconds{0}) { - delta += std::chrono::seconds{1}; - delay += delta; - return true; - } - return false; -} - -static bool downThreadDelay(std::chrono::milliseconds& delay, std::chrono::seconds& delta) { - if (delta > std::chrono::seconds{0}) { - delay -= delta; - delta -= std::chrono::seconds{1}; - return true; - } - return false; -} - -bool waybar::modules::cava::CavaBackend::isSilence() { +bool waybar::modules::cava::CavaBackend::isSilent() { + pthread_mutex_lock(&audio_data_.lock); + bool silent = true; for (int i{0}; i < audio_data_.input_buffer_size; ++i) { if (audio_data_.cava_in[i]) { - return false; + silent = false; + break; } } - - return true; + pthread_mutex_unlock(&audio_data_.lock); + return silent; } -int waybar::modules::cava::CavaBackend::getAsciiRange() { return prm_.ascii_range; } +int waybar::modules::cava::CavaBackend::getAsciiRange() const { + std::lock_guard lock(state_mutex_); + return prm_.ascii_range; +} -// Process: execute cava void waybar::modules::cava::CavaBackend::invoke() { pthread_mutex_lock(&audio_data_.lock); ::cava::cava_execute(audio_data_.cava_in, audio_data_.samples_counter, audio_raw_.cava_out, @@ -83,7 +113,6 @@ void waybar::modules::cava::CavaBackend::invoke() { pthread_mutex_unlock(&audio_data_.lock); } -// Do transformation under raw data void waybar::modules::cava::CavaBackend::execute() { invoke(); audio_raw_fetch(&audio_raw_, &prm_, &re_paint_, plan_); @@ -99,183 +128,228 @@ void waybar::modules::cava::CavaBackend::execute() { } void waybar::modules::cava::CavaBackend::doPauseResume() { + std::lock_guard lock(state_mutex_); pthread_mutex_lock(&audio_data_.lock); if (audio_data_.suspendFlag) { audio_data_.suspendFlag = false; pthread_cond_broadcast(&audio_data_.resumeCond); - downThreadDelay(frame_time_milsec_, suspend_silence_delay_); + adaptive_delay_.decrease(); } else { audio_data_.suspendFlag = true; - upThreadDelay(frame_time_milsec_, suspend_silence_delay_); + adaptive_delay_.increase(); } pthread_mutex_unlock(&audio_data_.lock); - Update(); + update(); } -waybar::modules::cava::CavaBackend::type_signal_update -waybar::modules::cava::CavaBackend::signal_update() { +waybar::modules::cava::CavaBackend::SignalUpdate& +waybar::modules::cava::CavaBackend::signalUpdate() { return m_signal_update_; } -waybar::modules::cava::CavaBackend::type_signal_audio_raw_update -waybar::modules::cava::CavaBackend::signal_audio_raw_update() { +waybar::modules::cava::CavaBackend::SignalAudioRawUpdate& +waybar::modules::cava::CavaBackend::signalAudioRawUpdate() { return m_signal_audio_raw_; } -waybar::modules::cava::CavaBackend::type_signal_silence -waybar::modules::cava::CavaBackend::signal_silence() { +waybar::modules::cava::CavaBackend::SignalSilence& +waybar::modules::cava::CavaBackend::signalSilence() { return m_signal_silence_; } -void waybar::modules::cava::CavaBackend::Update() { doUpdate(true); } +waybar::modules::cava::CavaBackend::SignalConfigChanged& +waybar::modules::cava::CavaBackend::signalConfigChanged() { + return m_signal_config_changed_; +} + +void waybar::modules::cava::CavaBackend::update() { doUpdate(true); } void waybar::modules::cava::CavaBackend::doUpdate(bool force) { + std::lock_guard lock(state_mutex_); + if (!plan_ || !input_source_) return; if (audio_data_.suspendFlag && !force) return; - silence_ = isSilence(); + silence_ = isSilent(); if (!silence_) sleep_counter_ = 0; if (silence_ && prm_.sleep_timer != 0) { if (sleep_counter_ <= - (int)(std::chrono::milliseconds(prm_.sleep_timer * 1s) / frame_time_milsec_)) { + static_cast(std::chrono::milliseconds(prm_.sleep_timer * std::chrono::seconds(1)) / + adaptive_delay_.current())) { ++sleep_counter_; silence_ = false; } } if (!silence_ || prm_.sleep_timer == 0) { - if (downThreadDelay(frame_time_milsec_, suspend_silence_delay_)) Update(); + while (adaptive_delay_.decrease()) {} execute(); if (re_paint_ == 1 || force || prm_.continuous_rendering) { m_signal_update_.emit(output_); - m_signal_audio_raw_.emit(audio_raw_); + m_signal_audio_raw_.emit(AudioRaw{audio_raw_}); } } else { - if (upThreadDelay(frame_time_milsec_, suspend_silence_delay_)) Update(); + while (adaptive_delay_.increase()) {} if (silence_ != silence_prev_ || force) m_signal_silence_.emit(); } silence_prev_ = silence_; } void waybar::modules::cava::CavaBackend::freeBackend() { - if (plan_ != NULL) { + input_source_ = nullptr; + + if (plan_ != nullptr) { cava_destroy(plan_); - plan_ = NULL; + plan_ = nullptr; } - audio_raw_clean(&audio_raw_); + if (audio_raw_initialized_) { + audio_raw_clean(&audio_raw_); + audio_raw_initialized_ = false; + } pthread_mutex_lock(&audio_data_.lock); audio_data_.terminate = 1; pthread_mutex_unlock(&audio_data_.lock); free_config(&prm_); + prm_ = {}; free(audio_data_.source); + audio_data_.source = nullptr; free(audio_data_.cava_in); + audio_data_.cava_in = nullptr; } void waybar::modules::cava::CavaBackend::loadConfig() { - freeBackend(); - // Load waybar module config - char cfgPath[PATH_MAX]; - cfgPath[0] = '\0'; + std::lock_guard lock(state_mutex_); + if (shutdown_.load()) { + return; + } - if (config_["cava_config"].isString()) strcpy(cfgPath, config_["cava_config"].asString().data()); - // Load cava config + const Json::Value& cfg = config_; + + struct ::cava::config_params new_prm{}; + CavaConfigGuard new_prm_guard(&new_prm); + std::vector cfgPath(PATH_MAX, '\0'); + if (cfg["cava_config"].isString()) { + const std::string& s = cfg["cava_config"].asString(); + auto len = std::min(s.size(), static_cast(PATH_MAX - 1)); + std::copy_n(s.begin(), len, cfgPath.begin()); + cfgPath[len] = '\0'; + } error_.length = 0; - if (!load_config(cfgPath, &prm_, &error_)) { - // Throw, don't exit(): a bad config must disable only the cava module, not - // kill the whole bar (#4456). Caught by the factory (ctor) / read_thread_. + if (!load_config(cfgPath.data(), &new_prm, &error_)) { throw std::runtime_error(std::string{"cava backend: error loading config: "} + error_.message); } - // Override cava parameters by the user config - prm_.inAtty = 0; - auto const output{prm_.output}; - // prm_.output = ::cava::output_method::OUTPUT_RAW; - if (prm_.data_format) free(prm_.data_format); - // Default to ascii for format-icons output; allow user override - prm_.data_format = strdup( - config_["data_format"].isString() ? config_["data_format"].asString().c_str() : "ascii"); - if (config_["raw_target"].isString()) { - if (prm_.raw_target) free(prm_.raw_target); - prm_.raw_target = strdup(config_["raw_target"].asString().c_str()); + new_prm.inAtty = 0; + auto const output{new_prm.output}; + if (new_prm.data_format) free(new_prm.data_format); + new_prm.data_format = strdup( + cfg["data_format"].isString() ? cfg["data_format"].asString().c_str() : "ascii"); + if (cfg["raw_target"].isString()) { + if (new_prm.raw_target) free(new_prm.raw_target); + new_prm.raw_target = strdup(cfg["raw_target"].asString().c_str()); } - prm_.ascii_range = config_["format-icons"].size() - 1; - - if (config_["bar_spacing"].isInt()) prm_.bar_spacing = config_["bar_spacing"].asInt(); - if (config_["bar_width"].isInt()) prm_.bar_width = config_["bar_width"].asInt(); - if (config_["bar_height"].isInt()) prm_.bar_height = config_["bar_height"].asInt(); - prm_.orientation = ::cava::ORIENT_TOP; - prm_.xaxis = ::cava::xaxis_scale::NONE; - prm_.mono_opt = ::cava::AVERAGE; - prm_.autobars = 0; - if (config_["gravity"].isInt()) prm_.gravity = config_["gravity"].asInt(); - if (config_["integral"].isInt()) prm_.integral = config_["integral"].asInt(); - - if (config_["framerate"].isInt()) prm_.framerate = config_["framerate"].asInt(); - // Calculate delay for Update() thread - frame_time_milsec_ = std::chrono::milliseconds((int)(1e3 / prm_.framerate)); - if (config_["autosens"].isInt()) prm_.autosens = config_["autosens"].asInt(); - if (config_["sensitivity"].isInt()) prm_.sens = config_["sensitivity"].asInt(); - if (config_["bars"].isInt()) prm_.fixedbars = config_["bars"].asInt(); - if (config_["lower_cutoff_freq"].isNumeric()) - prm_.lower_cut_off = config_["lower_cutoff_freq"].asLargestInt(); - if (config_["higher_cutoff_freq"].isNumeric()) - prm_.upper_cut_off = config_["higher_cutoff_freq"].asLargestInt(); - if (config_["sleep_timer"].isInt()) prm_.sleep_timer = config_["sleep_timer"].asInt(); - if (config_["method"].isString()) - prm_.input = ::cava::input_method_by_name(config_["method"].asString().c_str()); - if (config_["source"].isString()) { - if (prm_.audio_source) free(prm_.audio_source); - prm_.audio_source = strdup(config_["source"].asString().c_str()); - } - if (config_["sample_rate"].isNumeric()) prm_.samplerate = config_["sample_rate"].asLargestInt(); - if (config_["sample_bits"].isInt()) prm_.samplebits = config_["sample_bits"].asInt(); - if (config_["stereo"].isBool()) prm_.stereo = config_["stereo"].asBool(); - if (config_["reverse"].isBool()) prm_.reverse = config_["reverse"].asBool(); - if (config_["bar_delimiter"].isInt()) prm_.bar_delim = config_["bar_delimiter"].asInt(); - if (config_["monstercat"].isBool()) prm_.monstercat = config_["monstercat"].asBool(); - if (config_["waves"].isBool()) prm_.waves = config_["waves"].asBool(); - if (config_["noise_reduction"].isDouble()) - prm_.noise_reduction = config_["noise_reduction"].asDouble(); - if (config_["input_delay"].isInt()) - fetch_input_delay_ = std::chrono::seconds(config_["input_delay"].asInt()); - if (config_["gradient"].isInt()) prm_.gradient = config_["gradient"].asInt(); - if (prm_.gradient == 0) - prm_.gradient_count = 0; - else if (config_["gradient_count"].isInt()) - prm_.gradient_count = config_["gradient_count"].asInt(); - if (config_["sdl_width"].isInt()) prm_.sdl_width = config_["sdl_width"].asInt(); - if (config_["sdl_height"].isInt()) prm_.sdl_height = config_["sdl_height"].asInt(); - - audio_raw_.height = prm_.ascii_range; - audio_data_.format = -1; - audio_data_.rate = 0; - audio_data_.samples_counter = 0; - audio_data_.channels = 2; - audio_data_.IEEE_FLOAT = 0; - audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels; - audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8; - audio_data_.terminate = 0; - audio_data_.suspendFlag = false; - input_source_ = get_input(&audio_data_, &prm_); - - if (!input_source_) { - throw std::runtime_error("cava backend: API didn't provide an input audio source"); + { + auto icon_count = cfg["format-icons"].size(); + new_prm.ascii_range = (icon_count > 0) ? static_cast(icon_count) - 1 : 0; } - prm_.output = ::cava::output_method::OUTPUT_RAW; + if (cfg["bar_spacing"].isInt()) new_prm.bar_spacing = cfg["bar_spacing"].asInt(); + if (cfg["bar_width"].isInt()) new_prm.bar_width = cfg["bar_width"].asInt(); + if (cfg["bar_height"].isInt()) new_prm.bar_height = cfg["bar_height"].asInt(); + new_prm.orientation = ::cava::ORIENT_TOP; + new_prm.xaxis = ::cava::xaxis_scale::NONE; + new_prm.mono_opt = ::cava::AVERAGE; + new_prm.autobars = 0; + if (cfg["gravity"].isInt()) new_prm.gravity = cfg["gravity"].asInt(); + if (cfg["integral"].isInt()) new_prm.integral = cfg["integral"].asInt(); - // Make cava parameters configuration - // Init cava plan, audio_raw structure - audio_raw_init(&audio_data_, &audio_raw_, &prm_, &plan_); - if (!plan_) spdlog::error("cava backend plan is not provided"); - audio_raw_.previous_frame[0] = -1; // For first Update() call need to rePaint text message + if (cfg["framerate"].isInt()) new_prm.framerate = cfg["framerate"].asInt(); + if (cfg["autosens"].isInt()) new_prm.autosens = cfg["autosens"].asInt(); + if (cfg["sensitivity"].isInt()) new_prm.sens = cfg["sensitivity"].asInt(); + if (cfg["bars"].isInt()) new_prm.fixedbars = cfg["bars"].asInt(); + if (cfg["lower_cutoff_freq"].isNumeric()) + new_prm.lower_cut_off = cfg["lower_cutoff_freq"].asLargestInt(); + if (cfg["higher_cutoff_freq"].isNumeric()) + new_prm.upper_cut_off = cfg["higher_cutoff_freq"].asLargestInt(); + if (cfg["sleep_timer"].isInt()) new_prm.sleep_timer = cfg["sleep_timer"].asInt(); + if (cfg["method"].isString()) + new_prm.input = ::cava::input_method_by_name(cfg["method"].asString().c_str()); + if (cfg["source"].isString()) { + if (new_prm.audio_source) free(new_prm.audio_source); + new_prm.audio_source = strdup(cfg["source"].asString().c_str()); + } + if (cfg["sample_rate"].isNumeric()) new_prm.samplerate = cfg["sample_rate"].asLargestInt(); + if (cfg["sample_bits"].isInt()) new_prm.samplebits = cfg["sample_bits"].asInt(); + if (cfg["stereo"].isBool()) new_prm.stereo = cfg["stereo"].asBool(); + if (cfg["reverse"].isBool()) new_prm.reverse = cfg["reverse"].asBool(); + if (cfg["bar_delimiter"].isInt()) new_prm.bar_delim = cfg["bar_delimiter"].asInt(); + if (cfg["monstercat"].isBool()) new_prm.monstercat = cfg["monstercat"].asBool(); + if (cfg["waves"].isBool()) new_prm.waves = cfg["waves"].asBool(); + if (cfg["noise_reduction"].isDouble()) + new_prm.noise_reduction = cfg["noise_reduction"].asDouble(); + if (cfg["input_delay"].isInt()) + fetch_input_delay_ = std::chrono::seconds(cfg["input_delay"].asInt()); + if (cfg["gradient"].isInt()) new_prm.gradient = cfg["gradient"].asInt(); + if (new_prm.gradient == 0) + new_prm.gradient_count = 0; + else if (cfg["gradient_count"].isInt()) + new_prm.gradient_count = cfg["gradient_count"].asInt(); + if (cfg["sdl_width"].isInt()) new_prm.sdl_width = cfg["sdl_width"].asInt(); + if (cfg["sdl_height"].isInt()) new_prm.sdl_height = cfg["sdl_height"].asInt(); + if (new_prm.framerate <= 0) { + throw std::runtime_error(std::string{"cava backend: framerate must be positive, got: "} + + std::to_string(new_prm.framerate)); + } + + adaptive_delay_.reset(std::chrono::milliseconds(static_cast(1e3 / new_prm.framerate))); + freeBackend(); + + try { + audio_raw_.height = new_prm.ascii_range; + audio_data_.format = -1; + audio_data_.rate = 0; + audio_data_.samples_counter = 0; + audio_data_.channels = 2; + audio_data_.IEEE_FLOAT = 0; + audio_data_.input_buffer_size = BUFFER_SIZE * audio_data_.channels; + audio_data_.cava_buffer_size = audio_data_.input_buffer_size * 8; + audio_data_.terminate = 0; + audio_data_.suspendFlag = false; + + input_source_ = get_input(&audio_data_, &new_prm); + if (!input_source_) { + throw std::runtime_error("cava backend: API didn't provide an input audio source"); + } + + new_prm.output = ::cava::output_method::OUTPUT_RAW; + + audio_raw_init(&audio_data_, &audio_raw_, &new_prm, &plan_); + if (!plan_) { + throw std::runtime_error("cava backend plan is not provided"); + } + audio_raw_.previous_frame[0] = -1; + audio_raw_initialized_ = true; + } catch (...) { + freeBackend(); + throw; + } + + prm_ = new_prm; + new_prm_guard.release(); prm_.output = output; + + m_signal_config_changed_.emit(); } -const struct ::cava::config_params* waybar::modules::cava::CavaBackend::getPrm() { return &prm_; } -std::chrono::milliseconds waybar::modules::cava::CavaBackend::getFrameTimeMilsec() { - return frame_time_milsec_; -}; +const ::cava::config_params& waybar::modules::cava::CavaBackend::getPrm() const { + std::lock_guard lock(state_mutex_); + return prm_; +} + +std::chrono::milliseconds waybar::modules::cava::CavaBackend::getFrameTimeMilsec() const { + std::lock_guard lock(state_mutex_); + return adaptive_delay_.current(); +} From 52a5f74cd23b64066fa26b759c96b3586d5e39fc Mon Sep 17 00:00:00 2001 From: Viktar Lukashonak Date: Tue, 14 Jul 2026 09:29:06 +0300 Subject: [PATCH 398/416] docs: merge coding conventions into CONTRIBUTING.md Consolidate the standalone `CONVENTION.md` into `CONTRIBUTING.md` so that all contributor guidelines live in a single, discoverable file. Added sections: - Language, build system, and formatting rules (Google style, clang-format) - Naming conventions (PascalCase types, snake_case_ members, lowerCamelCase methods) - Include ordering and header self-sufficiency requirements - Class/module design patterns (AModule hierarchy, SafeSignal threading) - JSON configuration and string handling guidelines - GTK/Glib and OpenGL patterns - Platform portability and RAII conventions - Thread-safety rules (GTK single-threaded, background thread marshalling) - Unsafe patterns to avoid (C-style casts, unbounded buffer copies) - Singleton lifetime guidance (store config by value, no dangling refs)docs: merge coding conventions into CONTRIBUTING.md --- CONTRIBUTING.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc6fd972..5c57d93a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,3 +43,145 @@ add its man page and a line in [`.github/wiki/mapping.json`](.github/wiki/mappin - Build and test against the module(s) you touched. Have fun :) + +--- + +# Coding Conventions + +## 1. Language & Build +- **Standard**: C++20. +- **Build system**: Meson (`meson.build`). Project version is defined there. +- **Compiler flags**: Added via `add_project_arguments()` in Meson. Feature flags use `HAVE_*` / `WANT_*` prefixes (e.g. `-DHAVE_NIRI`, `-DHAVE_HYPRLAND`, `-DHAVE_LIBUDEV`). + +## 2. Formatting +- **Tool**: `.clang-format` is checked in. **Never bypass it.** +- **Style**: Google base style. +- **Indent**: 2 spaces. No tabs. +- **Column limit**: 100. +- **Braces**: K&R (opening brace on the same line). +- **Declaration alignment**: Disabled (`AlignConsecutiveDeclarations: false`). +- **Pointer/reference alignment**: Left (`const Json::Value& config`, `int* ptr`, not `int *ptr`). + +## 3. Naming + +### Files +- Match the primary exported class exactly: `AAppIconLabel.hpp`, `workspaces.cpp`, `backlight_backend.hpp`. +- Corresponding header and source should live in predictable paths: + - `include/.hpp` + - `src/.cpp` + +### Types +- **Classes / Structs**: `PascalCase`. + - Abstract base classes are prefixed with `A` (e.g., `AModule`, `ALabel`, `AIconLabel`, `AAppIconLabel`). +- **Enums / Enum classes**: `PascalCase` name. + - Enumerators: `UPPER_SNAKE_CASE` (e.g., `SCROLL_DIR::NONE`, `KillSignalAction::RELOAD`, `ChangeType::Increase`). +- **Concepts / Type aliases**: `PascalCase`. + +### Variables +- **Member variables**: `snake_case_` with a **trailing underscore**. + - Examples: `config_`, `bar_`, `label_`, `app_icon_size_`, `distance_scrolled_y_`, `on_updated_cb_`. +- **Function parameters & locals**: `snake_case` (no trailing underscore). + - Examples: `workspace_data`, `should_refresh`, `app_identifier`, `preferred_device`. +- **Static / constexpr constants**: `UPPER_SNAKE_CASE` or descriptive `kPascalCase`. + - Examples: `MODULE_CLASS`, `EPOLL_MAX_EVENTS`, `kExecFailureExitCode`. + +### Functions & Methods +- **Free functions**: `snake_case`. + - Examples: `sanitize_string()`, `rewrite_string()`, `get_total_memory()`, `best_device()`. +- **Class methods**: `lowerCamelCase`. + - Examples: `update()`, `tooltipEnabled()`, `handleScroll()`, `getScrollDir()`, `resolveFormat()`, `setBrightness()`. +- **Virtual overrides**: Mark with `override` (and `final` where applicable). Header signatures often use a trailing return type: + ```cpp + auto update() -> void override; + auto refresh(int should_refresh) -> void; + ``` + +### Namespaces +- All lowercase, nested by module path: + ```cpp + namespace waybar { } + namespace waybar::modules::niri { } + namespace waybar::util { } + ``` +- Close every namespace with a comment: + ```cpp + } // namespace waybar::modules::niri + ``` + +## 4. Includes & Headers +- Use `#pragma once` in all project headers. +- Include order in `.cpp` files: + 1. Corresponding header first. + 2. Blank line. + 3. External library headers (``, ``, ``, ``). + 4. Standard library headers (``, ``, ``). + 5. Blank line. + 6. Other project headers (`"util/..."`, `"modules/..."`). +- Do not use `using namespace` in headers. In `.cpp` files it is acceptable for narrow scopes (e.g., `using namespace std::literals::chrono_literals;`). +- Headers that expose standard-library types in their public interface (e.g. `std::chrono::milliseconds` as a return type or `std::vector` as a member) must `#include` the corresponding standard header directly. Do not rely on transitive includes from other headers. + +## 5. Class & Module Design + +### Base Class Patterns +- All UI modules ultimately derive from `AModule` (and often `ALabel` or `AIconLabel`). +- Accept configuration in constructors: + ```cpp + MyModule(const Json::Value& config, const std::string& name, const std::string& id, ...); + ``` + +### Signals & Threading +- Use `Glib::Dispatcher` (via `waybar::SafeSignal`) to marshal work to the GTK main thread. +- Use `sigc::signal` for normal GTK++ signals. +- If a scope must not be interrupted by `pthread_cancel`, guard it with `waybar::util::CancellationGuard`. + +### State / IPC +- Modules that talk to a compositor often implement a small `EventHandler` interface (`onEvent(...)`) and delegate to a singleton backend (e.g., `gIPC`). + +### RAII +- Prefer `std::unique_ptr` with custom deleters over raw `new/delete` for C-API resources (see `ScopedFd`, `UdevDeleter`, `UdevDeviceDeleter`, `ScopeGuard`). + +## 6. JSON Configuration +- Every module receives `const Json::Value& config` (usually as the first constructor argument). +- Always validate node type before reading: + ```cpp + if (config_["sort-by-id"].isBool()) { ... } + if (config.isMember("window-rewrite-default") && config["window-rewrite-default"].isString()) { ... } + ``` +- Use `waybar::util::JsonParser` if you need to pre-process JSON with non-standard escape sequences. + +## 7. String & UI Formatting +- Use `fmt::format` / `fmt::join` for all string composition. +- Use `fmt::dynamic_format_arg_store` when building arguments dynamically. +- Custom `fmt::formatter` specializations are allowed for domain types (e.g., `Glib::ustring`, project enums). +- Sanitize arbitrary text before inserting into Pango markup with `waybar::util::sanitize_string`. +- Use `waybar::util::rewriteString` for user-configurable regex rewrites. +- Truncate UTF-8 safely with `waybar::util::utf8_truncate` / `utf8_width`. + +## 8. Error Handling & Logging +- Use `spdlog` for all logging: + - `spdlog::error("Context: {}", e.what());` + - `spdlog::warn("Deprecated key '{}', prefer '{}'", old, replacement);` + - `spdlog::debug("State changed to {}", value);` +- Throw `std::runtime_error` (or similar) for fatal initialization failures that should bubble up to `main()`. + +## 9. GTK / Glib Patterns +- Prefer gtkmm-3.0 types (`Gtk::Button`, `Gtk::Label`, `Gdk::Pixbuf`, `Glib::RefPtr`, `Glib::ustring`) over raw C GTK APIs. +- Access the default icon theme through thread-safe wrappers if off the main thread (`DefaultGtkIconThemeWrapper`). +- Tooltips and labels should respect the module `tooltip` toggle (see `tooltipEnabled()` in `AModule`). + +## 10. Platform Portability +- Isolate platform-specific code in dedicated files (e.g., `linux.cpp`, `bsd.cpp`). +- Use preprocessor guards for platform differences (`#if defined(__FreeBSD__)`, `#if defined(HAVE_LIBNL)`). +- Keep the common interface in a shared header or base class. + +## 11. Thread Safety & Cross-Thread Communication +- GTK is strictly single-threaded. Never emit raw `sigc::signal` from background threads. +- Use `waybar::SafeSignal` to marshal events from worker threads to the GTK main loop. +- When a module manages background threads, use `std::mutex`, `std::recursive_mutex`, or atomic variables to protect shared state, and ensure the destructor joins or synchronizes with those threads before destroying resources. + +## 12. Unsafe Patterns to Avoid +- Do not use `strcpy`, `strcat`, or `sprintf` into fixed-size buffers (e.g. `char buf[PATH_MAX]`). Prefer `std::string`, `std::vector`, or `std::array` with bounds-safe operations. +- When passing a `std::vector` buffer to a C API that expects a mutable `char*` string, always ensure the buffer is null-terminated and clamp the written length to `size() - 1`. Never use `std::copy` from an unbounded source into a fixed-size buffer. + +## 13. Singleton Lifetime +- Singletons or objects with process-wide lifetime must not store references (`&`) or pointers to objects with shorter lifetime (e.g., configuration trees, GTK widgets, or bar instances) unless they are explicitly notified of destruction. Prefer storing configuration by value (`Json::Value`, `std::string`, etc.) if the singleton outlives the config loader. From 50f636236a172c4f498674d00739edb9730f682d Mon Sep 17 00:00:00 2001 From: Viktar Lukashonak Date: Wed, 15 Jul 2026 15:46:37 +0300 Subject: [PATCH 399/416] man(waybar-cava): refresh and polish documentation Synchronize the cava module man page with the current implementation: - Fix scdoc table syntax (correct header/body cell prefixes) - Add missing Waybar-side options: format-icons, vertex_shader, fragment_shader - Remove options not consumed by the module (data_format, raw_target) - Clarify option scope: raw-only, GLSL-only, and cava-config-only settings - Update descriptions to match backend behavior: - ascii_range is derived from format-icons length - bar_height is ignored by Waybar - bar_delimiter is used for raw output - Polish grammar, tighten structure, and fix example filenames - Refresh STYLE and FRONTENDS sections to match the actual source --- man/waybar-cava.5.scd | 309 ++++++++++++++++++++---------------------- 1 file changed, 147 insertions(+), 162 deletions(-) diff --git a/man/waybar-cava.5.scd b/man/waybar-cava.5.scd index 60aa32ee..47d2e0dc 100644 --- a/man/waybar-cava.5.scd +++ b/man/waybar-cava.5.scd @@ -6,10 +6,10 @@ waybar - cava module # DESCRIPTION -*cava* module for karlstav/cava project. See it on github: https://github.com/karlstav/cava. - -Module supports two different frontends starting from the 0.15.0 release. The frontend that -will be used is managed by the method parameter in the [output] section of the cava configuration file. +The *cava* module integrates the *karlstav/cava* audio visualizer into Waybar. +It supports two frontends: a text-based *raw* frontend and a GPU-based *GLSL* +frontend. The active frontend is selected by the *method* option in the +*[output]* section of the cava configuration file. # FILES @@ -27,185 +27,186 @@ libcava lives in: # CONFIGURATION [- *Option* -:- *Typeof* -:- *Default* -:- *Description* +:[ *Type* +:[ *Default* +:[ *Description* |[ *cava_config* :[ string :[ -:< Path where cava configuration file is placed to -|[ *method* \[output\] -:[ string -:[ -:< Manages which frontend Waybar cava module should use. Values: raw, sdl_glsl. Not a waybar JSON key: it is set through the *method* option in the *\[output\]* section of the cava configuration file (*cava_config*) +:[ Path to the cava configuration file. When provided, cava settings are read from it first. |[ *framerate* :[ integer :[ 30 -:[ Frames per second. Is used as a replacement for *interval* +:[ Target frames per second. Replaces the generic *interval* option. |[ *autosens* :[ integer :[ 1 -:[ Will attempt to decrease sensitivity if the bars peak +:[ Automatically decrease sensitivity when the bars peak. |[ *sensitivity* :[ integer :[ 100 -:[ Manual sensitivity in %. If autosens is enabled, this will only be the initial value. 200 means double height. Accepts only non-negative values +:[ Manual sensitivity in %. If *autosens* is enabled, this is only the initial value. 200 means double height. Accepts only non-negative values. |[ *bars* :[ integer :[ 12 -:[ The number of bars +:[ The number of bars. |[ *lower_cutoff_freq* :[ long integer :[ 50 -:[ Lower cutoff frequencies for lowest bars the bandwidth of the visualizer +:[ Lower cutoff frequency for the visualizer bandwidth. |[ *higher_cutoff_freq* :[ long integer :[ 10000 -:[ Higher cutoff frequencies for highest bars the bandwidth of the visualizer +:[ Higher cutoff frequency for the visualizer bandwidth. |[ *sleep_timer* :[ integer :[ 5 -:[ Seconds with no input before cava main thread goes to sleep mode +:[ Seconds of silence before cava enters sleep mode. |[ *hide_on_silence* :[ bool :[ false -:[ Hides the widget if no input (after sleep_timer elapsed) +:[ Hide the widget when silence lasts longer than *sleep_timer*. |[ *format_silent* :[ string :[ -:[ Widget's text after sleep_timer elapsed (hide_on_silence has to be false) +:[ Text shown when the module is silent and *hide_on_silence* is false. **Raw frontend only.** +|[ *format-icons* +:[ array +:[ +:[ Array of characters used to render bar levels in the raw frontend. The number of items determines the dynamic range. |[ *method* \[input\] :[ string :[ pulse -:[ Audio capturing method. Possible methods are: pipewire, pulse, alsa, fifo, sndio or shmem +:[ Audio capture backend. Supported values: pipewire, pulse, alsa, fifo, sndio, shmem. |[ *source* :[ string :[ auto -:[ See cava configuration +:[ Audio source identifier. See the cava documentation for details. |[ *sample_rate* :[ long integer :[ 44100 -:[ See cava configuration +:[ See the cava documentation. |[ *sample_bits* :[ integer :[ 16 -:[ See cava configuration +:[ See the cava documentation. |[ *stereo* :[ bool :[ true -:[ Visual channels +:[ Enable stereo visualization. |[ *reverse* :[ bool :[ false -:[ Displays frequencies the other way around +:[ Reverse the bar order (highest frequencies on the left). |[ *bar_delimiter* :[ integer :[ 0 -:[ Each bar is separated by a delimiter. Use decimal value in ascii table(i.e. 59 = ";"). 0 means no delimiter +:[ Delimiter placed between bars in the raw output. Use a decimal ASCII value (e.g. 59 = ";"). 0 means no delimiter. |[ *monstercat* :[ bool :[ false -:[ Disables or enables the so-called "Monstercat smoothing" with or without "waves" +:[ Enable Monstercat smoothing. |[ *waves* :[ bool :[ false -:[ Disables or enables the so-called "Monstercat smoothing" with or without "waves" +:[ Enable the waves effect alongside Monstercat smoothing. |[ *noise_reduction* :[ double :[ 0.77 -:[ Fractional value between 0.0 - 1.0. The raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth. Values near 1.0 will be very slow and smooth, near 0.0 will be fast but noisy +:[ Smoothing factor between 0.0 and 1.0. Higher values produce slower, smoother animation; lower values are more reactive but noisy. |[ *gravity* :[ integer :[ -:[ Gravity smoothing filter. Higher values make the bars drop faster. Adjusted by *noise_reduction* when set +:[ Gravity factor. Higher values make bars fall faster. When *noise_reduction* is set, this is derived automatically. |[ *integral* :[ integer :[ -:[ Integral smoothing filter. Higher values make the visualization smoother but less precise. Adjusted by *noise_reduction* when set +:[ Integral smoothing factor. Higher values produce smoother but less precise animation. When *noise_reduction* is set, this is derived automatically. |[ *input_delay* :[ integer :[ 4 -:[ Sets the delay before fetching audio source thread start working. On author's machine, Waybar starts much faster than pipewire audio server, and without a little delay cava module fails because pipewire is not ready -|[ *ascii_max_range* -:[ integer -:[ 7 -:[ It's impossible to set it directly. The value is dictated by the number of icons in the array *format-icons* -|[ *data_format* -:[ string -:[ ascii -:[ Raw data format. Can be 'binary' or 'ascii' -|[ *raw_target* -:[ string -:[ /dev/stdout -:[ Raw output target. A fifo will be created if target does not exist -|[ *menu* -:[ string -:[ -:[ Action that popups the menu. -|[ *menu-file* -:[ string -:[ -:[ Location of the menu descriptor file. There need to be an element of type GtkMenu with id *menu* -|[ *menu-actions* -:[ array -:[ -:[ The actions corresponding to the buttons of the menu. -|[ *bar_spacing* -:[ integer -:[ -:[ Bars' space between bars in number of characters +:[ Delay in seconds before starting audio capture. Increase this if Waybar starts before the audio server (e.g. PipeWire). |[ *bar_width* :[ integer :[ -:[ Bars' width between bars in number of characters +:[ Bar width in pixels. **Used by the GLSL frontend.** +|[ *bar_spacing* +:[ integer +:[ +:[ Space between bars in pixels. **Used by the GLSL frontend.** |[ *bar_height* :[ integer :[ -:[ Useless. bar_height is only used for output in "noritake" format -|[ *background* +:[ Ignored by Waybar. Used only by cava's "noritake" output format. +|[ *menu* :[ string :[ -:[ GLSL actual. Support hex code colors only. Must be within ''. Not a waybar JSON key: set it as *background* in the *\[color\]* section of the cava configuration file (*cava_config*) -|[ *foreground* +:[ Action that opens the menu. +|[ *menu-file* :[ string :[ -:[ GLSL actual. Support hex code colors only. Must be within ''. Not a waybar JSON key: set it as *foreground* in the *\[color\]* section of the cava configuration file (*cava_config*) -|[ *gradient* -:[ integer -:[ 0 -:[ GLSL actual. Gradient mode(0/1 - on/off) -|[ *gradient_count* -:[ integer -:[ 0 -:[ GLSL actual. The count of colors for the gradient -|[ *gradient_color_N* -:[ string +:[ Location of the menu descriptor file. There must be a GtkMenu element with id *menu*. +|[ *menu-actions* +:[ array :[ -:[ GLSL actual. N - the number of the gradient color between 1 and 8. Only hex defined colors are supported. Must be within '' +:[ Actions corresponding to the buttons of the menu. +|[ *method* \[output\] +:[ string +:[ raw +:[ Cava output method. Set to *raw* for the text frontend or *sdl_glsl* for the GPU frontend. **This is set inside the *[output]* section of the cava configuration file, not in Waybar's JSON.** |[ *sdl_width* :[ integer :[ -:[ GLSL actual. Manages the width of the waybar cava GLSL frontend module +:[ GLSL frontend width in pixels. **GLSL only.** |[ *sdl_height* :[ integer :[ -:[ GLSL actual. Manages the height of the waybar cava GLSL frontend module +:[ GLSL frontend height in pixels. **GLSL only.** +|[ *vertex_shader* +:[ string +:[ +:[ Path to the vertex shader. **GLSL only; set in the *[output]* section of the cava configuration file.** +|[ *fragment_shader* +:[ string +:[ +:[ Path to the fragment shader. **GLSL only; set in the *[output]* section of the cava configuration file.** |[ *continuous_rendering* :[ integer :[ 0 -:[ GLSL actual. Keep rendering even if no audio. Recommended to set to 1. Not a waybar JSON key: set it as *continuous_rendering* in the *\[output\]* section of the cava configuration file (*cava_config*) +:[ Continue rendering when silent. Set to 1 for smooth animation. **GLSL only; set in the *[output]* section of the cava configuration file.** +|[ *background* +:[ string +:[ +:[ Background color as a '#RRGGBB' hex string (must be quoted). **GLSL only; set in the *[color]* section of the cava configuration file.** +|[ *foreground* +:[ string +:[ +:[ Foreground color as a '#RRGGBB' hex string (must be quoted). **GLSL only; set in the *[color]* section of the cava configuration file.** +|[ *gradient* +:[ integer +:[ 0 +:[ Enable gradient mode (0 = off, 1 = on). **GLSL only; set in the *[color]* section of the cava configuration file.** +|[ *gradient_count* +:[ integer +:[ 0 +:[ Number of gradient colors (up to 8). **GLSL only; set in the *[color]* section of the cava configuration file.** +|[ *gradient_color_N* +:[ string +:[ +:[ Gradient color N (1–8) as a '#RRGGBB' hex string (must be quoted). **GLSL only; set in the *[color]* section of the cava configuration file.** -Configuration can be provided as: -- The only cava configuration file which is provided through *cava_config*. The rest configuration can be skipped -- Without cava configuration file. In such case cava should be configured through provided list of the configuration option -- Mix. When provided both And cava configuration file And configuration options. In such case, waybar applies configuration file first and then overrides particular options by the provided list of configuration options +Configuration can be provided in three ways: + +- **Cava config only**: set *cava_config* to a cava configuration file and omit all other options. +- **Waybar JSON only**: leave out *cava_config* and set every option in Waybar's module configuration. +- **Mixed**: provide a *cava_config* and also set specific options in Waybar's JSON. Waybar reads the file first, then overrides any values present in the JSON. # ACTIONS [- *String* -:- *Action* +:[ *Action* |[ *mode* -:< Switch main cava thread and fetch audio source thread from/to pause/resume +:[ Toggle pause/resume for the audio capture and output threads. # DEPENDENCIES @@ -215,31 +216,32 @@ Configuration can be provided as: # SOLVING ISSUES -. On start Waybar throws an exception "error while loading shared libraries: libcava.so: cannot open shared object file: No such file or directory". - It might happen when libcava for some reason hasn't been registered in the system. sudo ldconfig should help. - It might also happen when Waybar was installed into /usr/local while libcava lives elsewhere. In that case: - 1. Drop the local cava library: sudo rm -rfv /usr/local/include/cava /usr/local/lib64/pkgconfig/cava.pc /usr/local/lib64/libcava.so - 2. Set the prefix where Waybar should be installed: meson configure build -Dprefix="/usr" - 3. Build Waybar: make - 4. Install Waybar into the system: sudo meson install -C build -. Waybar is starting but cava module doesn't react to the music - 1. In such cases at first need to make sure usual cava application is working as well - 2. If so, need to comment all configuration options. Uncomment cava_config and provide the path to the working cava config - 3. You might set too huge or too small input_delay. Try to setup to 4 seconds, restart waybar, and check again 4 seconds past. Usual even on weak machines it should be enough - 4. You might accidentally switch action mode to pause mode +. At startup Waybar fails with *"error while loading shared libraries: libcava.so: cannot open shared object file: No such file or directory"*. + This happens when libcava has not been registered in the system library cache. Run *sudo ldconfig* to refresh the cache. + This can also occur when Waybar is installed under */usr/local* but libcava is elsewhere. To fix it: + 1. Remove the local libcava installation: *sudo rm -rfv /usr/local/include/cava /usr/local/lib64/pkgconfig/cava.pc /usr/local/lib64/libcava.so* + 2. Reconfigure Waybar to use the system prefix: *meson configure build -Dprefix="/usr"* + 3. Rebuild Waybar: *ninja -C build* + 4. Install Waybar: *sudo meson install -C build* -# RISING ISSUES +. Waybar starts but the cava module does not react to audio. + 1. First, verify that standalone cava works correctly. + 2. If it does, comment out all Waybar cava options, uncomment *cava_config*, and point it to the working cava configuration file. + 3. The *input_delay* may be too large or too small. Try setting it to 4 seconds, restart Waybar, and check again after that delay. This is usually sufficient, even on slower machines. + 4. You may have accidentally toggled pause mode via an action. -For clear understanding: this module is a cava API's consumer. So for any bugs related to cava engine you should contact Cava upstream(https://github.com/karlstav/cava) ++ -with the one Exception. Cava upstream doesn't provide cava as a shared library. For that, this module author made a fork libcava(https://github.com/LukashonakV/cava). ++ -So the order is: -. cava upstream -. libcava upstream. -In case when cava releases new version and you're wanna get it, it should be raised an issue to libcava(https://github.com/LukashonakV/cava) with title ++ -\[Bump\]x.x.x where x.x.x is cava release version. +# REPORTING ISSUES + +This module is a consumer of the cava API. For bugs in the cava engine itself, please report them to [cava upstream](https://github.com/karlstav/cava) first. + +Upstream cava does not provide a shared library. The Waybar cava module uses [libcava](https://github.com/LukashonakV/cava), a fork maintained by the module author, to provide one. If the issue is specific to the shared library packaging, report it to libcava. + +When requesting a new upstream cava release to be packaged in libcava, open an issue at libcava with the title `[Bump] x.x.x`, where `x.x.x` is the desired cava version. # EXAMPLES +## Raw frontend + ``` "cava": { //"cava_config": "$XDG_CONFIG_HOME/cava/cava.conf", @@ -253,7 +255,6 @@ In case when cava releases new version and you're wanna get it, it should be rai "source": "auto", "stereo": true, "reverse": false, - "bar_delimiter": 0, "monstercat": false, "waves": false, "noise_reduction": 0.77, @@ -264,39 +265,6 @@ In case when cava releases new version and you're wanna get it, it should be rai } }, ``` -# STYLE - -- *#cava* Raw frontend widget -- *#cava.silent* Applied after no sound has been detected for sleep_timer seconds -- *#cava.updated* Applied when a new frame is shown -- *#cavaGLSL* GLSL frontend widget (used instead of *#cava* when the cava *method* is *sdl_glsl*) -- *#cavaGLSL.silent* Applied after no sound has been detected for sleep_timer seconds -- *#cavaGLSL.updated* Applied when a new frame is shown -# FRONTENDS - -## RAW -The cava raw frontend uses ASCII characters to visualize incoming audio data. Each ASCII symbol position corresponds to the value of the audio power pulse. - -Under the hood: -``` -. Incoming audio power pulse list is : 12684 -. Configured array of ASCII codes is: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█" ]. See `format-icons` https://github.com/Alexays/Waybar/wiki/Module:-Cava#example -``` -As a result cava frontend will give ▁▂▆█▄ - -Examples: - -waybar config -``` -"cava": { - "cava_config": "$XDG_CONFIG_HOME/cava/waybar_raw.conf", - "input_delay": 2, - "format-icons" : ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█" ], - "actions": { - "on-click-right": "mode" - } - }, -``` waybar_raw.conf ``` @@ -367,6 +335,7 @@ sleep_timer = 5 # README.md contains further information on how to setup CAVA for JACK. # + # The options 'sample_rate', 'sample_bits', 'channels' and 'autoconnect' can be configured for some input methods: # sample_rate: fifo, pipewire, sndio, oss # sample_bits: fifo, pipewire, sndio, oss @@ -468,7 +437,7 @@ bar_delimiter = 0 # Noise reduction, int 0 - 100. default 77 # the raw visualization is very noisy, this factor adjusts the integral and gravity filters to keep the signal smooth -# 100 will be very slow and smooth, 0 will be fast but noisy. +# 100 will be very slow and smooth, 0 will be fast and noisy. [eq] @@ -477,33 +446,32 @@ bar_delimiter = 0 # Remember to uncomment more than one key! More keys = more precision. # Look at readme.md on github for further explanations and examples. ``` -## GLSL -The Cava GLSL frontend delegates the visualization of incoming audio data to the GPU via OpenGL. -There are some mandatory dependencies that need to be satisfied in order for Cava GLSL to be built and function properly: +## GLSL frontend -. epoxy library must be installed on the system -. Vertex and fragment shaders from the original project must be used. They should be downloaded, and the file paths must be configured correctly in the Waybar Cava configuration: - 1. cava shaders [cava shaders](https://github.com/karlstav/cava/tree/master/output/shaders) - 2. libcava shaders [libcava shaders](https://github.com/LukashonakV/cava/tree/master/output/shaders) -. It is highly recommended to have a separate cava configuration for the Waybar Cava GLSL module and to use this as the cava_config in the Waybar configuration. -. It is common for cava configurations to be placed in the XDG_CONFIG_HOME directory, including shaders as well. Consider keeping them in the $XDG_CONFIG_HOME/cava/shaders folder. +The GLSL frontend requires: -Key configuration options: +. The *epoxy* library. +. Vertex and fragment shaders from the cava project. Download them and place under _$XDG_CONFIG_HOME/cava/shaders_, then reference them in the cava configuration: + 1. [cava shaders](https://github.com/karlstav/cava/tree/master/output/shaders) + 2. [libcava shaders](https://github.com/LukashonakV/cava/tree/master/output/shaders) +. A separate cava configuration file is highly recommended. -. bars. The more values the parameter has, the more interesting the visualization becomes. -. method in output section must be set to sdl_glsl -. sdl_width and sdl_height manage the size of the module. Adjust them according to your needs. -. Shaders for sdl_glsl, located in $HOME/.config/cava/shaders. Example: "vertex_shader" = "pass_through.vert" "fragment_shader" = "spectrogram.frag" -. Set continuous_rendering to 1 to enable smooth rendering; set it to 0 otherwise. It is recommended to keep it set to 1. -. background, foreground, and gradient_color_N (where N is a number between 1 and 8) must be defined using hex code +Key cava configuration options for GLSL: + +. *bars* — higher values produce more detailed visualization. +. *method* in *[output]* must be set to *sdl_glsl*. +. *sdl_width* and *sdl_height* control the module size. +. *vertex_shader* and *fragment_shader* point to the shader files under _$HOME/.config/cava/shaders_. +. *continuous_rendering* — set to 1 for smooth animation. +. *background*, *foreground*, and *gradient_color_N* must use hex codes inside single quotes. Example: waybar config ``` "cava": { - "cava_config": "$XDG_CONFIG_HOME/cava/waybar_cava#3.conf", + "cava_config": "$XDG_CONFIG_HOME/cava/waybar_cava.conf", "input_delay": 2, "actions": { "on-click-right": "mode" @@ -511,7 +479,7 @@ waybar config }, ``` -waybar_raw.conf +waybar_cava.conf ``` ## Configuration file for CAVA. # Remove the ; to change parameters. @@ -721,4 +689,21 @@ gradient_color_2 = '#45475A' # Look at readme.md on github for further explanations and examples. ``` -Different waybar_cava#N.conf see at [cava GLSL](https://github.com/Alexays/Waybar/wiki/Module:-Cava:-GLSL) +More GLSL examples are available on the [cava GLSL wiki page](https://github.com/Alexays/Waybar/wiki/Module:-Cava:-GLSL). + +# STYLE + +- *#cava* Raw frontend widget +- *#cava.silent* Applied after no sound has been detected for *sleep_timer* seconds +- *#cava.updated* Applied when a new frame is shown +- *#cavaGLSL* GLSL frontend widget (used instead of *#cava* when the cava *method* is *sdl_glsl*) +- *#cavaGLSL.silent* Applied after no sound has been detected for *sleep_timer* seconds +- *#cavaGLSL.updated* Applied when a new frame is shown + +# FRONTENDS + +## RAW +The raw frontend maps each bar's amplitude to a character from *format-icons*. The final widget text is the concatenation of all characters, optionally separated by *bar_delimiter*. See the EXAMPLES section above for a complete configuration. + +## GLSL +The GLSL frontend renders the visualization with OpenGL ES using user-provided shaders. It is selected by setting *method = sdl_glsl* in the cava configuration. See the EXAMPLES section above for a complete configuration. From 73fdafb5d3ebb8676bcf9fbfd7b815801cfe93a1 Mon Sep 17 00:00:00 2001 From: grisha Date: Wed, 15 Jul 2026 16:15:09 +0200 Subject: [PATCH 400/416] use nmcli implementation for calculating signal strength --- src/modules/network.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index d3adfbe5..003d3bc0 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -1067,18 +1067,18 @@ void waybar::modules::Network::parseSignal(struct nlattr** bss) { if (bss[NL80211_BSS_SIGNAL_MBM] != nullptr) { // signalstrength in dBm from mBm signal_strength_dbm_ = nla_get_s32(bss[NL80211_BSS_SIGNAL_MBM]) / 100; - // WiFi-hardware usually operates in the range -90 to -30dBm. - // If a signal is too strong, it can overwhelm receiving circuity that is designed - // to pick up and process a certain signal level. The following percentage is scaled to - // punish signals that are too strong (>= -45dBm) or too weak (<= -45 dBm). - const int hardwareOptimum = -45; - const int hardwareMin = -90; - const int strength = - 100 - - ((abs(signal_strength_dbm_ - hardwareOptimum) / double{hardwareOptimum - hardwareMin}) * - 100); - signal_strength_ = std::clamp(strength, 0, 100); + + // uses nmcli implementation for calculating strength + // https://github.com/NetworkManager/NetworkManager/blob/23ffa5fc6e7acbd7a96138c6c18f478f5127177d/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c#L411 + + const int noise_floor_dbm = -90; + const int signal_max_dbm = -20; + signal_strength_dbm_ = CLAMP(signal_strength_dbm_, noise_floor_dbm, signal_max_dbm); + signal_strength_ = 100 + - (70 + * (((float) signal_max_dbm - (float) signal_strength_dbm_) + / ((float) signal_max_dbm - (float) noise_floor_dbm))); if (signal_strength_dbm_ >= -50) { signal_strength_app_ = "Great Connectivity"; From e60ffa4a9cd33749fdc0b2bcae982094e5435e4a Mon Sep 17 00:00:00 2001 From: grisha Date: Wed, 15 Jul 2026 22:52:46 +0200 Subject: [PATCH 401/416] clang format --- src/modules/network.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 003d3bc0..6da1a5c8 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -1068,17 +1068,14 @@ void waybar::modules::Network::parseSignal(struct nlattr** bss) { // signalstrength in dBm from mBm signal_strength_dbm_ = nla_get_s32(bss[NL80211_BSS_SIGNAL_MBM]) / 100; - // uses nmcli implementation for calculating strength // https://github.com/NetworkManager/NetworkManager/blob/23ffa5fc6e7acbd7a96138c6c18f478f5127177d/src/libnm-platform/wifi/nm-wifi-utils-nl80211.c#L411 const int noise_floor_dbm = -90; const int signal_max_dbm = -20; signal_strength_dbm_ = CLAMP(signal_strength_dbm_, noise_floor_dbm, signal_max_dbm); - signal_strength_ = 100 - - (70 - * (((float) signal_max_dbm - (float) signal_strength_dbm_) - / ((float) signal_max_dbm - (float) noise_floor_dbm))); + signal_strength_ = 100 - (70 * (((float)signal_max_dbm - (float)signal_strength_dbm_) / + ((float)signal_max_dbm - (float)noise_floor_dbm))); if (signal_strength_dbm_ >= -50) { signal_strength_app_ = "Great Connectivity"; From 5b8c98729fcafeb1c56e6b821f01f86e4e6014df Mon Sep 17 00:00:00 2001 From: yubo <3597387401@qq.com> Date: Sat, 18 Jul 2026 21:14:08 +0800 Subject: [PATCH 402/416] refactor: rm direct dp.emit() call in constructor. --- src/bar.cpp | 1 + src/modules/backlight.cpp | 2 -- src/modules/bluetooth.cpp | 2 -- src/modules/custom.cpp | 2 +- src/modules/custom_graph.cpp | 2 +- src/modules/hyprland/submap.cpp | 1 - src/modules/hyprland/window.cpp | 2 -- src/modules/hyprland/windowcount.cpp | 1 - src/modules/inhibitor.cpp | 1 - src/modules/mango/keymode.cpp | 1 - src/modules/mango/language.cpp | 1 - src/modules/mango/layout.cpp | 1 - src/modules/mango/window.cpp | 1 - src/modules/mango/workspaces.cpp | 1 - src/modules/mpris/mpris.cpp | 3 --- src/modules/network.cpp | 1 - src/modules/niri/language.cpp | 1 - src/modules/niri/window.cpp | 2 -- src/modules/niri/workspaces.cpp | 2 -- src/modules/power_profiles_daemon.cpp | 2 -- src/modules/privacy/privacy.cpp | 2 -- src/modules/sni/tray.cpp | 1 - src/modules/sway/language.cpp | 1 - src/modules/sway/mode.cpp | 1 - src/modules/systemd_failed_units.cpp | 2 -- src/modules/upower.cpp | 2 -- src/modules/wayfire/window.cpp | 2 -- src/modules/wayfire/workspaces.cpp | 3 --- 28 files changed, 3 insertions(+), 41 deletions(-) diff --git a/src/bar.cpp b/src/bar.cpp index ec3189c6..c81a7e6e 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -624,6 +624,7 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, spdlog::error("{}: {}", ref, e.what()); } }); + module->dp.emit(); } catch (const std::exception& e) { spdlog::warn("module {}: {}", name.asString(), e.what()); } diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 24e08d54..48caad21 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -17,8 +17,6 @@ waybar::modules::Backlight::Backlight(const std::string& id, const Json::Value& : ALabel(config, "backlight", id, "{percent}%", 2), preferred_device_(config["device"].isString() ? config["device"].asString() : ""), backend(interval_, [this] { dp.emit(); }) { - dp.emit(); - // Set up scroll handler event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Backlight::handleScroll)); diff --git a/src/modules/bluetooth.cpp b/src/modules/bluetooth.cpp index 58b1711f..4c9994f4 100644 --- a/src/modules/bluetooth.cpp +++ b/src/modules/bluetooth.cpp @@ -213,8 +213,6 @@ waybar::modules::Bluetooth::Bluetooth(const std::string& id, const Json::Value& #ifdef WANT_RFKILL rfkill_.on_update.connect(sigc::hide(sigc::mem_fun(*this, &Bluetooth::update))); #endif - - dp.emit(); } auto waybar::modules::Bluetooth::update() -> void { diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 64577509..836a0d71 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -17,7 +17,7 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, if (config.isNull()) { spdlog::warn("There is no configuration for 'custom/{}', element will be hidden", name); } - dp.emit(); + if (!config_["signal"].empty() && config_["interval"].empty() && config_["restart-interval"].empty()) { waitingWorker(); diff --git a/src/modules/custom_graph.cpp b/src/modules/custom_graph.cpp index 06e45d06..2a044abf 100644 --- a/src/modules/custom_graph.cpp +++ b/src/modules/custom_graph.cpp @@ -22,7 +22,7 @@ waybar::modules::CustomGraph::CustomGraph(const std::string& name, const std::st if (config.isNull()) { spdlog::warn("There is no configuration for 'custom-graph/{}', element will be hidden", name); } - dp.emit(); + if (!config_["signal"].empty() && config_["interval"].empty() && config_["restart-interval"].empty()) { waitingWorker(); diff --git a/src/modules/hyprland/submap.cpp b/src/modules/hyprland/submap.cpp index a65a2861..408e667e 100644 --- a/src/modules/hyprland/submap.cpp +++ b/src/modules/hyprland/submap.cpp @@ -20,7 +20,6 @@ Submap::Submap(const std::string& id, const Bar& bar, const Json::Value& config) // register for hyprland ipc m_ipc.registerForIPC("submap", this); - dp.emit(); if (config["icons"].isObject()) { const Json::Value& icons = config["icons"]; diff --git a/src/modules/hyprland/window.cpp b/src/modules/hyprland/window.cpp index 13993482..38f291f7 100644 --- a/src/modules/hyprland/window.cpp +++ b/src/modules/hyprland/window.cpp @@ -32,8 +32,6 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) m_ipc.registerForIPC("changefloatingmode", this); m_ipc.registerForIPC("fullscreen", this); windowIpcUniqueLock.unlock(); - - dp.emit(); } Window::~Window() { diff --git a/src/modules/hyprland/windowcount.cpp b/src/modules/hyprland/windowcount.cpp index 5f5db80c..d0dcd96f 100644 --- a/src/modules/hyprland/windowcount.cpp +++ b/src/modules/hyprland/windowcount.cpp @@ -19,7 +19,6 @@ WindowCount::WindowCount(const std::string& id, const Bar& bar, const Json::Valu queryActiveWorkspace(); update(); - dp.emit(); // register for hyprland ipc m_ipc.registerForIPC("fullscreen", this); diff --git a/src/modules/inhibitor.cpp b/src/modules/inhibitor.cpp index 43c1f83e..88b711d3 100644 --- a/src/modules/inhibitor.cpp +++ b/src/modules/inhibitor.cpp @@ -104,7 +104,6 @@ Inhibitor::Inhibitor(const std::string& id, const Bar& bar, const Json::Value& c inhibitors_(::getInhibitors(config)) { event_box_.add_events(Gdk::BUTTON_PRESS_MASK); event_box_.signal_button_press_event().connect(sigc::mem_fun(*this, &Inhibitor::handleToggle)); - dp.emit(); } Inhibitor::~Inhibitor() { diff --git a/src/modules/mango/keymode.cpp b/src/modules/mango/keymode.cpp index 16009712..3d28a0c8 100644 --- a/src/modules/mango/keymode.cpp +++ b/src/modules/mango/keymode.cpp @@ -7,7 +7,6 @@ namespace waybar::modules::mango { Keymode::Keymode(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "keymode", id, "{}", 0, false), bar_(bar) { IPC::getInstance().registerForIPC("monitor", this); - dp.emit(); } Keymode::~Keymode() { IPC::getInstance().unregisterForIPC(this); } diff --git a/src/modules/mango/language.cpp b/src/modules/mango/language.cpp index 549d22ed..c04492ee 100644 --- a/src/modules/mango/language.cpp +++ b/src/modules/mango/language.cpp @@ -17,7 +17,6 @@ Language::Language(const std::string& id, const Bar& bar, const Json::Value& con IPC::getInstance().registerForIPC("monitor", this); updateFromIPC(); - dp.emit(); } Language::~Language() { diff --git a/src/modules/mango/layout.cpp b/src/modules/mango/layout.cpp index 124d26cb..999b0a96 100644 --- a/src/modules/mango/layout.cpp +++ b/src/modules/mango/layout.cpp @@ -7,7 +7,6 @@ namespace waybar::modules::mango { Layout::Layout(const std::string& id, const Bar& bar, const Json::Value& config) : ALabel(config, "layout", id, "{}", 0, false), bar_(bar) { IPC::getInstance().registerForIPC("monitor", this); - dp.emit(); } Layout::~Layout() { IPC::getInstance().unregisterForIPC(this); } diff --git a/src/modules/mango/window.cpp b/src/modules/mango/window.cpp index 54f677bb..db42b226 100644 --- a/src/modules/mango/window.cpp +++ b/src/modules/mango/window.cpp @@ -10,7 +10,6 @@ namespace waybar::modules::mango { Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) : AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar) { IPC::getInstance().registerForIPC("monitor", this); - dp.emit(); } Window::~Window() { IPC::getInstance().unregisterForIPC(this); } diff --git a/src/modules/mango/workspaces.cpp b/src/modules/mango/workspaces.cpp index 7424a352..2c2b790c 100644 --- a/src/modules/mango/workspaces.cpp +++ b/src/modules/mango/workspaces.cpp @@ -29,7 +29,6 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& } IPC::getInstance().registerForIPC("monitor", this); - dp.emit(); } Workspaces::~Workspaces() { diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index d70f5902..d079a898 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -177,9 +177,6 @@ Mpris::Mpris(const std::string& id, const Json::Value& config) thread_.sleep_for(interval_); }; } - - // trigger initial update - dp.emit(); } Mpris::~Mpris() { diff --git a/src/modules/network.cpp b/src/modules/network.cpp index 6da1a5c8..14922210 100644 --- a/src/modules/network.cpp +++ b/src/modules/network.cpp @@ -140,7 +140,6 @@ waybar::modules::Network::Network(const std::string& id, const Json::Value& conf createEventSocket(); createInfoSocket(); - dp.emit(); // Ask for a dump of interfaces and then addresses to populate our // information. First the interface dump, and once done, the callback // will be called again which will ask for addresses dump. diff --git a/src/modules/niri/language.cpp b/src/modules/niri/language.cpp index d739ce57..68d585af 100644 --- a/src/modules/niri/language.cpp +++ b/src/modules/niri/language.cpp @@ -18,7 +18,6 @@ Language::Language(const std::string& id, const Bar& bar, const Json::Value& con gIPC->registerForIPC("KeyboardLayoutSwitched", this); updateFromIPC(); - dp.emit(); } Language::~Language() { diff --git a/src/modules/niri/window.cpp b/src/modules/niri/window.cpp index 5785e6cb..9f209bfb 100644 --- a/src/modules/niri/window.cpp +++ b/src/modules/niri/window.cpp @@ -18,8 +18,6 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) gIPC->registerForIPC("WindowClosed", this); gIPC->registerForIPC("WindowFocusChanged", this); gIPC->registerForIPC("WindowLayoutsChanged", this); - - dp.emit(); } Window::~Window() { gIPC->unregisterForIPC(this); } diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index 0bdcf85b..720bb1cf 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -82,8 +82,6 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll)); } - - dp.emit(); } Workspaces::~Workspaces() { gIPC->unregisterForIPC(this); } diff --git a/src/modules/power_profiles_daemon.cpp b/src/modules/power_profiles_daemon.cpp index b897cd1c..adf08b58 100644 --- a/src/modules/power_profiles_daemon.cpp +++ b/src/modules/power_profiles_daemon.cpp @@ -44,8 +44,6 @@ PowerProfilesDaemon::PowerProfilesDaemon(const std::string& id, const Json::Valu Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::BUS_TYPE_SYSTEM, "net.hadess.PowerProfiles", "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", sigc::mem_fun(*this, &PowerProfilesDaemon::busConnectedCb)); - // Schedule update to set the initial visibility - dp.emit(); } void PowerProfilesDaemon::busConnectedCb(Glib::RefPtr& r) { diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 6b0a019c..7bb9de92 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -117,8 +117,6 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat geoclue_backend = util::GeoClueBackend::GeoClueBackend::getInstance(); geoclue_backend->in_use_changed_signal_event.connect( sigc::mem_fun(*this, &Privacy::onGeoCluePrivacyNodesChanged)); - - dp.emit(); } void Privacy::onPWPrivacyNodesChanged() { diff --git a/src/modules/sni/tray.cpp b/src/modules/sni/tray.cpp index 285c103f..1751e55d 100644 --- a/src/modules/sni/tray.cpp +++ b/src/modules/sni/tray.cpp @@ -49,7 +49,6 @@ Tray::Tray(const std::string& id, const Bar& bar, const Json::Value& config) box_.set_spacing(config_["spacing"].asUInt()); } nb_hosts_ += 1; - dp.emit(); } void Tray::checkIgnoreList(std::unique_ptr* item_ptr) { diff --git a/src/modules/sway/language.cpp b/src/modules/sway/language.cpp index c284e53c..755c27df 100644 --- a/src/modules/sway/language.cpp +++ b/src/modules/sway/language.cpp @@ -42,7 +42,6 @@ Language::Language(const std::string& id, const Json::Value& config) spdlog::error("Language: {}", e.what()); } }); - dp.emit(); } void Language::onCmd(const struct Ipc::ipc_response& res) { diff --git a/src/modules/sway/mode.cpp b/src/modules/sway/mode.cpp index 1f5f071a..cb351b65 100644 --- a/src/modules/sway/mode.cpp +++ b/src/modules/sway/mode.cpp @@ -16,7 +16,6 @@ Mode::Mode(const std::string& id, const Json::Value& config) spdlog::error("Mode: {}", e.what()); } }); - dp.emit(); } void Mode::onEvent(const struct Ipc::ipc_response& res) { diff --git a/src/modules/systemd_failed_units.cpp b/src/modules/systemd_failed_units.cpp index 3804cbe2..75f1b233 100644 --- a/src/modules/systemd_failed_units.cpp +++ b/src/modules/systemd_failed_units.cpp @@ -88,8 +88,6 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& throw std::runtime_error("Neither system nor user status is requested."); updateData(); - /* Always update for the first time. */ - dp.emit(); } auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, diff --git a/src/modules/upower.cpp b/src/modules/upower.cpp index f5d72639..f027a752 100644 --- a/src/modules/upower.cpp +++ b/src/modules/upower.cpp @@ -79,8 +79,6 @@ UPower::UPower(const std::string& id, const Json::Value& config) resetDevices(); setDisplayDevice(); - // Update the widget - dp.emit(); } UPower::~UPower() { diff --git a/src/modules/wayfire/window.cpp b/src/modules/wayfire/window.cpp index 8634f090..f40554b0 100644 --- a/src/modules/wayfire/window.cpp +++ b/src/modules/wayfire/window.cpp @@ -20,8 +20,6 @@ Window::Window(const std::string& id, const Bar& bar, const Json::Value& config) ipc->register_handler("view-app-id-changed", handler); ipc->register_handler("window-rules/get-focused-view", handler); - - dp.emit(); } Window::~Window() { ipc->unregister_handler(handler); } diff --git a/src/modules/wayfire/workspaces.cpp b/src/modules/wayfire/workspaces.cpp index 724a19f8..6e39e3a7 100644 --- a/src/modules/wayfire/workspaces.cpp +++ b/src/modules/wayfire/workspaces.cpp @@ -44,9 +44,6 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& ipc->register_handler("window-rules/list-outputs", handler); ipc->register_handler("window-rules/list-wsets", handler); ipc->register_handler("window-rules/get-focused-output", handler); - - // initial render - dp.emit(); } Workspaces::~Workspaces() { ipc->unregister_handler(handler); } From 937aca0b72156123d457cec4f97a01e87bdcdc3f Mon Sep 17 00:00:00 2001 From: gitmpr <89863774+gitmpr@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:38:50 +0200 Subject: [PATCH 403/416] config: add min-brightness to backlight example config --- resources/config.jsonc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/config.jsonc b/resources/config.jsonc index e816ee2f..eb92feb4 100644 --- a/resources/config.jsonc +++ b/resources/config.jsonc @@ -154,7 +154,8 @@ "backlight": { // "device": "acpi_video1", "format": "{percent}% {icon}", - "format-icons": ["", "", "", "", "", "", "", "", ""] + "format-icons": ["", "", "", "", "", "", "", "", ""], + "min-brightness": 1 }, "battery": { "states": { From 7409ff69765bd358133e6a1b2a19b51b1e6384d3 Mon Sep 17 00:00:00 2001 From: gitmpr <89863774+gitmpr@users.noreply.github.com> Date: Sat, 18 Jul 2026 20:41:40 +0200 Subject: [PATCH 404/416] backlight: clamp scroll step to min-brightness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When min-brightness is set, a scroll step that would cross it was not clamped — only scrolling while already at or below min was blocked. With scroll-step: 5 and min-brightness: 1, scrolling from 5% would pass the guard (5 > 1) and set brightness to 0%. Fix by computing the post-step value before applying it: if it would fall below min-brightness, set exactly to min-brightness instead. --- src/modules/backlight.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 24e08d54..8f7ff760 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -120,9 +120,15 @@ bool waybar::modules::Backlight::handleScroll(GdkEventScroll* e) { if (config_["min-brightness"].isDouble()) { min_brightness = config_["min-brightness"].asDouble(); } - if (backend.get_scaled_brightness(preferred_device_) <= min_brightness && - ct == util::ChangeType::Decrease) { - return true; + if (ct == util::ChangeType::Decrease) { + const double current = backend.get_scaled_brightness(preferred_device_); + if (current <= min_brightness) { + return true; + } + if (current - step < min_brightness) { + backend.set_scaled_brightness(preferred_device_, static_cast(std::round(min_brightness))); + return true; + } } backend.set_brightness(preferred_device_, ct, step); From d32e7fa86f679f36bee804a527e26b22ade813fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bene-J=C3=A1zem=20B=2E=20Nobre?= Date: Mon, 20 Jul 2026 11:03:31 -0400 Subject: [PATCH 405/416] fix(tooltips): keep dynamic tooltips from resetting hover delay Cache ALabel tooltip markup and provide it through query-tooltip instead of repeatedly setting GtkWidget's tooltip-markup property. This prevents frequently updating modules from restarting GTK's display-wide tooltip timer. Keep the active tooltip updated directly so dynamic tooltip contents continue refreshing while the pointer remains stationary. --- include/ALabel.hpp | 2 ++ src/ALabel.cpp | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/include/ALabel.hpp b/include/ALabel.hpp index 3e3690b1..948289ae 100644 --- a/include/ALabel.hpp +++ b/include/ALabel.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -92,6 +93,7 @@ class ALabel : public AModule { // no collation weight, so two different icons compare equal. std::optional last_label_markup_; std::optional last_tooltip_markup_; + Glib::RefPtr active_tooltip_; }; } // namespace waybar diff --git a/src/ALabel.cpp b/src/ALabel.cpp index 3af6f5a3..23ee7851 100644 --- a/src/ALabel.cpp +++ b/src/ALabel.cpp @@ -42,6 +42,26 @@ ALabel::ALabel(const Json::Value& config, const std::string& name, const std::st } label_.get_style_context()->add_class(MODULE_CLASS); event_box_.add(label_); + if (tooltipEnabled()) { + // Keep dynamic tooltip contents out of GtkWidget's tooltip-markup property. + // Setting that property queues a display-wide tooltip query, which restarts + // GTK's hover delay when modules update faster than the delay can expire. + label_.set_has_tooltip(true); + label_.signal_query_tooltip().connect( + [this](int, int, bool, const Glib::RefPtr& tooltip) { + if (!last_tooltip_markup_.has_value() || last_tooltip_markup_->empty()) { + active_tooltip_.reset(); + return false; + } + active_tooltip_ = tooltip; + tooltip->set_markup(*last_tooltip_markup_); + return true; + }); + event_box_.signal_leave_notify_event().connect([this](GdkEventCrossing*) { + active_tooltip_.reset(); + return false; + }); + } if (config_["max-length"].isUInt()) { label_.set_max_width_chars(config_["max-length"].asInt()); label_.set_ellipsize(Pango::EllipsizeMode::ELLIPSIZE_END); @@ -162,8 +182,10 @@ bool ALabel::setTooltipMarkup(const Glib::ustring& markup) { return false; } - label_.set_tooltip_markup(markup); last_tooltip_markup_ = markup.raw(); + if (active_tooltip_) { + active_tooltip_->set_markup(markup); + } return true; } From 5bd3cd480579cefcf56ccf8155e396c858a96456 Mon Sep 17 00:00:00 2001 From: Viktar Lukashonak Date: Tue, 21 Jul 2026 15:17:55 +0300 Subject: [PATCH 406/416] cavaBump --- man/waybar-cava.5.scd | 30 +++++++++++++++++++----------- meson.build | 2 +- nix/default.nix | 4 ++-- src/modules/cava/cava_backend.cpp | 2 -- subprojects/libcava.wrap | 8 ++++---- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/man/waybar-cava.5.scd b/man/waybar-cava.5.scd index 47d2e0dc..14ab9ef3 100644 --- a/man/waybar-cava.5.scd +++ b/man/waybar-cava.5.scd @@ -102,6 +102,14 @@ libcava lives in: :[ integer :[ 0 :[ Delimiter placed between bars in the raw output. Use a decimal ASCII value (e.g. 59 = ";"). 0 means no delimiter. +|[ *data_format* +:[ string +:[ ascii +:[ Raw data format. Can be 'binary' or 'ascii'. **Raw frontend only.** +|[ *raw_target* +:[ string +:[ +:[ Raw output target. A fifo will be created if target does not exist. **Raw frontend only.** |[ *monstercat* :[ bool :[ false @@ -114,14 +122,6 @@ libcava lives in: :[ double :[ 0.77 :[ Smoothing factor between 0.0 and 1.0. Higher values produce slower, smoother animation; lower values are more reactive but noisy. -|[ *gravity* -:[ integer -:[ -:[ Gravity factor. Higher values make bars fall faster. When *noise_reduction* is set, this is derived automatically. -|[ *integral* -:[ integer -:[ -:[ Integral smoothing factor. Higher values produce smoother but less precise animation. When *noise_reduction* is set, this is derived automatically. |[ *input_delay* :[ integer :[ 4 @@ -162,6 +162,14 @@ libcava lives in: :[ integer :[ :[ GLSL frontend height in pixels. **GLSL only.** +|[ *min-length* +:[ integer +:[ +:[ Requested width of the GLSL widget in pixels. If not set, falls back to *max-length*, then *sdl_width*. **GLSL only.** +|[ *max-length* +:[ integer +:[ +:[ Fallback width of the GLSL widget if *min-length* is not set. **GLSL only.** |[ *vertex_shader* :[ string :[ @@ -185,11 +193,11 @@ libcava lives in: |[ *gradient* :[ integer :[ 0 -:[ Enable gradient mode (0 = off, 1 = on). **GLSL only; set in the *[color]* section of the cava configuration file.** +:[ Enable gradient mode (0 = off, 1 = on). **GLSL only.** Can also be set in the *[color]* section of the cava configuration file. |[ *gradient_count* :[ integer :[ 0 -:[ Number of gradient colors (up to 8). **GLSL only; set in the *[color]* section of the cava configuration file.** +:[ Number of gradient colors (up to 8). **GLSL only.** Can also be set in the *[color]* section of the cava configuration file. |[ *gradient_color_N* :[ string :[ @@ -625,7 +633,7 @@ vertex_shader = pass_through.vert fragment_shader = bar_spectrum.frag ; for glsl output mode, keep rendering even if no audio -continuous_rendering = 1; +continuous_rendering = 1 # disable console blank (screen saver) in tty # (Not supported on FreeBSD) diff --git a/meson.build b/meson.build index e3839cf5..63cbe8ed 100644 --- a/meson.build +++ b/meson.build @@ -538,7 +538,7 @@ else endif cava = dependency('libcava', - version : '>=0.10.7', + version : '>=1.0.0', required: get_option('cava'), fallback : ['libcava', 'cava_dep'], not_found_message: 'cava is not found. Building waybar without cava') diff --git a/nix/default.nix b/nix/default.nix index 8102363a..46514d4f 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -6,13 +6,13 @@ }: let libcava = rec { - version = "0.10.7"; + version = "1.0.0"; src = pkgs.fetchFromGitHub { owner = "LukashonakV"; repo = "cava"; # NOTE: Needs to match the cava.wrap tag = "${version}"; - hash = "sha256-zkyj1vBzHtoypX4Bxdh1Vmwh967DKKxN751v79hzmgQ="; + hash = "sha256-0r5aAmTs+FcmS501tNYKxG9H+Pq6i32BDRBEjWW6M74="; }; }; in diff --git a/src/modules/cava/cava_backend.cpp b/src/modules/cava/cava_backend.cpp index 00239131..0d1f2bdf 100644 --- a/src/modules/cava/cava_backend.cpp +++ b/src/modules/cava/cava_backend.cpp @@ -262,8 +262,6 @@ void waybar::modules::cava::CavaBackend::loadConfig() { new_prm.xaxis = ::cava::xaxis_scale::NONE; new_prm.mono_opt = ::cava::AVERAGE; new_prm.autobars = 0; - if (cfg["gravity"].isInt()) new_prm.gravity = cfg["gravity"].asInt(); - if (cfg["integral"].isInt()) new_prm.integral = cfg["integral"].asInt(); if (cfg["framerate"].isInt()) new_prm.framerate = cfg["framerate"].asInt(); if (cfg["autosens"].isInt()) new_prm.autosens = cfg["autosens"].asInt(); diff --git a/subprojects/libcava.wrap b/subprojects/libcava.wrap index d66d657b..821bbd5f 100644 --- a/subprojects/libcava.wrap +++ b/subprojects/libcava.wrap @@ -4,9 +4,9 @@ #depth = 1 [wrap-file] -directory = cava-0.10.7 -source_url = https://github.com/LukashonakV/cava/archive/0.10.7.tar.gz -source_filename = cava-0.10.7.tar.gz -source_hash = 50cc6413e9c96c503657f814744a2baf429a24ff9fed31a8343e0ed285269eff +directory = cava-1.0.0 +source_url = https://github.com/LukashonakV/cava/archive/1.0.0.tar.gz +source_filename = cava-1.0.0.tar.gz +source_hash = 437df0a29e52e555357a06238f4c5cbe6d51b5dd4225700cb7adbb19d8bb9474 [provide] libcava = cava_dep From 2d87bd5ba9d5df62a140067a2a786ad00ecc5b46 Mon Sep 17 00:00:00 2001 From: Viktar Lukashonak Date: Thu, 23 Jul 2026 11:58:35 +0300 Subject: [PATCH 407/416] Drop mandatory hyprland dependency --- include/modules/hyprland/workspaces.hpp | 6 ++-- include/util/enum.hpp | 28 +++++++++++---- include/util/kill_signal.hpp | 6 ++-- meson.build | 1 - src/bar.cpp | 5 ++- src/modules/hyprland/workspaces.cpp | 4 +-- src/util/enum.cpp | 47 ------------------------- 7 files changed, 30 insertions(+), 67 deletions(-) delete mode 100644 src/util/enum.cpp diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 1237ef24..c29ec30c 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -167,9 +167,8 @@ class Workspaces : public AModule, public EventHandler { std::map> m_orphanWindowMap; enum class SortMethod { ID, NAME, NUMBER, SPECIAL_CENTERED, DEFAULT }; - util::EnumParser m_enumParser; SortMethod m_sortBy = SortMethod::DEFAULT; - std::map m_sortMap = {{"ID", SortMethod::ID}, + static inline const std::map m_sortMap = {{"ID", SortMethod::ID}, {"NAME", SortMethod::NAME}, {"NUMBER", SortMethod::NUMBER}, {"SPECIAL-CENTERED", SortMethod::SPECIAL_CENTERED}, @@ -207,9 +206,8 @@ class Workspaces : public AModule, public EventHandler { int m_taskbarMaxIcons = 0; // 0 means unlimited Gtk::Orientation m_taskbarOrientation = Gtk::ORIENTATION_HORIZONTAL; bool m_taskbarReverseDirection = false; - util::EnumParser m_activeWindowEnumParser; ActiveWindowPosition m_activeWindowPosition = ActiveWindowPosition::NONE; - std::map m_activeWindowPositionMap = { + static inline std::map m_activeWindowPositionMap = { {"NONE", ActiveWindowPosition::NONE}, {"FIRST", ActiveWindowPosition::FIRST}, {"LAST", ActiveWindowPosition::LAST}, diff --git a/include/util/enum.hpp b/include/util/enum.hpp index 681385fd..6780a9e1 100644 --- a/include/util/enum.hpp +++ b/include/util/enum.hpp @@ -1,19 +1,33 @@ #pragma once +#include +#include #include #include #include +#include "util/string.hpp" + namespace waybar::util { template -struct EnumParser { - public: - EnumParser(); - ~EnumParser(); - +//struct EnumParser { EnumType parseStringToEnum(const std::string& str, - const std::map& enumMap); -}; + const std::map& enumMap) { + std::string uppercaseStr = capitalize(str); + std::map capitalizedEnumMap; + std::transform( + enumMap.begin(), enumMap.end(), + std::inserter(capitalizedEnumMap, capitalizedEnumMap.end()), + [](const auto& pair) { + return std::make_pair(capitalize(pair.first), pair.second); + }); + + auto it = capitalizedEnumMap.find(uppercaseStr); + if (it != capitalizedEnumMap.end()) return it->second; + + throw std::invalid_argument("Invalid string representation for enum"); +// } +} } // namespace waybar::util diff --git a/include/util/kill_signal.hpp b/include/util/kill_signal.hpp index 26d156e9..221c554a 100644 --- a/include/util/kill_signal.hpp +++ b/include/util/kill_signal.hpp @@ -13,14 +13,14 @@ enum class KillSignalAction : std::uint8_t { HIDE, NOOP, }; -const std::map userKillSignalActions = { +inline const std::map userKillSignalActions = { {"TOGGLE", KillSignalAction::TOGGLE}, {"RELOAD", KillSignalAction::RELOAD}, {"SHOW", KillSignalAction::SHOW}, {"HIDE", KillSignalAction::HIDE}, {"NOOP", KillSignalAction::NOOP}}; -const KillSignalAction SIGNALACTION_DEFAULT_SIGUSR1 = KillSignalAction::TOGGLE; -const KillSignalAction SIGNALACTION_DEFAULT_SIGUSR2 = KillSignalAction::RELOAD; +inline const KillSignalAction SIGNALACTION_DEFAULT_SIGUSR1 = KillSignalAction::TOGGLE; +inline const KillSignalAction SIGNALACTION_DEFAULT_SIGUSR2 = KillSignalAction::RELOAD; }; // namespace waybar::util diff --git a/meson.build b/meson.build index 63cbe8ed..5dc7338d 100644 --- a/meson.build +++ b/meson.build @@ -185,7 +185,6 @@ src_files = files( 'src/config.cpp', 'src/group.cpp', 'src/util/portal.cpp', - 'src/util/enum.cpp', 'src/util/prepare_for_sleep.cpp', 'src/util/ustring_clen.cpp', 'src/util/sanitize_str.cpp', diff --git a/src/bar.cpp b/src/bar.cpp index c81a7e6e..d84aafed 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -296,13 +296,12 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) } #endif - waybar::util::EnumParser m_signalActionEnumParser; const auto& configSigusr1 = config["on-sigusr1"]; if (configSigusr1.isString()) { auto strSigusr1 = configSigusr1.asString(); try { onSigusr1 = - m_signalActionEnumParser.parseStringToEnum(strSigusr1, util::userKillSignalActions); + util::parseStringToEnum(strSigusr1, util::userKillSignalActions); } catch (const std::invalid_argument& e) { onSigusr1 = util::SIGNALACTION_DEFAULT_SIGUSR1; spdlog::warn( @@ -314,7 +313,7 @@ waybar::Bar::Bar(struct waybar_output* w_output, const Json::Value& w_config) auto strSigusr2 = configSigusr2.asString(); try { onSigusr2 = - m_signalActionEnumParser.parseStringToEnum(strSigusr2, util::userKillSignalActions); + util::parseStringToEnum(strSigusr2, util::userKillSignalActions); } catch (const std::invalid_argument& e) { onSigusr2 = util::SIGNALACTION_DEFAULT_SIGUSR2; spdlog::warn( diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 202b892b..780e041d 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -682,7 +682,7 @@ auto Workspaces::populateSortByConfig(const Json::Value& config) -> void { if (configSortBy.isString()) { auto sortByStr = configSortBy.asString(); try { - m_sortBy = m_enumParser.parseStringToEnum(sortByStr, m_sortMap); + m_sortBy = waybar::util::parseStringToEnum(sortByStr, m_sortMap); } catch (const std::invalid_argument& e) { m_sortBy = SortMethod::DEFAULT; spdlog::warn( @@ -806,7 +806,7 @@ auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value& config) -> vo auto posStr = workspaceTaskbar["active-window-position"].asString(); try { m_activeWindowPosition = - m_activeWindowEnumParser.parseStringToEnum(posStr, m_activeWindowPositionMap); + util::parseStringToEnum(posStr, m_activeWindowPositionMap); } catch (const std::invalid_argument& e) { spdlog::warn( "Invalid string representation for active-window-position. Falling back to 'none'."); diff --git a/src/util/enum.cpp b/src/util/enum.cpp deleted file mode 100644 index 6b5d5562..00000000 --- a/src/util/enum.cpp +++ /dev/null @@ -1,47 +0,0 @@ -#include "util/enum.hpp" - -#include // for std::transform -#include // for std::toupper -#include -#include -#include -#include - -#include "modules/hyprland/workspaces.hpp" -#include "util/string.hpp" - -namespace waybar::util { - -template -EnumParser::EnumParser() = default; - -template -EnumParser::~EnumParser() = default; - -template -EnumType EnumParser::parseStringToEnum(const std::string& str, - const std::map& enumMap) { - // Convert the input string to uppercase - std::string uppercaseStr = capitalize(str); - - // Capitalize the map keys before searching - std::map capitalizedEnumMap; - std::transform( - enumMap.begin(), enumMap.end(), std::inserter(capitalizedEnumMap, capitalizedEnumMap.end()), - [](const auto& pair) { return std::make_pair(capitalize(pair.first), pair.second); }); - - // Return enum match of string - auto it = capitalizedEnumMap.find(uppercaseStr); - if (it != capitalizedEnumMap.end()) return it->second; - - // Throw error if it doesn't return - throw std::invalid_argument("Invalid string representation for enum"); -} - -// Explicit instantiations for specific EnumType types you intend to use -// Add explicit instantiations for all relevant EnumType types -template struct EnumParser; -template struct EnumParser; -template struct EnumParser; - -} // namespace waybar::util From 6e0b50567ee353870d34e3aa391f9f728371fc9b Mon Sep 17 00:00:00 2001 From: Adrian Ratiu Date: Fri, 3 Jul 2026 15:53:45 +0300 Subject: [PATCH 408/416] fix(sway/ipc): don't mistake events for the subscribe reply Ipc::subscribe() assumes the next message on the event socket is the reply to the IPC_SUBSCRIBE it just sent. When a module subscribes more than once (sway/workspaces subscribes to "workspace" and then to "window"), an event from the first subscription can arrive before the reply to the second one. The payload check fails and the thrown exception permanently disables the module for that bar. This is easy to hit when bars are (re)created on output hotplug, since sway emits a burst of workspace events at exactly that moment while moving workspaces to the new output: [warning] module sway/workspaces: Disabling module "sway/workspaces", Unable to subscribe ipc event Fix it by skipping over event messages (type high bit set) until the subscribe reply is found, re-emitting them on signal_event so none are lost. Fixes: #5218 Co-Authored-By: Claude Fable 5 Signed-off-by: Adrian Ratiu --- src/modules/sway/ipc/client.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/modules/sway/ipc/client.cpp b/src/modules/sway/ipc/client.cpp index ce67279c..6d6c07ff 100644 --- a/src/modules/sway/ipc/client.cpp +++ b/src/modules/sway/ipc/client.cpp @@ -193,6 +193,13 @@ void Ipc::sendCmd(uint32_t type, const std::string& payload) { void Ipc::subscribe(const std::string& payload) { auto res = Ipc::send(fd_event_, IPC_SUBSCRIBE, payload); + // Events subscribed to by a previous subscribe() call may arrive on the + // socket before the reply to this one; deliver them and keep reading until + // the subscribe reply is found. + while ((res.type >> 31) != 0U) { + signal_event.emit(res); + res = Ipc::recv(fd_event_); + } if (res.payload != "{\"success\": true}") { throw std::runtime_error("Unable to subscribe ipc event"); } From f9e2b1dea7f862a1be5495ad86043fc1bff6439b Mon Sep 17 00:00:00 2001 From: Ibrahim Issa Date: Tue, 28 Jul 2026 13:48:40 +0200 Subject: [PATCH 409/416] fix(mpris): sanitize metadata to prevent multiline UI expansion --- src/modules/mpris/mpris.cpp | 50 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/modules/mpris/mpris.cpp b/src/modules/mpris/mpris.cpp index d079a898..e9b87e3f 100644 --- a/src/modules/mpris/mpris.cpp +++ b/src/modules/mpris/mpris.cpp @@ -543,34 +543,40 @@ auto Mpris::getPlayerInfo() -> std::optional { .length = std::nullopt, }; - if (auto* artist_ = playerctl_player_get_artist(last_active_player_, &error)) { - spdlog::debug("mpris[{}]: artist = {}", info.name, artist_); - info.artist = artist_; - g_free(artist_); - } - if (error) goto errorexit; + auto sanitize = [ ]( char* raw_str ) { + std::string s( raw_str ); + std::replace( s.begin(), s.end(), '\n', ' ' ); + std::replace( s.begin(), s.end(), '\r', ' ' ); + return s; + }; - if (auto* album_artist_ = - 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(auto* artist_ = playerctl_player_get_artist( last_active_player_, &error )) { + spdlog::debug( "mpris[{}]: artist = {}", info.name, artist_ ); + info.artist = sanitize( artist_ ); + g_free( artist_ ); } - if (error) goto errorexit; + if(error) goto errorexit; - if (auto* album_ = playerctl_player_get_album(last_active_player_, &error)) { - spdlog::debug("mpris[{}]: album = {}", info.name, album_); - info.album = album_; - g_free(album_); + if(auto* album_artist_ = playerctl_player_print_metadata_prop( last_active_player_, "xesam:albumArtist", &error )) { + spdlog::debug( "mpris[{}]: albumArtist = {}", info.name, album_artist_ ); + info.album_artist = sanitize( album_artist_ ); + g_free( album_artist_ ); } - if (error) goto errorexit; + if(error) goto errorexit; - if (auto* title_ = playerctl_player_get_title(last_active_player_, &error)) { - spdlog::debug("mpris[{}]: title = {}", info.name, title_); - info.title = title_; - g_free(title_); + if(auto* album_ = playerctl_player_get_album( last_active_player_, &error )) { + spdlog::debug( "mpris[{}]: album = {}", info.name, album_ ); + info.album = sanitize( album_ ); + g_free( album_ ); } - if (error) goto errorexit; + if(error) goto errorexit; + + if(auto* title_ = playerctl_player_get_title( last_active_player_, &error )) { + spdlog::debug( "mpris[{}]: title = {}", info.name, title_ ); + info.title = sanitize( title_ ); + g_free( title_ ); + } + if(error) goto errorexit; if (auto* length_ = playerctl_player_print_metadata_prop(last_active_player_, "mpris:length", &error)) { From e07ee87ab374203bdeebadd162d4825dc1c31283 Mon Sep 17 00:00:00 2001 From: Arnaud Venturi Date: Thu, 30 Jul 2026 18:16:29 +0200 Subject: [PATCH 410/416] Replace lone tab character in style.css with spaces for consistency --- resources/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/style.css b/resources/style.css index fe29f8d5..86396cd2 100644 --- a/resources/style.css +++ b/resources/style.css @@ -302,7 +302,7 @@ label:focus { } #scratchpad.empty { - background-color: transparent; + background-color: transparent; } #privacy { From d01f1f44ec16866c084f7c792502dc4c3a5c08fa Mon Sep 17 00:00:00 2001 From: Brian Brown Date: Fri, 31 Jul 2026 22:27:44 -0400 Subject: [PATCH 411/416] fix(hyprland/workspaces): respect enable-bar-scroll config option Removed hasScrollConfig boolean variable in Workspaces init function. It was Or'd with barScroll boolean variable and overriding it when enable-bar-scroll is declared false in waybar config. Setting any scroll up or down events caused enable-bar-scroll to be overwritten. Tested on Arch/Hyprland, removing hasScrollConfig and the or condition restored expected behavior. Fixes #5229 --- src/modules/hyprland/workspaces.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 780e041d..2e852dc0 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -55,8 +55,8 @@ void Workspaces::init() { if (m_scrollEventConnection_.connected()) { m_scrollEventConnection_.disconnect(); } - bool hasScrollConfig = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString(); - if (barScroll() || hasScrollConfig) { + + if (barScroll()) { auto& window = const_cast(m_bar).window; window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); m_scrollEventConnection_ = From 5ef2da41be02303848c8317816de0fa8f53501cc Mon Sep 17 00:00:00 2001 From: Guilherme Daher <> Date: Wed, 12 Aug 2026 16:09:48 -0300 Subject: [PATCH 412/416] fix(hyprland/workspaces): handle workspace ID changes --- include/modules/hyprland/workspace.hpp | 1 + include/modules/hyprland/workspaces.hpp | 1 + src/modules/hyprland/workspaces.cpp | 32 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/include/modules/hyprland/workspace.hpp b/include/modules/hyprland/workspace.hpp index 519c347b..b8cbc889 100644 --- a/include/modules/hyprland/workspace.hpp +++ b/include/modules/hyprland/workspace.hpp @@ -61,6 +61,7 @@ class Workspace { void setUrgent(bool value = true) { m_isUrgent = value; }; void setVisible(bool value = true) { m_isVisible = value; }; void setWindows(uint value) { m_windows = value; }; + void setId(int value) { m_id = value; }; void setName(std::string const& value) { m_name = value; }; void setOutput(std::string const& value) { m_output = value; }; bool containsWindow(WindowAddress const& addr) const { diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index c29ec30c..5b14d294 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -108,6 +108,7 @@ class Workspaces : public AModule, public EventHandler { Json::Value const& clientsData = Json::Value::nullRef); void onWorkspaceMoved(std::string const& payload); void onWorkspaceRenamed(std::string const& payload); + void onWorkspaceIdChanged(std::string const& payload); static std::optional parseWorkspaceId(std::string const& workspaceIdStr); // monitor events diff --git a/src/modules/hyprland/workspaces.cpp b/src/modules/hyprland/workspaces.cpp index 780e041d..61144609 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -312,6 +312,8 @@ void Workspaces::onEvent(const std::string& ev) { onSpecialWorkspaceActivated(payload); } else if (eventName == "destroyworkspacev2") { onWorkspaceDestroyed(payload); + } else if (eventName == "changeworkspaceid") { + onWorkspaceIdChanged(payload); } else if (eventName == "createworkspacev2") { onWorkspaceCreated(payload); } else if (eventName == "focusedmonv2") { @@ -449,6 +451,35 @@ void Workspaces::onWorkspaceRenamed(std::string const& payload) { sortWorkspaces(); } +void Workspaces::onWorkspaceIdChanged(std::string const& payload) { + spdlog::debug("Workspace ID changed: {}", payload); + + const auto [oldIdStr, newIdStr] = splitDoublePayload(payload); + + const auto oldId = parseWorkspaceId(oldIdStr); + const auto newId = parseWorkspaceId(newIdStr); + + if (!oldId.has_value() || !newId.has_value()) { + spdlog::warn("Invalid workspace ID change payload: {}", payload); + return; + } + + for (auto& workspace : m_workspaces) { + if (workspace->id() == *oldId) { + spdlog::debug("Changing workspace ID from {} to {}", *oldId, *newId); + workspace->setId(*newId); + break; + } + } + + if (m_activeWorkspaceId == *oldId) { + m_activeWorkspaceId = *newId; + } + + sortWorkspaces(); + updateWindowCount(); +} + void Workspaces::onMonitorFocused(std::string const& payload) { spdlog::trace("Monitor focused: {}", payload); @@ -827,6 +858,7 @@ auto Workspaces::registerIpc() -> void { m_ipc.registerForIPC("createworkspacev2", this); m_ipc.registerForIPC("destroyworkspacev2", this); m_ipc.registerForIPC("focusedmonv2", this); + m_ipc.registerForIPC("changeworkspaceid", this); m_ipc.registerForIPC("moveworkspacev2", this); m_ipc.registerForIPC("renameworkspace", this); m_ipc.registerForIPC("openwindow", this); From 38cc11c3b083c473e421ac9a6ead16e77f3469eb Mon Sep 17 00:00:00 2001 From: Shivank Garg Date: Thu, 13 Aug 2026 00:50:42 +0530 Subject: [PATCH 413/416] privacy: disconnect pending timeouts on destruction The privacy module registers timeout callbacks in the GLib main context. The GeoClue callback uses sigc::mem_fun() with the Privacy object, but the connection remains active when the module is destroyed during a bar reload. If the timeout runs after destruction, it dereferences the freed Privacy object and crashes Waybar: #0 waybar::modules::privacy::Privacy::locationTimeout(bool) #4 g_main_context_iteration() #5 g_application_run() Disconnect the GeoClue and visibility timeout connections in the Privacy destructor so no callback can run after the module is gone. The unpatched binary crashed after four SIGUSR2 reloads at 1.5-second intervals. The patched binary survived ten reloads. Fixes: eeb7bc702e8e ("Added GeoClue2 privacy item") Assisted-by: Claude Opus + Cursor --- include/modules/privacy/privacy.hpp | 1 + src/modules/privacy/privacy.cpp | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index 7307afc9..41145e50 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -17,6 +17,7 @@ namespace waybar::modules::privacy { class Privacy : public AModule { public: Privacy(const std::string&, const Json::Value&, Gtk::Orientation, const std::string& pos); + ~Privacy() override; auto update() -> void override; private: diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 7bb9de92..76762cf5 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -119,6 +119,15 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, Gtk::Orientat sigc::mem_fun(*this, &Privacy::onGeoCluePrivacyNodesChanged)); } +Privacy::~Privacy() { + // Both timeouts are attached to the global main context, which outlives this + // module when the bar is reloaded. sigc::connection's destructor does not + // disconnect, and the members are torn down after the backends they use, so a + // pending timeout would dispatch onto freed memory. + geoclue_timeout_conn.disconnect(); + visibility_conn.disconnect(); +} + void Privacy::onPWPrivacyNodesChanged() { mutex_.lock(); nodes_audio_out.clear(); From e92f280a9429bda024df96f4fd9cb5b597daa356 Mon Sep 17 00:00:00 2001 From: Guilherme Daher <> Date: Wed, 12 Aug 2026 17:06:44 -0300 Subject: [PATCH 414/416] fix: formatted with clang --- include/modules/hyprland/workspaces.hpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 5b14d294..f71a6e42 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -169,11 +169,12 @@ class Workspaces : public AModule, public EventHandler { enum class SortMethod { ID, NAME, NUMBER, SPECIAL_CENTERED, DEFAULT }; SortMethod m_sortBy = SortMethod::DEFAULT; - static inline const std::map m_sortMap = {{"ID", SortMethod::ID}, - {"NAME", SortMethod::NAME}, - {"NUMBER", SortMethod::NUMBER}, - {"SPECIAL-CENTERED", SortMethod::SPECIAL_CENTERED}, - {"DEFAULT", SortMethod::DEFAULT}}; + static inline const std::map m_sortMap = { + {"ID", SortMethod::ID}, + {"NAME", SortMethod::NAME}, + {"NUMBER", SortMethod::NUMBER}, + {"SPECIAL-CENTERED", SortMethod::SPECIAL_CENTERED}, + {"DEFAULT", SortMethod::DEFAULT}}; std::string m_formatBefore; std::string m_formatAfter; From ab08e22494e7a7e5917a848ed4cef00253026fc2 Mon Sep 17 00:00:00 2001 From: Guilherme Daher <> Date: Thu, 13 Aug 2026 08:50:04 -0300 Subject: [PATCH 415/416] fix: fix module backlight format using clang --- src/modules/backlight.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/backlight.cpp b/src/modules/backlight.cpp index 3116816a..2f7bbdff 100644 --- a/src/modules/backlight.cpp +++ b/src/modules/backlight.cpp @@ -124,7 +124,8 @@ bool waybar::modules::Backlight::handleScroll(GdkEventScroll* e) { return true; } if (current - step < min_brightness) { - backend.set_scaled_brightness(preferred_device_, static_cast(std::round(min_brightness))); + backend.set_scaled_brightness(preferred_device_, + static_cast(std::round(min_brightness))); return true; } } From ca46b9321f6e59d4bc82cf2c4d6331469a06de22 Mon Sep 17 00:00:00 2001 From: Guilherme Daher <> Date: Thu, 13 Aug 2026 08:56:48 -0300 Subject: [PATCH 416/416] feat: clang format options added on makefile and docker image for archlinux --- Dockerfiles/archlinux | 2 +- Makefile | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Dockerfiles/archlinux b/Dockerfiles/archlinux index d4274a46..8743a7c4 100644 --- a/Dockerfiles/archlinux +++ b/Dockerfiles/archlinux @@ -3,5 +3,5 @@ FROM archlinux:base-devel RUN pacman -Syu --noconfirm && \ - pacman -S --noconfirm git meson base-devel libinput wayland wayland-protocols glib2-devel pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient gobject-introspection libxkbcommon playerctl iniparser fftw && \ + pacman -S --noconfirm git meson base-devel libinput wayland wayland-protocols glib2-devel pixman libxkbcommon mesa gtkmm3 jsoncpp pugixml scdoc libpulse libdbusmenu-gtk3 libmpdclient gobject-introspection libxkbcommon playerctl iniparser fftw clang && \ sed -Ei 's/#(en_(US|GB)\.UTF)/\1/' /etc/locale.gen && locale-gen diff --git a/Makefile b/Makefile index 12af8e0e..54e72ce0 100644 --- a/Makefile +++ b/Makefile @@ -27,5 +27,10 @@ test-detailed: meson test -C build --verbose --print-errorlogs --test-args='--reporter console -s' .PHONY: test-detailed +format: + git diff --name-only --diff-filter=ACMR | \ + grep -E '\.(c|h|cpp|hpp)$$' | \ + xargs -r clang-format -i + clean: rm -rf build