diff --git a/Helpers/Hypr.qml b/Helpers/Hypr.qml new file mode 100644 index 0000000..1c2b6bc --- /dev/null +++ b/Helpers/Hypr.qml @@ -0,0 +1,130 @@ +pragma Singleton + +import ZShell +import ZShell.Internal +import Quickshell +import Quickshell.Hyprland +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + readonly property var toplevels: Hyprland.toplevels + readonly property var workspaces: Hyprland.workspaces + readonly property var monitors: Hyprland.monitors + + readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel?.wayland?.activated ? Hyprland.activeToplevel : null + readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace + readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor + readonly property int activeWsId: focusedWorkspace?.id ?? 1 + + readonly property HyprKeyboard keyboard: extras.devices.keyboards.find(kb => kb.main) ?? null + readonly property bool capsLock: keyboard?.capsLock ?? false + readonly property bool numLock: keyboard?.numLock ?? false + readonly property string defaultKbLayout: keyboard?.layout.split(",")[0] ?? "??" + readonly property string kbLayoutFull: keyboard?.activeKeymap ?? "Unknown" + readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? "??" + readonly property var kbMap: new Map() + + readonly property alias extras: extras + readonly property alias options: extras.options + readonly property alias devices: extras.devices + + property bool hadKeyboard + + signal configReloaded + + function dispatch(request: string): void { + Hyprland.dispatch(request); + } + + function monitorFor(screen: ShellScreen): HyprlandMonitor { + return Hyprland.monitorFor(screen); + } + + function reloadDynamicConfs(): void { + extras.batchMessage(["keyword bindlni ,Caps_Lock,global,caelestia:refreshDevices", "keyword bindlni ,Num_Lock,global,caelestia:refreshDevices"]); + } + + Component.onCompleted: reloadDynamicConfs() + + Connections { + target: Hyprland + + function onRawEvent(event: HyprlandEvent): void { + const n = event.name; + if (n.endsWith("v2")) + return; + + if (n === "configreloaded") { + root.configReloaded(); + root.reloadDynamicConfs(); + } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { + Hyprland.refreshWorkspaces(); + Hyprland.refreshMonitors(); + } else if (["openwindow", "closewindow", "movewindow"].includes(n)) { + Hyprland.refreshToplevels(); + Hyprland.refreshWorkspaces(); + } else if (n.includes("mon")) { + Hyprland.refreshMonitors(); + } else if (n.includes("workspace")) { + Hyprland.refreshWorkspaces(); + } else if (n.includes("window") || n.includes("group") || ["pin", "fullscreen", "changefloatingmode", "minimize"].includes(n)) { + Hyprland.refreshToplevels(); + } + } + } + + FileView { + id: kbLayoutFile + + path: Quickshell.env("CAELESTIA_XKB_RULES_PATH") || "/usr/share/X11/xkb/rules/base.lst" + onLoaded: { + const layoutMatch = text().match(/! layout\n([\s\S]*?)\n\n/); + if (layoutMatch) { + const lines = layoutMatch[1].split("\n"); + for (const line of lines) { + if (!line.trim() || line.trim().startsWith("!")) + continue; + + const match = line.match(/^\s*([a-z]{2,})\s+([a-zA-Z() ]+)$/); + if (match) + root.kbMap.set(match[2], match[1]); + } + } + + const variantMatch = text().match(/! variant\n([\s\S]*?)\n\n/); + if (variantMatch) { + const lines = variantMatch[1].split("\n"); + for (const line of lines) { + if (!line.trim() || line.trim().startsWith("!")) + continue; + + const match = line.match(/^\s*([a-zA-Z0-9_-]+)\s+([a-z]{2,}): (.+)$/); + if (match) + root.kbMap.set(match[3], match[2]); + } + } + } + } + + IpcHandler { + target: "hypr" + + function refreshDevices(): void { + extras.refreshDevices(); + } + } + + GlobalShortcut { + name: "refreshDevices" + appid: "ZShell" + onPressed: extras.refreshDevices() + onReleased: extras.refreshDevices() + } + + HyprExtras { + id: extras + } +} diff --git a/Helpers/Picker.qml b/Helpers/Picker.qml index 3485b65..8508cff 100644 --- a/Helpers/Picker.qml +++ b/Helpers/Picker.qml @@ -3,11 +3,11 @@ pragma ComponentBehavior: Bound import ZShell import Quickshell import Quickshell.Wayland -import Quickshell.Hyprland import QtQuick import QtQuick.Effects import qs.Modules import qs.Config +import qs.Helpers MouseArea { id: root @@ -17,8 +17,8 @@ MouseArea { property bool onClient - property real realRounding: 6 - property real realBorderWidth: 1 + property real realBorderWidth: onClient ? (Hypr.options["general:border_size"] ?? 1) : 2 + property real realRounding: onClient ? (Hypr.options["decoration:rounding"] ?? 0) : 0 property real ssx property real ssy @@ -34,14 +34,14 @@ MouseArea { property real sh: Math.abs(sy - ey) property list clients: { - const mon = Hyprland.monitorFor(screen); + const mon = Hypr.monitorFor(screen); if (!mon) return []; const special = mon.lastIpcObject.specialWorkspace; const wsId = special.name ? special.id : mon.activeWorkspace.id; - return Hyprland.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => { + return Hypr.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => { const ac = a.lastIpcObject; const bc = b.lastIpcObject; return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating); @@ -84,6 +84,7 @@ MouseArea { cursorShape: Qt.CrossCursor Component.onCompleted: { + Hypr.extras.refreshOptions(); if (loader.freeze) clients = clients; diff --git a/Modules/GroupListView.qml b/Modules/GroupListView.qml index c4140b7..ea77724 100644 --- a/Modules/GroupListView.qml +++ b/Modules/GroupListView.qml @@ -103,8 +103,8 @@ Repeater { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.fillHeight: true Layout.preferredWidth: 30 - color: collapseArea.containsMouse ? "#15FFFFFF" : "transparent" - radius: groupColumn.isExpanded ? 4 : Layout.preferredHeight / 2 + color: !groupColumn.isExpanded ? "#E53935" : collapseArea.containsMouse ? "#15FFFFFF" : "transparent" + radius: groupColumn.isExpanded ? 4 : height / 2 visible: true Text { diff --git a/Modules/SystemStats.qml b/Modules/SystemStats.qml deleted file mode 100644 index e69de29..0000000 diff --git a/Modules/TrackedNotification.qml b/Modules/TrackedNotification.qml index 4ab05d2..6aebee1 100644 --- a/Modules/TrackedNotification.qml +++ b/Modules/TrackedNotification.qml @@ -131,6 +131,20 @@ PanelWindow { border.color: "#555555" radius: 8 + // Rectangle { + // anchors.bottom: parent.bottom + // anchors.left: parent.left + // anchors.right: parent.right + // height: 4 + // bottomLeftRadius: parent.radius + // bottomRightRadius: parent.radius + // color: "#40000000" + // Rectangle { + // anchors.fill: parent + // width: parent.width * ( Math.max(0, Math.min( rootItem.modelData.timer ) ) ) + // } + // } + Component.onCompleted: { root.notifRegions.push( notifRegion.createObject(root, { item: backgroundRect })); } @@ -217,6 +231,23 @@ PanelWindow { } } } + + ElapsedTimer { + id: timer + } + + } + MouseArea { + property int timePassed + anchors.fill: parent + hoverEnabled: true + onEntered: { + // rootItem.modelData.timer.interval = 5000 - timer.restartMs(); + rootItem.modelData.timer.stop(); + } + onExited: { + rootItem.modelData.timer.start(); + } } } } diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index 118bcdf..5e0187b 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -1,5 +1,3 @@ -//@ pragma Env QT_STYLE_OVERRIDE=Breeze - import QtQuick import Quickshell import Quickshell.Services.SystemTray @@ -50,12 +48,6 @@ MouseArea { Connections { target: trayMenu - function onVisibleChanged() { - if ( !trayMenu.visible ) { - trayMenu.trayMenu = null; - } - } - function onFinishedLoading() { if ( !root.hasLoaded ) trayMenu.visible = false; @@ -67,9 +59,8 @@ MouseArea { if ( mouse.button === Qt.LeftButton ) { root.item.activate(); } else if ( mouse.button === Qt.RightButton ) { - if ( root.item?.menu !== trayMenu.trayMenu ) { - trayMenu.trayMenu = root.item?.menu; - } + trayMenu.trayMenu = null; + trayMenu.trayMenu = root.item?.menu; trayMenu.visible = !trayMenu.visible; trayMenu.focusGrab = true; } diff --git a/Modules/TrayMenu.qml b/Modules/TrayMenu.qml index 13044e1..15a67bc 100644 --- a/Modules/TrayMenu.qml +++ b/Modules/TrayMenu.qml @@ -7,6 +7,7 @@ import QtQuick.Layouts import Qt5Compat.GraphicalEffects import Quickshell.Hyprland import QtQml +import qs.Effects PanelWindow { id: root @@ -58,26 +59,11 @@ PanelWindow { } onVisibleChanged: { - if ( visible ) { - scaleValue = 0; - scaleAnimation.start(); - } else { + if ( !visible ) root.menuStack.pop(); backEntry.visible = false; - } - } - NumberAnimation { - id: scaleAnimation - target: root - property: "scaleValue" - from: 0 - to: 1 - duration: 150 - easing.type: Easing.OutCubic - onStopped: { - root.updateMask(); - } + openAnim.start(); } HyprlandFocusGrab { @@ -85,7 +71,7 @@ PanelWindow { windows: [ root ] active: false onCleared: { - root.visible = false; + closeAnim.start(); } } @@ -143,6 +129,54 @@ PanelWindow { } } + ParallelAnimation { + id: closeAnim + Anim { + target: menuRect + property: "implicitHeight" + to: 0 + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + Anim { + targets: [ menuRect, shadowRect ] + property: "opacity" + from: 1 + to: 0 + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + onFinished: { + root.visible = false; + } + } + + ParallelAnimation { + id: openAnim + Anim { + target: menuRect + property: "implicitHeight" + from: 0 + to: listLayout.contentHeight + ( root.menuStack.length > 0 ? root.entryHeight + 10 : 10 ) + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + Anim { + targets: [ menuRect, shadowRect ] + property: "opacity" + from: 0 + to: 1 + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + } + } + + ShadowRect { + id: shadowRect + anchors.fill: menuRect + radius: menuRect.radius + } + Rectangle { id: menuRect x: Math.round( root.trayItemRect.x - ( menuRect.implicitWidth / 2 ) + 11 ) @@ -154,15 +188,6 @@ PanelWindow { border.color: "#40FFFFFF" clip: true - transform: [ - Scale { - origin.x: menuRect.width / 2 - origin.y: 0 - xScale: root.scaleValue - yScale: root.scaleValue - } - ] - Behavior on implicitWidth { NumberAnimation { duration: MaterialEasing.expressiveEffectsTime @@ -242,7 +267,7 @@ PanelWindow { if ( !menuItem.modelData.hasChildren ) { if ( menuItem.modelData.enabled ) { menuItem.modelData.triggered(); - root.visible = false; + closeAnim.start(); } } else { root.menuStack.push(root.trayMenu); diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index 860e67e..2d9880c 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -41,3 +41,4 @@ qml_module(ZShell ) add_subdirectory(Models) +add_subdirectory(Internal) diff --git a/Plugins/ZShell/Internal/CMakeLists.txt b/Plugins/ZShell/Internal/CMakeLists.txt new file mode 100644 index 0000000..7537037 --- /dev/null +++ b/Plugins/ZShell/Internal/CMakeLists.txt @@ -0,0 +1,6 @@ +qml_module(ZShell-internal + URI ZShell.Internal + SOURCES + hyprextras.hpp hyprextras.cpp + hyprdevices.hpp hyprdevices.cpp +) diff --git a/Plugins/ZShell/Internal/hyprdevices.cpp b/Plugins/ZShell/Internal/hyprdevices.cpp new file mode 100644 index 0000000..7b877dc --- /dev/null +++ b/Plugins/ZShell/Internal/hyprdevices.cpp @@ -0,0 +1,134 @@ +#include "hyprdevices.hpp" + +#include + +namespace ZShell::internal::hypr { + +HyprKeyboard::HyprKeyboard(QJsonObject ipcObject, QObject* parent) + : QObject(parent) + , m_lastIpcObject(ipcObject) {} + +QVariantHash HyprKeyboard::lastIpcObject() const { + return m_lastIpcObject.toVariantHash(); +} + +QString HyprKeyboard::address() const { + return m_lastIpcObject.value("address").toString(); +} + +QString HyprKeyboard::name() const { + return m_lastIpcObject.value("name").toString(); +} + +QString HyprKeyboard::layout() const { + return m_lastIpcObject.value("layout").toString(); +} + +QString HyprKeyboard::activeKeymap() const { + return m_lastIpcObject.value("active_keymap").toString(); +} + +bool HyprKeyboard::capsLock() const { + return m_lastIpcObject.value("capsLock").toBool(); +} + +bool HyprKeyboard::numLock() const { + return m_lastIpcObject.value("numLock").toBool(); +} + +bool HyprKeyboard::main() const { + return m_lastIpcObject.value("main").toBool(); +} + +bool HyprKeyboard::updateLastIpcObject(QJsonObject object) { + if (m_lastIpcObject == object) { + return false; + } + + const auto last = m_lastIpcObject; + + m_lastIpcObject = object; + emit lastIpcObjectChanged(); + + bool dirty = false; + if (last.value("address") != object.value("address")) { + dirty = true; + emit addressChanged(); + } + if (last.value("name") != object.value("name")) { + dirty = true; + emit nameChanged(); + } + if (last.value("layout") != object.value("layout")) { + dirty = true; + emit layoutChanged(); + } + if (last.value("active_keymap") != object.value("active_keymap")) { + dirty = true; + emit activeKeymapChanged(); + } + if (last.value("capsLock") != object.value("capsLock")) { + dirty = true; + emit capsLockChanged(); + } + if (last.value("numLock") != object.value("numLock")) { + dirty = true; + emit numLockChanged(); + } + if (last.value("main") != object.value("main")) { + dirty = true; + emit mainChanged(); + } + return dirty; +} + +HyprDevices::HyprDevices(QObject* parent) + : QObject(parent) {} + +QQmlListProperty HyprDevices::keyboards() { + return QQmlListProperty(this, &m_keyboards); +} + +bool HyprDevices::updateLastIpcObject(QJsonObject object) { + const auto val = object.value("keyboards").toArray(); + bool dirty = false; + + for (auto it = m_keyboards.begin(); it != m_keyboards.end();) { + auto* const keyboard = *it; + const auto inNewValues = std::any_of(val.begin(), val.end(), [keyboard](const QJsonValue& o) { + return o.toObject().value("address").toString() == keyboard->address(); + }); + + if (!inNewValues) { + dirty = true; + it = m_keyboards.erase(it); + keyboard->deleteLater(); + } else { + ++it; + } + } + + for (const auto& o : val) { + const auto obj = o.toObject(); + const auto addr = obj.value("address").toString(); + + auto it = std::find_if(m_keyboards.begin(), m_keyboards.end(), [addr](const HyprKeyboard* kb) { + return kb->address() == addr; + }); + + if (it != m_keyboards.end()) { + dirty |= (*it)->updateLastIpcObject(obj); + } else { + dirty = true; + m_keyboards << new HyprKeyboard(obj, this); + } + } + + if (dirty) { + emit keyboardsChanged(); + } + + return dirty; +} + +} // namespace ZShell::internal::hypr diff --git a/Plugins/ZShell/Internal/hyprdevices.hpp b/Plugins/ZShell/Internal/hyprdevices.hpp new file mode 100644 index 0000000..4d1d342 --- /dev/null +++ b/Plugins/ZShell/Internal/hyprdevices.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include + +namespace ZShell::internal::hypr { + +class HyprKeyboard : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("HyprKeyboard instances can only be retrieved from a HyprDevices") + + Q_PROPERTY(QVariantHash lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged) + Q_PROPERTY(QString address READ address NOTIFY addressChanged) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(QString layout READ layout NOTIFY layoutChanged) + Q_PROPERTY(QString activeKeymap READ activeKeymap NOTIFY activeKeymapChanged) + Q_PROPERTY(bool capsLock READ capsLock NOTIFY capsLockChanged) + Q_PROPERTY(bool numLock READ numLock NOTIFY numLockChanged) + Q_PROPERTY(bool main READ main NOTIFY mainChanged) + +public: + explicit HyprKeyboard(QJsonObject ipcObject, QObject* parent = nullptr); + + [[nodiscard]] QVariantHash lastIpcObject() const; + [[nodiscard]] QString address() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString layout() const; + [[nodiscard]] QString activeKeymap() const; + [[nodiscard]] bool capsLock() const; + [[nodiscard]] bool numLock() const; + [[nodiscard]] bool main() const; + + bool updateLastIpcObject(QJsonObject object); + +signals: + void lastIpcObjectChanged(); + void addressChanged(); + void nameChanged(); + void layoutChanged(); + void activeKeymapChanged(); + void capsLockChanged(); + void numLockChanged(); + void mainChanged(); + +private: + QJsonObject m_lastIpcObject; +}; + +class HyprDevices : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("HyprDevices instances can only be retrieved from a HyprExtras") + + Q_PROPERTY( + QQmlListProperty keyboards READ keyboards NOTIFY keyboardsChanged) + +public: + explicit HyprDevices(QObject* parent = nullptr); + + [[nodiscard]] QQmlListProperty keyboards(); + + bool updateLastIpcObject(QJsonObject object); + +signals: + void keyboardsChanged(); + +private: + QList m_keyboards; +}; + +} // namespace ZShell::internal::hypr diff --git a/Plugins/ZShell/Internal/hyprextras.cpp b/Plugins/ZShell/Internal/hyprextras.cpp new file mode 100644 index 0000000..6ff5f3b --- /dev/null +++ b/Plugins/ZShell/Internal/hyprextras.cpp @@ -0,0 +1,217 @@ +#include "hyprextras.hpp" + +#include +#include +#include +#include + +namespace ZShell::internal::hypr { + +HyprExtras::HyprExtras(QObject* parent) + : QObject(parent) + , m_requestSocket("") + , m_eventSocket("") + , m_socket(nullptr) + , m_socketValid(false) + , m_devices(new HyprDevices(this)) { + const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); + if (his.isEmpty()) { + qWarning() + << "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; + return; + } + + auto hyprDir = QString("%1/hypr/%2").arg(qEnvironmentVariable("XDG_RUNTIME_DIR"), his); + if (!QDir(hyprDir).exists()) { + hyprDir = "/tmp/hypr/" + his; + + if (!QDir(hyprDir).exists()) { + qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to " + "Hyprland socket."; + return; + } + } + + m_requestSocket = hyprDir + "/.socket.sock"; + m_eventSocket = hyprDir + "/.socket2.sock"; + + refreshOptions(); + refreshDevices(); + + m_socket = new QLocalSocket(this); + + QObject::connect(m_socket, &QLocalSocket::errorOccurred, this, &HyprExtras::socketError); + QObject::connect(m_socket, &QLocalSocket::stateChanged, this, &HyprExtras::socketStateChanged); + QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent); + + m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly); +} + +QVariantHash HyprExtras::options() const { + return m_options; +} + +HyprDevices* HyprExtras::devices() const { + return m_devices; +} + +void HyprExtras::message(const QString& message) { + if (message.isEmpty()) { + return; + } + + makeRequest(message, [](bool success, const QByteArray& res) { + if (!success) { + qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res); + } + }); +} + +void HyprExtras::batchMessage(const QStringList& messages) { + if (messages.isEmpty()) { + return; + } + + makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { + if (!success) { + qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res); + } + }); +} + +void HyprExtras::applyOptions(const QVariantHash& options) { + if (options.isEmpty()) { + return; + } + + QString request = "[[BATCH]]"; + for (auto it = options.constBegin(); it != options.constEnd(); ++it) { + request += QString("keyword %1 %2;").arg(it.key(), it.value().toString()); + } + + makeRequest(request, [this](bool success, const QByteArray& res) { + if (success) { + refreshOptions(); + } else { + qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res); + } + }); +} + +void HyprExtras::refreshOptions() { + if (!m_optionsRefresh.isNull()) { + m_optionsRefresh->close(); + } + + m_optionsRefresh = makeRequestJson("descriptions", [this](bool success, const QJsonDocument& response) { + m_optionsRefresh.reset(); + if (!success) { + return; + } + + const auto options = response.array(); + bool dirty = false; + + for (const auto& o : std::as_const(options)) { + const auto obj = o.toObject(); + const auto key = obj.value("value").toString(); + const auto value = obj.value("data").toObject().value("current").toVariant(); + if (m_options.value(key) != value) { + dirty = true; + m_options.insert(key, value); + } + } + + if (dirty) { + emit optionsChanged(); + } + }); +} + +void HyprExtras::refreshDevices() { + if (!m_devicesRefresh.isNull()) { + m_devicesRefresh->close(); + } + + m_devicesRefresh = makeRequestJson("devices", [this](bool success, const QJsonDocument& response) { + m_devicesRefresh.reset(); + if (success) { + m_devices->updateLastIpcObject(response.object()); + } + }); +} + +void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { + if (!m_socketValid) { + qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error; + } else { + qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error; + } +} + +void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { + if (state == QLocalSocket::UnconnectedState && m_socketValid) { + qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected."; + } + + m_socketValid = state == QLocalSocket::ConnectedState; +} + +void HyprExtras::readEvent() { + while (true) { + auto rawEvent = m_socket->readLine(); + if (rawEvent.isEmpty()) { + break; + } + rawEvent.truncate(rawEvent.length() - 1); // Remove trailing \n + const auto event = QByteArrayView(rawEvent.data(), rawEvent.indexOf(">>")); + handleEvent(QString::fromUtf8(event)); + } +} + +void HyprExtras::handleEvent(const QString& event) { + if (event == "configreloaded") { + refreshOptions(); + } else if (event == "activelayout") { + refreshDevices(); + } +} + +HyprExtras::SocketPtr HyprExtras::makeRequestJson( + const QString& request, const std::function& callback) { + return makeRequest("j/" + request, [callback](bool success, const QByteArray& response) { + callback(success, QJsonDocument::fromJson(response)); + }); +} + +HyprExtras::SocketPtr HyprExtras::makeRequest( + const QString& request, const std::function& callback) { + if (m_requestSocket.isEmpty()) { + return SocketPtr(); + } + + auto socket = SocketPtr::create(this); + + QObject::connect(socket.data(), &QLocalSocket::connected, this, [=, this]() { + QObject::connect(socket.data(), &QLocalSocket::readyRead, this, [socket, callback]() { + const auto response = socket->readAll(); + callback(true, std::move(response)); + socket->close(); + }); + + socket->write(request.toUtf8()); + socket->flush(); + }); + + QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { + qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request; + callback(false, {}); + socket->close(); + }); + + socket->connectToServer(m_requestSocket); + + return socket; +} + +} // namespace ZShell::internal::hypr diff --git a/Plugins/ZShell/Internal/hyprextras.hpp b/Plugins/ZShell/Internal/hyprextras.hpp new file mode 100644 index 0000000..ee58170 --- /dev/null +++ b/Plugins/ZShell/Internal/hyprextras.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "hyprdevices.hpp" +#include +#include +#include + +namespace ZShell::internal::hypr { + +class HyprExtras : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged) + Q_PROPERTY(ZShell::internal::hypr::HyprDevices* devices READ devices CONSTANT) + +public: + explicit HyprExtras(QObject* parent = nullptr); + + [[nodiscard]] QVariantHash options() const; + [[nodiscard]] HyprDevices* devices() const; + + Q_INVOKABLE void message(const QString& message); + Q_INVOKABLE void batchMessage(const QStringList& messages); + Q_INVOKABLE void applyOptions(const QVariantHash& options); + + Q_INVOKABLE void refreshOptions(); + Q_INVOKABLE void refreshDevices(); + +signals: + void optionsChanged(); + +private: + using SocketPtr = QSharedPointer; + + QString m_requestSocket; + QString m_eventSocket; + QLocalSocket* m_socket; + bool m_socketValid; + + QVariantHash m_options; + HyprDevices* const m_devices; + + SocketPtr m_optionsRefresh; + SocketPtr m_devicesRefresh; + + void socketError(QLocalSocket::LocalSocketError error) const; + void socketStateChanged(QLocalSocket::LocalSocketState state); + void readEvent(); + void handleEvent(const QString& event); + + SocketPtr makeRequestJson(const QString& request, const std::function& callback); + SocketPtr makeRequest(const QString& request, const std::function& callback); +}; + +} // namespace ZShell::internal::hypr