Merge pull request #4861 from eonphi/master

feat(hyprland/workspaces): grouping icons
This commit is contained in:
Alexis Rouillard
2026-07-03 23:59:01 +02:00
committed by GitHub
4 changed files with 89 additions and 23 deletions
+4
View File
@@ -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 getRewrite(const std::string& window_class, const std::string& window_title);
std::string& getWindowSeparator() { return m_formatWindowSeparator; } 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 isWorkspaceIgnored(std::string const& workspace_name);
bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; } bool windowRewriteConfigUsesTitle() const { return m_anyWindowRewriteRuleUsesTitle; }
@@ -177,6 +179,8 @@ class Workspaces : public AModule, public EventHandler {
util::RegexCollection m_windowRewriteRules; util::RegexCollection m_windowRewriteRules;
bool m_anyWindowRewriteRuleUsesTitle = false; bool m_anyWindowRewriteRuleUsesTitle = false;
std::string m_formatWindowSeparator; std::string m_formatWindowSeparator;
int m_windowRewriteGroupThreshold = 0;
std::string m_windowRewriteGroupFormat = "{icon}×{count}";
bool m_withIcon; bool m_withIcon;
uint64_t m_monitorId; uint64_t m_monitorId;
+13
View File
@@ -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. ++ The separator to be used between windows in a workspace. ++
This setting is ignored if *workspace-taskbar.enable* is set to true. 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*: ++ *workspace-taskbar*: ++
typeof: object ++ 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. Contains settings for the workspace taskbar, an alternative mode for the workspaces module which displays the window icons as images instead of text.
+62 -23
View File
@@ -1,7 +1,8 @@
#include <glibmm/main.h>
#include <json/value.h> #include <json/value.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <glibmm/main.h>
#include <algorithm>
#include <cctype> #include <cctype>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -152,8 +153,7 @@ bool Workspace::pointerInsideButton() {
const int buttonHeight = allocation.get_height(); const int buttonHeight = allocation.get_height();
return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY && return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY &&
pointerRootX < buttonRootX + buttonWidth && pointerRootX < buttonRootX + buttonWidth && pointerRootY < buttonRootY + buttonHeight;
pointerRootY < buttonRootY + buttonHeight;
} }
bool Workspace::syncHoverClass() { bool Workspace::syncHoverClass() {
@@ -174,9 +174,8 @@ void Workspace::startHoverCheck() {
return; return;
} }
m_hoverCheckConnection = Glib::signal_timeout().connect( m_hoverCheckConnection =
sigc::mem_fun(*this, &Workspace::syncHoverClass), Glib::signal_timeout().connect(sigc::mem_fun(*this, &Workspace::syncHoverClass), 50);
50);
} }
void Workspace::stopHoverCheck() { void Workspace::stopHoverCheck() {
@@ -252,8 +251,7 @@ void Workspace::setActiveWindow(WindowAddress const& addr) {
auto activeWindowPos = m_workspaceManager.activeWindowPosition(); auto activeWindowPos = m_workspaceManager.activeWindowPosition();
const bool has_active_window = const bool has_active_window =
activeIdx.has_value() && activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE;
activeWindowPos != Workspaces::ActiveWindowPosition::NONE;
if (has_active_window) { if (has_active_window) {
auto window = std::move(m_windowMap[*activeIdx]); 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)) { if (!create_window_payload.isEmpty(m_workspaceManager)) {
auto repr = create_window_payload.repr(m_workspaceManager); auto repr = create_window_payload.repr(m_workspaceManager);
const bool should_display = const bool should_display = !repr.empty() || m_workspaceManager.enableTaskbar();
!repr.empty() || m_workspaceManager.enableTaskbar();
if (should_display) { if (should_display) {
auto addr = create_window_payload.getAddress(); auto addr = create_window_payload.getAddress();
@@ -337,7 +334,7 @@ std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map
if (specialNamedIconIt != icons_map.end()) { if (specialNamedIconIt != icons_map.end()) {
return specialNamedIconIt->second; return specialNamedIconIt->second;
} }
auto specialIconIt = icons_map.find("special"); auto specialIconIt = icons_map.find("special");
if (specialIconIt != icons_map.end()) { if (specialIconIt != icons_map.end()) {
return specialIconIt->second; return specialIconIt->second;
@@ -378,7 +375,6 @@ std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map
return m_name; return m_name;
} }
void Workspace::update(const std::string& workspace_icon) { void Workspace::update(const std::string& workspace_icon) {
if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) { if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) {
m_button.hide(); m_button.hide();
@@ -436,16 +432,56 @@ void Workspace::update(const std::string& workspace_icon) {
// need to compute this if enableTaskbar() is true // need to compute this if enableTaskbar() is true
if (!m_workspaceManager.enableTaskbar()) { if (!m_workspaceManager.enableTaskbar()) {
auto windowSeparator = m_workspaceManager.getWindowSeparator(); auto windowSeparator = m_workspaceManager.getWindowSeparator();
auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold();
bool isNotFirst = false; auto end_it = m_workspaceManager.maxWindows() == 0
auto end_it = m_workspaceManager.maxWindows() == 0 ? m_windowMap.end() : m_windowMap.begin() + m_workspaceManager.maxWindows(); ? m_windowMap.end()
: m_windowMap.begin() + m_workspaceManager.maxWindows();
for (auto it = m_windowMap.begin(); it != end_it; ++it) { if (groupThreshold > 0) {
if (isNotFirst) { // Build ordered counts of each unique icon (including singular ones when threshold set to 1)
windows.append(windowSeparator); std::vector<std::pair<std::string, int>> 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() != "") { if (m_workspaceManager.onClickWindow() != "") {
button->signal_button_press_event().connect( button->signal_button_press_event().connect(
sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr.address), false);
false);
} }
auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()), 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()) { 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) { for (auto it = m_windowMap.rbegin(); it != rend_it; ++it) {
processWindow(*it); processWindow(*it);
} }
} else { } 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) { for (auto it = m_windowMap.begin(); it != end_it; ++it) {
processWindow(*it); processWindow(*it);
+10
View File
@@ -670,6 +670,16 @@ auto Workspaces::parseConfig(const Json::Value& config) -> void {
populateSortByConfig(config); populateSortByConfig(config);
populateIgnoreWorkspacesConfig(config); populateIgnoreWorkspacesConfig(config);
populateFormatWindowSeparatorConfig(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); populateWindowRewriteConfig(config);
populateMaxWindowsConfig(config); populateMaxWindowsConfig(config);