Files
Waybar/src/modules/hyprland/workspaces.cpp
Skylar Abruzese 64b64d0316 Moved workspace id failing to parse from an error to part of the trace.
With named persistent workspaces it is expected behavior that they have
no id since their workspace may not have been created by hyprland yet.
2025-08-09 18:33:35 -04:00

1138 lines
40 KiB
C++

#include "modules/hyprland/workspaces.hpp"
#include <json/value.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include "util/regex_collection.hpp"
#include "util/string.hpp"
namespace waybar::modules::hyprland {
Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value &config)
: AModule(config, "workspaces", id, false, false),
m_bar(bar),
m_box(bar.orientation, 0),
m_ipc(IPC::inst()) {
modulesReady = true;
parseConfig(config);
m_box.set_name("workspaces");
if (!id.empty()) {
m_box.get_style_context()->add_class(id);
}
m_box.get_style_context()->add_class(MODULE_CLASS);
event_box_.add(m_box);
setCurrentMonitorId();
init();
registerIpc();
}
Workspaces::~Workspaces() {
m_ipc.unregisterForIPC(this);
// wait for possible event handler to finish
std::lock_guard<std::mutex> lg(m_mutex);
}
void Workspaces::init() {
m_activeWorkspaceId = m_ipc.getSocket1JsonReply("activeworkspace")["id"].asInt();
initializeWorkspaces();
dp.emit();
}
Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name,
std::string const &monitor) {
spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor);
Json::Value workspaceData;
auto workspaceId = parseWorkspaceId(name);
if (!workspaceId.has_value()) {
workspaceId = 0;
}
workspaceData["id"] = *workspaceId;
workspaceData["name"] = name;
workspaceData["monitor"] = monitor;
workspaceData["windows"] = 0;
return workspaceData;
}
void Workspaces::createWorkspace(Json::Value const &workspace_data,
Json::Value const &clients_data) {
auto workspaceName = workspace_data["name"].asString();
auto workspaceId = workspace_data["id"].asInt();
spdlog::debug("Creating workspace {}", workspaceName);
// avoid recreating existing workspaces
auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr<Workspace> const &w) {
if (workspaceId > 0) {
return w->id() == workspaceId;
}
return (workspaceName.starts_with("special:") && workspaceName.substr(8) == w->name()) ||
workspaceName == w->name();
});
if (workspace != m_workspaces.end()) {
// don't recreate workspace, but update persistency if necessary
const auto keys = workspace_data.getMemberNames();
const auto *k = "persistent-rule";
if (std::ranges::find(keys, k) != keys.end()) {
spdlog::debug("Set dynamic persistency of workspace {} to: {}", workspaceName,
workspace_data[k].asBool() ? "true" : "false");
(*workspace)->setPersistentRule(workspace_data[k].asBool());
}
k = "persistent-config";
if (std::ranges::find(keys, k) != keys.end()) {
spdlog::debug("Set config persistency of workspace {} to: {}", workspaceName,
workspace_data[k].asBool() ? "true" : "false");
(*workspace)->setPersistentConfig(workspace_data[k].asBool());
}
return;
}
// create new workspace
m_workspaces.emplace_back(std::make_unique<Workspace>(workspace_data, *this, clients_data));
Gtk::Button &newWorkspaceButton = m_workspaces.back()->button();
m_box.pack_start(newWorkspaceButton, false, false);
sortWorkspaces();
newWorkspaceButton.show_all();
}
void Workspaces::createWorkspacesToCreate() {
for (const auto &[workspaceData, clientsData] : m_workspacesToCreate) {
createWorkspace(workspaceData, clientsData);
}
if (!m_workspacesToCreate.empty()) {
updateWindowCount();
sortWorkspaces();
}
m_workspacesToCreate.clear();
}
/**
* Workspaces::doUpdate - update workspaces in UI thread.
*
* Note: some memberfields are modified by both UI thread and event listener thread, use m_mutex to
* protect these member fields, and lock should released before calling AModule::update().
*/
void Workspaces::doUpdate() {
std::unique_lock lock(m_mutex);
removeWorkspacesToRemove();
createWorkspacesToCreate();
updateWorkspaceStates();
updateWindowCount();
sortWorkspaces();
bool anyWindowCreated = updateWindowsToCreate();
if (anyWindowCreated) {
dp.emit();
}
}
void Workspaces::extendOrphans(int workspaceId, Json::Value const &clientsJson) {
spdlog::trace("Extending orphans with workspace {}", workspaceId);
for (const auto &client : clientsJson) {
if (client["workspace"]["id"].asInt() == workspaceId) {
registerOrphanWindow({client});
}
}
}
std::string Workspaces::getRewrite(std::string window_class, std::string window_title) {
std::string windowReprKey;
if (windowRewriteConfigUsesTitle()) {
windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title);
} else {
windowReprKey = fmt::format("class<{}>", window_class);
}
auto const rewriteRule = m_windowRewriteRules.get(windowReprKey);
return fmt::format(fmt::runtime(rewriteRule), fmt::arg("class", window_class),
fmt::arg("title", window_title));
}
std::vector<int> Workspaces::getVisibleWorkspaces() {
std::vector<int> visibleWorkspaces;
auto monitors = IPC::inst().getSocket1JsonReply("monitors");
for (const auto &monitor : monitors) {
auto ws = monitor["activeWorkspace"];
if (ws.isObject() && ws["id"].isInt()) {
visibleWorkspaces.push_back(ws["id"].asInt());
}
auto sws = monitor["specialWorkspace"];
auto name = sws["name"].asString();
if (sws.isObject() && sws["id"].isInt() && !name.empty()) {
visibleWorkspaces.push_back(sws["id"].asInt());
}
}
return visibleWorkspaces;
}
void Workspaces::initializeWorkspaces() {
spdlog::debug("Initializing workspaces");
// if the workspace rules changed since last initialization, make sure we reset everything:
for (auto &workspace : m_workspaces) {
m_workspacesToRemove.push_back(std::to_string(workspace->id()));
}
// get all current workspaces
auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces");
auto const clientsJson = m_ipc.getSocket1JsonReply("clients");
for (Json::Value workspaceJson : workspacesJson) {
std::string workspaceName = workspaceJson["name"].asString();
if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) &&
(!workspaceName.starts_with("special") || showSpecial()) &&
!isWorkspaceIgnored(workspaceName)) {
m_workspacesToCreate.emplace_back(workspaceJson, clientsJson);
} else {
extendOrphans(workspaceJson["id"].asInt(), clientsJson);
}
}
spdlog::debug("Initializing persistent workspaces");
if (m_persistentWorkspaceConfig.isObject()) {
// a persistent workspace config is defined, so use that instead of workspace rules
loadPersistentWorkspacesFromConfig(clientsJson);
}
// load Hyprland's workspace rules
loadPersistentWorkspacesFromWorkspaceRules(clientsJson);
}
bool isDoubleSpecial(std::string const &workspace_name) {
// Hyprland's IPC sometimes reports the creation of workspaces strangely named
// `special:special:<some_name>`. This function checks for that and is used
// to avoid creating (and then removing) such workspaces.
// See hyprwm/Hyprland#3424 for more info.
return workspace_name.find("special:special:") != std::string::npos;
}
bool Workspaces::isWorkspaceIgnored(std::string const &name) {
for (auto &rule : m_ignoreWorkspaces) {
if (std::regex_match(name, rule)) {
return true;
break;
}
}
return false;
}
void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJson) {
spdlog::info("Loading persistent workspaces from Waybar config");
const std::vector<std::string> keys = m_persistentWorkspaceConfig.getMemberNames();
std::vector<std::string> persistentWorkspacesToCreate;
const std::string currentMonitor = m_bar.output->name;
const bool monitorInConfig = std::ranges::find(keys, currentMonitor) != keys.end();
for (const std::string &key : keys) {
// only add if either:
// 1. key is the current monitor name
// 2. key is "*" and this monitor is not already defined in the config
bool canCreate = key == currentMonitor || (key == "*" && !monitorInConfig);
const Json::Value &value = m_persistentWorkspaceConfig[key];
spdlog::trace("Parsing persistent workspace config: {} => {}", key, value.toStyledString());
if (value.isInt()) {
// value is a number => create that many workspaces for this monitor
if (canCreate) {
int amount = value.asInt();
spdlog::debug("Creating {} persistent workspaces for monitor {}", amount, currentMonitor);
for (int i = 0; i < amount; i++) {
persistentWorkspacesToCreate.emplace_back(std::to_string((m_monitorId * amount) + i + 1));
}
}
} else if (value.isArray() && !value.empty()) {
// value is an array => create defined workspaces for this monitor
if (canCreate) {
for (const Json::Value &workspace : value) {
spdlog::debug("Creating workspace {} on monitor {}", workspace, currentMonitor);
persistentWorkspacesToCreate.emplace_back(workspace.asString());
}
} else {
// key is the workspace and value is array of monitors to create on
for (const Json::Value &monitor : value) {
if (monitor.isString() && monitor.asString() == currentMonitor) {
persistentWorkspacesToCreate.emplace_back(currentMonitor);
break;
}
}
}
} else {
// this workspace should be displayed on all monitors
persistentWorkspacesToCreate.emplace_back(key);
}
}
for (auto const &workspace : persistentWorkspacesToCreate) {
auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name);
workspaceData["persistent-config"] = true;
m_workspacesToCreate.emplace_back(workspaceData, clientsJson);
}
}
void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &clientsJson) {
spdlog::info("Loading persistent workspaces from Hyprland workspace rules");
auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules");
for (Json::Value const &rule : workspaceRules) {
if (!rule["workspaceString"].isString()) {
spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule);
continue;
}
if (!rule["persistent"].asBool()) {
continue;
}
auto workspace = rule.isMember("defaultName") ? rule["defaultName"].asString()
: rule["workspaceString"].asString();
// The prefix "name:" cause mismatches with workspace names taken anywhere else.
if (workspace.starts_with("name:")) {
workspace = workspace.substr(5);
}
auto const &monitor = rule["monitor"].asString();
// create this workspace persistently if:
// 1. the allOutputs config option is enabled
// 2. the rule's monitor is the current monitor
// 3. no monitor is specified in the rule => assume it needs to be persistent on every monitor
if (allOutputs() || m_bar.output->name == monitor || monitor.empty()) {
// => persistent workspace should be shown on this monitor
auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name);
workspaceData["persistent-rule"] = true;
m_workspacesToCreate.emplace_back(workspaceData, clientsJson);
} else {
// This can be any workspace selector.
m_workspacesToRemove.emplace_back(workspace);
}
}
}
void Workspaces::onEvent(const std::string &ev) {
std::lock_guard<std::mutex> lock(m_mutex);
std::string eventName(begin(ev), begin(ev) + ev.find_first_of('>'));
std::string payload = ev.substr(eventName.size() + 2);
if (eventName == "workspacev2") {
onWorkspaceActivated(payload);
} else if (eventName == "activespecial") {
onSpecialWorkspaceActivated(payload);
} else if (eventName == "destroyworkspacev2") {
onWorkspaceDestroyed(payload);
} else if (eventName == "createworkspacev2") {
onWorkspaceCreated(payload);
} else if (eventName == "focusedmonv2") {
onMonitorFocused(payload);
} else if (eventName == "moveworkspacev2") {
onWorkspaceMoved(payload);
} else if (eventName == "openwindow") {
onWindowOpened(payload);
} else if (eventName == "closewindow") {
onWindowClosed(payload);
} else if (eventName == "movewindowv2") {
onWindowMoved(payload);
} else if (eventName == "urgent") {
setUrgentWorkspace(payload);
} else if (eventName == "renameworkspace") {
onWorkspaceRenamed(payload);
} else if (eventName == "windowtitlev2") {
onWindowTitleEvent(payload);
} else if (eventName == "activewindowv2") {
onActiveWindowChanged(payload);
} else if (eventName == "configreloaded") {
onConfigReloaded();
}
dp.emit();
}
void Workspaces::onWorkspaceActivated(std::string const &payload) {
const auto [workspaceIdStr, workspaceName] = splitDoublePayload(payload);
const auto workspaceId = parseWorkspaceId(workspaceIdStr);
if (workspaceId.has_value()) {
m_activeWorkspaceId = *workspaceId;
}
}
void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) {
std::string name(begin(payload), begin(payload) + payload.find_first_of(','));
m_activeSpecialWorkspaceName = (!name.starts_with("special:") ? name : name.substr(8));
}
void Workspaces::onWorkspaceDestroyed(std::string const &payload) {
const auto [workspaceId, workspaceName] = splitDoublePayload(payload);
if (!isDoubleSpecial(workspaceName)) {
m_workspacesToRemove.push_back(workspaceId);
}
}
void Workspaces::onWorkspaceCreated(std::string const &payload, Json::Value const &clientsData) {
spdlog::debug("Workspace created: {}", payload);
const auto [workspaceIdStr, _] = splitDoublePayload(payload);
const auto workspaceId = parseWorkspaceId(workspaceIdStr);
if (!workspaceId.has_value()) {
return;
}
auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules");
auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces");
for (Json::Value workspaceJson : workspacesJson) {
const auto currentId = workspaceJson["id"].asInt();
if (currentId == *workspaceId) {
std::string workspaceName = workspaceJson["name"].asString();
// This workspace name is more up-to-date than the one in the event payload.
if (isWorkspaceIgnored(workspaceName)) {
spdlog::trace("Not creating workspace because it is ignored: id={} name={}", *workspaceId,
workspaceName);
break;
}
if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) &&
(showSpecial() || !workspaceName.starts_with("special")) &&
!isDoubleSpecial(workspaceName)) {
for (Json::Value const &rule : workspaceRules) {
auto ruleWorkspaceName = rule.isMember("defaultName")
? rule["defaultName"].asString()
: rule["workspaceString"].asString();
if (ruleWorkspaceName == workspaceName) {
workspaceJson["persistent-rule"] = rule["persistent"].asBool();
break;
}
}
m_workspacesToCreate.emplace_back(workspaceJson, clientsData);
break;
}
} else {
extendOrphans(*workspaceId, clientsData);
}
}
}
void Workspaces::onWorkspaceMoved(std::string const &payload) {
spdlog::debug("Workspace moved: {}", payload);
// Update active workspace
m_activeWorkspaceId = (m_ipc.getSocket1JsonReply("activeworkspace"))["id"].asInt();
if (allOutputs()) return;
const auto [workspaceIdStr, workspaceName, monitorName] = splitTriplePayload(payload);
const auto subPayload = makePayload(workspaceIdStr, workspaceName);
if (m_bar.output->name == monitorName) {
Json::Value clientsData = m_ipc.getSocket1JsonReply("clients");
onWorkspaceCreated(subPayload, clientsData);
} else {
spdlog::debug("Removing workspace because it was moved to another monitor: {}", subPayload);
onWorkspaceDestroyed(subPayload);
}
}
void Workspaces::onWorkspaceRenamed(std::string const &payload) {
spdlog::debug("Workspace renamed: {}", payload);
const auto [workspaceIdStr, newName] = splitDoublePayload(payload);
const auto workspaceId = parseWorkspaceId(workspaceIdStr);
if (!workspaceId.has_value()) {
return;
}
for (auto &workspace : m_workspaces) {
if (workspace->id() == *workspaceId) {
workspace->setName(newName);
break;
}
}
sortWorkspaces();
}
void Workspaces::onMonitorFocused(std::string const &payload) {
spdlog::trace("Monitor focused: {}", payload);
const auto [monitorName, workspaceIdStr] = splitDoublePayload(payload);
const auto workspaceId = parseWorkspaceId(workspaceIdStr);
if (!workspaceId.has_value()) {
return;
}
m_activeWorkspaceId = *workspaceId;
for (Json::Value &monitor : m_ipc.getSocket1JsonReply("monitors")) {
if (monitor["name"].asString() == monitorName) {
const auto name = monitor["specialWorkspace"]["name"].asString();
m_activeSpecialWorkspaceName = !name.starts_with("special:") ? name : name.substr(8);
}
}
}
void Workspaces::onWindowOpened(std::string const &payload) {
spdlog::trace("Window opened: {}", payload);
updateWindowCount();
size_t lastCommaIdx = 0;
size_t nextCommaIdx = payload.find(',');
std::string windowAddress = payload.substr(lastCommaIdx, nextCommaIdx - lastCommaIdx);
lastCommaIdx = nextCommaIdx;
nextCommaIdx = payload.find(',', nextCommaIdx + 1);
std::string workspaceName = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1);
lastCommaIdx = nextCommaIdx;
nextCommaIdx = payload.find(',', nextCommaIdx + 1);
std::string windowClass = payload.substr(lastCommaIdx + 1, nextCommaIdx - lastCommaIdx - 1);
std::string windowTitle = payload.substr(nextCommaIdx + 1, payload.length() - nextCommaIdx);
bool isActive = m_currentActiveWindowAddress == windowAddress;
m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle, isActive);
}
void Workspaces::onWindowClosed(std::string const &addr) {
spdlog::trace("Window closed: {}", addr);
updateWindowCount();
m_orphanWindowMap.erase(addr);
for (auto &workspace : m_workspaces) {
if (workspace->closeWindow(addr)) {
break;
}
}
}
void Workspaces::onWindowMoved(std::string const &payload) {
spdlog::trace("Window moved: {}", payload);
updateWindowCount();
auto [windowAddress, _, workspaceName] = splitTriplePayload(payload);
WindowRepr windowRepr;
// If the window was still queued to be created, just change its destination
// and exit
for (auto &window : m_windowsToCreate) {
if (window.getAddress() == windowAddress) {
window.moveToWorkspace(workspaceName);
return;
}
}
// Take the window's representation from the old workspace...
for (auto &workspace : m_workspaces) {
if (auto windowAddr = workspace->closeWindow(windowAddress); windowAddr != std::nullopt) {
windowRepr = windowAddr.value();
break;
}
}
// ...if it was empty, check if the window is an orphan...
if (windowRepr.empty() && m_orphanWindowMap.contains(windowAddress)) {
windowRepr = m_orphanWindowMap[windowAddress];
}
// ...and then add it to the new workspace
if (!windowRepr.empty()) {
m_orphanWindowMap.erase(windowAddress);
m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowRepr);
}
}
void Workspaces::onWindowTitleEvent(std::string const &payload) {
spdlog::trace("Window title changed: {}", payload);
std::optional<std::function<void(WindowCreationPayload)>> inserter;
const auto [windowAddress, _] = splitDoublePayload(payload);
// If the window was an orphan, rename it at the orphan's vector
if (m_orphanWindowMap.contains(windowAddress)) {
inserter = [this](WindowCreationPayload wcp) { this->registerOrphanWindow(std::move(wcp)); };
} else {
auto windowWorkspace = std::ranges::find_if(m_workspaces, [windowAddress](auto &workspace) {
return workspace->containsWindow(windowAddress);
});
// If the window exists on a workspace, rename it at the workspace's window
// map
if (windowWorkspace != m_workspaces.end()) {
inserter = [windowWorkspace](WindowCreationPayload wcp) {
(*windowWorkspace)->insertWindow(std::move(wcp));
};
} else {
auto queuedWindow =
std::ranges::find_if(m_windowsToCreate, [&windowAddress](auto &windowPayload) {
return windowPayload.getAddress() == windowAddress;
});
// If the window was queued, rename it in the queue
if (queuedWindow != m_windowsToCreate.end()) {
inserter = [queuedWindow](WindowCreationPayload wcp) { *queuedWindow = std::move(wcp); };
}
}
}
if (inserter.has_value()) {
Json::Value clientsData = m_ipc.getSocket1JsonReply("clients");
std::string jsonWindowAddress = fmt::format("0x{}", windowAddress);
auto client = std::ranges::find_if(clientsData, [jsonWindowAddress](auto &client) {
return client["address"].asString() == jsonWindowAddress;
});
if (client != clientsData.end() && !client->empty()) {
(*inserter)({*client});
}
}
}
void Workspaces::onActiveWindowChanged(WindowAddress const &activeWindowAddress) {
spdlog::trace("Active window changed: {}", activeWindowAddress);
m_currentActiveWindowAddress = activeWindowAddress;
for (auto &[address, window] : m_orphanWindowMap) {
window.setActive(address == activeWindowAddress);
}
for (auto const &workspace : m_workspaces) {
workspace->setActiveWindow(activeWindowAddress);
}
for (auto &window : m_windowsToCreate) {
window.setActive(window.getAddress() == activeWindowAddress);
}
}
void Workspaces::onConfigReloaded() {
spdlog::info("Hyprland config reloaded, reinitializing hyprland/workspaces module...");
init();
}
auto Workspaces::parseConfig(const Json::Value &config) -> void {
const auto &configFormat = config["format"];
m_formatBefore = configFormat.isString() ? configFormat.asString() : "{name}";
m_withIcon = m_formatBefore.find("{icon}") != std::string::npos;
auto withWindows = m_formatBefore.find("{windows}") != std::string::npos;
if (m_withIcon && m_iconsMap.empty()) {
populateIconsMap(config["format-icons"]);
}
populateBoolConfig(config, "all-outputs", m_allOutputs);
populateBoolConfig(config, "show-special", m_showSpecial);
populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly);
populateBoolConfig(config, "persistent-only", m_persistentOnly);
populateBoolConfig(config, "active-only", m_activeOnly);
populateBoolConfig(config, "move-to-monitor", m_moveToMonitor);
m_persistentWorkspaceConfig = config.get("persistent-workspaces", Json::Value());
populateSortByConfig(config);
populateIgnoreWorkspacesConfig(config);
populateFormatWindowSeparatorConfig(config);
populateWindowRewriteConfig(config);
if (withWindows) {
populateWorkspaceTaskbarConfig(config);
}
if (m_enableTaskbar) {
auto parts = split(m_formatBefore, "{windows}", 1);
m_formatBefore = parts[0];
m_formatAfter = parts.size() > 1 ? parts[1] : "";
}
}
auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void {
for (const auto &name : formatIcons.getMemberNames()) {
m_iconsMap.emplace(name, formatIcons[name].asString());
}
m_iconsMap.emplace("", "");
}
auto Workspaces::populateBoolConfig(const Json::Value &config, const std::string &key, bool &member)
-> void {
const auto &configValue = config[key];
if (configValue.isBool()) {
member = configValue.asBool();
}
}
auto Workspaces::populateSortByConfig(const Json::Value &config) -> void {
const auto &configSortBy = config["sort-by"];
if (configSortBy.isString()) {
auto sortByStr = configSortBy.asString();
try {
m_sortBy = m_enumParser.parseStringToEnum(sortByStr, m_sortMap);
} catch (const std::invalid_argument &e) {
m_sortBy = SortMethod::DEFAULT;
spdlog::warn(
"Invalid string representation for sort-by. Falling back to default sort method.");
}
}
}
auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value &config) -> void {
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};
m_ignoreWorkspaces.emplace_back(rule);
} catch (const std::regex_error &e) {
spdlog::error("Invalid rule {}: {}", ruleString, e.what());
}
} else {
spdlog::error("Not a string: '{}'", workspaceRegex);
}
}
}
}
auto Workspaces::populateFormatWindowSeparatorConfig(const Json::Value &config) -> void {
const auto &formatWindowSeparator = config["format-window-separator"];
m_formatWindowSeparator =
formatWindowSeparator.isString() ? formatWindowSeparator.asString() : " ";
}
auto Workspaces::populateWindowRewriteConfig(const Json::Value &config) -> void {
const auto &windowRewrite = config["window-rewrite"];
if (!windowRewrite.isObject()) {
spdlog::debug("window-rewrite is not defined or is not an object, using default rules.");
return;
}
const auto &windowRewriteDefaultConfig = config["window-rewrite-default"];
std::string windowRewriteDefault =
windowRewriteDefaultConfig.isString() ? windowRewriteDefaultConfig.asString() : "?";
m_windowRewriteRules = util::RegexCollection(
windowRewrite, windowRewriteDefault,
[this](std::string &window_rule) { return windowRewritePriorityFunction(window_rule); });
}
auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value &config) -> void {
const auto &workspaceTaskbar = config["workspace-taskbar"];
if (!workspaceTaskbar.isObject()) {
spdlog::debug("workspace-taskbar is not defined or is not an object, using default rules.");
return;
}
populateBoolConfig(workspaceTaskbar, "enable", m_enableTaskbar);
populateBoolConfig(workspaceTaskbar, "update-active-window", m_updateActiveWindow);
if (workspaceTaskbar["format"].isString()) {
/* The user defined a format string, use it */
std::string format = workspaceTaskbar["format"].asString();
m_taskbarWithTitle = format.find("{title") != std::string::npos; /* {title} or {title.length} */
auto parts = split(format, "{icon}", 1);
m_taskbarFormatBefore = parts[0];
if (parts.size() > 1) {
m_taskbarWithIcon = true;
m_taskbarFormatAfter = parts[1];
}
} else {
/* The default is to only show the icon */
m_taskbarWithIcon = true;
}
auto iconTheme = workspaceTaskbar["icon-theme"];
if (iconTheme.isArray()) {
for (auto &c : iconTheme) {
m_iconLoader.add_custom_icon_theme(c.asString());
}
} else if (iconTheme.isString()) {
m_iconLoader.add_custom_icon_theme(iconTheme.asString());
}
if (workspaceTaskbar["icon-size"].isInt()) {
m_taskbarIconSize = workspaceTaskbar["icon-size"].asInt();
}
if (workspaceTaskbar["orientation"].isString() &&
toLower(workspaceTaskbar["orientation"].asString()) == "vertical") {
m_taskbarOrientation = Gtk::ORIENTATION_VERTICAL;
}
if (workspaceTaskbar["on-click-window"].isString()) {
m_onClickWindow = workspaceTaskbar["on-click-window"].asString();
}
if (workspaceTaskbar["ignore-list"].isArray()) {
for (auto &windowRegex : workspaceTaskbar["ignore-list"]) {
std::string ruleString = windowRegex.asString();
try {
m_ignoreWindows.emplace_back(ruleString, std::regex_constants::icase);
} catch (const std::regex_error &e) {
spdlog::error("Invalid rule {}: {}", ruleString, e.what());
}
}
}
}
void Workspaces::registerOrphanWindow(WindowCreationPayload create_window_payload) {
if (!create_window_payload.isEmpty(*this)) {
m_orphanWindowMap[create_window_payload.getAddress()] = create_window_payload.repr(*this);
}
}
auto Workspaces::registerIpc() -> void {
m_ipc.registerForIPC("workspacev2", this);
m_ipc.registerForIPC("activespecial", this);
m_ipc.registerForIPC("createworkspacev2", this);
m_ipc.registerForIPC("destroyworkspacev2", this);
m_ipc.registerForIPC("focusedmonv2", this);
m_ipc.registerForIPC("moveworkspacev2", this);
m_ipc.registerForIPC("renameworkspace", this);
m_ipc.registerForIPC("openwindow", this);
m_ipc.registerForIPC("closewindow", this);
m_ipc.registerForIPC("movewindowv2", this);
m_ipc.registerForIPC("urgent", this);
m_ipc.registerForIPC("configreloaded", this);
if (windowRewriteConfigUsesTitle() || m_taskbarWithTitle) {
spdlog::info(
"Registering for Hyprland's 'windowtitlev2' events because a user-defined window "
"rewrite rule uses the 'title' field.");
m_ipc.registerForIPC("windowtitlev2", this);
}
if (m_updateActiveWindow) {
spdlog::info(
"Registering for Hyprland's 'activewindowv2' events because 'update-active-window' is set "
"to true.");
m_ipc.registerForIPC("activewindowv2", this);
}
}
void Workspaces::removeWorkspacesToRemove() {
for (const auto &workspaceString : m_workspacesToRemove) {
removeWorkspace(workspaceString);
}
m_workspacesToRemove.clear();
}
void Workspaces::removeWorkspace(std::string const &workspaceString) {
spdlog::debug("Removing workspace {}", workspaceString);
// If this succeeds, we have a workspace ID.
const auto workspaceId = parseWorkspaceId(workspaceString);
std::string name;
// TODO: At some point we want to support all workspace selectors
// This is just a subset.
// https://wiki.hyprland.org/Configuring/Workspace-Rules/#workspace-selectors
if (workspaceString.starts_with("special:")) {
name = workspaceString.substr(8);
} else if (workspaceString.starts_with("name:")) {
name = workspaceString.substr(5);
} else {
name = workspaceString;
}
const auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr<Workspace> &x) {
if (workspaceId.has_value()) {
return *workspaceId == x->id();
}
return name == x->name();
});
if (workspace == m_workspaces.end()) {
// happens when a workspace on another monitor is destroyed
return;
}
if ((*workspace)->isPersistentConfig()) {
spdlog::trace("Not removing config persistent workspace id={} name={}", (*workspace)->id(),
(*workspace)->name());
return;
}
m_box.remove(workspace->get()->button());
m_workspaces.erase(workspace);
}
void Workspaces::setCurrentMonitorId() {
// get monitor ID from name (used by persistent workspaces)
m_monitorId = 0;
auto monitors = m_ipc.getSocket1JsonReply("monitors");
auto currentMonitor = std::ranges::find_if(monitors, [this](const Json::Value &m) {
return m["name"].asString() == m_bar.output->name;
});
if (currentMonitor == monitors.end()) {
spdlog::error("Monitor '{}' does not have an ID? Using 0", m_bar.output->name);
} else {
m_monitorId = (*currentMonitor)["id"].asInt();
spdlog::trace("Current monitor ID: {}", m_monitorId);
}
}
void Workspaces::sortSpecialCentered() {
std::vector<std::unique_ptr<Workspace>> specialWorkspaces;
std::vector<std::unique_ptr<Workspace>> hiddenWorkspaces;
std::vector<std::unique_ptr<Workspace>> normalWorkspaces;
for (auto &workspace : m_workspaces) {
if (workspace->isSpecial()) {
specialWorkspaces.push_back(std::move(workspace));
} else {
if (workspace->button().is_visible()) {
normalWorkspaces.push_back(std::move(workspace));
} else {
hiddenWorkspaces.push_back(std::move(workspace));
}
}
}
m_workspaces.clear();
size_t center = normalWorkspaces.size() / 2;
m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(normalWorkspaces.begin()),
std::make_move_iterator(normalWorkspaces.begin() + center));
m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(specialWorkspaces.begin()),
std::make_move_iterator(specialWorkspaces.end()));
m_workspaces.insert(m_workspaces.end(),
std::make_move_iterator(normalWorkspaces.begin() + center),
std::make_move_iterator(normalWorkspaces.end()));
m_workspaces.insert(m_workspaces.end(), std::make_move_iterator(hiddenWorkspaces.begin()),
std::make_move_iterator(hiddenWorkspaces.end()));
}
void Workspaces::sortWorkspaces() {
std::ranges::sort( //
m_workspaces, [&](std::unique_ptr<Workspace> &a, std::unique_ptr<Workspace> &b) {
// Helper comparisons
auto isIdLess = a->id() < b->id();
auto isNameLess = a->name() < b->name();
switch (m_sortBy) {
case SortMethod::ID:
return isIdLess;
case SortMethod::NAME:
return isNameLess;
case SortMethod::NUMBER:
try {
return std::stoi(a->name()) < std::stoi(b->name());
} catch (const std::invalid_argument &) {
// Handle the exception if necessary.
break;
}
case SortMethod::DEFAULT:
default:
// Handle the default case here.
// normal -> named persistent -> named -> special -> named special
// both normal (includes numbered persistent) => sort by ID
if (a->id() > 0 && b->id() > 0) {
return isIdLess;
}
// one normal, one special => normal first
if ((a->isSpecial()) ^ (b->isSpecial())) {
return b->isSpecial();
}
// only one normal, one named
if ((a->id() > 0) ^ (b->id() > 0)) {
return a->id() > 0;
}
// both special
if (a->isSpecial() && b->isSpecial()) {
// if one is -99 => put it last
if (a->id() == -99 || b->id() == -99) {
return b->id() == -99;
}
// both are 0 (not yet named persistents) / named specials
// (-98 <= ID <= -1)
return isNameLess;
}
// sort non-special named workspaces by name (ID <= -1377)
return isNameLess;
break;
}
// Return a default value if none of the cases match.
return isNameLess; // You can adjust this to your specific needs.
});
if (m_sortBy == SortMethod::SPECIAL_CENTERED) {
this->sortSpecialCentered();
}
for (size_t i = 0; i < m_workspaces.size(); ++i) {
m_box.reorder_child(m_workspaces[i]->button(), i);
}
}
void Workspaces::setUrgentWorkspace(std::string const &windowaddress) {
const Json::Value clientsJson = m_ipc.getSocket1JsonReply("clients");
int workspaceId = -1;
for (Json::Value clientJson : clientsJson) {
if (clientJson["address"].asString().ends_with(windowaddress)) {
workspaceId = clientJson["workspace"]["id"].asInt();
break;
}
}
auto workspace = std::ranges::find_if(m_workspaces, [workspaceId](std::unique_ptr<Workspace> &x) {
return x->id() == workspaceId;
});
if (workspace != m_workspaces.end()) {
workspace->get()->setUrgent();
}
}
auto Workspaces::update() -> void {
doUpdate();
AModule::update();
}
void Workspaces::updateWindowCount() {
const Json::Value workspacesJson = m_ipc.getSocket1JsonReply("workspaces");
for (auto const &workspace : m_workspaces) {
auto workspaceJson = std::ranges::find_if(workspacesJson, [&](Json::Value const &x) {
return x["name"].asString() == workspace->name() ||
(workspace->isSpecial() && x["name"].asString() == "special:" + workspace->name());
});
uint32_t count = 0;
if (workspaceJson != workspacesJson.end()) {
try {
count = (*workspaceJson)["windows"].asUInt();
} catch (const std::exception &e) {
spdlog::error("Failed to update window count: {}", e.what());
}
}
workspace->setWindows(count);
}
}
bool Workspaces::updateWindowsToCreate() {
bool anyWindowCreated = false;
std::vector<WindowCreationPayload> notCreated;
for (auto &windowPayload : m_windowsToCreate) {
bool created = false;
for (auto &workspace : m_workspaces) {
if (workspace->onWindowOpened(windowPayload)) {
created = true;
anyWindowCreated = true;
break;
}
}
if (!created) {
static auto const WINDOW_CREATION_TIMEOUT = 2;
if (windowPayload.incrementTimeSpentUncreated() < WINDOW_CREATION_TIMEOUT) {
notCreated.push_back(windowPayload);
} else {
registerOrphanWindow(windowPayload);
}
}
}
m_windowsToCreate.clear();
m_windowsToCreate = notCreated;
return anyWindowCreated;
}
void Workspaces::updateWorkspaceStates() {
const std::vector<int> visibleWorkspaces = getVisibleWorkspaces();
auto updatedWorkspaces = m_ipc.getSocket1JsonReply("workspaces");
auto currentWorkspace = m_ipc.getSocket1JsonReply("activeworkspace");
std::string currentWorkspaceName =
currentWorkspace.isMember("name") ? currentWorkspace["name"].asString() : "";
for (auto &workspace : m_workspaces) {
bool isActiveByName =
!currentWorkspaceName.empty() && workspace->name() == currentWorkspaceName;
workspace->setActive(
workspace->id() == m_activeWorkspaceId || isActiveByName ||
(workspace->isSpecial() && workspace->name() == m_activeSpecialWorkspaceName));
if (workspace->isActive() && workspace->isUrgent()) {
workspace->setUrgent(false);
}
workspace->setVisible(std::ranges::find(visibleWorkspaces, workspace->id()) !=
visibleWorkspaces.end());
std::string &workspaceIcon = m_iconsMap[""];
if (m_withIcon) {
workspaceIcon = workspace->selectIcon(m_iconsMap);
}
auto updatedWorkspace = std::ranges::find_if(updatedWorkspaces, [&workspace](const auto &w) {
auto wNameRaw = w["name"].asString();
auto wName = wNameRaw.starts_with("special:") ? wNameRaw.substr(8) : wNameRaw;
return wName == workspace->name();
});
if (updatedWorkspace != updatedWorkspaces.end()) {
workspace->setOutput((*updatedWorkspace)["monitor"].asString());
}
workspace->update(workspaceIcon);
}
}
int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) {
// Rules that match against title are prioritized
// Rules that don't specify if they're matching against either title or class are deprioritized
bool const hasTitle = window_rule.find("title") != std::string::npos;
bool const hasClass = window_rule.find("class") != std::string::npos;
if (hasTitle && hasClass) {
m_anyWindowRewriteRuleUsesTitle = true;
return 3;
}
if (hasTitle) {
m_anyWindowRewriteRuleUsesTitle = true;
return 2;
}
if (hasClass) {
return 1;
}
return 0;
}
template <typename... Args>
std::string Workspaces::makePayload(Args const &...args) {
std::ostringstream result;
bool first = true;
((result << (first ? "" : ",") << args, first = false), ...);
return result.str();
}
std::pair<std::string, std::string> Workspaces::splitDoublePayload(std::string const &payload) {
const std::string part1 = payload.substr(0, payload.find(','));
const std::string part2 = payload.substr(part1.size() + 1);
return {part1, part2};
}
std::tuple<std::string, std::string, std::string> Workspaces::splitTriplePayload(
std::string const &payload) {
const size_t firstComma = payload.find(',');
const size_t secondComma = payload.find(',', firstComma + 1);
const std::string part1 = payload.substr(0, firstComma);
const std::string part2 = payload.substr(firstComma + 1, secondComma - (firstComma + 1));
const std::string part3 = payload.substr(secondComma + 1);
return {part1, part2, part3};
}
std::optional<int> Workspaces::parseWorkspaceId(std::string const &workspaceIdStr) {
try {
return workspaceIdStr == "special" ? -99 : std::stoi(workspaceIdStr);
} catch (std::exception const &e) {
spdlog::debug("Workspace \"{}\" is not bound to an id: {}", workspaceIdStr, e.what());
return std::nullopt;
}
}
} // namespace waybar::modules::hyprland