421 lines
15 KiB
C++
421 lines
15 KiB
C++
#include "modules/niri/workspaces.hpp"
|
|
|
|
#include <fmt/ranges.h> // Needed for joining window representations
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
|
|
#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)
|
|
: AModule(config, "workspaces", id, false, false), bar_(bar), box_(bar.orientation, 0) {
|
|
const auto config_sort_by_number = config_["sort-by-number"];
|
|
if (config_sort_by_number.isBool()) {
|
|
spdlog::warn("[niri/workspaces]: Prefer sort-by-id instead of sort-by-number");
|
|
sort_by_id_ = config_sort_by_number.asBool();
|
|
}
|
|
|
|
const auto config_sort_by_id = config_["sort-by-id"];
|
|
if (config_sort_by_id.isBool()) {
|
|
sort_by_id_ = config_sort_by_id.asBool();
|
|
}
|
|
|
|
const auto config_sort_by_name = config_["sort-by-name"];
|
|
if (config_sort_by_name.isBool()) {
|
|
sort_by_name_ = config_sort_by_name.asBool();
|
|
}
|
|
|
|
const auto config_sort_by_coordinates = config_["sort-by-coordinates"];
|
|
if (config_sort_by_coordinates.isBool()) {
|
|
sort_by_coordinates_ = config_sort_by_coordinates.asBool();
|
|
}
|
|
|
|
box_.set_name("workspaces");
|
|
if (!id.empty()) {
|
|
box_.get_style_context()->add_class(id);
|
|
}
|
|
box_.get_style_context()->add_class(MODULE_CLASS);
|
|
event_box_.add(box_);
|
|
|
|
// Parse ignore-workspaces rules.
|
|
auto ignoreWorkspaces = config["ignore-workspaces"];
|
|
if (ignoreWorkspaces.isArray()) {
|
|
for (const auto& workspaceRegex : ignoreWorkspaces) {
|
|
if (workspaceRegex.isString()) {
|
|
std::string ruleString = workspaceRegex.asString();
|
|
try {
|
|
const std::regex rule{ruleString, std::regex_constants::icase};
|
|
ignoreWorkspaces_.emplace_back(rule);
|
|
} catch (const std::regex_error& e) {
|
|
spdlog::error("Invalid ignore-workspaces rule {}: {}", ruleString, e.what());
|
|
}
|
|
} else {
|
|
spdlog::error("Not a string in ignore-workspaces: '{}'", workspaceRegex);
|
|
}
|
|
}
|
|
}
|
|
|
|
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("WorkspaceActivated", this);
|
|
gIPC->registerForIPC("WorkspaceActiveWindowChanged", this);
|
|
gIPC->registerForIPC("WorkspaceUrgencyChanged", this);
|
|
|
|
gIPC->registerForIPC("WindowsChanged", this);
|
|
gIPC->registerForIPC("WindowOpenedOrChanged", this);
|
|
gIPC->registerForIPC("WindowLayoutsChanged", this);
|
|
gIPC->registerForIPC("WindowFocusChanged", this);
|
|
gIPC->registerForIPC("WindowClosed", this);
|
|
|
|
if (config["enable-bar-scroll"].asBool()) {
|
|
auto& window = const_cast<Bar&>(bar_).window;
|
|
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
|
|
window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll));
|
|
}
|
|
}
|
|
|
|
Workspaces::~Workspaces() { gIPC->unregisterForIPC(this); }
|
|
|
|
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();
|
|
const auto& all_workspaces = gIPC->workspaces();
|
|
const auto& all_windows = gIPC->windows();
|
|
|
|
std::vector<const Json::Value*> my_workspaces;
|
|
my_workspaces.reserve(all_workspaces.size());
|
|
for (const auto& ws : all_workspaces) {
|
|
std::string name;
|
|
if (ws["name"]) {
|
|
name = ws["name"].asString();
|
|
} else {
|
|
name = std::to_string(ws["idx"].asUInt());
|
|
}
|
|
if (isWorkspaceIgnored(name)) {
|
|
continue;
|
|
}
|
|
|
|
if (display_cond == "only-populated") {
|
|
if (ws["active_window_id"].isNull() && !ws["is_active"].asBool()) continue;
|
|
} else if (display_cond == "keep-named") {
|
|
if (ws["name"].isNull() && ws["active_window_id"].isNull() && !ws["is_active"].asBool())
|
|
continue;
|
|
}
|
|
|
|
if (alloutputs || ws["output"].asString() == bar_.output->name) {
|
|
my_workspaces.push_back(&ws);
|
|
}
|
|
}
|
|
|
|
sortWorkspaces(my_workspaces);
|
|
|
|
workspaces_.erase(std::remove_if(workspaces_.begin(), workspaces_.end(),
|
|
[&](const std::unique_ptr<Workspace>& w) {
|
|
bool gone = std::none_of(
|
|
my_workspaces.begin(), my_workspaces.end(),
|
|
[&](const Json::Value* ws) {
|
|
return ws->operator[]("id").asUInt64() == w->id();
|
|
});
|
|
if (gone) box_.remove(w->button());
|
|
return gone;
|
|
}),
|
|
workspaces_.end());
|
|
|
|
for (const auto* ws_ptr : my_workspaces) {
|
|
const auto& ws = *ws_ptr;
|
|
const auto ws_id = ws.isMember("id") ? ws["id"].asUInt64() : 0;
|
|
|
|
auto it =
|
|
std::find_if(workspaces_.begin(), workspaces_.end(),
|
|
[ws_id](const std::unique_ptr<Workspace>& w) { return w->id() == ws_id; });
|
|
|
|
if (it == workspaces_.end()) {
|
|
createWorkspace(ws);
|
|
it = workspaces_.end() - 1;
|
|
}
|
|
|
|
std::vector<Json::Value> windows_vec(all_windows.begin(), all_windows.end());
|
|
const auto windows_str = getWindowsRepresentation(ws);
|
|
(*it)->update(ws, windows_vec, windows_str, my_workspaces.size());
|
|
}
|
|
|
|
for (auto pos_it = my_workspaces.cbegin(); pos_it != my_workspaces.cend(); ++pos_it) {
|
|
const auto& ws = **pos_it;
|
|
const auto ws_id = ws.isMember("id") ? ws["id"].asUInt64() : 0;
|
|
|
|
const auto pos = static_cast<int>(std::distance(my_workspaces.cbegin(), pos_it));
|
|
|
|
auto it =
|
|
std::find_if(workspaces_.begin(), workspaces_.end(),
|
|
[ws_id](const std::unique_ptr<Workspace>& w) { return w->id() == ws_id; });
|
|
if (it != workspaces_.end()) {
|
|
box_.reorder_child((*it)->button(), pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Workspaces::update() {
|
|
doUpdate();
|
|
AModule::update();
|
|
}
|
|
|
|
void Workspaces::createWorkspace(const Json::Value& workspace_data) {
|
|
auto ws = std::make_unique<Workspace>(workspace_data, *this);
|
|
box_.pack_start(ws->button(), false, false, 0);
|
|
workspaces_.push_back(std::move(ws));
|
|
}
|
|
|
|
// --- 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);
|
|
}
|
|
|
|
// Build the "{windows}" replacement string for a workspace using window-rewrite rules.
|
|
std::string Workspaces::getWindowsRepresentation(const Json::Value& ws) {
|
|
std::vector<std::string> window_reps;
|
|
auto collect = [&](const Json::Value& win) {
|
|
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()) {
|
|
window_reps.push_back(getRewrite(app_id, title));
|
|
}
|
|
};
|
|
|
|
if (ws.isMember("windows") && ws["windows"].isArray()) {
|
|
for (const auto& win : ws["windows"]) {
|
|
collect(win);
|
|
}
|
|
} else {
|
|
// 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;
|
|
collect(win);
|
|
}
|
|
}
|
|
|
|
return fmt::format("{}", fmt::join(window_reps, m_formatWindowSeparator));
|
|
}
|
|
// --- End New Helper Functions ---
|
|
|
|
std::string Workspaces::getIcon(const std::string& value, const Json::Value& ws) const {
|
|
const auto& icons = config_["format-icons"];
|
|
if (!icons) return value;
|
|
|
|
if (ws["is_urgent"].asBool() && icons["urgent"]) return icons["urgent"].asString();
|
|
if (ws["is_active"].asBool() && icons["active"]) return icons["active"].asString();
|
|
if (ws["is_focused"].asBool() && icons["focused"]) return icons["focused"].asString();
|
|
if (ws["active_window_id"].isNull() && icons["empty"]) return icons["empty"].asString();
|
|
|
|
if (ws["name"]) {
|
|
const auto& name = ws["name"].asString();
|
|
if (icons[name]) return icons[name].asString();
|
|
}
|
|
|
|
const auto idx = ws["idx"].asString();
|
|
if (icons[idx]) return icons[idx].asString();
|
|
|
|
if (icons["default"]) return icons["default"].asString();
|
|
|
|
return value;
|
|
}
|
|
|
|
bool Workspaces::isWorkspaceIgnored(const std::string& name) {
|
|
for (auto& rule : ignoreWorkspaces_) {
|
|
if (std::regex_match(name, rule)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Workspaces::handleScroll(GdkEventScroll* e) {
|
|
if (gdk_event_get_pointer_emulated((GdkEvent*)e) != 0) {
|
|
/**
|
|
* Ignore emulated scroll events on window
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
auto dir = AModule::getScrollDir(e);
|
|
if (dir == SCROLL_DIR::NONE) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
Json::Value request(Json::objectValue);
|
|
auto& action = (request["Action"] = Json::Value(Json::objectValue));
|
|
|
|
std::string action_name;
|
|
|
|
if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) {
|
|
action_name = "FocusWorkspaceDown";
|
|
} else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) {
|
|
action_name = "FocusWorkspaceUp";
|
|
} else {
|
|
return true;
|
|
}
|
|
|
|
action[action_name] = Json::Value(Json::objectValue);
|
|
|
|
IPC::send(request);
|
|
|
|
} catch (const std::exception& e) {
|
|
spdlog::error("Workspaces: {}", e.what());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Workspaces::sortWorkspaces(std::vector<const Json::Value*>& workspaces) const {
|
|
auto get_name = [](const Json::Value& ws) -> std::string {
|
|
if (ws["name"]) return ws["name"].asString();
|
|
return std::to_string(ws["idx"].asUInt());
|
|
};
|
|
|
|
auto is_numeric = [](const std::string& value) {
|
|
return !value.empty() &&
|
|
std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c); });
|
|
};
|
|
|
|
const bool names_are_numeric =
|
|
std::all_of(workspaces.begin(), workspaces.end(),
|
|
[&](const auto* ws) { return is_numeric(get_name(*ws)); });
|
|
|
|
auto compare_numeric_strings = [](const std::string& a, const std::string& b) {
|
|
if (a.size() != b.size()) return a.size() < b.size();
|
|
return a < b;
|
|
};
|
|
|
|
std::sort(workspaces.begin(), workspaces.end(), [&](const auto* ap, const auto* bp) {
|
|
const auto& a = *ap;
|
|
const auto& b = *bp;
|
|
if (sort_by_id_) {
|
|
return a["id"].asUInt64() < b["id"].asUInt64();
|
|
}
|
|
|
|
if (sort_by_name_) {
|
|
const auto a_name = get_name(a);
|
|
const auto b_name = get_name(b);
|
|
if (a_name == b_name) return a["id"].asUInt64() < b["id"].asUInt64();
|
|
if (names_are_numeric) return compare_numeric_strings(a_name, b_name);
|
|
return a_name < b_name;
|
|
}
|
|
|
|
if (sort_by_coordinates_) {
|
|
const auto& a_output = a["output"].asString();
|
|
const auto& b_output = b["output"].asString();
|
|
if (a_output == b_output) {
|
|
const auto a_idx = a["idx"].asUInt();
|
|
const auto b_idx = b["idx"].asUInt();
|
|
if (a_idx == b_idx) return a["id"].asUInt64() < b["id"].asUInt64();
|
|
return a_idx < b_idx;
|
|
}
|
|
return a_output < b_output;
|
|
}
|
|
|
|
// Default to sorting by workspace index on each output.
|
|
const auto& a_output = a["output"].asString();
|
|
const auto& b_output = b["output"].asString();
|
|
const auto a_idx = a["idx"].asUInt();
|
|
const auto b_idx = b["idx"].asUInt();
|
|
if (a_output == b_output) return a_idx < b_idx;
|
|
return a_output < b_output;
|
|
});
|
|
}
|
|
|
|
} // namespace waybar::modules::niri
|