diff --git a/include/modules/niri/workspaces.hpp b/include/modules/niri/workspaces.hpp index 9e548c98..7b717e9f 100644 --- a/include/modules/niri/workspaces.hpp +++ b/include/modules/niri/workspaces.hpp @@ -9,6 +9,7 @@ #include "AModule.hpp" #include "bar.hpp" #include "modules/niri/backend.hpp" +#include "util/regex_collection.hpp" // Added for rewrite rules namespace waybar::modules::niri { @@ -26,6 +27,10 @@ class Workspaces : public AModule, public EventHandler { std::string getIcon(const std::string& value, const Json::Value& ws); bool isWorkspaceIgnored(const std::string& name); bool handleScroll(GdkEventScroll* /*unused*/) override; + // Added for window rewrite + void populateWindowRewriteConfig(); + void populateFormatWindowSeparatorConfig(); + std::string getRewrite(const std::string& app_id, const std::string& title); const Bar& bar_; Gtk::Box box_; @@ -37,6 +42,12 @@ class Workspaces : public AModule, public EventHandler { bool sort_by_id_ = false; bool sort_by_name_ = false; bool sort_by_coordinates_ = false; + + // Added for window rewrite + util::RegexCollection m_windowRewriteRules; + std::string m_windowRewriteDefault; + std::string m_formatWindowSeparator; + }; } // namespace waybar::modules::niri diff --git a/man/waybar-niri-workspaces.5.scd b/man/waybar-niri-workspaces.5.scd index 977d6948..13e2d27c 100644 --- a/man/waybar-niri-workspaces.5.scd +++ b/man/waybar-niri-workspaces.5.scd @@ -97,6 +97,21 @@ If none of the sorting options are enabled, workspaces keep their output/index o default: [] ++ Regexes to match against workspaces names and index's. If there's a match, the workspace will not be shown. +*format-window-separator*: ++ + typeof: string ++ + default: " " ++ + Separator used between window representations generated by *window-rewrite*. + +*window-rewrite*: ++ + typeof: object ++ + default: {} ++ + Rules to map window app_id and/or title to a specific string representation (e.g., an icon). Keys are regular expressions matching `app_id` and/or `title`. Values are the replacement strings, which can contain `{app_id}` and `{title}` placeholders. + +*window-rewrite-default*: ++ + typeof: string ++ + default: "?" ++ + The default representation for windows that don't match any rule in *window-rewrite*. + # FORMAT REPLACEMENTS *{value}*: Name of the workspace, or index for unnamed workspaces, @@ -112,6 +127,8 @@ as defined by niri. *{total}*: The total number of workspaces. +*{windows}*: Concatenated representation of windows in the workspace, using *window-rewrite* rules and *format-window-separator*. + # ICONS Additional to workspace name matching, the following *format-icons* can be set. @@ -126,18 +143,26 @@ Additional to workspace name matching, the following *format-icons* can be set. ``` "niri/workspaces": { - "format": "{icon}", - "format-icons": { - // Named workspaces - // (you need to configure them in niri) - "browser": "", - "discord": "", - "chat": "", + "format": "{icon} {windows}", + "format-window-separator": "|", + "window-rewrite-default": "", + "window-rewrite": { + "app_id": "", + "app_id": "", + "app_id title<.*Downloads.*>": "", + "app_id": "" + }, + "format-icons": { + // Named workspaces + // (you need to configure them in niri) + "browser": "", + "discord": "", + "chat": "", - // Icons by state - "active": "", - "default": "" - } + // Icons by state + "active": "", + "default": "" + } } ``` diff --git a/resources/config.jsonc b/resources/config.jsonc index b616d182..e816ee2f 100644 --- a/resources/config.jsonc +++ b/resources/config.jsonc @@ -50,6 +50,27 @@ // "default": "" // } // }, + "niri/workspaces": { + "format": "{icon} {windows}", + "format-window-separator": " ", + "window-rewrite-default": "", + "window-rewrite": { + "app_id": "", + "app_id": "", + "app_id": "", + "app_id": "", + "app_id": "" + }, + "format-icons": { + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "active": "", + "default": "" + } + }, "keyboard-state": { "numlock": true, "capslock": true, diff --git a/src/modules/niri/workspaces.cpp b/src/modules/niri/workspaces.cpp index a3b9be9a..21e12c0e 100644 --- a/src/modules/niri/workspaces.cpp +++ b/src/modules/niri/workspaces.cpp @@ -1,5 +1,6 @@ #include "modules/niri/workspaces.hpp" +#include // Needed for joining window representations #include #include #include @@ -7,6 +8,8 @@ #include #include +#include "util/rewrite_string.hpp" // Needed for rewrite logic + namespace waybar::modules::niri { Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config) @@ -59,6 +62,12 @@ Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& if (!gIPC) gIPC = std::make_unique(); + // Parse new config options first + populateWindowRewriteConfig(); + populateFormatWindowSeparatorConfig(); + + // Niri's WorkspacesChanged event already includes window info, + // so no need to register for separate window events like in Hyprland. gIPC->registerForIPC("WorkspacesChanged", this); gIPC->registerForIPC("WorkspaceActivated", this); gIPC->registerForIPC("WorkspaceActiveWindowChanged", this); @@ -80,6 +89,10 @@ void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); } void Workspaces::doUpdate() { auto ipcLock = gIPC->lockData(); + // Debug: log global IPC lists + spdlog::debug("[niri/workspaces] gIPC workspaces count: {}", gIPC->workspaces().size()); + spdlog::debug("[niri/workspaces] gIPC windows count: {}", gIPC->windows().size()); + const auto alloutputs = config_["all-outputs"].asBool(); const auto display_cond = config_["display-condition"].asString(); std::vector my_workspaces; @@ -123,6 +136,10 @@ void Workspaces::doUpdate() { // Add buttons for new workspaces, update existing ones. for (const auto& ws : my_workspaces) { + // Debug: print workspace JSON + spdlog::debug("[niri/workspaces] workspace id={} json={} ", ws["id"].asUInt64(), + ws.toStyledString()); + auto bit = buttons_.find(ws["id"].asUInt64()); auto& button = bit == buttons_.end() ? addButton(ws) : bit->second; auto style_context = button.get_style_context(); @@ -156,6 +173,52 @@ void Workspaces::doUpdate() { else style_context->remove_class("empty"); + // --- Start Window Rewrite Logic --- + std::vector window_reps; + if (ws.isMember("windows") && ws["windows"].isArray()) { + spdlog::debug("[niri/workspaces] workspace id={} has {} windows", ws["id"].asUInt64(), + ws["windows"].size()); + for (const auto& win : ws["windows"]) { + spdlog::debug("[niri/workspaces] window json: {}", win.toStyledString()); + std::string app_id = + win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; + std::string title = + win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; + if (!app_id.empty() || !title.empty()) { // Only add if we have some identifier + auto rep = getRewrite(app_id, title); + spdlog::debug("[niri/workspaces] rewrite: app_id='{}' title='{}' => '{}'", app_id, title, + rep); + window_reps.push_back(rep); + } + } + } else { + spdlog::debug( + "[niri/workspaces] workspace id={} has no 'windows' array, collecting from global " + "windows", + ws["id"].asUInt64()); + // Fallback: collect from global windows list by matching workspace_id + for (const auto& win : gIPC->windows()) { + if (!win.isMember("workspace_id")) continue; + if (win["workspace_id"].asUInt64() != ws["id"].asUInt64()) continue; + spdlog::debug("[niri/workspaces] global window json: {}", win.toStyledString()); + std::string app_id = + win.isMember("app_id") && win["app_id"].isString() ? win["app_id"].asString() : ""; + std::string title = + win.isMember("title") && win["title"].isString() ? win["title"].asString() : ""; + if (!app_id.empty() || !title.empty()) { + auto rep = getRewrite(app_id, title); + spdlog::debug("[niri/workspaces] rewrite (global): app_id='{}' title='{}' => '{}'", + app_id, title, rep); + window_reps.push_back(rep); + } + } + } + // Join representations with the separator + auto windows_str = fmt::format("{}", fmt::join(window_reps, m_formatWindowSeparator)); + spdlog::debug("[niri/workspaces] workspace id={} windows_str='{}'", ws["id"].asUInt64(), + windows_str); + // --- End Window Rewrite Logic --- + std::string name; if (ws["name"]) { name = ws["name"].asString(); @@ -169,7 +232,8 @@ void Workspaces::doUpdate() { name = fmt::format( fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name), fmt::arg("name", ws["name"].asString()), fmt::arg("index", ws["idx"].asUInt()), - fmt::arg("output", ws["output"].asString()), fmt::arg("total", my_workspaces.size())); + fmt::arg("output", ws["output"].asString()), fmt::arg("total", my_workspaces.size()), + fmt::arg("windows", windows_str)); // Added windows arg } if (!config_["disable-markup"].asBool()) { auto* child = gtk_bin_get_child(GTK_BIN(button.gobj())); @@ -243,6 +307,84 @@ Gtk::Button& Workspaces::addButton(const Json::Value& ws) { return button; } +// --- Start New Helper Functions --- +void Workspaces::populateWindowRewriteConfig() { + // Reconstruct RegexCollection instead of clearing/adding + const Json::Value& rewrite_rules_config = config_["window-rewrite"]; + if (rewrite_rules_config.isObject()) { + // Assuming a constructor that takes the Json::Value object exists. + // If Niri needs rule prioritization like Hyprland, a priority function + // would be needed as a second argument here. + try { + m_windowRewriteRules = util::RegexCollection(rewrite_rules_config); + } catch (const std::exception& e) { + spdlog::error("Error initializing RegexCollection: {}", e.what()); + // Initialize with an empty collection if error occurs + m_windowRewriteRules = util::RegexCollection(Json::Value(Json::objectValue)); + } + + } else { + // Initialize with an empty collection if config is not an object + m_windowRewriteRules = util::RegexCollection(Json::Value(Json::objectValue)); + } + + if (config_.isMember("window-rewrite-default") && config_["window-rewrite-default"].isString()) { + m_windowRewriteDefault = config_["window-rewrite-default"].asString(); + } else { + m_windowRewriteDefault = "?"; // Default fallback + } +} + +void Workspaces::populateFormatWindowSeparatorConfig() { + if (config_.isMember("format-window-separator") && + config_["format-window-separator"].isString()) { + m_formatWindowSeparator = config_["format-window-separator"].asString(); + } else { + m_formatWindowSeparator = " "; // Default fallback + } +} + +std::string Workspaces::getRewrite(const std::string& app_id, const std::string& title) { + // Niri uses app_id, Hyprland uses class. Adapt the key format. + std::string lookup_key = "app_id<" + app_id + "> title<" + title + ">"; + std::string res = m_windowRewriteRules.get(lookup_key); + if (!res.empty()) { + // Create Json::Value for substitutions + Json::Value substitutions(Json::objectValue); + substitutions["app_id"] = app_id; + substitutions["title"] = title; + return util::rewriteString(res, substitutions); + } + // Fallback to app_id only + lookup_key = "app_id<" + app_id + ">"; + res = m_windowRewriteRules.get(lookup_key); + if (!res.empty()) { + // Create Json::Value for substitutions + Json::Value substitutions(Json::objectValue); + substitutions["app_id"] = app_id; + substitutions["title"] = title; + return util::rewriteString(res, substitutions); + } + // Fallback to title only + lookup_key = "title<" + title + ">"; + res = m_windowRewriteRules.get(lookup_key); + if (!res.empty()) { + // Create Json::Value for substitutions + Json::Value substitutions(Json::objectValue); + substitutions["app_id"] = app_id; + substitutions["title"] = title; + return util::rewriteString(res, substitutions); + } + + // No rule matched, return default + // Apply substitutions to default as well, in case it uses placeholders + Json::Value substitutions(Json::objectValue); + substitutions["app_id"] = app_id; + substitutions["title"] = title; + return util::rewriteString(m_windowRewriteDefault, substitutions); +} +// --- End New Helper Functions --- + std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) { const auto& icons = config_["format-icons"]; if (!icons) return value;