From 31adcaac7662d6c7fbbc901ba11e0d95f0c7fc56 Mon Sep 17 00:00:00 2001 From: Nydragon Date: Sat, 2 Nov 2024 03:52:27 +0100 Subject: [PATCH] i3/sway: add support for the I3 and Sway IPC sway: add urgent and focused dispatchers to workspaces flake: add sway toggle WIP sway: add monitor status sway: handle multiple ipc events in one line sway: reuse socket connection for dispatches & better command type handling WIP sway: add associated monitor to a workspace i3/sway: update to allow for i3 compatibility i3/sway: manage setting the focused monitors i3/sway: fix multi monitor crash i3/sway: fix linting errors i3/sway: update nix package flag naming to i3 i3/sway: add documentation, fix module.md and impl monitorFor i3/sway: handle more workspace ipc events i3/sway: fix review i3/sway: fix crash due to newline breaking up an IPC message i3/sway: handle broken messages by forwarding to the next magic sequence i3/sway: break loop when buffer is empty i3/sway: fix monitor focus & focused monitor signal not being emitted i3/sway: use datastreams instead of qbytearrays for socket reading i3/sway: fix lint issues i3/sway: drop second socket connection, remove dispatch return value, recreate IPC connection on fatal error i3/sway: handle run_command responses i3/sway: remove reconnection on unknown event i3/sway: fix formatting, lint & avoid writing to socket if connection is not open --- BUILD.md | 12 +- CMakeLists.txt | 2 + default.nix | 2 + src/x11/CMakeLists.txt | 4 + src/x11/i3/CMakeLists.txt | 23 ++ src/x11/i3/ipc/CMakeLists.txt | 22 ++ src/x11/i3/ipc/connection.cpp | 542 ++++++++++++++++++++++++++++++++++ src/x11/i3/ipc/connection.hpp | 151 ++++++++++ src/x11/i3/ipc/monitor.cpp | 111 +++++++ src/x11/i3/ipc/monitor.hpp | 100 +++++++ src/x11/i3/ipc/qml.cpp | 55 ++++ src/x11/i3/ipc/qml.hpp | 74 +++++ src/x11/i3/ipc/workspace.cpp | 73 +++++ src/x11/i3/ipc/workspace.hpp | 73 +++++ src/x11/i3/module.md | 9 + 15 files changed, 1252 insertions(+), 1 deletion(-) create mode 100644 src/x11/i3/CMakeLists.txt create mode 100644 src/x11/i3/ipc/CMakeLists.txt create mode 100644 src/x11/i3/ipc/connection.cpp create mode 100644 src/x11/i3/ipc/connection.hpp create mode 100644 src/x11/i3/ipc/monitor.cpp create mode 100644 src/x11/i3/ipc/monitor.hpp create mode 100644 src/x11/i3/ipc/qml.cpp create mode 100644 src/x11/i3/ipc/qml.hpp create mode 100644 src/x11/i3/ipc/workspace.cpp create mode 100644 src/x11/i3/ipc/workspace.hpp create mode 100644 src/x11/i3/module.md diff --git a/BUILD.md b/BUILD.md index 659a616..cf6b3a0 100644 --- a/BUILD.md +++ b/BUILD.md @@ -182,13 +182,23 @@ To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF` [hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml #### Hyprland Focus Grab -Enables windows to grab focus similarly to a context menu undr hyprland through the +Enables windows to grab focus similarly to a context menu under hyprland through the [hyprland-focus-grab-v1] protocol. This feature has no extra dependencies. To disable: `-DHYPRLAND_FOCUS_GRAB=OFF` [hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml +### i3/Sway +Enables i3 and Sway specific features, does not have any dependency on Wayland or x11. + +To disable: `-DI3=OFF` + +#### i3/Sway IPC +Enables interfacing with i3 and Sway's IPC. + +To disable: `-DI3_IPC=OFF` + ## Building *For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).* diff --git a/CMakeLists.txt b/CMakeLists.txt index 61cc296..5c8fb48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,8 @@ boption(HYPRLAND " Hyprland" ON REQUIRES WAYLAND) boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND) boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND) boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND) +boption(I3 " I3/Sway" ON) +boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) boption(X11 "X11" ON) boption(SERVICE_STATUS_NOTIFIER "System Tray" ON) boption(SERVICE_PIPEWIRE "PipeWire" ON) diff --git a/default.nix b/default.nix index ba4e7cb..fab038a 100644 --- a/default.nix +++ b/default.nix @@ -38,6 +38,7 @@ withPipewire ? true, withPam ? true, withHyprland ? true, + withI3 ? true, }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -81,6 +82,7 @@ (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PAM" withPam) (lib.cmakeBool "HYPRLAND" withHyprland) + (lib.cmakeBool "I3" withI3) ]; # How to get debuginfo in gdb from a release build: diff --git a/src/x11/CMakeLists.txt b/src/x11/CMakeLists.txt index b37b8fb..2a085d9 100644 --- a/src/x11/CMakeLists.txt +++ b/src/x11/CMakeLists.txt @@ -11,6 +11,10 @@ qt_add_qml_module(quickshell-x11 DEPENDENCIES QtQuick ) +if(I3) + add_subdirectory(i3) +endif() + install_qml_module(quickshell-x11) add_library(quickshell-x11-init OBJECT init.cpp) diff --git a/src/x11/i3/CMakeLists.txt b/src/x11/i3/CMakeLists.txt new file mode 100644 index 0000000..4a7b99c --- /dev/null +++ b/src/x11/i3/CMakeLists.txt @@ -0,0 +1,23 @@ +qt_add_library(quickshell-i3 STATIC) + +target_link_libraries(quickshell-i3 PRIVATE ${QT_DEPS}) + +set(I3_MODULES) + +if (I3_IPC) + add_subdirectory(ipc) + list(APPEND I3_MODULES Quickshell.I3._Ipc) +endif() + +qt_add_qml_module(quickshell-i3 + URI Quickshell.I3 + VERSION 0.1 + IMPORTS ${I3_MODULES} +) + +install_qml_module(quickshell-i3) + +qs_pch(quickshell-i3) +qs_pch(quickshell-i3plugin) + +target_link_libraries(quickshell PRIVATE quickshell-i3plugin) diff --git a/src/x11/i3/ipc/CMakeLists.txt b/src/x11/i3/ipc/CMakeLists.txt new file mode 100644 index 0000000..27a4484 --- /dev/null +++ b/src/x11/i3/ipc/CMakeLists.txt @@ -0,0 +1,22 @@ +qt_add_library(quickshell-i3-ipc STATIC + connection.cpp + qml.cpp + workspace.cpp + monitor.cpp +) + +qt_add_qml_module(quickshell-i3-ipc + URI Quickshell.I3._Ipc + VERSION 0.1 + DEPENDENCIES QtQml +) + +qs_add_module_deps_light(quickshell-i3-ipc Quickshell) + +install_qml_module(quickshell-i3-ipc) + +target_link_libraries(quickshell-i3-ipc PRIVATE Qt::Quick) + +qs_module_pch(quickshell-i3-ipc SET large) + +target_link_libraries(quickshell PRIVATE quickshell-i3-ipcplugin) diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp new file mode 100644 index 0000000..8ce7822 --- /dev/null +++ b/src/x11/i3/ipc/connection.cpp @@ -0,0 +1,542 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" +#include "workspace.hpp" + +Q_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); +Q_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); + +namespace qs::i3::ipc { + +void I3Ipc::makeRequest(const QByteArray& request) { + if (!this->valid) { + qCWarning(logI3IpcEvents) << "IPC connection is not open, ignoring request."; + return; + } + this->liveEventSocket.write(request); + this->liveEventSocket.flush(); +} + +void I3Ipc::dispatch(const QString& payload) { + auto message = I3Ipc::buildRequestMessage(EventCode::RunCommand, payload.toLocal8Bit()); + + this->makeRequest(message); +} + +QByteArray I3Ipc::buildRequestMessage(EventCode cmd, const QByteArray& payload) { + auto payloadLength = static_cast(payload.length()); + + auto type = QByteArray(std::bit_cast>(cmd).data(), 4); + auto len = QByteArray(std::bit_cast>(payloadLength).data(), 4); + + return MAGIC.data() + len + type + payload; +} + +I3Ipc::I3Ipc() { + auto sock = qEnvironmentVariable("I3SOCK"); + + if (sock.isEmpty()) { + qCWarning(logI3Ipc) << "$I3SOCK is unset. Trying $SWAYSOCK."; + + sock = qEnvironmentVariable("SWAYSOCK"); + + if (sock.isEmpty()) { + qCWarning(logI3Ipc) << "$SWAYSOCK and I3SOCK are unset. Cannot connect to socket."; + return; + } + } + + this->mSocketPath = sock; + + // clang-format off + QObject::connect(&this->liveEventSocket, &QLocalSocket::errorOccurred, this, &I3Ipc::eventSocketError); + QObject::connect(&this->liveEventSocket, &QLocalSocket::stateChanged, this, &I3Ipc::eventSocketStateChanged); + QObject::connect(&this->liveEventSocket, &QLocalSocket::readyRead, this, &I3Ipc::eventSocketReady); + QObject::connect(&this->liveEventSocket, &QLocalSocket::connected, this, &I3Ipc::subscribe); + // clang-format on + + this->liveEventSocketDs.setDevice(&this->liveEventSocket); + this->liveEventSocketDs.setByteOrder(static_cast(QSysInfo::ByteOrder)); + + this->liveEventSocket.connectToServer(this->mSocketPath); +} + +void I3Ipc::subscribe() { + auto payload = QByteArray(R"(["workspace","output"])"); + auto message = I3Ipc::buildRequestMessage(EventCode::Subscribe, payload); + + this->makeRequest(message); + + this->refreshWorkspaces(); + this->refreshMonitors(); +} + +void I3Ipc::eventSocketReady() { + for (auto& [type, data]: this->parseResponse()) { + this->event.mCode = type; + this->event.mData = data; + + this->onEvent(&this->event); + emit this->rawEvent(&this->event); + } +} + +void I3Ipc::reconnectIPC() { + qCWarning(logI3Ipc) << "Fatal IPC error occured, recreating connection"; + this->liveEventSocket.disconnectFromServer(); + this->liveEventSocket.connectToServer(this->mSocketPath); +} + +QVector I3Ipc::parseResponse() { + QVector> events; + const int magicLen = 6; + + while (!this->liveEventSocketDs.atEnd()) { + this->liveEventSocketDs.startTransaction(); + this->liveEventSocketDs.startTransaction(); + + std::array buffer = {}; + qint32 size = 0; + qint32 type = EventCode::Unknown; + + this->liveEventSocketDs.readRawData(buffer.data(), magicLen); + this->liveEventSocketDs >> size; + this->liveEventSocketDs >> type; + + if (!this->liveEventSocketDs.commitTransaction()) break; + + QByteArray payload(size, Qt::Uninitialized); + + this->liveEventSocketDs.readRawData(payload.data(), size); + + if (!this->liveEventSocketDs.commitTransaction()) break; + + if (strncmp(buffer.data(), MAGIC.data(), 6) != 0) { + qCWarning(logI3Ipc) << "No magic sequence found in string."; + this->reconnectIPC(); + break; + }; + + if (I3IpcEvent::intToEvent(type) == EventCode::Unknown) { + qCWarning(logI3Ipc) << "Received unknown event"; + break; + } + + QJsonParseError e; + + auto data = QJsonDocument::fromJson(payload, &e); + if (e.error != QJsonParseError::NoError) { + qCWarning(logI3Ipc) << "Invalid JSON value:" << e.errorString(); + break; + } else { + events.push_back(std::tuple(I3IpcEvent::intToEvent(type), data)); + } + } + + return events; +} + +void I3Ipc::eventSocketError(QLocalSocket::LocalSocketError error) const { + if (!this->valid) { + qCWarning(logI3Ipc) << "Unable to connect to I3 socket:" << error; + } else { + qCWarning(logI3Ipc) << "I3 socket error:" << error; + } +} + +void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { + if (state == QLocalSocket::ConnectedState) { + qCInfo(logI3Ipc) << "I3 event socket connected."; + emit this->connected(); + } else if (state == QLocalSocket::UnconnectedState && this->valid) { + qCWarning(logI3Ipc) << "I3 event socket disconnected."; + } + + this->valid = state == QLocalSocket::ConnectedState; +} + +QString I3Ipc::socketPath() const { return this->mSocketPath; } +I3Workspace* I3Ipc::focusedWorkspace() const { return this->mFocusedWorkspace; } +I3Monitor* I3Ipc::focusedMonitor() const { return this->mFocusedMonitor; } + +void I3Ipc::setFocusedWorkspace(I3Workspace* workspace) { + if (workspace == this->mFocusedWorkspace) return; + + if (this->mFocusedWorkspace != nullptr) { + this->mFocusedWorkspace->setFocus(false); + QObject::disconnect(this->mFocusedWorkspace, nullptr, this, nullptr); + } + + this->mFocusedWorkspace = workspace; + + if (workspace != nullptr) { + if (auto* monitor = this->mFocusedWorkspace->monitor()) { + monitor->setFocusedWorkspace(this->mFocusedWorkspace); + } + + QObject::connect(workspace, &QObject::destroyed, this, &I3Ipc::onFocusedWorkspaceDestroyed); + workspace->setFocus(true); + this->setFocusedMonitor(workspace->monitor()); + } + + emit this->focusedWorkspaceChanged(); +} + +void I3Ipc::setFocusedMonitor(I3Monitor* monitor) { + if (monitor == this->mFocusedMonitor) return; + + if (this->mFocusedMonitor != nullptr) { + this->mFocusedMonitor->setFocus(false); + QObject::disconnect(this->mFocusedMonitor, nullptr, this, nullptr); + } + + this->mFocusedMonitor = monitor; + + if (monitor != nullptr) { + monitor->setFocus(true); + QObject::connect(monitor, &QObject::destroyed, this, &I3Ipc::onFocusedMonitorDestroyed); + } + + emit this->focusedMonitorChanged(); +} + +void I3Ipc::onFocusedWorkspaceDestroyed() { + this->mFocusedWorkspace = nullptr; + emit this->focusedWorkspaceChanged(); +} + +void I3Ipc::onFocusedMonitorDestroyed() { + this->mFocusedMonitor = nullptr; + emit this->focusedMonitorChanged(); +} + +I3Ipc* I3Ipc::instance() { + static I3Ipc* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new I3Ipc(); + } + + return instance; +} + +void I3Ipc::refreshWorkspaces() { + this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetWorkspaces)); +} + +void I3Ipc::handleGetWorkspacesEvent(I3IpcEvent* event) { + auto data = event->mData; + + auto workspaces = data.array(); + + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); + + qCDebug(logI3Ipc) << "There are" << workspaces.toVariantList().length() << "workspaces"; + for (auto entry: workspaces) { + auto object = entry.toObject().toVariantMap(); + auto name = object["name"].toString(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const I3Workspace* m) { + return m->name() == name; + }); + + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + workspace = new I3Workspace(this); + } + + workspace->updateFromObject(object); + + if (workspace->focused()) { + this->setFocusedWorkspace(workspace); + } + + if (!existed) { + this->mWorkspaces.insertObject(workspace); + } + + names.push_back(name); + } + + auto removedWorkspaces = QVector(); + + for (auto* workspace: mList) { + if (!names.contains(workspace->name())) { + removedWorkspaces.push_back(workspace); + } + } + + qCDebug(logI3Ipc) << "Removing" << removedWorkspaces.length() << "deleted workspaces."; + + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } +} + +void I3Ipc::refreshMonitors() { + this->makeRequest(I3Ipc::buildRequestMessage(EventCode::GetOutputs)); +} + +void I3Ipc::handleGetOutputsEvent(I3IpcEvent* event) { + auto data = event->mData; + + auto monitors = data.array(); + const auto& mList = this->mMonitors.valueList(); + auto names = QVector(); + + qCDebug(logI3Ipc) << "There are" << monitors.toVariantList().length() << "monitors"; + + for (auto elem: monitors) { + auto object = elem.toObject().toVariantMap(); + auto name = object["name"].toString(); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const I3Monitor* m) { + return m->name() == name; + }); + + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + monitor = new I3Monitor(this); + } + + monitor->updateFromObject(object); + + if (monitor->focused()) { + this->setFocusedMonitor(monitor); + } + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + names.push_back(name); + } + + auto removedMonitors = QVector(); + + for (auto* monitor: mList) { + if (!names.contains(monitor->name())) { + removedMonitors.push_back(monitor); + } + } + + qCDebug(logI3Ipc) << "Removing" << removedMonitors.length() << "disconnected monitors."; + + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + delete monitor; + } +} + +void I3Ipc::onEvent(I3IpcEvent* event) { + switch (event->mCode) { + case EventCode::Workspace: this->handleWorkspaceEvent(event); return; + case EventCode::Output: + /// I3 only sends an "unspecified" event, so we have to query the data changes ourselves + qCInfo(logI3Ipc) << "Refreshing Monitors..."; + this->refreshMonitors(); + return; + case EventCode::Subscribe: qCInfo(logI3Ipc) << "Connected to IPC"; return; + case EventCode::GetOutputs: this->handleGetOutputsEvent(event); return; + case EventCode::GetWorkspaces: this->handleWorkspaceEvent(event); return; + case EventCode::RunCommand: I3Ipc::handleRunCommand(event); return; + case EventCode::Unknown: + qCWarning(logI3Ipc) << "Unknown event:" << event->type() << event->data(); + return; + default: qCWarning(logI3Ipc) << "Unhandled event:" << event->type(); + } +} + +void I3Ipc::handleRunCommand(I3IpcEvent* event) { + for (auto r: event->mData.array()) { + auto obj = r.toObject(); + const bool success = obj["success"].toBool(); + + if (!success) { + const QString error = obj["error"].toString(); + qCWarning(logI3Ipc) << "Error occured while running command:" << error; + } + } +} + +void I3Ipc::handleWorkspaceEvent(I3IpcEvent* event) { + // If a workspace doesn't exist, and is being switch to, no focus change event is emited, + // only the init one, which does not contain the previously focused workspace + auto change = event->mData["change"]; + + if (change == "init") { + qCInfo(logI3IpcEvents) << "New workspace has been created"; + + auto workspaceData = event->mData["current"]; + + auto* workspace = this->findWorkspaceByID(workspaceData["id"].toInt(-1)); + + if (workspace == nullptr) { + workspace = new I3Workspace(this); + } + + if (workspaceData.isObject()) { + workspace->updateFromObject(workspaceData.toObject().toVariantMap()); + } + + this->mWorkspaces.insertObject(workspace); + qCInfo(logI3Ipc) << "Added workspace" << workspace->name() << "to list"; + } else if (change == "focus") { + auto oldData = event->mData["old"]; + auto newData = event->mData["current"]; + auto oldName = oldData["name"].toString(); + auto newName = newData["name"].toString(); + + qCInfo(logI3IpcEvents) << "Focus changed: " << oldName << "->" << newName; + + if (auto* oldWorkspace = this->findWorkspaceByName(oldName)) { + oldWorkspace->updateFromObject(oldData.toObject().toVariantMap()); + } + + auto* newWorkspace = this->findWorkspaceByName(newName); + + if (newWorkspace == nullptr) { + newWorkspace = new I3Workspace(this); + } + + newWorkspace->updateFromObject(newData.toObject().toVariantMap()); + this->setFocusedWorkspace(newWorkspace); + } else if (change == "empty") { + auto name = event->mData["current"]["name"].toString(); + + auto* oldWorkspace = this->findWorkspaceByName(name); + + if (oldWorkspace != nullptr) { + qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->id() << name; + + if (this->mFocusedWorkspace == oldWorkspace) { + this->setFocusedWorkspace(nullptr); + } + + this->workspaces()->removeObject(oldWorkspace); + + delete oldWorkspace; + } else { + qCInfo(logI3Ipc) << "Workspace" << name << "has already been deleted"; + } + } else if (change == "move" || change == "rename" || change == "urgent") { + auto name = event->mData["current"]["name"].toString(); + + auto* workspace = this->findWorkspaceByName(name); + + if (workspace != nullptr) { + auto data = event->mData["current"].toObject().toVariantMap(); + + workspace->updateFromObject(data); + } else { + qCWarning(logI3Ipc) << "Workspace" << name << "doesn't exist"; + } + } else if (change == "reload") { + qCInfo(logI3Ipc) << "Refreshing Workspaces..."; + this->refreshWorkspaces(); + } +} + +I3Monitor* I3Ipc::monitorFor(QuickshellScreenInfo* screen) { + if (screen == nullptr) return nullptr; + + return this->findMonitorByName(screen->name()); +} + +I3Workspace* I3Ipc::findWorkspaceByID(qint32 id) { + auto list = this->mWorkspaces.valueList(); + auto workspaceIter = + std::find_if(list.begin(), list.end(), [id](const I3Workspace* m) { return m->id() == id; }); + + return workspaceIter == list.end() ? nullptr : *workspaceIter; +} + +I3Workspace* I3Ipc::findWorkspaceByName(const QString& name) { + auto list = this->mWorkspaces.valueList(); + auto workspaceIter = std::find_if(list.begin(), list.end(), [name](const I3Workspace* m) { + return m->name() == name; + }); + + return workspaceIter == list.end() ? nullptr : *workspaceIter; +} + +I3Monitor* I3Ipc::findMonitorByName(const QString& name) { + auto list = this->mMonitors.valueList(); + auto monitorIter = std::find_if(list.begin(), list.end(), [name](const I3Monitor* m) { + return m->name() == name; + }); + + return monitorIter == list.end() ? nullptr : *monitorIter; +} + +ObjectModel* I3Ipc::monitors() { return &this->mMonitors; } +ObjectModel* I3Ipc::workspaces() { return &this->mWorkspaces; } + +QString I3IpcEvent::type() const { return I3IpcEvent::eventToString(this->mCode); } +QString I3IpcEvent::data() const { return QString::fromUtf8(this->mData.toJson()); } + +EventCode I3IpcEvent::intToEvent(quint32 raw) { + if ((EventCode::Workspace <= raw && raw <= EventCode::Input) + || (EventCode::RunCommand <= raw && raw <= EventCode::GetTree)) + { + return static_cast(raw); + } else { + return EventCode::Unknown; + } +} + +QString I3IpcEvent::eventToString(EventCode event) { + switch (event) { + case EventCode::RunCommand: return "run_command"; break; + case EventCode::GetWorkspaces: return "get_workspaces"; break; + case EventCode::Subscribe: return "subscribe"; break; + case EventCode::GetOutputs: return "get_outputs"; break; + case EventCode::GetTree: return "get_tree"; break; + + case EventCode::Output: return "output"; break; + case EventCode::Workspace: return "workspace"; break; + case EventCode::Mode: return "mode"; break; + case EventCode::Window: return "window"; break; + case EventCode::BarconfigUpdate: return "barconfig_update"; break; + case EventCode::Binding: return "binding"; break; + case EventCode::Shutdown: return "shutdown"; break; + case EventCode::Tick: return "tick"; break; + case EventCode::BarStateUpdate: return "bar_state_update"; break; + case EventCode::Input: return "input"; break; + + case EventCode::Unknown: return "unknown"; break; + } +} + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/connection.hpp b/src/x11/i3/ipc/connection.hpp new file mode 100644 index 0000000..04cccc2 --- /dev/null +++ b/src/x11/i3/ipc/connection.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" + +namespace qs::i3::ipc { + +class I3Workspace; +class I3Monitor; +} // namespace qs::i3::ipc + +Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Workspace*); +Q_DECLARE_OPAQUE_POINTER(qs::i3::ipc::I3Monitor*); + +namespace qs::i3::ipc { + +constexpr std::string MAGIC = "i3-ipc"; + +enum EventCode { + RunCommand = 0, + GetWorkspaces = 1, + Subscribe = 2, + GetOutputs = 3, + GetTree = 4, + + Workspace = 0x80000000, + Output = 0x80000001, + Mode = 0x80000002, + Window = 0x80000003, + BarconfigUpdate = 0x80000004, + Binding = 0x80000005, + Shutdown = 0x80000006, + Tick = 0x80000007, + BarStateUpdate = 0x80000014, + Input = 0x80000015, + Unknown = 999, +}; + +using Event = std::tuple; + +///! I3/Sway IPC Events +/// Emitted by @@I3.rawEvent(s) +class I3IpcEvent: public QObject { + Q_OBJECT; + + /// The name of the event + Q_PROPERTY(QString type READ type CONSTANT); + /// The payload of the event in JSON format. + Q_PROPERTY(QString data READ data CONSTANT); + + QML_NAMED_ELEMENT(I3Event); + QML_UNCREATABLE("I3IpcEvents cannot be created."); + +public: + I3IpcEvent(QObject* parent): QObject(parent) {} + + [[nodiscard]] QString type() const; + [[nodiscard]] QString data() const; + + EventCode mCode = EventCode::Unknown; + QJsonDocument mData; + + static EventCode intToEvent(uint32_t raw); + static QString eventToString(EventCode event); +}; + +class I3Ipc: public QObject { + Q_OBJECT; + +public: + static I3Ipc* instance(); + + [[nodiscard]] QString socketPath() const; + + void makeRequest(const QByteArray& request); + void dispatch(const QString& payload); + + static QByteArray buildRequestMessage(EventCode cmd, const QByteArray& payload = QByteArray()); + + I3Workspace* findWorkspaceByName(const QString& name); + I3Monitor* findMonitorByName(const QString& name); + I3Workspace* findWorkspaceByID(qint32 id); + + void setFocusedWorkspace(I3Workspace* workspace); + void setFocusedMonitor(I3Monitor* monitor); + + void refreshWorkspaces(); + void refreshMonitors(); + + I3Monitor* monitorFor(QuickshellScreenInfo* screen); + + [[nodiscard]] I3Monitor* focusedMonitor() const; + [[nodiscard]] I3Workspace* focusedWorkspace() const; + [[nodiscard]] ObjectModel* monitors(); + [[nodiscard]] ObjectModel* workspaces(); +signals: + void connected(); + void rawEvent(I3IpcEvent* event); + void focusedWorkspaceChanged(); + void focusedMonitorChanged(); + +private slots: + void eventSocketError(QLocalSocket::LocalSocketError error) const; + void eventSocketStateChanged(QLocalSocket::LocalSocketState state); + void eventSocketReady(); + void subscribe(); + + void onFocusedWorkspaceDestroyed(); + void onFocusedMonitorDestroyed(); + +private: + explicit I3Ipc(); + + void onEvent(I3IpcEvent* event); + + void handleWorkspaceEvent(I3IpcEvent* event); + void handleGetWorkspacesEvent(I3IpcEvent* event); + void handleGetOutputsEvent(I3IpcEvent* event); + static void handleRunCommand(I3IpcEvent* event); + + void reconnectIPC(); + + QVector> parseResponse(); + + QLocalSocket liveEventSocket; + QDataStream liveEventSocketDs; + + QString mSocketPath; + + bool valid = false; + + ObjectModel mMonitors {this}; + ObjectModel mWorkspaces {this}; + + I3IpcEvent event {this}; + + I3Workspace* mFocusedWorkspace = nullptr; + I3Monitor* mFocusedMonitor = nullptr; +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/monitor.cpp b/src/x11/i3/ipc/monitor.cpp new file mode 100644 index 0000000..de54e0c --- /dev/null +++ b/src/x11/i3/ipc/monitor.cpp @@ -0,0 +1,111 @@ +#include "monitor.hpp" + +#include +#include +#include +#include + +#include "workspace.hpp" + +namespace qs::i3::ipc { + +qint32 I3Monitor::id() const { return this->mId; }; +QString I3Monitor::name() const { return this->mName; }; +bool I3Monitor::power() const { return this->mPower; }; +I3Workspace* I3Monitor::focusedWorkspace() const { return this->mFocusedWorkspace; }; +qint32 I3Monitor::x() const { return this->mX; }; +qint32 I3Monitor::y() const { return this->mY; }; +qint32 I3Monitor::width() const { return this->mWidth; }; +qint32 I3Monitor::height() const { return this->mHeight; }; +qreal I3Monitor::scale() const { return this->mScale; }; +bool I3Monitor::focused() const { return this->mFocused; }; +QVariantMap I3Monitor::lastIpcObject() const { return this->mLastIpcObject; }; + +void I3Monitor::updateFromObject(const QVariantMap& obj) { + auto id = obj.value("id").value(); + auto name = obj.value("name").value(); + auto power = obj.value("power").value(); + auto activeWorkspaceId = obj.value("current_workspace").value(); + auto rect = obj.value("rect").toMap(); + auto x = rect.value("x").value(); + auto y = rect.value("y").value(); + auto width = rect.value("width").value(); + auto height = rect.value("height").value(); + auto scale = obj.value("scale").value(); + auto focused = obj.value("focused").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = name; + emit this->nameChanged(); + } + + if (power != this->mPower) { + this->mPower = power; + this->powerChanged(); + } + + if (activeWorkspaceId != this->mFocusedWorkspaceName) { + auto* workspace = this->ipc->findWorkspaceByName(activeWorkspaceId); + if (activeWorkspaceId.isEmpty() || workspace == nullptr) { // is null when output is disabled + this->mFocusedWorkspace = nullptr; + this->mFocusedWorkspaceName = ""; + } else { + this->mFocusedWorkspaceName = activeWorkspaceId; + this->mFocusedWorkspace = workspace; + } + emit this->focusedWorkspaceChanged(); + }; + + if (x != this->mX) { + this->mX = x; + emit this->xChanged(); + } + + if (y != this->mY) { + this->mY = y; + emit this->yChanged(); + } + + if (width != this->mWidth) { + this->mWidth = width; + emit this->widthChanged(); + } + + if (height != this->mHeight) { + this->mHeight = height; + emit this->heightChanged(); + } + + if (scale != this->mScale) { + this->mScale = scale; + emit this->scaleChanged(); + } + + if (focused != this->mFocused) { + this->mFocused = focused; + emit this->focusedChanged(); + } + + if (obj != this->mLastIpcObject) { + this->mLastIpcObject = obj; + emit this->lastIpcObjectChanged(); + } +} + +void I3Monitor::setFocus(bool focused) { + this->mFocused = focused; + emit this->focusedChanged(); +} + +void I3Monitor::setFocusedWorkspace(I3Workspace* workspace) { + this->mFocusedWorkspace = workspace; + this->mFocusedWorkspaceName = workspace->name(); + emit this->focusedWorkspaceChanged(); +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/monitor.hpp b/src/x11/i3/ipc/monitor.hpp new file mode 100644 index 0000000..cd02e1a --- /dev/null +++ b/src/x11/i3/ipc/monitor.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include + +#include "connection.hpp" + +namespace qs::i3::ipc { + +///! I3/Sway monitors +class I3Monitor: public QObject { + Q_OBJECT; + + /// The ID of this monitor + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + /// The name of this monitor + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + /// Wether this monitor is turned on or not + Q_PROPERTY(bool power READ power NOTIFY powerChanged); + + /// The current workspace + Q_PROPERTY(qs::i3::ipc::I3Workspace* focusedWorkspace READ focusedWorkspace NOTIFY + focusedWorkspaceChanged); + + /// The X coordinate of this monitor inside the monitor layout + Q_PROPERTY(qint32 x READ x NOTIFY xChanged); + + /// The Y coordinate of this monitor inside the monitor layout + Q_PROPERTY(qint32 y READ y NOTIFY yChanged); + + /// The width in pixels of this monitor + Q_PROPERTY(qint32 width READ width NOTIFY widthChanged); + + /// The height in pixels of this monitor + Q_PROPERTY(qint32 height READ height NOTIFY heightChanged); + + /// The scaling factor of this monitor, 1 means it runs at native resolution + Q_PROPERTY(qreal scale READ scale NOTIFY scaleChanged); + + /// Whether this monitor is currently in focus + Q_PROPERTY(bool focused READ focused NOTIFY focusedChanged); + + /// Last JSON returned for this monitor, as a JavaScript object. + /// + /// This updates every time Quickshell receives an `output` event from i3/Sway + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + + QML_ELEMENT; + QML_UNCREATABLE("I3Monitors must be retrieved from the I3Ipc object."); + +public: + explicit I3Monitor(I3Ipc* ipc): QObject(ipc), ipc(ipc) {} + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] bool power() const; + [[nodiscard]] I3Workspace* focusedWorkspace() const; + [[nodiscard]] qint32 x() const; + [[nodiscard]] qint32 y() const; + [[nodiscard]] qint32 width() const; + [[nodiscard]] qint32 height() const; + [[nodiscard]] qreal scale() const; + [[nodiscard]] bool focused() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void updateFromObject(const QVariantMap& obj); + + void setFocusedWorkspace(I3Workspace* workspace); + void setFocus(bool focus); +signals: + void idChanged(); + void nameChanged(); + void powerChanged(); + void focusedWorkspaceChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void scaleChanged(); + void lastIpcObjectChanged(); + void focusedChanged(); + +private: + I3Ipc* ipc; + + qint32 mId = -1; + QString mName; + bool mPower = false; + qint32 mX = 0; + qint32 mY = 0; + qint32 mWidth = 0; + qint32 mHeight = 0; + qreal mScale = 1; + bool mFocused = false; + QVariantMap mLastIpcObject; + + I3Workspace* mFocusedWorkspace = nullptr; + QString mFocusedWorkspaceName; // use for faster change detection +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/qml.cpp b/src/x11/i3/ipc/qml.cpp new file mode 100644 index 0000000..d47cd8e --- /dev/null +++ b/src/x11/i3/ipc/qml.cpp @@ -0,0 +1,55 @@ +#include "qml.hpp" + +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "workspace.hpp" + +namespace qs::i3::ipc { + +I3IpcQml::I3IpcQml() { + auto* instance = I3Ipc::instance(); + + QObject::connect(instance, &I3Ipc::rawEvent, this, &I3IpcQml::rawEvent); + QObject::connect(instance, &I3Ipc::connected, this, &I3IpcQml::connected); + QObject::connect( + instance, + &I3Ipc::focusedWorkspaceChanged, + this, + &I3IpcQml::focusedWorkspaceChanged + ); + QObject::connect(instance, &I3Ipc::focusedMonitorChanged, this, &I3IpcQml::focusedMonitorChanged); +} + +void I3IpcQml::dispatch(const QString& request) { I3Ipc::instance()->dispatch(request); } + +void I3IpcQml::refreshMonitors() { I3Ipc::instance()->refreshMonitors(); } + +void I3IpcQml::refreshWorkspaces() { I3Ipc::instance()->refreshWorkspaces(); } + +QString I3IpcQml::socketPath() { return I3Ipc::instance()->socketPath(); } + +ObjectModel* I3IpcQml::monitors() { return I3Ipc::instance()->monitors(); } + +ObjectModel* I3IpcQml::workspaces() { return I3Ipc::instance()->workspaces(); } + +I3Workspace* I3IpcQml::focusedWorkspace() { return I3Ipc::instance()->focusedWorkspace(); } + +I3Monitor* I3IpcQml::focusedMonitor() { return I3Ipc::instance()->focusedMonitor(); } + +I3Workspace* I3IpcQml::findWorkspaceByName(const QString& name) { + return I3Ipc::instance()->findWorkspaceByName(name); +} + +I3Monitor* I3IpcQml::findMonitorByName(const QString& name) { + return I3Ipc::instance()->findMonitorByName(name); +} + +I3Monitor* I3IpcQml::monitorFor(QuickshellScreenInfo* screen) { + return I3Ipc::instance()->monitorFor(screen); +} + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/qml.hpp b/src/x11/i3/ipc/qml.hpp new file mode 100644 index 0000000..732c829 --- /dev/null +++ b/src/x11/i3/ipc/qml.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#include "../../../core/doc.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" + +namespace qs::i3::ipc { + +///! I3/Sway IPC integration +class I3IpcQml: public QObject { + Q_OBJECT; + // clang-format off + /// Path to the I3 socket + Q_PROPERTY(QString socketPath READ socketPath CONSTANT); + + Q_PROPERTY(qs::i3::ipc::I3Workspace* focusedWorkspace READ focusedWorkspace NOTIFY focusedWorkspaceChanged); + Q_PROPERTY(qs::i3::ipc::I3Monitor* focusedMonitor READ focusedMonitor NOTIFY focusedMonitorChanged); + /// All I3 monitors. + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* monitors READ monitors CONSTANT); + /// All I3 workspaces. + QSDOC_TYPE_OVERRIDE(ObjectModel*); + Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT); + // clang-format on + QML_NAMED_ELEMENT(I3); + QML_SINGLETON; + +public: + explicit I3IpcQml(); + + /// Execute an [I3/Sway command](https://i3wm.org/docs/userguide.html#list_of_commands) + Q_INVOKABLE static void dispatch(const QString& request); + + /// Refresh monitor information. + Q_INVOKABLE static void refreshMonitors(); + + /// Refresh workspace information. + Q_INVOKABLE static void refreshWorkspaces(); + + /// Find an I3Workspace using its name, returns null if the workspace doesn't exist. + Q_INVOKABLE static I3Workspace* findWorkspaceByName(const QString& name); + + /// Find an I3Monitor using its name, returns null if the monitor doesn't exist. + Q_INVOKABLE static I3Monitor* findMonitorByName(const QString& name); + + /// Return the i3/Sway monitor associated with `screen` + Q_INVOKABLE static I3Monitor* monitorFor(QuickshellScreenInfo* screen); + + /// The path to the I3 or Sway socket currently being used + [[nodiscard]] static QString socketPath(); + + /// All I3Monitors + [[nodiscard]] static ObjectModel* monitors(); + + /// All I3Workspaces + [[nodiscard]] static ObjectModel* workspaces(); + + /// The currently focused Workspace + [[nodiscard]] static I3Workspace* focusedWorkspace(); + + /// The currently focused Monitor + [[nodiscard]] static I3Monitor* focusedMonitor(); + +signals: + void rawEvent(I3IpcEvent* event); + void connected(); + void focusedWorkspaceChanged(); + void focusedMonitorChanged(); +}; + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/workspace.cpp b/src/x11/i3/ipc/workspace.cpp new file mode 100644 index 0000000..19c9edf --- /dev/null +++ b/src/x11/i3/ipc/workspace.cpp @@ -0,0 +1,73 @@ +#include "workspace.hpp" + +#include +#include +#include +#include + +#include "monitor.hpp" + +namespace qs::i3::ipc { + +qint32 I3Workspace ::id() const { return this->mId; } +QString I3Workspace::name() const { return this->mName; } +qint32 I3Workspace ::num() const { return this->mNum; } +bool I3Workspace ::urgent() const { return this->mUrgent; } +bool I3Workspace::focused() const { return this->mFocused; } +I3Monitor* I3Workspace::monitor() const { return this->mMonitor; } +QVariantMap I3Workspace::lastIpcObject() const { return this->mLastIpcObject; } + +void I3Workspace::updateFromObject(const QVariantMap& obj) { + auto id = obj.value("id").value(); + auto name = obj.value("name").value(); + auto num = obj.value("num").value(); + auto urgent = obj.value("urgent").value(); + auto focused = obj.value("focused").value(); + auto monitorName = obj.value("output").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = name; + emit this->nameChanged(); + } + + if (num != this->mNum) { + this->mNum = num; + emit this->numChanged(); + } + + if (urgent != this->mUrgent) { + this->mUrgent = urgent; + emit this->urgentChanged(); + } + + if (focused != this->mFocused) { + this->mFocused = focused; + emit this->focusedChanged(); + } + + if (obj != this->mLastIpcObject) { + this->mLastIpcObject = obj; + emit this->lastIpcObjectChanged(); + } + + if (monitorName != this->mMonitorName) { + auto* monitor = this->ipc->findMonitorByName(monitorName); + if (monitorName.isEmpty() || monitor == nullptr) { // is null when output is disabled + this->mMonitor = nullptr; + this->mMonitorName = ""; + } else { + this->mMonitorName = monitorName; + this->mMonitor = monitor; + } + emit this->monitorChanged(); + } +} + +void I3Workspace::setFocus(bool focus) { this->mFocused = focus; } + +} // namespace qs::i3::ipc diff --git a/src/x11/i3/ipc/workspace.hpp b/src/x11/i3/ipc/workspace.hpp new file mode 100644 index 0000000..6df8c8b --- /dev/null +++ b/src/x11/i3/ipc/workspace.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include "connection.hpp" + +namespace qs::i3::ipc { + +///! I3/Sway workspaces +class I3Workspace: public QObject { + Q_OBJECT; + + /// The ID of this workspace, it is unique for i3/Sway launch + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + + /// The name of this workspace + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + + /// The number of this workspace + Q_PROPERTY(qint32 num READ num NOTIFY numChanged); + + /// If a window in this workspace has an urgent notification + Q_PROPERTY(bool urgent READ urgent NOTIFY urgentChanged); + + /// If this workspace is the one currently in focus + Q_PROPERTY(bool focused READ focused NOTIFY focusedChanged); + + /// The monitor this workspace is being displayed on + Q_PROPERTY(qs::i3::ipc::I3Monitor* monitor READ monitor NOTIFY monitorChanged); + + /// Last JSON returned for this workspace, as a JavaScript object. + /// + /// This updates every time we receive a `workspace` event from i3/Sway + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + + QML_ELEMENT; + QML_UNCREATABLE("I3Workspaces must be retrieved from the I3 object."); + +public: + I3Workspace(qs::i3::ipc::I3Ipc* ipc): QObject(ipc), ipc(ipc) {} + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] qint32 num() const; + [[nodiscard]] bool urgent() const; + [[nodiscard]] bool focused() const; + [[nodiscard]] I3Monitor* monitor() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void updateFromObject(const QVariantMap& obj); + void setFocus(bool focus); + +signals: + void idChanged(); + void nameChanged(); + void urgentChanged(); + void focusedChanged(); + void numChanged(); + void monitorChanged(); + void lastIpcObjectChanged(); + +private: + I3Ipc* ipc; + + qint32 mId = -1; + QString mName; + qint32 mNum = -1; + bool mFocused = false; + bool mUrgent = false; + + QVariantMap mLastIpcObject; + I3Monitor* mMonitor = nullptr; + QString mMonitorName; +}; +} // namespace qs::i3::ipc diff --git a/src/x11/i3/module.md b/src/x11/i3/module.md new file mode 100644 index 0000000..10afb98 --- /dev/null +++ b/src/x11/i3/module.md @@ -0,0 +1,9 @@ +name = "Quickshell.I3" +description = "I3 specific Quickshell types" +headers = [ + "ipc/connection.hpp", + "ipc/qml.hpp", + "ipc/workspace.hpp", + "ipc/monitor.hpp", +] +-----