feat(niri/workspaces): add window icons via window-rewrite
Adds format-window-separator, window-rewrite and window-rewrite-default
options plus the {windows} format replacement to the niri/workspaces
module (rebased onto master; coexists with ignore-workspaces).
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
#include "AModule.hpp"
|
#include "AModule.hpp"
|
||||||
#include "bar.hpp"
|
#include "bar.hpp"
|
||||||
#include "modules/niri/backend.hpp"
|
#include "modules/niri/backend.hpp"
|
||||||
|
#include "util/regex_collection.hpp" // Added for rewrite rules
|
||||||
|
|
||||||
namespace waybar::modules::niri {
|
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);
|
std::string getIcon(const std::string& value, const Json::Value& ws);
|
||||||
bool isWorkspaceIgnored(const std::string& name);
|
bool isWorkspaceIgnored(const std::string& name);
|
||||||
bool handleScroll(GdkEventScroll* /*unused*/) override;
|
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_;
|
const Bar& bar_;
|
||||||
Gtk::Box box_;
|
Gtk::Box box_;
|
||||||
@@ -37,6 +42,12 @@ class Workspaces : public AModule, public EventHandler {
|
|||||||
bool sort_by_id_ = false;
|
bool sort_by_id_ = false;
|
||||||
bool sort_by_name_ = false;
|
bool sort_by_name_ = false;
|
||||||
bool sort_by_coordinates_ = 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
|
} // namespace waybar::modules::niri
|
||||||
|
|||||||
@@ -97,6 +97,21 @@ If none of the sorting options are enabled, workspaces keep their output/index o
|
|||||||
default: [] ++
|
default: [] ++
|
||||||
Regexes to match against workspaces names and index's. If there's a match, the workspace will not be shown.
|
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<regex>` and/or `title<regex>`. 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
|
# FORMAT REPLACEMENTS
|
||||||
|
|
||||||
*{value}*: Name of the workspace, or index for unnamed workspaces,
|
*{value}*: Name of the workspace, or index for unnamed workspaces,
|
||||||
@@ -112,6 +127,8 @@ as defined by niri.
|
|||||||
|
|
||||||
*{total}*: The total number of workspaces.
|
*{total}*: The total number of workspaces.
|
||||||
|
|
||||||
|
*{windows}*: Concatenated representation of windows in the workspace, using *window-rewrite* rules and *format-window-separator*.
|
||||||
|
|
||||||
# ICONS
|
# ICONS
|
||||||
|
|
||||||
Additional to workspace name matching, the following *format-icons* can be set.
|
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": {
|
"niri/workspaces": {
|
||||||
"format": "{icon}",
|
"format": "{icon} {windows}",
|
||||||
"format-icons": {
|
"format-window-separator": "|",
|
||||||
// Named workspaces
|
"window-rewrite-default": "",
|
||||||
// (you need to configure them in niri)
|
"window-rewrite": {
|
||||||
"browser": "",
|
"app_id<firefox>": "",
|
||||||
"discord": "",
|
"app_id<foot>": "",
|
||||||
"chat": "<b></b>",
|
"app_id<org.gnome.Nautilus> title<.*Downloads.*>": "",
|
||||||
|
"app_id<org.gnome.Nautilus>": ""
|
||||||
|
},
|
||||||
|
"format-icons": {
|
||||||
|
// Named workspaces
|
||||||
|
// (you need to configure them in niri)
|
||||||
|
"browser": "",
|
||||||
|
"discord": "",
|
||||||
|
"chat": "<b></b>",
|
||||||
|
|
||||||
// Icons by state
|
// Icons by state
|
||||||
"active": "",
|
"active": "",
|
||||||
"default": ""
|
"default": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,27 @@
|
|||||||
// "default": ""
|
// "default": ""
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
|
"niri/workspaces": {
|
||||||
|
"format": "{icon} {windows}",
|
||||||
|
"format-window-separator": " ",
|
||||||
|
"window-rewrite-default": "",
|
||||||
|
"window-rewrite": {
|
||||||
|
"app_id<firefox>": "",
|
||||||
|
"app_id<dolphin>": "",
|
||||||
|
"app_id<org.gnome.Nautilus>": "",
|
||||||
|
"app_id<thunar>": "",
|
||||||
|
"app_id<foot>": ""
|
||||||
|
},
|
||||||
|
"format-icons": {
|
||||||
|
"1": "1",
|
||||||
|
"2": "2",
|
||||||
|
"3": "3",
|
||||||
|
"4": "4",
|
||||||
|
"5": "5",
|
||||||
|
"active": "",
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"keyboard-state": {
|
"keyboard-state": {
|
||||||
"numlock": true,
|
"numlock": true,
|
||||||
"capslock": true,
|
"capslock": true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "modules/niri/workspaces.hpp"
|
#include "modules/niri/workspaces.hpp"
|
||||||
|
|
||||||
|
#include <fmt/ranges.h> // Needed for joining window representations
|
||||||
#include <gtkmm/button.h>
|
#include <gtkmm/button.h>
|
||||||
#include <gtkmm/label.h>
|
#include <gtkmm/label.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
@@ -7,6 +8,8 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
|
||||||
|
#include "util/rewrite_string.hpp" // Needed for rewrite logic
|
||||||
|
|
||||||
namespace waybar::modules::niri {
|
namespace waybar::modules::niri {
|
||||||
|
|
||||||
Workspaces::Workspaces(const std::string& id, const Bar& bar, const Json::Value& config)
|
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<IPC>();
|
if (!gIPC) gIPC = std::make_unique<IPC>();
|
||||||
|
|
||||||
|
// 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("WorkspacesChanged", this);
|
||||||
gIPC->registerForIPC("WorkspaceActivated", this);
|
gIPC->registerForIPC("WorkspaceActivated", this);
|
||||||
gIPC->registerForIPC("WorkspaceActiveWindowChanged", this);
|
gIPC->registerForIPC("WorkspaceActiveWindowChanged", this);
|
||||||
@@ -80,6 +89,10 @@ void Workspaces::onEvent(const Json::Value& ev) { dp.emit(); }
|
|||||||
void Workspaces::doUpdate() {
|
void Workspaces::doUpdate() {
|
||||||
auto ipcLock = gIPC->lockData();
|
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 alloutputs = config_["all-outputs"].asBool();
|
||||||
const auto display_cond = config_["display-condition"].asString();
|
const auto display_cond = config_["display-condition"].asString();
|
||||||
std::vector<Json::Value> my_workspaces;
|
std::vector<Json::Value> my_workspaces;
|
||||||
@@ -123,6 +136,10 @@ void Workspaces::doUpdate() {
|
|||||||
|
|
||||||
// Add buttons for new workspaces, update existing ones.
|
// Add buttons for new workspaces, update existing ones.
|
||||||
for (const auto& ws : my_workspaces) {
|
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 bit = buttons_.find(ws["id"].asUInt64());
|
||||||
auto& button = bit == buttons_.end() ? addButton(ws) : bit->second;
|
auto& button = bit == buttons_.end() ? addButton(ws) : bit->second;
|
||||||
auto style_context = button.get_style_context();
|
auto style_context = button.get_style_context();
|
||||||
@@ -156,6 +173,52 @@ void Workspaces::doUpdate() {
|
|||||||
else
|
else
|
||||||
style_context->remove_class("empty");
|
style_context->remove_class("empty");
|
||||||
|
|
||||||
|
// --- Start Window Rewrite Logic ---
|
||||||
|
std::vector<std::string> 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;
|
std::string name;
|
||||||
if (ws["name"]) {
|
if (ws["name"]) {
|
||||||
name = ws["name"].asString();
|
name = ws["name"].asString();
|
||||||
@@ -169,7 +232,8 @@ void Workspaces::doUpdate() {
|
|||||||
name = fmt::format(
|
name = fmt::format(
|
||||||
fmt::runtime(format), fmt::arg("icon", getIcon(name, ws)), fmt::arg("value", name),
|
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("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()) {
|
if (!config_["disable-markup"].asBool()) {
|
||||||
auto* child = gtk_bin_get_child(GTK_BIN(button.gobj()));
|
auto* child = gtk_bin_get_child(GTK_BIN(button.gobj()));
|
||||||
@@ -243,6 +307,84 @@ Gtk::Button& Workspaces::addButton(const Json::Value& ws) {
|
|||||||
return button;
|
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) {
|
std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) {
|
||||||
const auto& icons = config_["format-icons"];
|
const auto& icons = config_["format-icons"];
|
||||||
if (!icons) return value;
|
if (!icons) return value;
|
||||||
|
|||||||
Reference in New Issue
Block a user