hyprland/workspaces: expose workspace name as CSS class

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; }
This commit is contained in:
vdermichev
2026-04-05 11:44:56 +02:00
parent 1e965ccce0
commit 2c2901e8f7
2 changed files with 41 additions and 0 deletions
+1
View File
@@ -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;
+40
View File
@@ -1,6 +1,7 @@
#include <json/value.h>
#include <spdlog/spdlog.h>
#include <cctype>
#include <memory>
#include <string>
#include <utility>
@@ -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<unsigned char>(c);
if (std::isalnum(uc)) {
result += static_cast<char>(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<unsigned char>(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<std::string, std::string>& 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