diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-09-24 23:48:47 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-09-24 23:48:47 +1000 |
| commit | 57e3989185b8209a11d79bac477479019d515180 (patch) | |
| tree | 27e94dcaef9f41bb8f294eab1a91a27ce4da03d0 | |
| parent | plugin: sub namespace modules (diff) | |
| download | caelestia-shell-57e3989185b8209a11d79bac477479019d515180.tar.gz caelestia-shell-57e3989185b8209a11d79bac477479019d515180.tar.bz2 caelestia-shell-57e3989185b8209a11d79bac477479019d515180.zip | |
plugin: add hypr extras (#690)
* move machine
* works
* prevent duplicate refreshes
* use instead of hyprctl proc
| -rw-r--r-- | modules/areapicker/Picker.qml | 26 | ||||
| -rw-r--r-- | modules/bar/popouts/KbLayout.qml | 2 | ||||
| -rw-r--r-- | plugin/src/Caelestia/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | plugin/src/Caelestia/Internal/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | plugin/src/Caelestia/Internal/hyprdevices.cpp | 129 | ||||
| -rw-r--r-- | plugin/src/Caelestia/Internal/hyprdevices.hpp | 72 | ||||
| -rw-r--r-- | plugin/src/Caelestia/Internal/hyprextras.cpp | 158 | ||||
| -rw-r--r-- | plugin/src/Caelestia/Internal/hyprextras.hpp | 51 | ||||
| -rw-r--r-- | services/GameMode.qml | 15 | ||||
| -rw-r--r-- | services/Hypr.qml | 48 |
10 files changed, 445 insertions, 62 deletions
diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index d51d1f3..1625ccf 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -5,7 +5,6 @@ import qs.services import qs.config import Caelestia import Quickshell -import Quickshell.Io import Quickshell.Wayland import QtQuick import QtQuick.Effects @@ -16,13 +15,10 @@ MouseArea { required property LazyLoader loader required property ShellScreen screen - property int borderWidth - property int rounding - property bool onClient - property real realBorderWidth: onClient ? borderWidth : 2 - property real realRounding: onClient ? rounding : 0 + 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 @@ -89,6 +85,8 @@ MouseArea { cursorShape: Qt.CrossCursor Component.onCompleted: { + Hypr.extras.refreshOptions(); + // Break binding if frozen if (loader.freeze) clients = clients; @@ -186,22 +184,6 @@ MouseArea { } } - Process { - running: true - command: ["hyprctl", "-j", "getoption", "general:border_size"] - stdout: StdioCollector { - onStreamFinished: root.borderWidth = JSON.parse(text).int - } - } - - Process { - running: true - command: ["hyprctl", "-j", "getoption", "decoration:rounding"] - stdout: StdioCollector { - onStreamFinished: root.rounding = JSON.parse(text).int - } - } - Loader { id: screencopy diff --git a/modules/bar/popouts/KbLayout.qml b/modules/bar/popouts/KbLayout.qml index b12f2ef..4ecea9e 100644 --- a/modules/bar/popouts/KbLayout.qml +++ b/modules/bar/popouts/KbLayout.qml @@ -23,6 +23,6 @@ ColumnLayout { Layout.fillWidth: true text: qsTr("Switch layout") - onClicked: Quickshell.execDetached(["hyprctl", "switchxkblayout", "all", "next"]) + onClicked: Hypr.message("switchxkblayout all next") } } diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 9dea712..d344ef7 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql) +find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network) find_package(PkgConfig REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) @@ -49,6 +49,7 @@ qml_module(caelestia LIBRARIES Qt::Gui Qt::Quick + Qt::Concurrent Qt::Sql PkgConfig::Qalculate ) diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index 62aa516..96538b8 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -3,8 +3,11 @@ qml_module(caelestia-internal SOURCES cachingimagemanager.hpp cachingimagemanager.cpp circularindicatormanager.hpp circularindicatormanager.cpp + hyprdevices.hpp hyprdevices.cpp + hyprextras.hpp hyprextras.cpp LIBRARIES Qt::Gui Qt::Quick Qt::Concurrent + Qt::Network ) diff --git a/plugin/src/Caelestia/Internal/hyprdevices.cpp b/plugin/src/Caelestia/Internal/hyprdevices.cpp new file mode 100644 index 0000000..8f63d67 --- /dev/null +++ b/plugin/src/Caelestia/Internal/hyprdevices.cpp @@ -0,0 +1,129 @@ +#include "hyprdevices.hpp" + +#include <qjsonarray.h> + +namespace caelestia::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(const 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) {} + +QList<HyprKeyboard*> HyprDevices::keyboards() const { + return m_keyboards; +} + +bool HyprDevices::updateLastIpcObject(const QJsonObject& object) { + const auto val = object.value("keyboards").toArray(); + bool dirty = false; + + for (const auto& keyboard : std::as_const(m_keyboards)) { + if (std::find_if(val.begin(), val.end(), [keyboard](const QJsonValue& object) { + return object.toObject().value("address").toString() == keyboard->address(); + }) == val.end()) { + dirty = true; + m_keyboards.removeAll(keyboard); + keyboard->deleteLater(); + } + } + + 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* keyboard) { + return keyboard->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 caelestia::internal::hypr diff --git a/plugin/src/Caelestia/Internal/hyprdevices.hpp b/plugin/src/Caelestia/Internal/hyprdevices.hpp new file mode 100644 index 0000000..ad762b9 --- /dev/null +++ b/plugin/src/Caelestia/Internal/hyprdevices.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include <qjsonobject.h> +#include <qobject.h> +#include <qqmlintegration.h> + +namespace caelestia::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(const 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(QList<HyprKeyboard*> keyboards READ keyboards NOTIFY keyboardsChanged) + +public: + explicit HyprDevices(QObject* parent = nullptr); + + [[nodiscard]] QList<HyprKeyboard*> keyboards() const; + + bool updateLastIpcObject(const QJsonObject& object); + +signals: + void keyboardsChanged(); + +private: + QList<HyprKeyboard*> m_keyboards; +}; + +} // namespace caelestia::internal::hypr diff --git a/plugin/src/Caelestia/Internal/hyprextras.cpp b/plugin/src/Caelestia/Internal/hyprextras.cpp new file mode 100644 index 0000000..23495b2 --- /dev/null +++ b/plugin/src/Caelestia/Internal/hyprextras.cpp @@ -0,0 +1,158 @@ +#include "hyprextras.hpp" + +#include <qdir.h> +#include <qjsonarray.h> +#include <qlocalsocket.h> +#include <qvariant.h> + +namespace caelestia::internal::hypr { + +HyprExtras::HyprExtras(QObject* parent) + : QObject(parent) + , m_requestSocket("") + , m_eventSocket("") + , m_socket(nullptr) + , 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 + "/.event.sock"; + + refreshOptions(); + refreshDevices(); + + m_socket = new QLocalSocket(this); + QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent); + m_socket->connectToServer(m_eventSocket); +} + +QVariantHash HyprExtras::options() const { + return m_options; +} + +HyprDevices* HyprExtras::devices() const { + return m_devices; +} + +void HyprExtras::message(const QString& message) { + makeRequest(message, [](bool, const QByteArray&) { + }); +} + +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::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<void(bool, QJsonDocument)>& 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<void(bool, QByteArray)>& 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 caelestia::internal::hypr diff --git a/plugin/src/Caelestia/Internal/hyprextras.hpp b/plugin/src/Caelestia/Internal/hyprextras.hpp new file mode 100644 index 0000000..ca1b2f2 --- /dev/null +++ b/plugin/src/Caelestia/Internal/hyprextras.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "hyprdevices.hpp" +#include <qlocalsocket.h> +#include <qobject.h> +#include <qqmlintegration.h> + +namespace caelestia::internal::hypr { + +class HyprExtras : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged) + Q_PROPERTY(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 refreshOptions(); + Q_INVOKABLE void refreshDevices(); + +signals: + void optionsChanged(); + +private: + using SocketPtr = QSharedPointer<QLocalSocket>; + + QString m_requestSocket; + QString m_eventSocket; + QLocalSocket* m_socket; + + QVariantHash m_options; + HyprDevices* const m_devices; + + SocketPtr m_optionsRefresh; + SocketPtr m_devicesRefresh; + + void readEvent(); + void handleEvent(const QString& event); + + SocketPtr makeRequestJson(const QString& request, const std::function<void(bool, QJsonDocument)>& callback); + SocketPtr makeRequest(const QString& request, const std::function<void(bool, QByteArray)>& callback); +}; + +} // namespace caelestia::internal::hypr diff --git a/services/GameMode.qml b/services/GameMode.qml index 6771d59..edd741c 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -1,5 +1,6 @@ pragma Singleton +import qs.services import qs.config import Caelestia import Quickshell @@ -12,7 +13,7 @@ Singleton { property alias enabled: props.enabled function setDynamicConfs(): void { - Quickshell.execDetached(["hyprctl", "--batch", "keyword animations:enabled 0;keyword decoration:shadow:enabled 0;keyword decoration:blur:enabled 0;keyword general:gaps_in 0;keyword general:gaps_out 0;keyword general:border_size 1;keyword decoration:rounding 0;keyword general:allow_tearing 1"]); + Hypr.message("[[BATCH]]keyword animations:enabled 0;keyword decoration:shadow:enabled 0;keyword decoration:blur:enabled 0;keyword general:gaps_in 0;keyword general:gaps_out 0;keyword general:border_size 1;keyword decoration:rounding 0;keyword general:allow_tearing 1"); } onEnabledChanged: { @@ -21,7 +22,7 @@ Singleton { if (Config.utilities.toasts.gameModeChanged) Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad"); } else { - Quickshell.execDetached(["hyprctl", "reload"]); + Hypr.message("reload"); if (Config.utilities.toasts.gameModeChanged) Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad"); } @@ -30,7 +31,7 @@ Singleton { PersistentProperties { id: props - property bool enabled + property bool enabled: Hypr.options["animations:enabled"] === 0 reloadableId: "gameMode" } @@ -44,14 +45,6 @@ Singleton { } } - Process { - running: true - command: ["hyprctl", "getoption", "animations:enabled", "-j"] - stdout: StdioCollector { - onStreamFinished: props.enabled = JSON.parse(text).int === 0 - } - } - IpcHandler { target: "gameMode" diff --git a/services/Hypr.qml b/services/Hypr.qml index cd68d72..7a17212 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -3,6 +3,7 @@ pragma Singleton import qs.components.misc import qs.config import Caelestia +import Caelestia.Internal import Quickshell import Quickshell.Hyprland import Quickshell.Io @@ -20,16 +21,24 @@ Singleton { readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor readonly property int activeWsId: focusedWorkspace?.id ?? 1 - property var keyboard + 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?.active_keymap ?? "Unknown" + 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 + signal configReloaded + function message(message: string): void { + extras.message(message); + } + function dispatch(request: string): void { Hyprland.dispatch(request); } @@ -68,9 +77,7 @@ Singleton { if (n === "configreloaded") { root.configReloaded(); - setDynamicConfsProc.running = true; - } else if (n === "activelayout") { - devicesProc.running = true; + extras.message("[[BATCH]]keyword bindln ,Caps_Lock,global,caelestia:refreshDevices;keyword bindln ,Num_Lock,global,caelestia:refreshDevices"); } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { Hyprland.refreshWorkspaces(); Hyprland.refreshMonitors(); @@ -104,35 +111,22 @@ Singleton { } } - Process { - id: devicesProc - - running: true - command: ["hyprctl", "-j", "devices"] - stdout: StdioCollector { - onStreamFinished: root.keyboard = JSON.parse(text).keyboards.find(k => k.main) - } - } - - Process { - id: setDynamicConfsProc - - running: true - command: ["hyprctl", "--batch", "keyword bindln ,Caps_Lock,global,caelestia:reloadDevices;keyword bindln ,Num_Lock,global,caelestia:reloadDevices"] - } - IpcHandler { target: "hypr" - function reloadDevices(): void { - devicesProc.running = true; + function refreshDevices(): void { + extras.refreshDevices(); } } CustomShortcut { - name: "reloadDevices" + name: "refreshDevices" description: "Reload devices" - onPressed: devicesProc.running = true - onReleased: devicesProc.running = true + onPressed: extras.refreshDevices() + onReleased: extras.refreshDevices() + } + + HyprExtras { + id: extras } } |