update hypr toplevels
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-5
@@ -3,11 +3,11 @@ pragma ComponentBehavior: Bound
|
|||||||
import ZShell
|
import ZShell
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Hyprland
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: root
|
id: root
|
||||||
@@ -17,8 +17,8 @@ MouseArea {
|
|||||||
|
|
||||||
property bool onClient
|
property bool onClient
|
||||||
|
|
||||||
property real realRounding: 6
|
property real realBorderWidth: onClient ? (Hypr.options["general:border_size"] ?? 1) : 2
|
||||||
property real realBorderWidth: 1
|
property real realRounding: onClient ? (Hypr.options["decoration:rounding"] ?? 0) : 0
|
||||||
|
|
||||||
property real ssx
|
property real ssx
|
||||||
property real ssy
|
property real ssy
|
||||||
@@ -34,14 +34,14 @@ MouseArea {
|
|||||||
property real sh: Math.abs(sy - ey)
|
property real sh: Math.abs(sy - ey)
|
||||||
|
|
||||||
property list<var> clients: {
|
property list<var> clients: {
|
||||||
const mon = Hyprland.monitorFor(screen);
|
const mon = Hypr.monitorFor(screen);
|
||||||
if (!mon)
|
if (!mon)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
const special = mon.lastIpcObject.specialWorkspace;
|
const special = mon.lastIpcObject.specialWorkspace;
|
||||||
const wsId = special.name ? special.id : mon.activeWorkspace.id;
|
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 ac = a.lastIpcObject;
|
||||||
const bc = b.lastIpcObject;
|
const bc = b.lastIpcObject;
|
||||||
return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating);
|
return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating);
|
||||||
@@ -84,6 +84,7 @@ MouseArea {
|
|||||||
cursorShape: Qt.CrossCursor
|
cursorShape: Qt.CrossCursor
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
Hypr.extras.refreshOptions();
|
||||||
if (loader.freeze)
|
if (loader.freeze)
|
||||||
clients = clients;
|
clients = clients;
|
||||||
|
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ Repeater {
|
|||||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.preferredWidth: 30
|
Layout.preferredWidth: 30
|
||||||
color: collapseArea.containsMouse ? "#15FFFFFF" : "transparent"
|
color: !groupColumn.isExpanded ? "#E53935" : collapseArea.containsMouse ? "#15FFFFFF" : "transparent"
|
||||||
radius: groupColumn.isExpanded ? 4 : Layout.preferredHeight / 2
|
radius: groupColumn.isExpanded ? 4 : height / 2
|
||||||
visible: true
|
visible: true
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
|
|||||||
@@ -131,6 +131,20 @@ PanelWindow {
|
|||||||
border.color: "#555555"
|
border.color: "#555555"
|
||||||
radius: 8
|
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: {
|
Component.onCompleted: {
|
||||||
root.notifRegions.push( notifRegion.createObject(root, { item: backgroundRect }));
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-10
@@ -1,5 +1,3 @@
|
|||||||
//@ pragma Env QT_STYLE_OVERRIDE=Breeze
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Services.SystemTray
|
import Quickshell.Services.SystemTray
|
||||||
@@ -50,12 +48,6 @@ MouseArea {
|
|||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: trayMenu
|
target: trayMenu
|
||||||
function onVisibleChanged() {
|
|
||||||
if ( !trayMenu.visible ) {
|
|
||||||
trayMenu.trayMenu = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFinishedLoading() {
|
function onFinishedLoading() {
|
||||||
if ( !root.hasLoaded )
|
if ( !root.hasLoaded )
|
||||||
trayMenu.visible = false;
|
trayMenu.visible = false;
|
||||||
@@ -67,9 +59,8 @@ MouseArea {
|
|||||||
if ( mouse.button === Qt.LeftButton ) {
|
if ( mouse.button === Qt.LeftButton ) {
|
||||||
root.item.activate();
|
root.item.activate();
|
||||||
} else if ( mouse.button === Qt.RightButton ) {
|
} else if ( mouse.button === Qt.RightButton ) {
|
||||||
if ( root.item?.menu !== trayMenu.trayMenu ) {
|
trayMenu.trayMenu = null;
|
||||||
trayMenu.trayMenu = root.item?.menu;
|
trayMenu.trayMenu = root.item?.menu;
|
||||||
}
|
|
||||||
trayMenu.visible = !trayMenu.visible;
|
trayMenu.visible = !trayMenu.visible;
|
||||||
trayMenu.focusGrab = true;
|
trayMenu.focusGrab = true;
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-28
@@ -7,6 +7,7 @@ import QtQuick.Layouts
|
|||||||
import Qt5Compat.GraphicalEffects
|
import Qt5Compat.GraphicalEffects
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import QtQml
|
import QtQml
|
||||||
|
import qs.Effects
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: root
|
id: root
|
||||||
@@ -58,26 +59,11 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if ( visible ) {
|
if ( !visible )
|
||||||
scaleValue = 0;
|
|
||||||
scaleAnimation.start();
|
|
||||||
} else {
|
|
||||||
root.menuStack.pop();
|
root.menuStack.pop();
|
||||||
backEntry.visible = false;
|
backEntry.visible = false;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NumberAnimation {
|
openAnim.start();
|
||||||
id: scaleAnimation
|
|
||||||
target: root
|
|
||||||
property: "scaleValue"
|
|
||||||
from: 0
|
|
||||||
to: 1
|
|
||||||
duration: 150
|
|
||||||
easing.type: Easing.OutCubic
|
|
||||||
onStopped: {
|
|
||||||
root.updateMask();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
@@ -85,7 +71,7 @@ PanelWindow {
|
|||||||
windows: [ root ]
|
windows: [ root ]
|
||||||
active: false
|
active: false
|
||||||
onCleared: {
|
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 {
|
Rectangle {
|
||||||
id: menuRect
|
id: menuRect
|
||||||
x: Math.round( root.trayItemRect.x - ( menuRect.implicitWidth / 2 ) + 11 )
|
x: Math.round( root.trayItemRect.x - ( menuRect.implicitWidth / 2 ) + 11 )
|
||||||
@@ -154,15 +188,6 @@ PanelWindow {
|
|||||||
border.color: "#40FFFFFF"
|
border.color: "#40FFFFFF"
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
transform: [
|
|
||||||
Scale {
|
|
||||||
origin.x: menuRect.width / 2
|
|
||||||
origin.y: 0
|
|
||||||
xScale: root.scaleValue
|
|
||||||
yScale: root.scaleValue
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Behavior on implicitWidth {
|
Behavior on implicitWidth {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: MaterialEasing.expressiveEffectsTime
|
duration: MaterialEasing.expressiveEffectsTime
|
||||||
@@ -242,7 +267,7 @@ PanelWindow {
|
|||||||
if ( !menuItem.modelData.hasChildren ) {
|
if ( !menuItem.modelData.hasChildren ) {
|
||||||
if ( menuItem.modelData.enabled ) {
|
if ( menuItem.modelData.enabled ) {
|
||||||
menuItem.modelData.triggered();
|
menuItem.modelData.triggered();
|
||||||
root.visible = false;
|
closeAnim.start();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
root.menuStack.push(root.trayMenu);
|
root.menuStack.push(root.trayMenu);
|
||||||
|
|||||||
@@ -41,3 +41,4 @@ qml_module(ZShell
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(Models)
|
add_subdirectory(Models)
|
||||||
|
add_subdirectory(Internal)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
qml_module(ZShell-internal
|
||||||
|
URI ZShell.Internal
|
||||||
|
SOURCES
|
||||||
|
hyprextras.hpp hyprextras.cpp
|
||||||
|
hyprdevices.hpp hyprdevices.cpp
|
||||||
|
)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#include "hyprdevices.hpp"
|
||||||
|
|
||||||
|
#include <qjsonarray.h>
|
||||||
|
|
||||||
|
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<HyprKeyboard> HyprDevices::keyboards() {
|
||||||
|
return QQmlListProperty<HyprKeyboard>(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
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <qjsonobject.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
#include <qqmllist.h>
|
||||||
|
|
||||||
|
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<ZShell::internal::hypr::HyprKeyboard> keyboards READ keyboards NOTIFY keyboardsChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit HyprDevices(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] QQmlListProperty<HyprKeyboard> keyboards();
|
||||||
|
|
||||||
|
bool updateLastIpcObject(QJsonObject object);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void keyboardsChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<HyprKeyboard*> m_keyboards;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::internal::hypr
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#include "hyprextras.hpp"
|
||||||
|
|
||||||
|
#include <qdir.h>
|
||||||
|
#include <qjsonarray.h>
|
||||||
|
#include <qlocalsocket.h>
|
||||||
|
#include <qvariant.h>
|
||||||
|
|
||||||
|
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<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 ZShell::internal::hypr
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "hyprdevices.hpp"
|
||||||
|
#include <qlocalsocket.h>
|
||||||
|
#include <qobject.h>
|
||||||
|
#include <qqmlintegration.h>
|
||||||
|
|
||||||
|
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<QLocalSocket>;
|
||||||
|
|
||||||
|
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<void(bool, QJsonDocument)>& callback);
|
||||||
|
SocketPtr makeRequest(const QString& request, const std::function<void(bool, QByteArray)>& callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ZShell::internal::hypr
|
||||||
Reference in New Issue
Block a user