#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 { namespace { QS_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); QS_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); } // namespace 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->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(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); // Workspaces must be refreshed before monitors or no focus will be // detected on launch. 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; } // Importing this makes CI builds fail for some reason. QJsonParseError e; // NOLINT (misc-include-cleaner) 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; } 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(); 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()) { this->bFocusedMonitor = workspace->bindableMonitor().value(); } names.push_back(name); } auto removedWorkspaces = QVector(); 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(); 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(); 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* I3Ipc::monitors() { return &this->mMonitors; } ObjectModel* 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(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