i3/ipc: implement IPC listener to receive arbitrary events

This commit is contained in:
Ala Alkhafaji 2025-11-13 19:43:11 +01:00 committed by outfoxxed
parent fdbb86a06a
commit ab494dd982
No known key found for this signature in database
GPG key ID: 4C88A185FB89301E
14 changed files with 693 additions and 484 deletions

View file

@ -1,4 +1,4 @@
#include <algorithm>
#include "connection.hpp"
#include <array>
#include <cstring>
#include <tuple>
@ -23,11 +23,6 @@
#include <qtypes.h>
#include "../../../core/logcat.hpp"
#include "../../../core/model.hpp"
#include "../../../core/qmlscreen.hpp"
#include "connection.hpp"
#include "monitor.hpp"
#include "workspace.hpp"
namespace qs::i3::ipc {
@ -36,6 +31,69 @@ QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg);
QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg);
} // namespace
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<EventCode>(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;
default: return "unknown"; break;
}
}
I3Ipc::I3Ipc(const QList<QString>& events): mEvents(events) {
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<QDataStream::ByteOrder>(QSysInfo::ByteOrder));
}
void I3Ipc::makeRequest(const QByteArray& request) {
if (!this->valid) {
qCWarning(logI3IpcEvents) << "IPC connection is not open, ignoring request.";
@ -60,50 +118,13 @@ QByteArray I3Ipc::buildRequestMessage(EventCode cmd, const QByteArray& payload)
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->bFocusedWorkspace.setBinding([this]() -> I3Workspace* {
if (!this->bFocusedMonitor) return nullptr;
return this->bFocusedMonitor->bindableActiveWorkspace().value();
});
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<QDataStream::ByteOrder>(QSysInfo::ByteOrder));
this->liveEventSocket.connectToServer(this->mSocketPath);
}
void I3Ipc::subscribe() {
auto payload = QByteArray(R"(["workspace","output"])");
auto jsonArray = QJsonArray::fromStringList(this->mEvents);
auto jsonDoc = QJsonDocument(jsonArray);
auto payload = jsonDoc.toJson(QJsonDocument::Compact);
auto message = I3Ipc::buildRequestMessage(EventCode::Subscribe, payload);
this->makeRequest(message);
// Workspaces must be refreshed before monitors or no focus will be
// detected on launch.
this->refreshWorkspaces();
this->refreshMonitors();
}
void I3Ipc::eventSocketReady() {
@ -111,15 +132,16 @@ void I3Ipc::eventSocketReady() {
this->event.mCode = type;
this->event.mData = data;
this->onEvent(&this->event);
emit this->rawEvent(&this->event);
}
}
void I3Ipc::connect() { this->liveEventSocket.connectToServer(this->mSocketPath); }
void I3Ipc::reconnectIPC() {
qCWarning(logI3Ipc) << "Fatal IPC error occured, recreating connection";
this->liveEventSocket.disconnectFromServer();
this->liveEventSocket.connectToServer(this->mSocketPath);
this->connect();
}
QVector<Event> I3Ipc::parseResponse() {
@ -193,347 +215,4 @@ void I3Ipc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) {
QString I3Ipc::socketPath() const { return this->mSocketPath; }
void I3Ipc::setFocusedMonitor(I3Monitor* monitor) {
auto* oldMonitor = this->bFocusedMonitor.value();
if (monitor == oldMonitor) return;
if (oldMonitor != nullptr) {
QObject::disconnect(oldMonitor, nullptr, this, nullptr);
}
if (monitor != nullptr) {
QObject::connect(monitor, &QObject::destroyed, this, &I3Ipc::onFocusedMonitorDestroyed);
}
this->bFocusedMonitor = monitor;
}
void I3Ipc::onFocusedMonitorDestroyed() { this->bFocusedMonitor = nullptr; }
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<QString>();
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::ranges::find_if(mList, [name](I3Workspace* m) {
return m->bindableName().value() == name;
});
auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter;
auto existed = workspace != nullptr;
if (workspace == nullptr) {
workspace = new I3Workspace(this);
}
workspace->updateFromObject(object);
if (!existed) {
this->mWorkspaces.insertObjectSorted(workspace, &I3Ipc::compareWorkspaces);
}
if (!this->bFocusedWorkspace && object.value("focused").value<bool>()) {
this->bFocusedMonitor = workspace->bindableMonitor().value();
}
names.push_back(name);
}
auto removedWorkspaces = QVector<I3Workspace*>();
for (auto* workspace: mList) {
if (!names.contains(workspace->bindableName().value())) {
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<QString>();
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::ranges::find_if(mList, [name](I3Monitor* m) {
return m->bindableName().value() == name;
});
auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter;
auto existed = monitor != nullptr;
if (monitor == nullptr) {
monitor = new I3Monitor(this);
}
monitor->updateFromObject(object);
if (monitor->bindableFocused().value()) {
this->setFocusedMonitor(monitor);
}
if (!existed) {
this->mMonitors.insertObject(monitor);
}
names.push_back(name);
}
auto removedMonitors = QVector<I3Monitor*>();
for (auto* monitor: mList) {
if (!names.contains(monitor->bindableName().value())) {
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->handleGetWorkspacesEvent(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));
auto existed = workspace != nullptr;
if (!existed) {
workspace = new I3Workspace(this);
}
if (workspaceData.isObject()) {
workspace->updateFromObject(workspaceData.toObject().toVariantMap());
}
if (!existed) {
this->mWorkspaces.insertObjectSorted(workspace, &I3Ipc::compareWorkspaces);
qCInfo(logI3Ipc) << "Added workspace" << workspace->bindableName().value() << "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());
if (newWorkspace->bindableMonitor().value()) {
auto* monitor = newWorkspace->bindableMonitor().value();
monitor->setFocusedWorkspace(newWorkspace);
this->bFocusedMonitor = monitor;
}
} else if (change == "empty") {
auto name = event->mData["current"]["name"].toString();
auto* oldWorkspace = this->findWorkspaceByName(name);
if (oldWorkspace != nullptr) {
qCInfo(logI3Ipc) << "Deleting" << oldWorkspace->bindableId().value() << name;
if (this->bFocusedWorkspace == oldWorkspace) {
this->bFocusedMonitor->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::ranges::find_if(list, [id](I3Workspace* m) { return m->bindableId().value() == id; });
return workspaceIter == list.end() ? nullptr : *workspaceIter;
}
I3Workspace* I3Ipc::findWorkspaceByName(const QString& name) {
auto list = this->mWorkspaces.valueList();
auto workspaceIter = std::ranges::find_if(list, [name](I3Workspace* m) {
return m->bindableName().value() == name;
});
return workspaceIter == list.end() ? nullptr : *workspaceIter;
}
I3Monitor* I3Ipc::findMonitorByName(const QString& name, bool createIfMissing) {
auto list = this->mMonitors.valueList();
auto monitorIter = std::ranges::find_if(list, [name](I3Monitor* m) {
return m->bindableName().value() == name;
});
if (monitorIter != list.end()) {
return *monitorIter;
} else if (createIfMissing) {
qCDebug(logI3Ipc) << "Monitor" << name << "requested before creation, performing early init";
auto* monitor = new I3Monitor(this);
monitor->updateInitial(name);
this->mMonitors.insertObject(monitor);
return monitor;
} else {
return nullptr;
}
}
ObjectModel<I3Monitor>* I3Ipc::monitors() { return &this->mMonitors; }
ObjectModel<I3Workspace>* I3Ipc::workspaces() { return &this->mWorkspaces; }
bool I3Ipc::compareWorkspaces(I3Workspace* a, I3Workspace* b) {
return a->bindableNumber().value() > b->bindableNumber().value();
}
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<EventCode>(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;
default: return "unknown"; break;
}
}
} // namespace qs::i3::ipc