plugin: add hypr extras (#690)

* move machine

* works

* prevent duplicate refreshes

* use instead of hyprctl proc
This commit is contained in:
2 * r + 2 * t 2025-09-24 23:48:47 +10:00 committed by GitHub
parent 17509aa2d9
commit 57e3989185
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 445 additions and 62 deletions

View file

@ -5,7 +5,6 @@ import qs.services
import qs.config import qs.config
import Caelestia import Caelestia
import Quickshell import Quickshell
import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
@ -16,13 +15,10 @@ MouseArea {
required property LazyLoader loader required property LazyLoader loader
required property ShellScreen screen required property ShellScreen screen
property int borderWidth
property int rounding
property bool onClient property bool onClient
property real realBorderWidth: onClient ? borderWidth : 2 property real realBorderWidth: onClient ? (Hypr.options["general:border_size"] ?? 1) : 2
property real realRounding: onClient ? rounding : 0 property real realRounding: onClient ? (Hypr.options["decoration:rounding"] ?? 0) : 0
property real ssx property real ssx
property real ssy property real ssy
@ -89,6 +85,8 @@ MouseArea {
cursorShape: Qt.CrossCursor cursorShape: Qt.CrossCursor
Component.onCompleted: { Component.onCompleted: {
Hypr.extras.refreshOptions();
// Break binding if frozen // Break binding if frozen
if (loader.freeze) if (loader.freeze)
clients = clients; 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 { Loader {
id: screencopy id: screencopy

View file

@ -23,6 +23,6 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Switch layout") text: qsTr("Switch layout")
onClicked: Quickshell.execDetached(["hyprctl", "switchxkblayout", "all", "next"]) onClicked: Hypr.message("switchxkblayout all next")
} }
} }

View file

@ -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) find_package(PkgConfig REQUIRED)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
@ -49,6 +49,7 @@ qml_module(caelestia
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
Qt::Concurrent
Qt::Sql Qt::Sql
PkgConfig::Qalculate PkgConfig::Qalculate
) )

View file

@ -3,8 +3,11 @@ qml_module(caelestia-internal
SOURCES SOURCES
cachingimagemanager.hpp cachingimagemanager.cpp cachingimagemanager.hpp cachingimagemanager.cpp
circularindicatormanager.hpp circularindicatormanager.cpp circularindicatormanager.hpp circularindicatormanager.cpp
hyprdevices.hpp hyprdevices.cpp
hyprextras.hpp hyprextras.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
Qt::Concurrent Qt::Concurrent
Qt::Network
) )

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
import qs.services
import qs.config import qs.config
import Caelestia import Caelestia
import Quickshell import Quickshell
@ -12,7 +13,7 @@ Singleton {
property alias enabled: props.enabled property alias enabled: props.enabled
function setDynamicConfs(): void { 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: { onEnabledChanged: {
@ -21,7 +22,7 @@ Singleton {
if (Config.utilities.toasts.gameModeChanged) if (Config.utilities.toasts.gameModeChanged)
Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad"); Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad");
} else { } else {
Quickshell.execDetached(["hyprctl", "reload"]); Hypr.message("reload");
if (Config.utilities.toasts.gameModeChanged) if (Config.utilities.toasts.gameModeChanged)
Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad"); Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad");
} }
@ -30,7 +31,7 @@ Singleton {
PersistentProperties { PersistentProperties {
id: props id: props
property bool enabled property bool enabled: Hypr.options["animations:enabled"] === 0
reloadableId: "gameMode" 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 { IpcHandler {
target: "gameMode" target: "gameMode"

View file

@ -3,6 +3,7 @@ pragma Singleton
import qs.components.misc import qs.components.misc
import qs.config import qs.config
import Caelestia import Caelestia
import Caelestia.Internal
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
@ -20,16 +21,24 @@ Singleton {
readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor
readonly property int activeWsId: focusedWorkspace?.id ?? 1 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 capsLock: keyboard?.capsLock ?? false
readonly property bool numLock: keyboard?.numLock ?? false readonly property bool numLock: keyboard?.numLock ?? false
readonly property string defaultKbLayout: keyboard?.layout.split(",")[0] ?? "??" 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 string kbLayout: kbMap.get(kbLayoutFull) ?? "??"
readonly property var kbMap: new Map() 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 signal configReloaded
function message(message: string): void {
extras.message(message);
}
function dispatch(request: string): void { function dispatch(request: string): void {
Hyprland.dispatch(request); Hyprland.dispatch(request);
} }
@ -68,9 +77,7 @@ Singleton {
if (n === "configreloaded") { if (n === "configreloaded") {
root.configReloaded(); root.configReloaded();
setDynamicConfsProc.running = true; extras.message("[[BATCH]]keyword bindln ,Caps_Lock,global,caelestia:refreshDevices;keyword bindln ,Num_Lock,global,caelestia:refreshDevices");
} else if (n === "activelayout") {
devicesProc.running = true;
} else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) { } else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) {
Hyprland.refreshWorkspaces(); Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors(); 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 { IpcHandler {
target: "hypr" target: "hypr"
function reloadDevices(): void { function refreshDevices(): void {
devicesProc.running = true; extras.refreshDevices();
} }
} }
CustomShortcut { CustomShortcut {
name: "reloadDevices" name: "refreshDevices"
description: "Reload devices" description: "Reload devices"
onPressed: devicesProc.running = true onPressed: extras.refreshDevices()
onReleased: devicesProc.running = true onReleased: extras.refreshDevices()
}
HyprExtras {
id: extras
} }
} }