From 0fdc4c11f3eab5b97c42116f69ac7656d8d3e194 Mon Sep 17 00:00:00 2001 From: Narice Date: Tue, 6 Dec 2022 18:40:38 +0100 Subject: [PATCH 01/66] 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 02/66] 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 626c52cc79f4aa4798e026f6250e8c61943a95cc Mon Sep 17 00:00:00 2001 From: lilly-lizard Date: Sat, 13 Jul 2024 14:08:48 +1200 Subject: [PATCH 03/66] 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 04/66] 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 6e4160dc532069e3fe5136f8424d036ad0abbb86 Mon Sep 17 00:00:00 2001 From: ico277 Date: Tue, 22 Oct 2024 03:49:52 +0200 Subject: [PATCH 05/66] 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 06/66] 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 07/66] 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 08/66] 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 807208cba8c52b56c5c8430a4fc7c88d9666c098 Mon Sep 17 00:00:00 2001 From: Alex Denes Date: Sat, 8 Feb 2025 03:53:28 +0000 Subject: [PATCH 09/66] 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 10/66] 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 9b7a8a8c21a43f94a421a0db65244e8277b4a603 Mon Sep 17 00:00:00 2001 From: hecate cantus Date: Sun, 27 Apr 2025 20:30:14 -0700 Subject: [PATCH 11/66] 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 12/66] 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 e3e80773422b4b56166967f9aca0f8fc4a1fcc63 Mon Sep 17 00:00:00 2001 From: gred Date: Sat, 7 Jun 2025 12:11:22 +0300 Subject: [PATCH 13/66] 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 14/66] 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 15/66] 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 16/66] 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 17/66] 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 aaffebed4c0c7f3940e206319d7fc0ef8ca9f6b8 Mon Sep 17 00:00:00 2001 From: Marc Bornand Date: Fri, 24 Jan 2025 14:46:25 +0100 Subject: [PATCH 18/66] 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 19/66] 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 192baa4556b681f68ccc698000be62c37be671ab Mon Sep 17 00:00:00 2001 From: dimti Date: Tue, 7 Oct 2025 10:41:19 +0300 Subject: [PATCH 20/66] 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 21/66] 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 22/66] 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 cd820a100dfbbf6908f33ac4495d2e982f5ee85a Mon Sep 17 00:00:00 2001 From: dimti Date: Fri, 10 Oct 2025 11:14:30 +0300 Subject: [PATCH 23/66] 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 24/66] 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 25/66] 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 26/66] 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 27/66] 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 a96f1caa197f5f83e6e98c47669885bf6fbfe0c1 Mon Sep 17 00:00:00 2001 From: lilly-lizard Date: Sat, 11 Oct 2025 10:25:50 +1300 Subject: [PATCH 28/66] 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 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 29/66] 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 30/66] 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 85a8fca0de0b2488ed81b4f3b88eca300da051e7 Mon Sep 17 00:00:00 2001 From: Raphael Kimmig Date: Sun, 7 Dec 2025 12:18:24 +0100 Subject: [PATCH 31/66] 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 ca0b068ad86ed3b6ba949a699fbfe676d1d3f320 Mon Sep 17 00:00:00 2001 From: szbmrk Date: Fri, 9 Jan 2026 17:59:11 +0100 Subject: [PATCH 32/66] 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 33/66] 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 a6a65cb6b79e8871ae82a7451c1cbc5cdb96774e Mon Sep 17 00:00:00 2001 From: Prakhar Chhalotre Date: Tue, 10 Feb 2026 18:59:56 +0530 Subject: [PATCH 34/66] 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 35/66] 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 0c8acbf39e4ce88adea5ff9f2eebaab7c6bb9cf0 Mon Sep 17 00:00:00 2001 From: Nick Janetakis Date: Mon, 2 Mar 2026 19:10:55 -0500 Subject: [PATCH 36/66] 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 37/66] 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 38/66] 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 39/66] 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 40/66] 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 41/66] 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 42/66] 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 fff1495efb1ea4360e88ef89a70ffd47111d3f69 Mon Sep 17 00:00:00 2001 From: Skyfrei Date: Tue, 24 Mar 2026 20:12:30 +0100 Subject: [PATCH 43/66] 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 2c2901e8f70a8391399c2f5b4d35aaaddb1566a7 Mon Sep 17 00:00:00 2001 From: vdermichev Date: Fri, 27 Mar 2026 23:34:34 +0100 Subject: [PATCH 44/66] 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 45/66] 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 46/66] 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 503d99b1288bd848758369cd51344add6092af10 Mon Sep 17 00:00:00 2001 From: xiyori Date: Thu, 9 Apr 2026 14:15:56 +0300 Subject: [PATCH 47/66] 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 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 48/66] 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 49/66] 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 50/66] 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 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 51/66] 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 b28af75e62f8c6739990e94c383447e30cd74411 Mon Sep 17 00:00:00 2001 From: shiyi Date: Wed, 29 Apr 2026 23:34:09 +0800 Subject: [PATCH 52/66] 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 53/66] 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 3229c2c9fc2a7dd97eae1cd4b7d4937e591f7c96 Mon Sep 17 00:00:00 2001 From: Agnibha Mukherjee Date: Thu, 14 May 2026 11:01:58 +0530 Subject: [PATCH 54/66] 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 546b53d6759c2b7cc54051889d99fccaa6c57295 Mon Sep 17 00:00:00 2001 From: Desuwa Date: Sat, 16 May 2026 15:41:09 -0700 Subject: [PATCH 55/66] 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 56/66] 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 6bdafede071c2e18711bfb3804f852208debd48b Mon Sep 17 00:00:00 2001 From: Ivan Ribeiro Date: Tue, 19 May 2026 21:21:56 +0100 Subject: [PATCH 57/66] 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 415da7aef4d7be5f9e004389aceb135e245cfb50 Mon Sep 17 00:00:00 2001 From: Artur Ciesielski Date: Tue, 26 May 2026 19:39:03 +0200 Subject: [PATCH 58/66] 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 59/66] 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 3eeffbe3891f517f977d474decc8d3b794e63d83 Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Thu, 28 May 2026 15:25:35 +0800 Subject: [PATCH 60/66] 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 e736a934ba2f6a5754428e8d61dbf84316eae96a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:10:13 +0200 Subject: [PATCH 61/66] 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 6dc168c4f2635bc62013ea418f39210fc0fae989 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:10:46 +0200 Subject: [PATCH 62/66] 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 573ad779bad545269ef52380a5cf5e9788b5c0a6 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 3 Jul 2026 22:21:03 +0200 Subject: [PATCH 63/66] 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 64/66] 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 65/66] 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 66/66] 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 ++