diff --git a/include/modules/hyprland/workspaces.hpp b/include/modules/hyprland/workspaces.hpp index 368cc6b8..e82d3d7f 100644 --- a/include/modules/hyprland/workspaces.hpp +++ b/include/modules/hyprland/workspaces.hpp @@ -64,6 +64,8 @@ class Workspaces : public AModule, public EventHandler { std::string getRewrite(const std::string& window_class, const std::string& window_title); std::string& getWindowSeparator() { return m_formatWindowSeparator; } + auto windowRewriteGroupThreshold() const -> int { return m_windowRewriteGroupThreshold; } + auto const& getWindowRewriteGroupFormat() const { return m_windowRewriteGroupFormat; } bool isWorkspaceIgnored(std::string const& workspace_name); bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } @@ -177,6 +179,8 @@ class Workspaces : public AModule, public EventHandler { util::RegexCollection m_windowRewriteRules; bool m_anyWindowRewriteRuleUsesTitle = false; std::string m_formatWindowSeparator; + int m_windowRewriteGroupThreshold = 0; + std::string m_windowRewriteGroupFormat = "{icon}×{count}"; bool m_withIcon; uint64_t m_monitorId; diff --git a/man/waybar-hyprland-workspaces.5.scd b/man/waybar-hyprland-workspaces.5.scd index 85bbf4be..759e76cc 100644 --- a/man/waybar-hyprland-workspaces.5.scd +++ b/man/waybar-hyprland-workspaces.5.scd @@ -41,6 +41,19 @@ This setting is ignored if *workspace-taskbar.enable* is set to true. The separator to be used between windows in a workspace. ++ This setting is ignored if *workspace-taskbar.enable* is set to true. +*window-rewrite-group-threshold*: ++ + typeof: int ++ + default: 0 ++ + When a workspace contains at least this many windows with the same rewrite result, they are collapsed into a single one using *window-rewrite-group-format*. ++ + Set to 0 to disable grouping. ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + +*window-rewrite-group-format*: ++ + typeof: string ++ + default: "{icon}×{count}" ++ + The format used to represent a group of collapsed windows. Available placeholders are {icon} (the icon being grouped) and {count} (how many windows share it). ++ + This setting is ignored if *workspace-taskbar.enable* is set to true. + *workspace-taskbar*: ++ typeof: object ++ Contains settings for the workspace taskbar, an alternative mode for the workspaces module which displays the window icons as images instead of text. diff --git a/src/modules/hyprland/workspace.cpp b/src/modules/hyprland/workspace.cpp index 184b5f0f..5046688f 100644 --- a/src/modules/hyprland/workspace.cpp +++ b/src/modules/hyprland/workspace.cpp @@ -1,7 +1,8 @@ +#include #include #include -#include +#include #include #include #include @@ -152,8 +153,7 @@ bool Workspace::pointerInsideButton() { const int buttonHeight = allocation.get_height(); return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY && - pointerRootX < buttonRootX + buttonWidth && - pointerRootY < buttonRootY + buttonHeight; + pointerRootX < buttonRootX + buttonWidth && pointerRootY < buttonRootY + buttonHeight; } bool Workspace::syncHoverClass() { @@ -174,9 +174,8 @@ void Workspace::startHoverCheck() { return; } - m_hoverCheckConnection = Glib::signal_timeout().connect( - sigc::mem_fun(*this, &Workspace::syncHoverClass), - 50); + m_hoverCheckConnection = + Glib::signal_timeout().connect(sigc::mem_fun(*this, &Workspace::syncHoverClass), 50); } void Workspace::stopHoverCheck() { @@ -252,8 +251,7 @@ void Workspace::setActiveWindow(WindowAddress const& addr) { auto activeWindowPos = m_workspaceManager.activeWindowPosition(); const bool has_active_window = - activeIdx.has_value() && - activeWindowPos != Workspaces::ActiveWindowPosition::NONE; + activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE; if (has_active_window) { auto window = std::move(m_windowMap[*activeIdx]); @@ -270,8 +268,7 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) { if (!create_window_payload.isEmpty(m_workspaceManager)) { auto repr = create_window_payload.repr(m_workspaceManager); - const bool should_display = - !repr.empty() || m_workspaceManager.enableTaskbar(); + const bool should_display = !repr.empty() || m_workspaceManager.enableTaskbar(); if (should_display) { auto addr = create_window_payload.getAddress(); @@ -337,7 +334,7 @@ std::string& Workspace::selectIcon(std::map& icons_map if (specialNamedIconIt != icons_map.end()) { return specialNamedIconIt->second; } - + auto specialIconIt = icons_map.find("special"); if (specialIconIt != icons_map.end()) { return specialIconIt->second; @@ -378,7 +375,6 @@ 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(); @@ -436,16 +432,56 @@ void Workspace::update(const std::string& workspace_icon) { // need to compute this if enableTaskbar() is true if (!m_workspaceManager.enableTaskbar()) { auto windowSeparator = m_workspaceManager.getWindowSeparator(); + auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold(); - bool isNotFirst = false; - auto end_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.end() : m_windowMap.begin() + m_workspaceManager.maxWindows(); + 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) { - if (isNotFirst) { - windows.append(windowSeparator); + if (groupThreshold > 0) { + // Build ordered counts of each unique icon (including singular ones when threshold set to 1) + std::vector> iconCounts; + for (auto it = m_windowMap.begin(); it != end_it; ++it) { + const auto& window_repr = *it; + auto found = std::ranges::find_if( + iconCounts, [&](const auto& p) { return p.first == window_repr.repr_rewrite; }); + if (found != iconCounts.end()) { + found->second++; + } else { + iconCounts.emplace_back(window_repr.repr_rewrite, 1); + } + } + + // Format the group string + auto groupFormat = m_workspaceManager.getWindowRewriteGroupFormat(); + bool isNotFirst = false; + for (const auto& [icon, count] : iconCounts) { + if (count >= groupThreshold) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + try { + windows.append(fmt::format(fmt::runtime(groupFormat), fmt::arg("icon", icon), + fmt::arg("count", count))); + } catch (const fmt::format_error& e) { + spdlog::warn("Formatting window-rewrite-group-format error: {}", e.what()); + windows.append(icon); + } + } else { + for (int i = 0; i < count; ++i) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + windows.append(icon); + } + } + } + } else { + // Not grouping icons + bool isNotFirst = false; + for (auto it = m_windowMap.begin(); it != end_it; ++it) { + if (isNotFirst) windows.append(windowSeparator); + isNotFirst = true; + windows.append(it->repr_rewrite); } - isNotFirst = true; - windows.append(it->repr_rewrite); } } @@ -505,8 +541,7 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { } if (m_workspaceManager.onClickWindow() != "") { button->signal_button_press_event().connect( - sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), - false); + sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), false); } auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), @@ -536,13 +571,17 @@ void Workspace::updateTaskbar(const std::string& workspace_icon) { }; if (m_workspaceManager.taskbarReverseDirection()) { - auto rend_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.rend() : m_windowMap.rbegin() + m_workspaceManager.maxWindows(); + 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 { - auto end_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.end() : m_windowMap.begin() + m_workspaceManager.maxWindows(); + 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 4fe09353..d29d32b4 100644 --- a/src/modules/hyprland/workspaces.cpp +++ b/src/modules/hyprland/workspaces.cpp @@ -670,6 +670,16 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void { populateSortByConfig(config); populateIgnoreWorkspacesConfig(config); populateFormatWindowSeparatorConfig(config); + + const auto& groupThreshold = config["window-rewrite-group-threshold"]; + if (groupThreshold.isInt()) { + m_windowRewriteGroupThreshold = groupThreshold.asInt(); + } + const auto& groupFormat = config["window-rewrite-group-format"]; + if (groupFormat.isString()) { + m_windowRewriteGroupFormat = groupFormat.asString(); + } + populateWindowRewriteConfig(config); populateMaxWindowsConfig(config);