Merge remote-tracking branch 'origin/master' into pr-4694

# Conflicts:
#	src/modules/hyprland/workspace.cpp
This commit is contained in:
Alex
2026-07-04 00:41:50 +02:00
272 changed files with 11596 additions and 3261 deletions
+181 -67
View File
@@ -9,44 +9,47 @@
#include <sys/un.h>
#include <unistd.h>
#include <array>
#include <cerrno>
#include <cstring>
#include <filesystem>
#include <optional>
#include <string>
#include "util/scoped_fd.hpp"
namespace waybar::modules::hyprland {
std::filesystem::path IPC::socketFolder_;
std::optional<bool> IPC::s_luaProtocolDetected_;
std::filesystem::path IPC::getSocketFolder(const char* instanceSig) {
static std::mutex folderMutex;
std::unique_lock lock(folderMutex);
// socket path, specified by EventManager of Hyprland
if (!socketFolder_.empty()) {
return socketFolder_;
if (socketFolder_.empty()) {
const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR");
std::filesystem::path xdgRuntimeDir;
// Only set path if env variable is set
if (xdgRuntimeDirEnv != nullptr) {
xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv);
}
if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) {
socketFolder_ = xdgRuntimeDir / "hypr";
} else {
spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr");
socketFolder_ = std::filesystem::path("/tmp") / "hypr";
}
}
const char* xdgRuntimeDirEnv = std::getenv("XDG_RUNTIME_DIR");
std::filesystem::path xdgRuntimeDir;
// Only set path if env variable is set
if (xdgRuntimeDirEnv != nullptr) {
xdgRuntimeDir = std::filesystem::path(xdgRuntimeDirEnv);
}
if (!xdgRuntimeDir.empty() && std::filesystem::exists(xdgRuntimeDir / "hypr")) {
socketFolder_ = xdgRuntimeDir / "hypr";
} else {
spdlog::warn("$XDG_RUNTIME_DIR/hypr does not exist, falling back to /tmp/hypr");
socketFolder_ = std::filesystem::path("/tmp") / "hypr";
}
socketFolder_ = socketFolder_ / instanceSig;
return socketFolder_;
return socketFolder_ / instanceSig;
}
IPC::IPC() {
// will start IPC and relay events to parseIPC
ipcThread_ = std::thread([this]() { socketListener(); });
socketOwnerPid_ = getpid();
ipcThread_ = std::thread([this]() { socketListener(); });
}
IPC::~IPC() {
@@ -54,19 +57,20 @@ IPC::~IPC() {
// failed exec()) exits.
if (getpid() != socketOwnerPid_) return;
running_ = false;
running_.store(false, std::memory_order_relaxed);
spdlog::info("Hyprland IPC stopping...");
if (socketfd_ != -1) {
spdlog::trace("Shutting down socket");
if (shutdown(socketfd_, SHUT_RDWR) == -1) {
spdlog::error("Hyprland IPC: Couldn't shutdown socket");
}
spdlog::trace("Closing socket");
if (close(socketfd_) == -1) {
spdlog::error("Hyprland IPC: Couldn't close socket");
{
std::lock_guard<std::mutex> lock(socketMutex_);
if (socketfd_ != -1) {
spdlog::trace("Shutting down socket");
if (shutdown(socketfd_, SHUT_RDWR) == -1 && errno != ENOTCONN) {
spdlog::error("Hyprland IPC: Couldn't shutdown socket");
}
}
}
ipcThread_.join();
if (ipcThread_.joinable()) {
ipcThread_.join();
}
}
IPC& IPC::inst() {
@@ -85,10 +89,10 @@ void IPC::socketListener() {
spdlog::info("Hyprland IPC starting");
struct sockaddr_un addr;
socketfd_ = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = {};
const int socketfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socketfd_ == -1) {
if (socketfd == -1) {
spdlog::error("Hyprland IPC: socketfd failed");
return;
}
@@ -96,44 +100,76 @@ void IPC::socketListener() {
addr.sun_family = AF_UNIX;
auto socketPath = IPC::getSocketFolder(his) / ".socket2.sock";
if (socketPath.native().size() >= sizeof(addr.sun_path)) {
spdlog::error("Hyprland IPC: Socket path is too long: {}", socketPath.string());
close(socketfd);
return;
}
strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1);
addr.sun_path[sizeof(addr.sun_path) - 1] = 0;
int l = sizeof(struct sockaddr_un);
if (connect(socketfd_, (struct sockaddr*)&addr, l) == -1) {
spdlog::error("Hyprland IPC: Unable to connect?");
if (connect(socketfd, (struct sockaddr*)&addr, l) == -1) {
spdlog::error("Hyprland IPC: Unable to connect? {}", std::strerror(errno));
close(socketfd);
return;
}
auto* file = fdopen(socketfd_, "r");
if (file == nullptr) {
spdlog::error("Hyprland IPC: Couldn't open file descriptor");
return;
{
std::lock_guard<std::mutex> lock(socketMutex_);
socketfd_ = socketfd;
}
while (running_) {
std::string pending;
while (running_.load(std::memory_order_relaxed)) {
std::array<char, 1024> buffer; // Hyprland socket2 events are max 1024 bytes
const ssize_t bytes_read = read(socketfd, buffer.data(), buffer.size());
auto* receivedCharPtr = fgets(buffer.data(), buffer.size(), file);
if (receivedCharPtr == nullptr) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
if (bytes_read == 0) {
if (running_.load(std::memory_order_relaxed)) {
spdlog::warn("Hyprland IPC: Socket closed by peer");
}
break;
}
std::string messageReceived(buffer.data());
messageReceived = messageReceived.substr(0, messageReceived.find_first_of('\n'));
spdlog::debug("hyprland IPC received {}", messageReceived);
try {
parseIPC(messageReceived);
} catch (std::exception& e) {
spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what());
} catch (...) {
throw;
if (bytes_read < 0) {
if (errno == EINTR) {
continue;
}
if (!running_.load(std::memory_order_relaxed)) {
break;
}
spdlog::error("Hyprland IPC: read failed: {}", std::strerror(errno));
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
pending.append(buffer.data(), static_cast<std::size_t>(bytes_read));
for (auto newline_pos = pending.find('\n'); newline_pos != std::string::npos;
newline_pos = pending.find('\n')) {
std::string messageReceived = pending.substr(0, newline_pos);
pending.erase(0, newline_pos + 1);
if (messageReceived.empty()) {
continue;
}
spdlog::debug("hyprland IPC received {}", messageReceived);
try {
parseIPC(messageReceived);
} catch (std::exception& e) {
spdlog::warn("Failed to parse IPC message: {}, reason: {}", messageReceived, e.what());
} catch (...) {
throw;
}
}
}
{
std::lock_guard<std::mutex> lock(socketMutex_);
if (socketfd_ != -1) {
if (close(socketfd_) == -1) {
spdlog::error("Hyprland IPC: Couldn't close socket");
}
socketfd_ = -1;
}
}
spdlog::debug("Hyprland IPC stopped");
}
@@ -178,7 +214,7 @@ void IPC::unregisterForIPC(EventHandler* ev_handler) {
std::string IPC::getSocket1Reply(const std::string& rq) {
// basically hyprctl
const auto serverSocket = socket(AF_UNIX, SOCK_STREAM, 0);
util::ScopedFd serverSocket(socket(AF_UNIX, SOCK_STREAM, 0));
if (serverSocket < 0) {
throw std::runtime_error("Hyprland IPC: Couldn't open a socket (1)");
@@ -198,8 +234,10 @@ std::string IPC::getSocket1Reply(const std::string& rq) {
std::string socketPath = IPC::getSocketFolder(instanceSig) / ".socket.sock";
// Use snprintf to copy the socketPath string into serverAddress.sun_path
if (snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str()) <
0) {
const auto socketPathLength =
snprintf(serverAddress.sun_path, sizeof(serverAddress.sun_path), "%s", socketPath.c_str());
if (socketPathLength < 0 ||
socketPathLength >= static_cast<int>(sizeof(serverAddress.sun_path))) {
throw std::runtime_error("Hyprland IPC: Couldn't copy socket path (6)");
}
@@ -208,28 +246,39 @@ std::string IPC::getSocket1Reply(const std::string& rq) {
throw std::runtime_error("Hyprland IPC: Couldn't connect to " + socketPath + ". (3)");
}
auto sizeWritten = write(serverSocket, rq.c_str(), rq.length());
std::size_t totalWritten = 0;
while (totalWritten < rq.length()) {
const auto sizeWritten =
write(serverSocket, rq.c_str() + totalWritten, rq.length() - totalWritten);
if (sizeWritten < 0) {
spdlog::error("Hyprland IPC: Couldn't write (4)");
return "";
if (sizeWritten < 0) {
if (errno == EINTR) {
continue;
}
spdlog::error("Hyprland IPC: Couldn't write (4)");
return "";
}
if (sizeWritten == 0) {
spdlog::error("Hyprland IPC: Socket write made no progress");
return "";
}
totalWritten += static_cast<std::size_t>(sizeWritten);
}
std::array<char, 8192> buffer = {0};
std::string response;
ssize_t sizeWritten = 0;
do {
sizeWritten = read(serverSocket, buffer.data(), 8192);
if (sizeWritten < 0) {
spdlog::error("Hyprland IPC: Couldn't read (5)");
close(serverSocket);
return "";
}
response.append(buffer.data(), sizeWritten);
} while (sizeWritten > 0);
close(serverSocket);
return response;
}
@@ -243,4 +292,69 @@ Json::Value IPC::getSocket1JsonReply(const std::string& rq) {
return parser_.parse(reply);
}
bool IPC::isLuaProtocol() {
if (s_luaProtocolDetected_.has_value()) {
return *s_luaProtocolDetected_;
}
// Probe: send a harmless old-style dispatch and check the error.
// In Lua-based Hyprland (>= 0.54) the error contains "hl.dispatch".
// In older versions it returns "ok" or a different error.
auto reply = getSocket1Reply("dispatch workspace __waybar_probe__");
bool luaProto = reply.find("hl.dispatch") != std::string::npos;
if (luaProto) {
spdlog::info("Hyprland IPC: detected Lua-based dispatch protocol (Hyprland >= 0.54)");
} else {
spdlog::info("Hyprland IPC: detected legacy dispatch protocol");
}
s_luaProtocolDetected_ = luaProto;
return luaProto;
}
std::string IPC::buildLuaDispatch(const std::string& dispatcher, const std::string& arg) {
// Map old-style dispatchers to the new Lua hl.dsp API.
//
// Old format: dispatch workspace 1
// New format: /dispatch hl.dsp.focus({ workspace = "1" })
//
// Old format: dispatch focusworkspaceoncurrentmonitor 2
// New format: /dispatch hl.dsp.focus({ workspace = "2", on_current_monitor = true })
//
// Old format: dispatch togglespecialworkspace name
// New format: /dispatch hl.dsp.workspace.toggle_special("name")
if (dispatcher == "workspace") {
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\" })";
}
if (dispatcher == "focusworkspaceoncurrentmonitor") {
return "/dispatch hl.dsp.focus({ workspace = \"" + arg + "\", on_current_monitor = true })";
}
if (dispatcher == "togglespecialworkspace") {
if (arg.empty()) {
return "/dispatch hl.dsp.workspace.toggle_special()";
}
return "/dispatch hl.dsp.workspace.toggle_special(\"" + arg + "\")";
}
// Fallback for any other dispatcher: try the old format wrapped in dispatch().
// This may not work for all dispatchers, but it's a reasonable default.
spdlog::warn("Hyprland IPC: unknown dispatcher '{}' in Lua mode, attempting generic format",
dispatcher);
return "/dispatch hl.dsp." + dispatcher + "(\"" + arg + "\")";
}
std::string IPC::dispatch(const std::string& dispatcher, const std::string& arg) {
if (isLuaProtocol()) {
return getSocket1Reply(buildLuaDispatch(dispatcher, arg));
}
// Legacy format: "dispatch <dispatcher> <arg>"
std::string cmd = "dispatch " + dispatcher;
if (!arg.empty()) {
cmd += " " + arg;
}
return getSocket1Reply(cmd);
}
} // namespace waybar::modules::hyprland
+89 -10
View File
@@ -48,41 +48,109 @@ auto Language::update() -> void {
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant)));
}
spdlog::debug("hyprland language formatted layout name {}", layoutName);
std::string tooltipContent = std::string{};
bool tooltip_enabled = tooltipEnabled();
if (tooltip_enabled) {
if (config_.isMember("tooltip-format")) {
auto tooltip_format = config_["tooltip-format"].asString();
if (config_.isMember("tooltip-format-" + layout_.short_description + "-" + layout_.variant)) {
const auto propName = "tooltip-format-" + layout_.short_description + "-" + layout_.variant;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString());
} else if (config_.isMember("tooltip-format-" + layout_.short_description)) {
const auto propName = "tooltip-format-" + layout_.short_description;
tooltipContent = fmt::format(fmt::runtime(tooltip_format), config_[propName].asString());
} else {
tooltipContent =
trim(fmt::format(fmt::runtime(tooltip_format), fmt::arg("long", layout_.full_name),
fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant)));
}
} else {
// if no tooltip format is provided, use the same text as the module
tooltipContent = layoutName;
}
spdlog::debug("hyprland language formatted tooltip content {}", tooltipContent);
}
if (!format_.empty()) {
label_.show();
label_.set_markup(layoutName);
setLabelMarkup(layoutName);
if (tooltip_enabled) {
setTooltipMarkup(tooltipContent);
}
} else {
label_.hide();
}
// Tooltip support
if (tooltipEnabled()) {
std::string tooltipFormat;
if (config_["tooltip-format"].isString()) {
tooltipFormat = config_["tooltip-format"].asString();
} else {
tooltipFormat = "{long}";
}
auto tooltipText =
trim(fmt::format(fmt::runtime(tooltipFormat), fmt::arg("long", layout_.full_name),
fmt::arg("short", layout_.short_name),
fmt::arg("shortDescription", layout_.short_description),
fmt::arg("variant", layout_.variant)));
label_.set_tooltip_text(tooltipText);
} else {
label_.set_tooltip_text("");
}
ALabel::update();
}
void Language::onEvent(const std::string& ev) {
std::lock_guard<std::mutex> lg(mutex_);
std::string kbName(begin(ev) + ev.find_last_of('>') + 1, begin(ev) + ev.find_first_of(','));
const auto payloadStart = ev.find(">>");
if (payloadStart == std::string::npos) {
spdlog::warn("hyprland language received malformed event: {}", ev);
return;
}
const auto payload = ev.substr(payloadStart + 2);
const auto kbSeparator = payload.find(',');
if (kbSeparator == std::string::npos) {
spdlog::warn("hyprland language received malformed event payload: {}", ev);
return;
}
// Last comma before variants parenthesis, eg:
// activelayout>>micro-star-int'l-co.,-ltd.-msi-gk50-elite-gaming-keyboard,English (US, intl.,
// with dead keys)
std::string beforeParenthesis;
auto parenthesisPos = ev.find_last_of('(');
auto parenthesisPos = payload.find_last_of('(');
if (parenthesisPos == std::string::npos) {
beforeParenthesis = ev;
beforeParenthesis = payload;
} else {
beforeParenthesis = std::string(begin(ev), begin(ev) + parenthesisPos);
beforeParenthesis = payload.substr(0, parenthesisPos);
}
auto layoutName = ev.substr(beforeParenthesis.find_last_of(',') + 1);
const auto layoutSeparator = beforeParenthesis.find_last_of(',');
if (layoutSeparator == std::string::npos) {
spdlog::warn("hyprland language received malformed layout payload: {}", ev);
return;
}
auto layoutName = payload.substr(layoutSeparator + 1);
if (config_.isMember("keyboard-name") && kbName != config_["keyboard-name"].asString())
return; // ignore
if (config_.isMember("keyboard-name")) {
const auto keyboardName = config_["keyboard-name"].asString();
// The keyboard name itself can contain commas, so match it as a full prefix
// (followed by the ',' separator) rather than comparing against the substring
// before the first comma, which would truncate such names and drop the event.
if (payload.size() <= keyboardName.size() || payload[keyboardName.size()] != ',' ||
payload.compare(0, keyboardName.size(), keyboardName) != 0)
return; // ignore
}
layoutName = waybar::util::sanitize_string(layoutName);
removeXkbLayoutCssClass();
layout_ = getLayout(layoutName);
addXkbLayoutCssClass();
spdlog::debug("hyprland language onevent with {}", layoutName);
@@ -104,6 +172,7 @@ void Language::initLanguage() {
searcher = waybar::util::sanitize_string(searcher);
layout_ = getLayout(searcher);
addXkbLayoutCssClass();
spdlog::debug("hyprland language initLanguage found {}", layout_.full_name);
@@ -113,6 +182,16 @@ void Language::initLanguage() {
}
}
auto Language::removeXkbLayoutCssClass() -> void {
label_.get_style_context()->remove_class(layout_.short_name);
spdlog::debug("hyprland language try to remove currently short_name css class {}",
layout_.short_name);
}
auto Language::addXkbLayoutCssClass() -> void {
label_.get_style_context()->add_class(layout_.short_name);
spdlog::debug("hyprland language add new short_name css class {}", layout_.short_name);
}
auto Language::getLayout(const std::string& fullName) -> Layout {
auto* const context = rxkb_context_new(RXKB_CONTEXT_LOAD_EXOTIC_RULES);
rxkb_context_parse_default_ruleset(context);
+17 -7
View File
@@ -44,12 +44,23 @@ auto Submap::parseConfig(const Json::Value& config) -> void {
auto Submap::update() -> void {
std::lock_guard<std::mutex> lg(mutex_);
// Handle style class changes
if (!prev_submap_.empty()) {
label_.get_style_context()->remove_class(prev_submap_);
}
if (!submap_.empty()) {
label_.get_style_context()->add_class(submap_);
}
prev_submap_ = submap_;
if (submap_.empty()) {
event_box_.hide();
} else {
label_.set_markup(fmt::format(fmt::runtime(format_), submap_));
if (tooltipEnabled()) {
label_.set_tooltip_text(submap_);
label_.set_tooltip_markup(submap_);
}
event_box_.show();
}
@@ -64,11 +75,12 @@ void Submap::onEvent(const std::string& ev) {
return;
}
auto submapName = ev.substr(ev.find_first_of('>') + 2);
if (!submap_.empty()) {
label_.get_style_context()->remove_class(submap_);
const auto separator = ev.find(">>");
if (separator == std::string::npos) {
spdlog::warn("hyprland submap received malformed event: {}", ev);
return;
}
auto submapName = ev.substr(separator + 2);
submap_ = submapName;
@@ -76,8 +88,6 @@ void Submap::onEvent(const std::string& ev) {
submap_ = default_submap_;
}
label_.get_style_context()->add_class(submap_);
spdlog::debug("hyprland submap onevent with {}", submap_);
dp.emit();
+76 -77
View File
@@ -1,5 +1,6 @@
#include "modules/hyprland/window.hpp"
#include <fmt/format.h>
#include <glibmm/fileutils.h>
#include <glibmm/keyfile.h>
#include <glibmm/miscutils.h>
@@ -19,21 +20,19 @@ std::shared_mutex windowIpcSmtx;
Window::Window(const std::string& id, const Bar& bar, const Json::Value& config)
: AAppIconLabel(config, "window", id, "{title}", 0, true), bar_(bar), m_ipc(IPC::inst()) {
std::unique_lock<std::shared_mutex> windowIpcUniqueLock(windowIpcSmtx);
separateOutputs_ = config["separate-outputs"].asBool();
update();
// register for hyprland ipc
std::unique_lock<std::shared_mutex> windowIpcUniqueLock(windowIpcSmtx);
m_ipc.registerForIPC("activewindow", this);
m_ipc.registerForIPC("closewindow", this);
m_ipc.registerForIPC("movewindow", this);
m_ipc.registerForIPC("changefloatingmode", this);
m_ipc.registerForIPC("fullscreen", this);
windowIpcUniqueLock.unlock();
queryActiveWorkspace();
update();
dp.emit();
}
@@ -45,6 +44,8 @@ Window::~Window() {
auto Window::update() -> void {
std::shared_lock<std::shared_mutex> windowIpcShareLock(windowIpcSmtx);
queryActiveWorkspace();
std::string windowName = waybar::util::sanitize_string(workspace_.last_window_title);
std::string windowAddress = workspace_.last_window;
@@ -53,13 +54,20 @@ auto Window::update() -> void {
std::string label_text;
if (!format_.empty()) {
label_.show();
// If the focused window name is empty and fallback is configured, use fallback text
std::string displayTitle = windowName;
if (displayTitle.empty() && config_["fallback"].isString()) {
displayTitle = config_["fallback"].asString();
}
label_text = waybar::util::rewriteString(
fmt::format(fmt::runtime(format_), fmt::arg("title", windowName),
fmt::format(fmt::runtime(format_), fmt::arg("title", displayTitle),
fmt::arg("initialTitle", windowData_.initial_title),
fmt::arg("class", windowData_.class_name),
fmt::arg("initialClass", windowData_.initial_class_name)),
config_["rewrite"]);
label_.set_markup(label_text);
setLabelMarkup(label_text);
} else {
label_.hide();
}
@@ -70,13 +78,12 @@ auto Window::update() -> void {
tooltip_format = config_["tooltip-format"].asString();
}
if (!tooltip_format.empty()) {
label_.set_tooltip_text(
fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName),
fmt::arg("initialTitle", windowData_.initial_title),
fmt::arg("class", windowData_.class_name),
fmt::arg("initialClass", windowData_.initial_class_name)));
setTooltipMarkup(fmt::format(fmt::runtime(tooltip_format), fmt::arg("title", windowName),
fmt::arg("initialTitle", windowData_.initial_title),
fmt::arg("class", windowData_.class_name),
fmt::arg("initialClass", windowData_.initial_class_name)));
} else if (!label_text.empty()) {
label_.set_tooltip_text(label_text);
setTooltipMarkup(label_text);
}
}
@@ -108,21 +115,14 @@ auto Window::update() -> void {
AAppIconLabel::update();
}
auto Window::getActiveWorkspace() -> Workspace {
const auto workspace = IPC::inst().getSocket1JsonReply("activeworkspace");
if (workspace.isObject()) {
return Workspace::parse(workspace);
}
return {};
}
auto Window::getActiveWorkspace() -> Workspace { return getActiveWorkspace(""); }
auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {
const auto monitors = IPC::inst().getSocket1JsonReply("monitors");
if (monitors.isArray()) {
auto monitor = std::ranges::find_if(
monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; });
auto monitor = std::ranges::find_if(monitors, [&](const Json::Value& monitor) {
return monitorName.empty() ? monitor["focused"].asBool() : monitor["name"] == monitorName;
});
if (monitor == std::end(monitors)) {
spdlog::warn("Monitor not found: {}", monitorName);
return Workspace{
@@ -132,12 +132,13 @@ auto Window::getActiveWorkspace(const std::string& monitorName) -> Workspace {
.last_window_title = "",
};
}
const int id = (*monitor)["activeWorkspace"]["id"].asInt();
const int special_id = (*monitor)["specialWorkspace"]["id"].asInt();
const int id = special_id != 0 ? special_id : (*monitor)["activeWorkspace"]["id"].asInt();
const auto workspaces = IPC::inst().getSocket1JsonReply("workspaces");
if (workspaces.isArray()) {
auto workspace = std::ranges::find_if(
workspaces, [&](Json::Value workspace) { return workspace["id"] == id; });
workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; });
if (workspace == std::end(workspaces)) {
spdlog::warn("No workspace with id {}", id);
return Workspace{
@@ -175,71 +176,69 @@ auto Window::WindowData::parse(const Json::Value& value) -> Window::WindowData {
}
void Window::queryActiveWorkspace() {
std::shared_lock<std::shared_mutex> windowIpcShareLock(windowIpcSmtx);
if (separateOutputs_) {
workspace_ = getActiveWorkspace(this->bar_.output->name);
} else {
workspace_ = getActiveWorkspace();
}
focused_ = false;
windowData_ = WindowData{};
allFloating_ = false;
swallowing_ = false;
fullscreen_ = false;
solo_ = false;
soloClass_.clear();
if (workspace_.windows <= 0) {
return;
}
const auto clients = m_ipc.getSocket1JsonReply("clients");
if (!clients.isArray()) {
return;
}
auto activeWindow = std::ranges::find_if(clients, [&](const Json::Value& window) {
return window["address"] == workspace_.last_window;
});
if (activeWindow == std::end(clients)) {
return;
}
focused_ = true;
if (workspace_.windows > 0) {
const auto clients = m_ipc.getSocket1JsonReply("clients");
if (clients.isArray()) {
auto activeWindow = std::ranges::find_if(
clients, [&](Json::Value window) { return window["address"] == workspace_.last_window; });
if (activeWindow == std::end(clients)) {
focused_ = false;
return;
}
windowData_ = WindowData::parse(*activeWindow);
updateAppIconName(windowData_.class_name, windowData_.initial_class_name);
std::vector<Json::Value> workspaceWindows;
std::ranges::copy_if(clients, std::back_inserter(workspaceWindows), [&](Json::Value window) {
windowData_ = WindowData::parse(*activeWindow);
updateAppIconName(windowData_.class_name, windowData_.initial_class_name);
std::vector<Json::Value> workspaceWindows;
std::ranges::copy_if(
clients, std::back_inserter(workspaceWindows), [&](const Json::Value& window) {
return window["workspace"]["id"] == workspace_.id && window["mapped"].asBool();
});
swallowing_ = std::ranges::any_of(workspaceWindows, [&](Json::Value window) {
return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0";
});
std::vector<Json::Value> visibleWindows;
std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows),
[&](Json::Value window) { return !window["hidden"].asBool(); });
solo_ = 1 == std::count_if(visibleWindows.begin(), visibleWindows.end(),
[&](Json::Value window) { return !window["floating"].asBool(); });
allFloating_ = std::ranges::all_of(
visibleWindows, [&](Json::Value window) { return window["floating"].asBool(); });
fullscreen_ = windowData_.fullscreen;
swallowing_ = std::ranges::any_of(workspaceWindows, [&](const Json::Value& window) {
return !window["swallowing"].isNull() && window["swallowing"].asString() != "0x0";
});
std::vector<Json::Value> visibleWindows;
std::ranges::copy_if(workspaceWindows, std::back_inserter(visibleWindows),
[&](const Json::Value& window) { return !window["hidden"].asBool(); });
solo_ =
1 == std::count_if(visibleWindows.begin(), visibleWindows.end(),
[&](const Json::Value& window) { return !window["floating"].asBool(); });
allFloating_ = std::ranges::all_of(
visibleWindows, [&](const Json::Value& window) { return window["floating"].asBool(); });
fullscreen_ = windowData_.fullscreen;
// Fullscreen windows look like they are solo
if (fullscreen_) {
solo_ = true;
}
// Fullscreen windows look like they are solo
if (fullscreen_) {
solo_ = true;
}
if (solo_) {
soloClass_ = windowData_.class_name;
} else {
soloClass_ = "";
}
}
} else {
focused_ = false;
windowData_ = WindowData{};
allFloating_ = false;
swallowing_ = false;
fullscreen_ = false;
solo_ = false;
soloClass_ = "";
if (solo_) {
soloClass_ = windowData_.class_name;
}
}
void Window::onEvent(const std::string& ev) {
queryActiveWorkspace();
dp.emit();
}
void Window::onEvent(const std::string& ev) { dp.emit(); }
void Window::setClass(const std::string& classname, bool enable) {
if (enable) {
+6 -9
View File
@@ -39,6 +39,8 @@ WindowCount::~WindowCount() {
auto WindowCount::update() -> void {
std::lock_guard<std::mutex> lg(mutex_);
queryActiveWorkspace();
std::string format = config_["format"].asString();
std::string formatEmpty = config_["format-empty"].asString();
std::string formatWindowed = config_["format-windowed"].asString();
@@ -56,7 +58,7 @@ auto WindowCount::update() -> void {
} else if (!format.empty()) {
label_.set_markup(fmt::format(fmt::runtime(format), workspace_.windows));
} else {
label_.set_text(fmt::format("{}", workspace_.windows));
label_.set_markup(fmt::format("{}", workspace_.windows));
}
label_.show();
@@ -77,7 +79,7 @@ auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspac
const auto monitors = m_ipc.getSocket1JsonReply("monitors");
if (monitors.isArray()) {
auto monitor = std::ranges::find_if(
monitors, [&](Json::Value monitor) { return monitor["name"] == monitorName; });
monitors, [&](const Json::Value& monitor) { return monitor["name"] == monitorName; });
if (monitor == std::end(monitors)) {
spdlog::warn("Monitor not found: {}", monitorName);
return Workspace{
@@ -91,7 +93,7 @@ auto WindowCount::getActiveWorkspace(const std::string& monitorName) -> Workspac
const auto workspaces = m_ipc.getSocket1JsonReply("workspaces");
if (workspaces.isArray()) {
auto workspace = std::ranges::find_if(
workspaces, [&](Json::Value workspace) { return workspace["id"] == id; });
workspaces, [&](const Json::Value& workspace) { return workspace["id"] == id; });
if (workspace == std::end(workspaces)) {
spdlog::warn("No workspace with id {}", id);
return Workspace{
@@ -116,8 +118,6 @@ auto WindowCount::Workspace::parse(const Json::Value& value) -> WindowCount::Wor
}
void WindowCount::queryActiveWorkspace() {
std::lock_guard<std::mutex> lg(mutex_);
if (separateOutputs_) {
workspace_ = getActiveWorkspace(this->bar_.output->name);
} else {
@@ -125,10 +125,7 @@ void WindowCount::queryActiveWorkspace() {
}
}
void WindowCount::onEvent(const std::string& ev) {
queryActiveWorkspace();
dp.emit();
}
void WindowCount::onEvent(const std::string& ev) { dp.emit(); }
void WindowCount::setClass(const std::string& classname, bool enable) {
if (enable) {
+10 -9
View File
@@ -11,7 +11,7 @@
namespace waybar::modules::hyprland {
WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data)
WindowCreationPayload::WindowCreationPayload(Json::Value const& client_data)
: m_window(std::make_pair(client_data["class"].asString(), client_data["title"].asString())),
m_windowAddress(client_data["address"].asString()),
m_workspaceName(client_data["workspace"]["name"].asString()) {
@@ -19,7 +19,7 @@ WindowCreationPayload::WindowCreationPayload(Json::Value const &client_data)
clearWorkspaceName();
}
WindowCreationPayload::WindowCreationPayload(std::string workspace_name,
WindowCreationPayload::WindowCreationPayload(const std::string& workspace_name,
WindowAddress window_address, WindowRepr window_repr)
: m_window(std::move(window_repr)),
m_windowAddress(std::move(window_address)),
@@ -28,9 +28,10 @@ WindowCreationPayload::WindowCreationPayload(std::string workspace_name,
clearWorkspaceName();
}
WindowCreationPayload::WindowCreationPayload(std::string workspace_name,
WindowAddress window_address, std::string window_class,
std::string window_title, bool is_active)
WindowCreationPayload::WindowCreationPayload(const std::string& workspace_name,
WindowAddress window_address,
const std::string& window_class,
const std::string& window_title, bool is_active)
: m_window(std::make_pair(std::move(window_class), std::move(window_title))),
m_windowAddress(std::move(window_address)),
m_workspaceName(std::move(workspace_name)),
@@ -73,7 +74,7 @@ void WindowCreationPayload::clearWorkspaceName() {
}
}
bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) {
bool WindowCreationPayload::isEmpty(Workspaces& workspace_manager) {
if (std::holds_alternative<Repr>(m_window)) {
return std::get<Repr>(m_window).empty();
}
@@ -89,16 +90,16 @@ bool WindowCreationPayload::isEmpty(Workspaces &workspace_manager) {
int WindowCreationPayload::incrementTimeSpentUncreated() { return m_timeSpentUncreated++; }
void WindowCreationPayload::moveToWorkspace(std::string &new_workspace_name) {
void WindowCreationPayload::moveToWorkspace(std::string& new_workspace_name) {
m_workspaceName = new_workspace_name;
}
WindowRepr WindowCreationPayload::repr(Workspaces &workspace_manager) {
WindowRepr WindowCreationPayload::repr(Workspaces& workspace_manager) {
if (std::holds_alternative<Repr>(m_window)) {
return std::get<Repr>(m_window);
}
if (std::holds_alternative<ClassAndTitle>(m_window)) {
auto const &[window_class, window_title] = std::get<ClassAndTitle>(m_window);
auto const& [window_class, window_title] = std::get<ClassAndTitle>(m_window);
return {m_windowAddress, window_class, window_title,
workspace_manager.getRewrite(window_class, window_title), m_isActive};
}
+291 -51
View File
@@ -1,6 +1,9 @@
#include <glibmm/main.h>
#include <json/value.h>
#include <spdlog/spdlog.h>
#include <algorithm>
#include <cctype>
#include <memory>
#include <set>
#include <string>
@@ -10,10 +13,37 @@
#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,
const Json::Value &clients_data)
Workspace::Workspace(const Json::Value& workspace_data, Workspaces& workspace_manager,
const Json::Value& clients_data)
: m_workspaceManager(workspace_manager),
m_id(workspace_data["id"].asInt()),
m_name(workspace_data["name"].asString()),
@@ -31,6 +61,11 @@ Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_ma
}
m_button.add_events(Gdk::BUTTON_PRESS_MASK);
m_button.add_events(Gdk::ENTER_NOTIFY_MASK | Gdk::LEAVE_NOTIFY_MASK);
m_button.signal_enter_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleEnter));
m_button.signal_leave_notify_event().connect(sigc::mem_fun(*this, &Workspace::handleLeave));
m_button.signal_button_press_event().connect(sigc::mem_fun(*this, &Workspace::handleClicked),
false);
@@ -46,8 +81,14 @@ Workspace::Workspace(const Json::Value &workspace_data, Workspaces &workspace_ma
initializeWindowMap(clients_data);
}
void addOrRemoveClass(const Glib::RefPtr<Gtk::StyleContext> &context, bool condition,
const std::string &class_name) {
Workspace::~Workspace() {
// Disconnect the hover-check timeout so it can't fire on this destroyed
// instance (Workspaces are removed at runtime while a check may be armed).
stopHoverCheck();
}
void addOrRemoveClass(const Glib::RefPtr<Gtk::StyleContext>& context, bool condition,
const std::string& class_name) {
if (condition) {
context->add_class(class_name);
} else {
@@ -55,9 +96,9 @@ void addOrRemoveClass(const Glib::RefPtr<Gtk::StyleContext> &context, bool condi
}
}
std::optional<WindowRepr> Workspace::closeWindow(WindowAddress const &addr) {
std::optional<WindowRepr> Workspace::closeWindow(WindowAddress const& addr) {
auto it = std::ranges::find_if(m_windowMap,
[&addr](const auto &window) { return window.address == addr; });
[&addr](const auto& window) { return window.address == addr; });
// If the vector contains the address, remove it and return the window representation
if (it != m_windowMap.end()) {
WindowRepr windowRepr = *it;
@@ -67,47 +108,141 @@ std::optional<WindowRepr> Workspace::closeWindow(WindowAddress const &addr) {
return std::nullopt;
}
bool Workspace::handleClicked(GdkEventButton *bt) const {
bool Workspace::pointerInsideButton() {
auto display = Gdk::Display::get_default();
if (!display) {
return false;
}
auto seat = display->get_default_seat();
if (!seat) {
return false;
}
auto pointer = seat->get_pointer();
if (!pointer) {
return false;
}
Glib::RefPtr<Gdk::Screen> screen;
int pointerRootX = 0;
int pointerRootY = 0;
pointer->get_position(screen, pointerRootX, pointerRootY);
Gtk::Widget* toplevel = m_button.get_toplevel();
if (toplevel == nullptr || !toplevel->get_window()) {
return false;
}
int buttonX = 0;
int buttonY = 0;
if (!m_button.translate_coordinates(*toplevel, 0, 0, buttonX, buttonY)) {
return false;
}
int windowRootX = 0;
int windowRootY = 0;
toplevel->get_window()->get_root_origin(windowRootX, windowRootY);
const auto allocation = m_button.get_allocation();
const int buttonRootX = windowRootX + buttonX;
const int buttonRootY = windowRootY + buttonY;
const int buttonWidth = allocation.get_width();
const int buttonHeight = allocation.get_height();
return pointerRootX >= buttonRootX && pointerRootY >= buttonRootY &&
pointerRootX < buttonRootX + buttonWidth && pointerRootY < buttonRootY + buttonHeight;
}
bool Workspace::syncHoverClass() {
auto styleContext = m_button.get_style_context();
if (pointerInsideButton()) {
styleContext->add_class("workspace-hover");
return true;
}
styleContext->remove_class("workspace-hover");
stopHoverCheck();
return false;
}
void Workspace::startHoverCheck() {
if (m_hoverCheckConnection.connected()) {
return;
}
m_hoverCheckConnection =
Glib::signal_timeout().connect(sigc::mem_fun(*this, &Workspace::syncHoverClass), 50);
}
void Workspace::stopHoverCheck() {
if (m_hoverCheckConnection.connected()) {
m_hoverCheckConnection.disconnect();
}
}
bool Workspace::handleEnter(GdkEventCrossing* /*event*/) {
m_button.get_style_context()->add_class("workspace-hover");
startHoverCheck();
return false;
}
bool Workspace::handleLeave(GdkEventCrossing* /*event*/) {
/*
* Do not remove immediately.
* Workspace taskbar children can fire misleading leave events while the
* pointer is still visually inside the workspace button.
*
* The polling check will remove the class once the pointer really leaves.
*/
startHoverCheck();
return false;
}
bool Workspace::handleClicked(GdkEventButton* bt) const {
if (bt->type == GDK_BUTTON_PRESS) {
try {
if (id() > 0) { // normal
if (m_workspaceManager.moveToMonitor()) {
m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor " + std::to_string(id()));
IPC::dispatch("focusworkspaceoncurrentmonitor", std::to_string(id()));
} else {
m_ipc.getSocket1Reply("dispatch workspace " + std::to_string(id()));
IPC::dispatch("workspace", std::to_string(id()));
}
} else if (!isSpecial()) { // named (this includes persistent)
if (m_workspaceManager.moveToMonitor()) {
m_ipc.getSocket1Reply("dispatch focusworkspaceoncurrentmonitor name:" + name());
IPC::dispatch("focusworkspaceoncurrentmonitor", "name:" + name());
} else {
m_ipc.getSocket1Reply("dispatch workspace name:" + name());
IPC::dispatch("workspace", "name:" + name());
}
} else if (id() != -99) { // named special
m_ipc.getSocket1Reply("dispatch togglespecialworkspace " + name());
IPC::dispatch("togglespecialworkspace", name());
} else { // special
m_ipc.getSocket1Reply("dispatch togglespecialworkspace");
IPC::dispatch("togglespecialworkspace", "");
}
return true;
} catch (const std::exception &e) {
} catch (const std::exception& e) {
spdlog::error("Failed to dispatch workspace: {}", e.what());
}
}
return false;
}
void Workspace::initializeWindowMap(const Json::Value &clients_data) {
void Workspace::initializeWindowMap(const Json::Value& clients_data) {
m_windowMap.clear();
for (auto client : clients_data) {
for (const auto& client : clients_data) {
if (client["workspace"]["id"].asInt() == id()) {
insertWindow({client});
}
}
}
void Workspace::setActiveWindow(WindowAddress const &addr) {
void Workspace::setActiveWindow(WindowAddress const& addr) {
std::optional<long> activeIdx;
for (size_t i = 0; i < m_windowMap.size(); ++i) {
auto &window = m_windowMap[i];
auto& window = m_windowMap[i];
bool isActive = (window.address == addr);
window.setActive(isActive);
if (isActive) {
@@ -116,7 +251,10 @@ void Workspace::setActiveWindow(WindowAddress const &addr) {
}
auto activeWindowPos = m_workspaceManager.activeWindowPosition();
if (activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE) {
const bool has_active_window =
activeIdx.has_value() && activeWindowPos != Workspaces::ActiveWindowPosition::NONE;
if (has_active_window) {
auto window = std::move(m_windowMap[*activeIdx]);
m_windowMap.erase(m_windowMap.begin() + *activeIdx);
if (activeWindowPos == Workspaces::ActiveWindowPosition::FIRST) {
@@ -131,10 +269,12 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
if (!create_window_payload.isEmpty(m_workspaceManager)) {
auto repr = create_window_payload.repr(m_workspaceManager);
if (!repr.empty() || m_workspaceManager.enableTaskbar()) {
const bool should_display = !repr.empty() || m_workspaceManager.enableTaskbar();
if (should_display) {
auto addr = create_window_payload.getAddress();
auto it = std::ranges::find_if(
m_windowMap, [&addr](const auto &window) { return window.address == addr; });
m_windowMap, [&addr](const auto& window) { return window.address == addr; });
// If the vector contains the address, update the window representation, otherwise insert it
if (it != m_windowMap.end()) {
*it = repr;
@@ -143,9 +283,9 @@ void Workspace::insertWindow(WindowCreationPayload create_window_payload) {
}
}
}
};
}
bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_payload) {
bool Workspace::onWindowOpened(WindowCreationPayload const& create_window_payload) {
if (create_window_payload.getWorkspaceName() == name()) {
insertWindow(create_window_payload);
return true;
@@ -153,16 +293,37 @@ bool Workspace::onWindowOpened(WindowCreationPayload const &create_window_payloa
return false;
}
std::string &Workspace::selectIcon(std::map<std::string, std::string> &icons_map) {
std::string& Workspace::selectIcon(std::map<std::string, std::string>& icons_map) {
spdlog::trace("Selecting icon for workspace {}", name());
if (isUrgent()) {
auto urgentNamedIconIt = icons_map.find("urgent:" + name());
if (urgentNamedIconIt != icons_map.end()) {
return urgentNamedIconIt->second;
}
auto urgentIconIt = icons_map.find("urgent");
if (urgentIconIt != icons_map.end()) {
return urgentIconIt->second;
}
}
if (isActive() && isSpecial()) {
auto activeIconIt = icons_map.find("active:" + name());
if (activeIconIt != icons_map.end()) {
return activeIconIt->second;
}
auto namedIconIt = icons_map.find(name());
if (namedIconIt != icons_map.end()) {
return namedIconIt->second;
}
}
if (isActive()) {
auto activeNamedIconIt = icons_map.find("active:" + name());
if (activeNamedIconIt != icons_map.end()) {
return activeNamedIconIt->second;
}
auto activeIconIt = icons_map.find("active");
if (activeIconIt != icons_map.end()) {
return activeIconIt->second;
@@ -170,6 +331,11 @@ std::string &Workspace::selectIcon(std::map<std::string, std::string> &icons_map
}
if (isSpecial()) {
auto specialNamedIconIt = icons_map.find("special:" + name());
if (specialNamedIconIt != icons_map.end()) {
return specialNamedIconIt->second;
}
auto specialIconIt = icons_map.find("special");
if (specialIconIt != icons_map.end()) {
return specialIconIt->second;
@@ -210,12 +376,21 @@ std::string &Workspace::selectIcon(std::map<std::string, std::string> &icons_map
return m_name;
}
void Workspace::update(const std::string &workspace_icon) {
void Workspace::update(const std::string& workspace_icon) {
if (this->m_workspaceManager.persistentOnly() && !this->isPersistent()) {
m_button.hide();
return;
}
// clang-format off
if (this->m_workspaceManager.hideActive() && \
this->isActive() && \
!this->isPersistent() && \
!this->isSpecial()) {
// clang-format on
m_button.hide();
return;
}
// clang-format off
if (this->m_workspaceManager.activeOnly() && \
!this->isActive() && \
!this->isPersistent() && \
@@ -235,26 +410,79 @@ void Workspace::update(const std::string &workspace_icon) {
auto styleContext = m_button.get_style_context();
addOrRemoveClass(styleContext, isActive(), "active");
addOrRemoveClass(styleContext, isSpecial(), "special");
addOrRemoveClass(styleContext, isSpecial(), name());
addOrRemoveClass(styleContext, isEmpty(), "empty");
addOrRemoveClass(styleContext, isPersistent(), "persistent");
addOrRemoveClass(styleContext, isUrgent(), "urgent");
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
if (!m_workspaceManager.enableTaskbar()) {
auto windowSeparator = m_workspaceManager.getWindowSeparator();
auto groupThreshold = m_workspaceManager.windowRewriteGroupThreshold();
bool isNotFirst = false;
auto end_it = m_workspaceManager.maxWindows() == 0
? m_windowMap.end()
: m_windowMap.begin() + m_workspaceManager.maxWindows();
for (const auto &window_repr : m_windowMap) {
if (isNotFirst) {
windows.append(windowSeparator);
if (groupThreshold > 0) {
// Build ordered counts of each unique icon (including singular ones when threshold set to 1)
std::vector<std::pair<std::string, int>> iconCounts;
for (auto it = m_windowMap.begin(); it != end_it; ++it) {
const auto& window_repr = *it;
auto found = std::ranges::find_if(
iconCounts, [&](const auto& p) { return p.first == window_repr.repr_rewrite; });
if (found != iconCounts.end()) {
found->second++;
} else {
iconCounts.emplace_back(window_repr.repr_rewrite, 1);
}
}
// Format the group string
auto groupFormat = m_workspaceManager.getWindowRewriteGroupFormat();
bool isNotFirst = false;
for (const auto& [icon, count] : iconCounts) {
if (count >= groupThreshold) {
if (isNotFirst) windows.append(windowSeparator);
isNotFirst = true;
try {
windows.append(fmt::format(fmt::runtime(groupFormat), fmt::arg("icon", icon),
fmt::arg("count", count)));
} catch (const fmt::format_error& e) {
spdlog::warn("Formatting window-rewrite-group-format error: {}", e.what());
windows.append(icon);
}
} else {
for (int i = 0; i < count; ++i) {
if (isNotFirst) windows.append(windowSeparator);
isNotFirst = true;
windows.append(icon);
}
}
}
} else {
// Not grouping icons
bool isNotFirst = false;
for (auto it = m_windowMap.begin(); it != end_it; ++it) {
if (isNotFirst) windows.append(windowSeparator);
isNotFirst = true;
windows.append(it->repr_rewrite);
}
isNotFirst = true;
windows.append(window_repr.repr_rewrite);
}
}
@@ -271,16 +499,18 @@ void Workspace::update(const std::string &workspace_icon) {
bool Workspace::isEmpty() const {
auto ignore_list = m_workspaceManager.getIgnoredWindows();
if (ignore_list.empty()) {
const bool no_ignore_rules = ignore_list.empty();
if (no_ignore_rules) {
return m_windows == 0;
}
// If there are windows but they are all ignored, consider the workspace empty
return std::all_of(
m_windowMap.begin(), m_windowMap.end(),
[this, &ignore_list](const auto &window_repr) { return shouldSkipWindow(window_repr); });
[this, &ignore_list](const auto& window_repr) { return shouldSkipWindow(window_repr); });
}
void Workspace::updateTaskbar(const std::string &workspace_icon) {
void Workspace::updateTaskbar(const std::string& workspace_icon) {
for (auto child : m_content.get_children()) {
if (child != &m_labelBefore) {
m_content.remove(*child);
@@ -289,10 +519,10 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) {
// Build a list of windows to display, removing duplicates by window_class
// and respecting max-icons limit
std::vector<const WindowRepr *> windowsToShow;
std::vector<const WindowRepr*> windowsToShow;
std::set<std::string> seenClasses;
auto addWindowIfUnique = [&](const WindowRepr &window_repr) {
auto addWindowIfUnique = [&](const WindowRepr& window_repr) {
if (shouldSkipWindow(window_repr)) {
return;
}
@@ -309,7 +539,7 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) {
addWindowIfUnique(*it);
}
} else {
for (const auto &window_repr : m_windowMap) {
for (const auto& window_repr : m_windowMap) {
addWindowIfUnique(window_repr);
}
}
@@ -320,8 +550,14 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) {
windowsToShow.resize(maxIcons);
}
// Apply max-windows limit if configured
int maxWindows = m_workspaceManager.maxWindows();
if (maxWindows > 0 && static_cast<int>(windowsToShow.size()) > maxWindows) {
windowsToShow.resize(maxWindows);
}
bool isFirst = true;
for (const auto *window_repr : windowsToShow) {
for (const auto* window_repr : windowsToShow) {
if (isFirst) {
isFirst = false;
} else if (m_workspaceManager.getWindowSeparator() != "") {
@@ -331,16 +567,18 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) {
}
auto window_box = Gtk::make_managed<Gtk::Box>(Gtk::ORIENTATION_HORIZONTAL);
window_box->set_tooltip_text(window_repr->window_title);
window_box->get_style_context()->add_class("taskbar-window");
window_box->set_tooltip_markup(window_repr->window_title);
auto button = Gtk::manage(new Gtk::Button());
button->set_relief(Gtk::RELIEF_NONE);
button->add(*window_box);
button->get_style_context()->add_class("taskbar-window");
if (window_repr->isActive) {
window_box->get_style_context()->add_class("active");
button->get_style_context()->add_class("active");
}
auto event_box = Gtk::manage(new Gtk::EventBox());
event_box->add(*window_box);
if (m_workspaceManager.onClickWindow() != "") {
event_box->signal_button_press_event().connect(
sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr->address));
button->signal_button_press_event().connect(
sigc::bind(sigc::mem_fun(*this, &Workspace::handleClick), window_repr->address), false);
}
auto text_before = fmt::format(fmt::runtime(m_workspaceManager.taskbarFormatBefore()),
@@ -365,12 +603,14 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) {
window_box->pack_start(*window_label_after, true, true);
}
m_content.pack_start(*event_box, true, false);
event_box->show_all();
m_content.pack_start(*button, true, false);
button->show_all();
}
auto formatAfter = m_workspaceManager.formatAfter();
if (!formatAfter.empty()) {
const bool has_format_after = !formatAfter.empty();
if (has_format_after) {
m_labelAfter.set_markup(fmt::format(fmt::runtime(formatAfter), fmt::arg("id", id()),
fmt::arg("name", name()),
fmt::arg("icon", workspace_icon)));
@@ -379,7 +619,7 @@ void Workspace::updateTaskbar(const std::string &workspace_icon) {
}
}
bool Workspace::handleClick(const GdkEventButton *event_button, WindowAddress const &addr) const {
bool Workspace::handleClick(const GdkEventButton* event_button, WindowAddress const& addr) const {
if (event_button->type == GDK_BUTTON_PRESS) {
std::string command = std::regex_replace(m_workspaceManager.onClickWindow(),
std::regex("\\{address\\}"), "0x" + addr);
@@ -393,9 +633,9 @@ bool Workspace::handleClick(const GdkEventButton *event_button, WindowAddress co
return true;
}
bool Workspace::shouldSkipWindow(const WindowRepr &window_repr) const {
bool Workspace::shouldSkipWindow(const WindowRepr& window_repr) const {
auto ignore_list = m_workspaceManager.getIgnoredWindows();
auto it = std::ranges::find_if(ignore_list, [&window_repr](const auto &ignoreItem) {
auto it = std::ranges::find_if(ignore_list, [&window_repr](const auto& ignoreItem) {
return std::regex_match(window_repr.window_class, ignoreItem) ||
std::regex_match(window_repr.window_title, ignoreItem);
});
+214 -118
View File
@@ -14,7 +14,7 @@
namespace waybar::modules::hyprland {
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)
: AModule(config, "workspaces", id, false, false),
m_bar(bar),
m_box(bar.orientation, 0),
@@ -34,6 +34,9 @@ Workspaces::Workspaces(const std::string &id, const Bar &bar, const Json::Value
}
Workspaces::~Workspaces() {
if (m_scrollEventConnection_.connected()) {
m_scrollEventConnection_.disconnect();
}
m_ipc.unregisterForIPC(this);
// wait for possible event handler to finish
std::lock_guard<std::mutex> lg(m_mutex);
@@ -43,11 +46,23 @@ void Workspaces::init() {
m_activeWorkspaceId = m_ipc.getSocket1JsonReply("activeworkspace")["id"].asInt();
initializeWorkspaces();
if (m_scrollEventConnection_.connected()) {
m_scrollEventConnection_.disconnect();
}
bool hasScrollConfig = config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString();
if (barScroll() || hasScrollConfig) {
auto& window = const_cast<Bar&>(m_bar).window;
window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
m_scrollEventConnection_ =
window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll));
}
dp.emit();
}
Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name,
std::string const &monitor) {
Json::Value Workspaces::createMonitorWorkspaceData(std::string const& name,
std::string const& monitor) {
spdlog::trace("Creating persistent workspace: {} on monitor {}", name, monitor);
Json::Value workspaceData;
@@ -62,14 +77,14 @@ Json::Value Workspaces::createMonitorWorkspaceData(std::string const &name,
return workspaceData;
}
void Workspaces::createWorkspace(Json::Value const &workspace_data,
Json::Value const &clients_data) {
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) {
auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr<Workspace> const& w) {
if (workspaceId > 0) {
return w->id() == workspaceId;
}
@@ -81,7 +96,7 @@ void Workspaces::createWorkspace(Json::Value const &workspace_data,
// don't recreate workspace, but update persistency if necessary
const auto keys = workspace_data.getMemberNames();
const auto *k = "persistent-rule";
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");
@@ -100,14 +115,14 @@ void Workspaces::createWorkspace(Json::Value const &workspace_data,
// create new workspace
m_workspaces.emplace_back(std::make_unique<Workspace>(workspace_data, *this, clients_data));
Gtk::Button &newWorkspaceButton = m_workspaces.back()->button();
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) {
for (const auto& [workspaceData, clientsData] : m_workspacesToCreate) {
createWorkspace(workspaceData, clientsData);
}
if (!m_workspacesToCreate.empty()) {
@@ -139,16 +154,17 @@ void Workspaces::doUpdate() {
}
}
void Workspaces::extendOrphans(int workspaceId, Json::Value const &clientsJson) {
void Workspaces::extendOrphans(int workspaceId, Json::Value const& clientsJson) {
spdlog::trace("Extending orphans with workspace {}", workspaceId);
for (const auto &client : clientsJson) {
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 Workspaces::getRewrite(const std::string& window_class,
const std::string& window_title) {
std::string windowReprKey;
if (windowRewriteConfigUsesTitle()) {
windowReprKey = fmt::format("class<{}> title<{}>", window_class, window_title);
@@ -163,7 +179,7 @@ std::string Workspaces::getRewrite(std::string window_class, std::string window_
std::vector<int> Workspaces::getVisibleWorkspaces() {
std::vector<int> visibleWorkspaces;
auto monitors = IPC::inst().getSocket1JsonReply("monitors");
for (const auto &monitor : monitors) {
for (const auto& monitor : monitors) {
auto ws = monitor["activeWorkspace"];
if (ws.isObject() && ws["id"].isInt()) {
visibleWorkspaces.push_back(ws["id"].asInt());
@@ -181,7 +197,7 @@ 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) {
for (auto& workspace : m_workspaces) {
m_workspacesToRemove.push_back(std::to_string(workspace->id()));
}
@@ -189,7 +205,7 @@ void Workspaces::initializeWorkspaces() {
auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces");
auto const clientsJson = m_ipc.getSocket1JsonReply("clients");
for (Json::Value workspaceJson : workspacesJson) {
for (const auto& workspaceJson : workspacesJson) {
std::string workspaceName = workspaceJson["name"].asString();
if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) &&
(!workspaceName.starts_with("special") || showSpecial()) &&
@@ -209,7 +225,7 @@ void Workspaces::initializeWorkspaces() {
loadPersistentWorkspacesFromWorkspaceRules(clientsJson);
}
bool isDoubleSpecial(std::string const &workspace_name) {
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.
@@ -217,8 +233,8 @@ bool isDoubleSpecial(std::string const &workspace_name) {
return workspace_name.find("special:special:") != std::string::npos;
}
bool Workspaces::isWorkspaceIgnored(std::string const &name) {
for (auto &rule : m_ignoreWorkspaces) {
bool Workspaces::isWorkspaceIgnored(std::string const& name) {
for (auto& rule : m_ignoreWorkspaces) {
if (std::regex_match(name, rule)) {
return true;
break;
@@ -228,19 +244,19 @@ bool Workspaces::isWorkspaceIgnored(std::string const &name) {
return false;
}
void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJson) {
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) {
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];
const Json::Value& value = m_persistentWorkspaceConfig[key];
spdlog::trace("Parsing persistent workspace config: {} => {}", key, value.toStyledString());
if (value.isInt()) {
@@ -255,15 +271,15 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJs
} else if (value.isArray() && !value.empty()) {
// value is an array => create defined workspaces for this monitor
if (canCreate) {
for (const Json::Value &workspace : value) {
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) {
for (const Json::Value& monitor : value) {
if (monitor.isString() && monitor.asString() == currentMonitor) {
persistentWorkspacesToCreate.emplace_back(currentMonitor);
persistentWorkspacesToCreate.emplace_back(key);
break;
}
}
@@ -274,18 +290,18 @@ void Workspaces::loadPersistentWorkspacesFromConfig(Json::Value const &clientsJs
}
}
for (auto const &workspace : persistentWorkspacesToCreate) {
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) {
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) {
for (Json::Value const& rule : workspaceRules) {
if (!rule["workspaceString"].isString()) {
spdlog::warn("Workspace rules: invalid workspaceString, skipping: {}", rule);
continue;
@@ -296,16 +312,25 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &c
auto workspace = rule.isMember("defaultName") ? rule["defaultName"].asString()
: rule["workspaceString"].asString();
// There could be persistent special workspaces, only show those when show-special is enabled.
if (workspace.starts_with("special:") && !showSpecial()) {
continue;
}
// 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();
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()) {
// => skip ignore-workspaces even if its a persistent
if(isWorkspaceIgnored(workspace)) {
continue;
}
// => persistent workspace should be shown on this monitor
auto workspaceData = createMonitorWorkspaceData(workspace, m_bar.output->name);
workspaceData["persistent-rule"] = true;
@@ -317,10 +342,15 @@ void Workspaces::loadPersistentWorkspacesFromWorkspaceRules(const Json::Value &c
}
}
void Workspaces::onEvent(const std::string &ev) {
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);
const auto separator = ev.find(">>");
if (separator == std::string::npos) {
spdlog::warn("Malformed Hyprland workspace event: {}", ev);
return;
}
std::string eventName = ev.substr(0, separator);
std::string payload = ev.substr(separator + 2);
if (eventName == "workspacev2") {
onWorkspaceActivated(payload);
@@ -355,7 +385,7 @@ void Workspaces::onEvent(const std::string &ev) {
dp.emit();
}
void Workspaces::onWorkspaceActivated(std::string const &payload) {
void Workspaces::onWorkspaceActivated(std::string const& payload) {
const auto [workspaceIdStr, workspaceName] = splitDoublePayload(payload);
const auto workspaceId = parseWorkspaceId(workspaceIdStr);
if (workspaceId.has_value()) {
@@ -363,19 +393,19 @@ void Workspaces::onWorkspaceActivated(std::string const &payload) {
}
}
void Workspaces::onSpecialWorkspaceActivated(std::string const &payload) {
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) {
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) {
void Workspaces::onWorkspaceCreated(std::string const& payload, Json::Value const& clientsData) {
spdlog::debug("Workspace created: {}", payload);
const auto [workspaceIdStr, _] = splitDoublePayload(payload);
@@ -388,7 +418,7 @@ void Workspaces::onWorkspaceCreated(std::string const &payload, Json::Value cons
auto const workspaceRules = m_ipc.getSocket1JsonReply("workspacerules");
auto const workspacesJson = m_ipc.getSocket1JsonReply("workspaces");
for (Json::Value workspaceJson : workspacesJson) {
for (auto workspaceJson : workspacesJson) {
const auto currentId = workspaceJson["id"].asInt();
if (currentId == *workspaceId) {
std::string workspaceName = workspaceJson["name"].asString();
@@ -402,7 +432,7 @@ void Workspaces::onWorkspaceCreated(std::string const &payload, Json::Value cons
if ((allOutputs() || m_bar.output->name == workspaceJson["monitor"].asString()) &&
(showSpecial() || !workspaceName.starts_with("special")) &&
!isDoubleSpecial(workspaceName)) {
for (Json::Value const &rule : workspaceRules) {
for (Json::Value const& rule : workspaceRules) {
auto ruleWorkspaceName = rule.isMember("defaultName")
? rule["defaultName"].asString()
: rule["workspaceString"].asString();
@@ -421,7 +451,7 @@ void Workspaces::onWorkspaceCreated(std::string const &payload, Json::Value cons
}
}
void Workspaces::onWorkspaceMoved(std::string const &payload) {
void Workspaces::onWorkspaceMoved(std::string const& payload) {
spdlog::debug("Workspace moved: {}", payload);
// Update active workspace
@@ -442,7 +472,7 @@ void Workspaces::onWorkspaceMoved(std::string const &payload) {
}
}
void Workspaces::onWorkspaceRenamed(std::string const &payload) {
void Workspaces::onWorkspaceRenamed(std::string const& payload) {
spdlog::debug("Workspace renamed: {}", payload);
const auto [workspaceIdStr, newName] = splitDoublePayload(payload);
@@ -451,7 +481,7 @@ void Workspaces::onWorkspaceRenamed(std::string const &payload) {
return;
}
for (auto &workspace : m_workspaces) {
for (auto& workspace : m_workspaces) {
if (workspace->id() == *workspaceId) {
workspace->setName(newName);
break;
@@ -460,7 +490,7 @@ void Workspaces::onWorkspaceRenamed(std::string const &payload) {
sortWorkspaces();
}
void Workspaces::onMonitorFocused(std::string const &payload) {
void Workspaces::onMonitorFocused(std::string const& payload) {
spdlog::trace("Monitor focused: {}", payload);
const auto [monitorName, workspaceIdStr] = splitDoublePayload(payload);
@@ -472,7 +502,7 @@ void Workspaces::onMonitorFocused(std::string const &payload) {
m_activeWorkspaceId = *workspaceId;
for (Json::Value &monitor : m_ipc.getSocket1JsonReply("monitors")) {
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);
@@ -480,39 +510,41 @@ void Workspaces::onMonitorFocused(std::string const &payload) {
}
}
void Workspaces::onWindowOpened(std::string const &payload) {
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);
const auto firstComma = payload.find(',');
const auto secondComma =
firstComma == std::string::npos ? std::string::npos : payload.find(',', firstComma + 1);
const auto thirdComma =
secondComma == std::string::npos ? std::string::npos : payload.find(',', secondComma + 1);
if (firstComma == std::string::npos || secondComma == std::string::npos ||
thirdComma == std::string::npos) {
spdlog::warn("Malformed Hyprland openwindow payload: {}", payload);
return;
}
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);
std::string windowAddress = payload.substr(0, firstComma);
std::string workspaceName = payload.substr(firstComma + 1, secondComma - firstComma - 1);
std::string windowClass = payload.substr(secondComma + 1, thirdComma - secondComma - 1);
std::string windowTitle = payload.substr(thirdComma + 1);
bool isActive = m_currentActiveWindowAddress == windowAddress;
m_windowsToCreate.emplace_back(workspaceName, windowAddress, windowClass, windowTitle, isActive);
}
void Workspaces::onWindowClosed(std::string const &addr) {
void Workspaces::onWindowClosed(std::string const& addr) {
spdlog::trace("Window closed: {}", addr);
updateWindowCount();
m_orphanWindowMap.erase(addr);
for (auto &workspace : m_workspaces) {
for (auto& workspace : m_workspaces) {
if (workspace->closeWindow(addr)) {
break;
}
}
}
void Workspaces::onWindowMoved(std::string const &payload) {
void Workspaces::onWindowMoved(std::string const& payload) {
spdlog::trace("Window moved: {}", payload);
updateWindowCount();
auto [windowAddress, _, workspaceName] = splitTriplePayload(payload);
@@ -521,7 +553,7 @@ void Workspaces::onWindowMoved(std::string const &payload) {
// If the window was still queued to be created, just change its destination
// and exit
for (auto &window : m_windowsToCreate) {
for (auto& window : m_windowsToCreate) {
if (window.getAddress() == windowAddress) {
window.moveToWorkspace(workspaceName);
return;
@@ -529,7 +561,7 @@ void Workspaces::onWindowMoved(std::string const &payload) {
}
// Take the window's representation from the old workspace...
for (auto &workspace : m_workspaces) {
for (auto& workspace : m_workspaces) {
if (auto windowAddr = workspace->closeWindow(windowAddress); windowAddr != std::nullopt) {
windowRepr = windowAddr.value();
break;
@@ -548,7 +580,7 @@ void Workspaces::onWindowMoved(std::string const &payload) {
}
}
void Workspaces::onWindowTitleEvent(std::string const &payload) {
void Workspaces::onWindowTitleEvent(std::string const& payload) {
spdlog::trace("Window title changed: {}", payload);
std::optional<std::function<void(WindowCreationPayload)>> inserter;
@@ -558,7 +590,7 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) {
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) {
auto windowWorkspace = std::ranges::find_if(m_workspaces, [windowAddress](auto& workspace) {
return workspace->containsWindow(windowAddress);
});
@@ -570,7 +602,7 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) {
};
} else {
auto queuedWindow =
std::ranges::find_if(m_windowsToCreate, [&windowAddress](auto &windowPayload) {
std::ranges::find_if(m_windowsToCreate, [&windowAddress](auto& windowPayload) {
return windowPayload.getAddress() == windowAddress;
});
@@ -585,7 +617,7 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) {
Json::Value clientsData = m_ipc.getSocket1JsonReply("clients");
std::string jsonWindowAddress = fmt::format("0x{}", windowAddress);
auto client = std::ranges::find_if(clientsData, [jsonWindowAddress](auto &client) {
auto client = std::ranges::find_if(clientsData, [jsonWindowAddress](auto& client) {
return client["address"].asString() == jsonWindowAddress;
});
@@ -595,17 +627,17 @@ void Workspaces::onWindowTitleEvent(std::string const &payload) {
}
}
void Workspaces::onActiveWindowChanged(WindowAddress const &activeWindowAddress) {
void Workspaces::onActiveWindowChanged(WindowAddress const& activeWindowAddress) {
spdlog::trace("Active window changed: {}", activeWindowAddress);
m_currentActiveWindowAddress = activeWindowAddress;
for (auto &[address, window] : m_orphanWindowMap) {
for (auto& [address, window] : m_orphanWindowMap) {
window.setActive(address == activeWindowAddress);
}
for (auto const &workspace : m_workspaces) {
for (auto const& workspace : m_workspaces) {
workspace->setActiveWindow(activeWindowAddress);
}
for (auto &window : m_windowsToCreate) {
for (auto& window : m_windowsToCreate) {
window.setActive(window.getAddress() == activeWindowAddress);
}
}
@@ -615,8 +647,8 @@ void Workspaces::onConfigReloaded() {
init();
}
auto Workspaces::parseConfig(const Json::Value &config) -> void {
const auto &configFormat = config["format"];
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;
@@ -630,13 +662,26 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void {
populateBoolConfig(config, "special-visible-only", m_specialVisibleOnly);
populateBoolConfig(config, "persistent-only", m_persistentOnly);
populateBoolConfig(config, "active-only", m_activeOnly);
populateBoolConfig(config, "hide-active", m_hideActive);
populateBoolConfig(config, "move-to-monitor", m_moveToMonitor);
populateBoolConfig(config, "enable-bar-scroll", m_barScroll);
m_persistentWorkspaceConfig = config.get("persistent-workspaces", Json::Value());
populateSortByConfig(config);
populateIgnoreWorkspacesConfig(config);
populateFormatWindowSeparatorConfig(config);
const auto& groupThreshold = config["window-rewrite-group-threshold"];
if (groupThreshold.isInt()) {
m_windowRewriteGroupThreshold = groupThreshold.asInt();
}
const auto& groupFormat = config["window-rewrite-group-format"];
if (groupFormat.isString()) {
m_windowRewriteGroupFormat = groupFormat.asString();
}
populateWindowRewriteConfig(config);
populateMaxWindowsConfig(config);
if (withWindows) {
populateWorkspaceTaskbarConfig(config);
@@ -648,28 +693,28 @@ auto Workspaces::parseConfig(const Json::Value &config) -> void {
}
}
auto Workspaces::populateIconsMap(const Json::Value &formatIcons) -> void {
for (const auto &name : formatIcons.getMemberNames()) {
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)
auto Workspaces::populateBoolConfig(const Json::Value& config, const std::string& key, bool& member)
-> void {
const auto &configValue = config[key];
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"];
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) {
} catch (const std::invalid_argument& e) {
m_sortBy = SortMethod::DEFAULT;
spdlog::warn(
"Invalid string representation for sort-by. Falling back to default sort method.");
@@ -677,16 +722,16 @@ auto Workspaces::populateSortByConfig(const Json::Value &config) -> void {
}
}
auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value &config) -> void {
auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value& config) -> void {
auto ignoreWorkspaces = config["ignore-workspaces"];
if (ignoreWorkspaces.isArray()) {
for (const auto &workspaceRegex : ignoreWorkspaces) {
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) {
} catch (const std::regex_error& e) {
spdlog::error("Invalid rule {}: {}", ruleString, e.what());
}
} else {
@@ -696,30 +741,39 @@ auto Workspaces::populateIgnoreWorkspacesConfig(const Json::Value &config) -> vo
}
}
auto Workspaces::populateFormatWindowSeparatorConfig(const Json::Value &config) -> void {
const auto &formatWindowSeparator = config["format-window-separator"];
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"];
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"];
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); });
[this](std::string& window_rule) { return windowRewritePriorityFunction(window_rule); });
}
auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value &config) -> void {
const auto &workspaceTaskbar = config["workspace-taskbar"];
auto Workspaces::populateMaxWindowsConfig(const Json::Value& config) -> void {
if (config["max-windows"].isInt()) {
m_maxWindows = config["max-windows"].asInt();
if (m_maxWindows < 0) {
m_maxWindows = 0;
}
}
}
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;
@@ -746,7 +800,7 @@ auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value &config) -> vo
auto iconTheme = workspaceTaskbar["icon-theme"];
if (iconTheme.isArray()) {
for (auto &c : iconTheme) {
for (auto& c : iconTheme) {
m_iconLoader.add_custom_icon_theme(c.asString());
}
} else if (iconTheme.isString()) {
@@ -769,11 +823,11 @@ auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value &config) -> vo
}
if (workspaceTaskbar["ignore-list"].isArray()) {
for (auto &windowRegex : workspaceTaskbar["ignore-list"]) {
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) {
} catch (const std::regex_error& e) {
spdlog::error("Invalid rule {}: {}", ruleString, e.what());
}
}
@@ -784,7 +838,7 @@ auto Workspaces::populateWorkspaceTaskbarConfig(const Json::Value &config) -> vo
try {
m_activeWindowPosition =
m_activeWindowEnumParser.parseStringToEnum(posStr, m_activeWindowPositionMap);
} catch (const std::invalid_argument &e) {
} catch (const std::invalid_argument& e) {
spdlog::warn(
"Invalid string representation for active-window-position. Falling back to 'none'.");
m_activeWindowPosition = ActiveWindowPosition::NONE;
@@ -827,13 +881,13 @@ auto Workspaces::registerIpc() -> void {
}
void Workspaces::removeWorkspacesToRemove() {
for (const auto &workspaceString : m_workspacesToRemove) {
for (const auto& workspaceString : m_workspacesToRemove) {
removeWorkspace(workspaceString);
}
m_workspacesToRemove.clear();
}
void Workspaces::removeWorkspace(std::string const &workspaceString) {
void Workspaces::removeWorkspace(std::string const& workspaceString) {
spdlog::debug("Removing workspace {}", workspaceString);
// If this succeeds, we have a workspace ID.
@@ -851,7 +905,7 @@ void Workspaces::removeWorkspace(std::string const &workspaceString) {
name = workspaceString;
}
const auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr<Workspace> &x) {
const auto workspace = std::ranges::find_if(m_workspaces, [&](std::unique_ptr<Workspace>& x) {
if (workspaceId.has_value()) {
return *workspaceId == x->id();
}
@@ -877,7 +931,7 @@ 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) {
auto currentMonitor = std::ranges::find_if(monitors, [this](const Json::Value& m) {
return m["name"].asString() == m_bar.output->name;
});
if (currentMonitor == monitors.end()) {
@@ -893,7 +947,7 @@ void Workspaces::sortSpecialCentered() {
std::vector<std::unique_ptr<Workspace>> hiddenWorkspaces;
std::vector<std::unique_ptr<Workspace>> normalWorkspaces;
for (auto &workspace : m_workspaces) {
for (auto& workspace : m_workspaces) {
if (workspace->isSpecial()) {
specialWorkspaces.push_back(std::move(workspace));
} else {
@@ -924,7 +978,7 @@ void Workspaces::sortSpecialCentered() {
void Workspaces::sortWorkspaces() {
std::ranges::sort( //
m_workspaces, [&](std::unique_ptr<Workspace> &a, std::unique_ptr<Workspace> &b) {
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();
@@ -937,7 +991,7 @@ void Workspaces::sortWorkspaces() {
case SortMethod::NUMBER:
try {
return std::stoi(a->name()) < std::stoi(b->name());
} catch (const std::invalid_argument &) {
} catch (const std::exception& e) {
// Handle the exception if necessary.
break;
}
@@ -989,18 +1043,20 @@ void Workspaces::sortWorkspaces() {
}
}
void Workspaces::setUrgentWorkspace(std::string const &windowaddress) {
void Workspaces::setUrgentWorkspace(std::string const& windowaddress) {
const Json::Value clientsJson = m_ipc.getSocket1JsonReply("clients");
const std::string normalizedAddress =
windowaddress.starts_with("0x") ? windowaddress : fmt::format("0x{}", windowaddress);
int workspaceId = -1;
for (Json::Value clientJson : clientsJson) {
if (clientJson["address"].asString().ends_with(windowaddress)) {
for (const auto& clientJson : clientsJson) {
if (clientJson["address"].asString() == normalizedAddress) {
workspaceId = clientJson["workspace"]["id"].asInt();
break;
}
}
auto workspace = std::ranges::find_if(m_workspaces, [workspaceId](std::unique_ptr<Workspace> &x) {
auto workspace = std::ranges::find_if(m_workspaces, [workspaceId](std::unique_ptr<Workspace>& x) {
return x->id() == workspaceId;
});
if (workspace != m_workspaces.end()) {
@@ -1015,8 +1071,8 @@ auto Workspaces::update() -> void {
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) {
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());
});
@@ -1024,7 +1080,7 @@ void Workspaces::updateWindowCount() {
if (workspaceJson != workspacesJson.end()) {
try {
count = (*workspaceJson)["windows"].asUInt();
} catch (const std::exception &e) {
} catch (const std::exception& e) {
spdlog::error("Failed to update window count: {}", e.what());
}
}
@@ -1035,9 +1091,9 @@ void Workspaces::updateWindowCount() {
bool Workspaces::updateWindowsToCreate() {
bool anyWindowCreated = false;
std::vector<WindowCreationPayload> notCreated;
for (auto &windowPayload : m_windowsToCreate) {
for (auto& windowPayload : m_windowsToCreate) {
bool created = false;
for (auto &workspace : m_workspaces) {
for (auto& workspace : m_workspaces) {
if (workspace->onWindowOpened(windowPayload)) {
created = true;
anyWindowCreated = true;
@@ -1066,7 +1122,7 @@ void Workspaces::updateWorkspaceStates() {
std::string currentWorkspaceName =
currentWorkspace.isMember("name") ? currentWorkspace["name"].asString() : "";
for (auto &workspace : m_workspaces) {
for (auto& workspace : m_workspaces) {
bool isActiveByName =
!currentWorkspaceName.empty() && workspace->name() == currentWorkspaceName;
@@ -1078,11 +1134,11 @@ void Workspaces::updateWorkspaceStates() {
}
workspace->setVisible(std::ranges::find(visibleWorkspaces, workspace->id()) !=
visibleWorkspaces.end());
std::string &workspaceIcon = m_iconsMap[""];
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 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();
@@ -1094,7 +1150,7 @@ void Workspaces::updateWorkspaceStates() {
}
}
int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) {
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;
@@ -1115,23 +1171,30 @@ int Workspaces::windowRewritePriorityFunction(std::string const &window_rule) {
}
template <typename... Args>
std::string Workspaces::makePayload(Args const &...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(','));
std::pair<std::string, std::string> Workspaces::splitDoublePayload(std::string const& payload) {
const auto separator = payload.find(',');
if (separator == std::string::npos) {
throw std::invalid_argument("Expected a two-part Hyprland payload");
}
const std::string part1 = payload.substr(0, separator);
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) {
std::string const& payload) {
const size_t firstComma = payload.find(',');
const size_t secondComma = payload.find(',', firstComma + 1);
if (firstComma == std::string::npos || secondComma == std::string::npos) {
throw std::invalid_argument("Expected a three-part Hyprland payload");
}
const std::string part1 = payload.substr(0, firstComma);
const std::string part2 = payload.substr(firstComma + 1, secondComma - (firstComma + 1));
@@ -1140,13 +1203,46 @@ std::tuple<std::string, std::string, std::string> Workspaces::splitTriplePayload
return {part1, part2, part3};
}
std::optional<int> Workspaces::parseWorkspaceId(std::string const &workspaceIdStr) {
std::optional<int> Workspaces::parseWorkspaceId(std::string const& workspaceIdStr) {
try {
return workspaceIdStr == "special" ? -99 : std::stoi(workspaceIdStr);
} catch (std::exception const &e) {
} catch (std::exception const& e) {
spdlog::debug("Workspace \"{}\" is not bound to an id: {}", workspaceIdStr, e.what());
return std::nullopt;
}
}
bool Workspaces::handleScroll(GdkEventScroll* e) {
// Ignore emulated scroll events on window
if (gdk_event_get_pointer_emulated((GdkEvent*)e)) {
return false;
}
// Check for custom scroll commands first; delegate to base class
if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
return AModule::handleScroll(e);
}
auto dir = AModule::getScrollDir(e);
if (dir == SCROLL_DIR::NONE) {
return true;
}
if (dir == SCROLL_DIR::DOWN || dir == SCROLL_DIR::RIGHT) {
if (allOutputs()) {
IPC::dispatch("workspace", "e+1");
} else {
IPC::dispatch("workspace", "m+1");
}
} else if (dir == SCROLL_DIR::UP || dir == SCROLL_DIR::LEFT) {
if (allOutputs()) {
IPC::dispatch("workspace", "e-1");
} else {
IPC::dispatch("workspace", "m-1");
}
}
return true;
}
} // namespace waybar::modules::hyprland