1
0
Fork 0

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
This commit is contained in:
Nydragon 2024-11-02 03:52:27 +01:00
parent 84ce47b6d3
commit 31adcaac76
No known key found for this signature in database
15 changed files with 1252 additions and 1 deletions

View file

@ -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).*

View file

@ -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)

View file

@ -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:

View file

@ -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)

23
src/x11/i3/CMakeLists.txt Normal file
View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1,542 @@
#include <algorithm>
#include <array>
#include <cstring>
#include <tuple>
#include <bit>
#include <qbytearray.h>
#include <qbytearrayview.h>
#include <qcontainerfwd.h>
#include <qdatastream.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qjsonvalue.h>
#include <qlocalsocket.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qnamespace.h>
#include <qobject.h>
#include <qsysinfo.h>
#include <qtenvironmentvariables.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<quint32>(payload.length());
auto type = QByteArray(std::bit_cast<std::array<char, 4>>(cmd).data(), 4);
auto len = QByteArray(std::bit_cast<std::array<char, 4>>(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<QDataStream::ByteOrder>(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<Event> I3Ipc::parseResponse() {
QVector<std::tuple<EventCode, QJsonDocument>> events;
const int magicLen = 6;
while (!this->liveEventSocketDs.atEnd()) {
this->liveEventSocketDs.startTransaction();
this->liveEventSocketDs.startTransaction();
std::array<char, 6> 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<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::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<I3Workspace*>();
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<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::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<I3Monitor*>();
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<I3Monitor>* I3Ipc::monitors() { return &this->mMonitors; }
ObjectModel<I3Workspace>* 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<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;
case EventCode::Unknown: return "unknown"; break;
}
}
} // namespace qs::i3::ipc

View file

@ -0,0 +1,151 @@
#pragma once
#include <qbytearrayview.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlocalsocket.h>
#include <qobject.h>
#include <qqml.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<EventCode, QJsonDocument>;
///! 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<I3Monitor>* monitors();
[[nodiscard]] ObjectModel<I3Workspace>* 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<std::tuple<EventCode, QJsonDocument>> parseResponse();
QLocalSocket liveEventSocket;
QDataStream liveEventSocketDs;
QString mSocketPath;
bool valid = false;
ObjectModel<I3Monitor> mMonitors {this};
ObjectModel<I3Workspace> mWorkspaces {this};
I3IpcEvent event {this};
I3Workspace* mFocusedWorkspace = nullptr;
I3Monitor* mFocusedMonitor = nullptr;
};
} // namespace qs::i3::ipc

111
src/x11/i3/ipc/monitor.cpp Normal file
View file

@ -0,0 +1,111 @@
#include "monitor.hpp"
#include <qcontainerfwd.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<qint32>();
auto name = obj.value("name").value<QString>();
auto power = obj.value("power").value<bool>();
auto activeWorkspaceId = obj.value("current_workspace").value<QString>();
auto rect = obj.value("rect").toMap();
auto x = rect.value("x").value<qint32>();
auto y = rect.value("y").value<qint32>();
auto width = rect.value("width").value<qint32>();
auto height = rect.value("height").value<qint32>();
auto scale = obj.value("scale").value<qreal>();
auto focused = obj.value("focused").value<bool>();
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

100
src/x11/i3/ipc/monitor.hpp Normal file
View file

@ -0,0 +1,100 @@
#pragma once
#include <qobject.h>
#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

55
src/x11/i3/ipc/qml.cpp Normal file
View file

@ -0,0 +1,55 @@
#include "qml.hpp"
#include <qobject.h>
#include <qstring.h>
#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<I3Monitor>* I3IpcQml::monitors() { return I3Ipc::instance()->monitors(); }
ObjectModel<I3Workspace>* 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

74
src/x11/i3/ipc/qml.hpp Normal file
View file

@ -0,0 +1,74 @@
#pragma once
#include <qjsonarray.h>
#include <qobject.h>
#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<qs::i3::ipc::I3Monitor>*);
Q_PROPERTY(UntypedObjectModel* monitors READ monitors CONSTANT);
/// All I3 workspaces.
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::i3::ipc::I3Workspace>*);
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<I3Monitor>* monitors();
/// All I3Workspaces
[[nodiscard]] static ObjectModel<I3Workspace>* 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

View file

@ -0,0 +1,73 @@
#include "workspace.hpp"
#include <qcontainerfwd.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#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<qint32>();
auto name = obj.value("name").value<QString>();
auto num = obj.value("num").value<qint32>();
auto urgent = obj.value("urgent").value<bool>();
auto focused = obj.value("focused").value<bool>();
auto monitorName = obj.value("output").value<QString>();
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

View file

@ -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

9
src/x11/i3/module.md Normal file
View file

@ -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",
]
-----