Add Nebula.Windows with Sway IPC backend

Introduces a new `core/windows` module (`Nebula.Windows`) with `WindowService`, window/workspace models, and a Sway IPC backend for live window tracking and control (focus, close, move/resize, snap, maximize/restore, minimize, workspace switch). The desktop shell now links this module, uses compositor focus for top-bar active app text, and adds an Open Windows section in the launcher with focus/close actions.

Also updates application metadata handling to parse `StartupWMClass` and map window `app_id`/class to friendly names and icons, and revises Sway config/docs for the new floating-first Desktop 0.2 behavior with temporary compositor-provided decorations.
This commit is contained in:
2026-08-26 18:20:56 +12:00
parent 8c07c10050
commit 2f59d3c115
29 changed files with 2177 additions and 35 deletions
@@ -0,0 +1,245 @@
#include "SwayIpcClient.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QtGlobal>
#include <cstring>
#include <utility>
namespace {
constexpr char kMagic[] = "i3-ipc";
constexpr int kMagicSize = 6;
constexpr int kHeaderSize = 14;
constexpr int kDefaultTimeoutMs = 2000;
} // namespace
SwayIpcClient::SwayIpcClient(QObject *parent)
: QObject(parent)
, m_command(new QLocalSocket(this))
, m_event(new QLocalSocket(this))
{
connect(m_command, &QLocalSocket::disconnected, this, &SwayIpcClient::handleCommandDisconnected);
connect(m_event, &QLocalSocket::disconnected, this, &SwayIpcClient::handleEventDisconnected);
connect(m_event, &QLocalSocket::readyRead, this, &SwayIpcClient::processEventSocket);
}
void SwayIpcClient::processEventSocket()
{
appendIncoming(m_event, &m_eventBuffer);
Message message;
while (takeMessage(&m_eventBuffer, &message)) {
if (m_inRequest > 0)
m_pendingEvents.append(std::move(message));
else
emit eventReceived(message.type, QJsonDocument::fromJson(message.payload));
}
}
SwayIpcClient::~SwayIpcClient()
{
disconnectFromSway();
}
QString SwayIpcClient::socketPath()
{
return QString::fromLocal8Bit(qgetenv("SWAYSOCK"));
}
bool SwayIpcClient::isConnected() const
{
return m_connected
&& m_command->state() == QLocalSocket::ConnectedState
&& m_event->state() == QLocalSocket::ConnectedState;
}
void SwayIpcClient::disconnectFromSway()
{
m_event->blockSignals(true);
m_command->blockSignals(true);
if (m_event->state() != QLocalSocket::UnconnectedState)
m_event->disconnectFromServer();
if (m_command->state() != QLocalSocket::UnconnectedState)
m_command->disconnectFromServer();
m_event->blockSignals(false);
m_command->blockSignals(false);
m_eventBuffer.clear();
m_pendingEvents.clear();
if (m_connected) {
m_connected = false;
emit connectedChanged();
emit disconnected();
}
}
bool SwayIpcClient::connectToSway()
{
const QString path = socketPath();
if (path.isEmpty())
return false;
disconnectFromSway();
m_command->connectToServer(path);
if (!m_command->waitForConnected(kDefaultTimeoutMs)) {
qWarning("nebula: Sway IPC command socket failed: %s", qPrintable(m_command->errorString()));
return false;
}
m_event->connectToServer(path);
if (!m_event->waitForConnected(kDefaultTimeoutMs)) {
qWarning("nebula: Sway IPC event socket failed: %s", qPrintable(m_event->errorString()));
m_command->disconnectFromServer();
return false;
}
m_connected = true;
emit connectedChanged();
return true;
}
QJsonDocument SwayIpcClient::request(quint32 type, const QByteArray &payload)
{
if (m_command->state() != QLocalSocket::ConnectedState)
return {};
++m_inRequest;
const bool wrote = writeMessage(m_command, type, payload);
Message reply;
const bool read = wrote && readMessage(m_command, &reply, kDefaultTimeoutMs);
--m_inRequest;
flushPendingEvents();
if (!read) {
qWarning("nebula: Sway IPC request failed (type %u)", type);
return {};
}
return QJsonDocument::fromJson(reply.payload);
}
bool SwayIpcClient::subscribe(const QStringList &events)
{
if (m_event->state() != QLocalSocket::ConnectedState)
return false;
QJsonArray array;
for (const QString &event : events)
array.append(event);
// Subscribe on the event connection so unsolicited events never interleave
// with command replies on the request socket.
m_event->blockSignals(true);
const bool wrote = writeMessage(m_event, Subscribe, QJsonDocument(array).toJson(QJsonDocument::Compact));
Message reply;
const bool read = wrote && readMessage(m_event, &reply, kDefaultTimeoutMs);
m_event->blockSignals(false);
if (m_event->bytesAvailable() > 0)
processEventSocket();
if (!read) {
qWarning("nebula: Sway IPC subscribe failed");
return false;
}
const QJsonDocument doc = QJsonDocument::fromJson(reply.payload);
if (!doc.isObject() || !doc.object().value(QStringLiteral("success")).toBool()) {
qWarning("nebula: Sway IPC subscribe was rejected");
return false;
}
return true;
}
bool SwayIpcClient::writeMessage(QLocalSocket *socket, quint32 type, const QByteArray &payload)
{
QByteArray header;
header.resize(kHeaderSize);
memcpy(header.data(), kMagic, kMagicSize);
const quint32 size = static_cast<quint32>(payload.size());
memcpy(header.data() + kMagicSize, &size, sizeof(size));
memcpy(header.data() + kMagicSize + sizeof(size), &type, sizeof(type));
if (socket->write(header) != header.size())
return false;
if (!payload.isEmpty() && socket->write(payload) != payload.size())
return false;
return socket->waitForBytesWritten(kDefaultTimeoutMs);
}
bool SwayIpcClient::readMessage(QLocalSocket *socket, Message *message, int timeoutMs)
{
QByteArray buffer;
while (!takeMessage(&buffer, message)) {
if (socket->bytesAvailable() <= 0 && !socket->waitForReadyRead(timeoutMs))
return false;
appendIncoming(socket, &buffer);
}
return true;
}
void SwayIpcClient::appendIncoming(QLocalSocket *socket, QByteArray *buffer)
{
buffer->append(socket->readAll());
}
bool SwayIpcClient::takeMessage(QByteArray *buffer, Message *message)
{
if (buffer->size() < kHeaderSize)
return false;
if (!buffer->startsWith(kMagic)) {
qWarning("nebula: Sway IPC header magic mismatch");
buffer->clear();
return false;
}
quint32 size = 0;
quint32 type = 0;
memcpy(&size, buffer->constData() + kMagicSize, sizeof(size));
memcpy(&type, buffer->constData() + kMagicSize + sizeof(size), sizeof(type));
const int total = kHeaderSize + static_cast<int>(size);
if (buffer->size() < total)
return false;
message->type = type;
message->payload = buffer->mid(kHeaderSize, static_cast<int>(size));
buffer->remove(0, total);
return true;
}
void SwayIpcClient::flushPendingEvents()
{
if (m_inRequest > 0 || m_pendingEvents.isEmpty())
return;
const QVector<Message> pending = std::move(m_pendingEvents);
m_pendingEvents.clear();
for (const Message &message : pending)
emit eventReceived(message.type, QJsonDocument::fromJson(message.payload));
}
void SwayIpcClient::handleCommandDisconnected()
{
if (!m_connected)
return;
qWarning("nebula: Sway IPC command socket disconnected");
m_connected = false;
emit connectedChanged();
emit disconnected();
}
void SwayIpcClient::handleEventDisconnected()
{
if (!m_connected)
return;
qWarning("nebula: Sway IPC event socket disconnected");
m_connected = false;
emit connectedChanged();
emit disconnected();
}
@@ -0,0 +1,71 @@
#pragma once
#include <QByteArray>
#include <QJsonDocument>
#include <QLocalSocket>
#include <QObject>
#include <QStringList>
#include <QVector>
class SwayIpcClient : public QObject
{
Q_OBJECT
public:
enum MessageType : quint32 {
RunCommand = 0,
GetWorkspaces = 1,
Subscribe = 2,
GetOutputs = 3,
GetTree = 4,
GetMarks = 5,
GetVersion = 7
};
enum EventType : quint32 {
WorkspaceEvent = 0x80000000u,
OutputEvent = 0x80000001u,
WindowEvent = 0x80000003u,
BindingEvent = 0x80000005u,
ShutdownEvent = 0x80000006u
};
explicit SwayIpcClient(QObject *parent = nullptr);
~SwayIpcClient() override;
bool connectToSway();
void disconnectFromSway();
bool isConnected() const;
QJsonDocument request(quint32 type, const QByteArray &payload = {});
bool subscribe(const QStringList &events);
static QString socketPath();
signals:
void connectedChanged();
void disconnected();
void eventReceived(quint32 type, const QJsonDocument &payload);
private:
struct Message {
quint32 type = 0;
QByteArray payload;
};
bool writeMessage(QLocalSocket *socket, quint32 type, const QByteArray &payload);
bool readMessage(QLocalSocket *socket, Message *message, int timeoutMs);
void appendIncoming(QLocalSocket *socket, QByteArray *buffer);
bool takeMessage(QByteArray *buffer, Message *message);
void processEventSocket();
void flushPendingEvents();
void handleCommandDisconnected();
void handleEventDisconnected();
QLocalSocket *m_command = nullptr;
QLocalSocket *m_event = nullptr;
QByteArray m_eventBuffer;
QVector<Message> m_pendingEvents;
int m_inRequest = 0;
bool m_connected = false;
};
@@ -0,0 +1,504 @@
#include "SwayWindowBackend.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QVariant>
#include <QtGlobal>
namespace {
constexpr int kMaxConnectAttempts = 8;
QString quoteCriteriaValue(const QString &value)
{
QString escaped = value;
escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
escaped.replace(QLatin1Char('"'), QStringLiteral("\\\""));
return escaped;
}
bool isScratchpadWorkspace(const QString &name)
{
return name == QLatin1String("__i3_scratch");
}
bool isPseudoOutput(const QString &name)
{
return name == QLatin1String("__i3");
}
qint64 jsonId(const QJsonValue &value)
{
if (value.isDouble())
return static_cast<qint64>(value.toDouble());
return value.toVariant().toLongLong();
}
QString floatingState(const QJsonObject &node)
{
return node.value(QStringLiteral("floating")).toString();
}
bool nodeIsFloating(const QJsonObject &node)
{
const QString state = floatingState(node);
if (state == QLatin1String("user_on") || state == QLatin1String("auto_on"))
return true;
return node.value(QStringLiteral("type")).toString() == QLatin1String("floating_con");
}
bool nodeLooksLikeView(const QJsonObject &node)
{
const int pid = node.value(QStringLiteral("pid")).toInt();
if (pid <= 0)
return false;
const QString appId = node.value(QStringLiteral("app_id")).toString();
if (!appId.isEmpty())
return true;
if (node.contains(QStringLiteral("window")) && !node.value(QStringLiteral("window")).isNull())
return true;
const QJsonObject properties = node.value(QStringLiteral("window_properties")).toObject();
return !properties.isEmpty();
}
} // namespace
SwayWindowBackend::SwayWindowBackend(QObject *parent)
: WindowBackend(parent)
, m_ipc(new SwayIpcClient(this))
, m_retryTimer(new QTimer(this))
{
m_retryTimer->setSingleShot(true);
connect(m_retryTimer, &QTimer::timeout, this, &SwayWindowBackend::tryConnect);
connect(m_ipc, &SwayIpcClient::eventReceived, this, &SwayWindowBackend::handleEvent);
connect(m_ipc, &SwayIpcClient::connectedChanged, this, &SwayWindowBackend::connectedChanged);
connect(m_ipc, &SwayIpcClient::disconnected, this, [this]() {
m_windows.clear();
m_workspaces.clear();
m_focusedWindowId = 0;
emit windowsChanged();
emit workspacesChanged();
emit focusChanged();
});
tryConnect();
}
bool SwayWindowBackend::isConnected() const
{
return m_ipc->isConnected();
}
QVector<WindowInfo> SwayWindowBackend::windows() const
{
return m_windows;
}
QVector<WorkspaceInfo> SwayWindowBackend::workspaces() const
{
return m_workspaces;
}
qint64 SwayWindowBackend::focusedWindowId() const
{
return m_focusedWindowId;
}
bool SwayWindowBackend::focusWindow(qint64 windowId)
{
if (m_minimized.contains(windowId) || (findWindow(windowId) && findWindow(windowId)->minimized)) {
if (!run(QStringLiteral("[con_id=%1] scratchpad show").arg(windowId)))
return false;
m_minimized.remove(windowId);
}
return run(QStringLiteral("[con_id=%1] focus").arg(windowId));
}
bool SwayWindowBackend::closeWindow(qint64 windowId)
{
// `kill` asks the client to close; it does not SIGKILL the process.
const bool ok = run(QStringLiteral("[con_id=%1] kill").arg(windowId));
m_savedGeometry.remove(windowId);
m_maximized.remove(windowId);
m_minimized.remove(windowId);
return ok;
}
bool SwayWindowBackend::maximizeWindow(qint64 windowId)
{
saveGeometry(windowId);
m_maximized.insert(windowId);
m_minimized.remove(windowId);
return run(QStringLiteral("[con_id=%1] floating enable, fullscreen disable, "
"resize set width 100 ppt height 100 ppt, move position 0 ppt 0 ppt")
.arg(windowId));
}
bool SwayWindowBackend::restoreWindow(qint64 windowId)
{
if (m_minimized.contains(windowId) || (findWindow(windowId) && findWindow(windowId)->minimized)) {
const bool ok = run(QStringLiteral("[con_id=%1] scratchpad show, floating enable").arg(windowId));
m_minimized.remove(windowId);
if (!ok)
return false;
if (!m_maximized.contains(windowId) && m_savedGeometry.contains(windowId)) {
const QRect rect = m_savedGeometry.value(windowId);
return run(QStringLiteral("[con_id=%1] resize set width %2 px height %3 px, "
"move absolute position %4 px %5 px")
.arg(windowId)
.arg(rect.width())
.arg(rect.height())
.arg(rect.x())
.arg(rect.y()));
}
return true;
}
if (m_savedGeometry.contains(windowId)) {
const QRect rect = m_savedGeometry.take(windowId);
m_maximized.remove(windowId);
return run(QStringLiteral("[con_id=%1] floating enable, fullscreen disable, "
"resize set width %2 px height %3 px, "
"move absolute position %4 px %5 px")
.arg(windowId)
.arg(rect.width())
.arg(rect.height())
.arg(rect.x())
.arg(rect.y()));
}
m_maximized.remove(windowId);
return run(QStringLiteral("[con_id=%1] floating enable, fullscreen disable, "
"resize set width 70 ppt height 70 ppt, move position center")
.arg(windowId));
}
bool SwayWindowBackend::minimizeWindow(qint64 windowId)
{
saveGeometry(windowId);
m_minimized.insert(windowId);
return run(QStringLiteral("[con_id=%1] move scratchpad").arg(windowId));
}
bool SwayWindowBackend::setWindowFloating(qint64 windowId, bool floating)
{
return run(QStringLiteral("[con_id=%1] floating %2")
.arg(windowId)
.arg(floating ? QStringLiteral("enable") : QStringLiteral("disable")));
}
bool SwayWindowBackend::moveWindow(qint64 windowId, int x, int y)
{
return run(QStringLiteral("[con_id=%1] move absolute position %2 px %3 px")
.arg(windowId)
.arg(x)
.arg(y));
}
bool SwayWindowBackend::resizeWindow(qint64 windowId, int width, int height)
{
if (width < 1 || height < 1)
return false;
return run(QStringLiteral("[con_id=%1] resize set width %2 px height %3 px")
.arg(windowId)
.arg(width)
.arg(height));
}
bool SwayWindowBackend::snapWindow(qint64 windowId, SnapEdge edge)
{
saveGeometry(windowId);
m_minimized.remove(windowId);
if (edge == SnapMaximize) {
m_maximized.insert(windowId);
return run(QStringLiteral("[con_id=%1] floating enable, fullscreen disable, "
"resize set width 100 ppt height 100 ppt, move position 0 ppt 0 ppt")
.arg(windowId));
}
m_maximized.remove(windowId);
const QString x = (edge == SnapRight) ? QStringLiteral("50") : QStringLiteral("0");
return run(QStringLiteral("[con_id=%1] floating enable, fullscreen disable, "
"resize set width 50 ppt height 100 ppt, move position %2 ppt 0 ppt")
.arg(windowId)
.arg(x));
}
bool SwayWindowBackend::switchWorkspace(const QString &workspaceId)
{
if (workspaceId.isEmpty())
return false;
return run(QStringLiteral("workspace \"%1\"").arg(quoteCriteriaValue(workspaceId)));
}
void SwayWindowBackend::tryConnect()
{
if (m_ipc->isConnected())
return;
const QString path = SwayIpcClient::socketPath();
if (path.isEmpty()) {
qWarning("nebula: SWAYSOCK is unset; WindowService will continue without compositor IPC");
return;
}
++m_connectAttempts;
if (!m_ipc->connectToSway()) {
if (m_connectAttempts < kMaxConnectAttempts) {
const int delay = 150 * m_connectAttempts;
qInfo("nebula: Sway IPC not ready, retrying in %d ms (%d/%d)",
delay, m_connectAttempts, kMaxConnectAttempts);
m_retryTimer->start(delay);
return;
}
qWarning("nebula: could not connect to Sway IPC at %s; window management is unavailable",
qPrintable(path));
return;
}
if (!m_ipc->subscribe({QStringLiteral("window"), QStringLiteral("workspace"), QStringLiteral("output")})) {
qWarning("nebula: Sway IPC subscribe failed; window management is unavailable");
m_ipc->disconnectFromSway();
return;
}
qInfo("nebula: WindowService connected to Sway IPC");
refresh();
}
void SwayWindowBackend::refresh()
{
if (!m_ipc->isConnected())
return;
const QJsonDocument tree = m_ipc->request(SwayIpcClient::GetTree);
if (!tree.isObject())
return;
QVector<WindowInfo> windows;
QVector<WorkspaceInfo> workspaces;
collect(tree.object(), {}, {}, false, &windows, &workspaces);
QHash<QString, int> counts;
qint64 focused = 0;
for (WindowInfo &window : windows) {
window.maximized = m_maximized.contains(window.id);
if (window.minimized)
m_minimized.insert(window.id);
else
m_minimized.remove(window.id);
if (!window.minimized)
counts[window.workspace] += 1;
if (window.focused && !window.minimized)
focused = window.id;
}
for (WorkspaceInfo &workspace : workspaces) {
workspace.windowCount = counts.value(workspace.name, 0);
if (workspace.focused)
continue;
for (const WindowInfo &window : windows) {
if (window.focused && !window.minimized && window.workspace == workspace.name) {
workspace.focused = true;
workspace.visible = true;
break;
}
}
}
const bool focusChanged = (focused != m_focusedWindowId);
m_windows = std::move(windows);
m_workspaces = std::move(workspaces);
m_focusedWindowId = focused;
emit windowsChanged();
emit workspacesChanged();
if (focusChanged)
emit focusChanged();
}
void SwayWindowBackend::handleEvent(quint32 type, const QJsonDocument &payload)
{
if (type == SwayIpcClient::WindowEvent) {
const QJsonObject object = payload.object();
const QString change = object.value(QStringLiteral("change")).toString();
if (change == QLatin1String("new")) {
const qint64 id = jsonId(object.value(QStringLiteral("container")).toObject().value(QStringLiteral("id")));
QTimer::singleShot(0, this, [this, id]() { handleNewWindow(id); });
return;
}
if (change == QLatin1String("close")) {
const qint64 id = jsonId(object.value(QStringLiteral("container")).toObject().value(QStringLiteral("id")));
m_savedGeometry.remove(id);
m_maximized.remove(id);
m_minimized.remove(id);
}
}
if (m_refreshQueued)
return;
m_refreshQueued = true;
QTimer::singleShot(0, this, [this]() {
m_refreshQueued = false;
refresh();
});
}
void SwayWindowBackend::handleNewWindow(qint64 windowId)
{
refresh();
const WindowInfo *window = findWindow(windowId);
if (!window || window->minimized || window->fullscreen)
return;
const int slot = m_cascadeIndex++ % 8;
if (slot == 0)
return;
const int offset = slot * 32;
run(QStringLiteral("[con_id=%1] move right %2 px, move down %3 px")
.arg(windowId)
.arg(offset)
.arg(offset));
}
bool SwayWindowBackend::run(const QString &command)
{
if (!m_ipc->isConnected())
return false;
const QJsonDocument reply = m_ipc->request(SwayIpcClient::RunCommand, command.toUtf8());
const QJsonArray results = reply.array();
bool ok = !results.isEmpty();
for (const QJsonValue &value : results) {
const QJsonObject object = value.toObject();
if (!object.value(QStringLiteral("success")).toBool(true)) {
qWarning("nebula: compositor command failed: %s (%s)",
qPrintable(command),
qPrintable(object.value(QStringLiteral("error")).toString()));
ok = false;
}
}
if (ok && !m_refreshQueued) {
m_refreshQueued = true;
QTimer::singleShot(0, this, [this]() {
m_refreshQueued = false;
refresh();
});
}
return ok;
}
bool SwayWindowBackend::saveGeometry(qint64 windowId)
{
const WindowInfo *window = findWindow(windowId);
if (!window)
return false;
if (!m_savedGeometry.contains(windowId) && !m_maximized.contains(windowId))
m_savedGeometry.insert(windowId, QRect(window->x, window->y, window->width, window->height));
return true;
}
const WindowInfo *SwayWindowBackend::findWindow(qint64 windowId) const
{
for (const WindowInfo &window : m_windows) {
if (window.id == windowId)
return &window;
}
return nullptr;
}
bool SwayWindowBackend::isShellChrome(const QJsonObject &node)
{
const QString appId = node.value(QStringLiteral("app_id")).toString();
if (appId == QLatin1String("org.nebulaos.shell") || appId.startsWith(QLatin1String("nebula-")))
return true;
const QString name = node.value(QStringLiteral("name")).toString();
if (name == QLatin1String("Nebula Desktop")
|| name == QLatin1String("Nebula Top Bar")
|| name == QLatin1String("Nebula Launcher")) {
return true;
}
const QString shell = node.value(QStringLiteral("shell")).toString();
if (shell.contains(QLatin1String("layer"), Qt::CaseInsensitive))
return true;
return false;
}
void SwayWindowBackend::collect(const QJsonObject &node,
const QString &workspace,
const QString &output,
bool onScratchpad,
QVector<WindowInfo> *windows,
QVector<WorkspaceInfo> *workspaces)
{
const QString type = node.value(QStringLiteral("type")).toString();
const QString name = node.value(QStringLiteral("name")).toString();
QString nextWorkspace = workspace;
QString nextOutput = output;
bool nextScratchpad = onScratchpad;
if (type == QLatin1String("output")) {
nextOutput = name;
if (isPseudoOutput(name))
nextScratchpad = true;
} else if (type == QLatin1String("workspace")) {
nextWorkspace = name;
nextScratchpad = isScratchpadWorkspace(name);
if (!nextScratchpad) {
WorkspaceInfo info;
info.id = name;
info.name = name;
info.number = node.value(QStringLiteral("num")).toInt(-1);
info.focused = node.value(QStringLiteral("focused")).toBool();
info.visible = node.value(QStringLiteral("visible")).toBool();
info.output = node.value(QStringLiteral("output")).toString(output);
const QJsonObject rect = node.value(QStringLiteral("rect")).toObject();
info.x = rect.value(QStringLiteral("x")).toInt();
info.y = rect.value(QStringLiteral("y")).toInt();
info.width = rect.value(QStringLiteral("width")).toInt();
info.height = rect.value(QStringLiteral("height")).toInt();
workspaces->append(info);
}
}
if (nodeLooksLikeView(node) && !isShellChrome(node)) {
WindowInfo window;
window.id = jsonId(node.value(QStringLiteral("id")));
window.title = name;
window.appId = node.value(QStringLiteral("app_id")).toString();
const QJsonObject properties = node.value(QStringLiteral("window_properties")).toObject();
window.windowClass = properties.value(QStringLiteral("class")).toString();
if (window.title.isEmpty())
window.title = properties.value(QStringLiteral("title")).toString();
window.workspace = nextWorkspace;
window.output = nextOutput;
window.focused = node.value(QStringLiteral("focused")).toBool();
window.floating = nodeIsFloating(node);
window.fullscreen = node.value(QStringLiteral("fullscreen_mode")).toInt() > 0;
window.minimized = nextScratchpad;
const QJsonObject rect = node.value(QStringLiteral("rect")).toObject();
window.x = rect.value(QStringLiteral("x")).toInt();
window.y = rect.value(QStringLiteral("y")).toInt();
window.width = rect.value(QStringLiteral("width")).toInt();
window.height = rect.value(QStringLiteral("height")).toInt();
windows->append(window);
}
const QJsonArray nodes = node.value(QStringLiteral("nodes")).toArray();
for (const QJsonValue &child : nodes)
collect(child.toObject(), nextWorkspace, nextOutput, nextScratchpad, windows, workspaces);
const QJsonArray floating = node.value(QStringLiteral("floating_nodes")).toArray();
for (const QJsonValue &child : floating)
collect(child.toObject(), nextWorkspace, nextOutput, nextScratchpad, windows, workspaces);
}
@@ -0,0 +1,63 @@
#pragma once
#include "SwayIpcClient.hpp"
#include "WindowBackend.hpp"
#include <QHash>
#include <QJsonObject>
#include <QRect>
#include <QSet>
#include <QTimer>
#include <QVector>
class SwayWindowBackend : public WindowBackend
{
Q_OBJECT
public:
explicit SwayWindowBackend(QObject *parent = nullptr);
bool isConnected() const override;
QVector<WindowInfo> windows() const override;
QVector<WorkspaceInfo> workspaces() const override;
qint64 focusedWindowId() const override;
bool focusWindow(qint64 windowId) override;
bool closeWindow(qint64 windowId) override;
bool maximizeWindow(qint64 windowId) override;
bool restoreWindow(qint64 windowId) override;
bool minimizeWindow(qint64 windowId) override;
bool setWindowFloating(qint64 windowId, bool floating) override;
bool moveWindow(qint64 windowId, int x, int y) override;
bool resizeWindow(qint64 windowId, int width, int height) override;
bool snapWindow(qint64 windowId, SnapEdge edge) override;
bool switchWorkspace(const QString &workspaceId) override;
private:
void tryConnect();
void refresh();
void handleEvent(quint32 type, const QJsonDocument &payload);
void handleNewWindow(qint64 windowId);
bool run(const QString &command);
bool saveGeometry(qint64 windowId);
const WindowInfo *findWindow(qint64 windowId) const;
static bool isShellChrome(const QJsonObject &node);
static void collect(const QJsonObject &node,
const QString &workspace,
const QString &output,
bool onScratchpad,
QVector<WindowInfo> *windows,
QVector<WorkspaceInfo> *workspaces);
SwayIpcClient *m_ipc = nullptr;
QTimer *m_retryTimer = nullptr;
QVector<WindowInfo> m_windows;
QVector<WorkspaceInfo> m_workspaces;
QHash<qint64, QRect> m_savedGeometry;
QSet<qint64> m_maximized;
QSet<qint64> m_minimized;
qint64 m_focusedWindowId = 0;
int m_connectAttempts = 0;
int m_cascadeIndex = 0;
bool m_refreshQueued = false;
};