1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import "Binds.js" as Binds
Scope {
id: root
property bool open: false
property string monitor: "DP-1"
property var groups: []
// Non-empty while loading or after a failure; the grid shows only when
// this is empty, so a stale list from the last opening never shows.
property string status: "Loading..."
readonly property var padActions: Binds.padActions(root.groups)
readonly property var screenObj:
Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0]
function toggle() { root.open = !root.open; }
function close() { root.open = false; }
// Asked afresh on every opening, so the list is what Hyprland has loaded
// right now: an edit to keybindings.lua shows after `hyprctl reload`, and
// not before, which is when it becomes true.
onOpenChanged: if (open) {
root.status = "Loading...";
// A Process already running ignores running = true.
proc.running = false;
proc.running = true;
}
Process {
id: proc
command: ["hyprctl", "binds", "-j"]
stdout: StdioCollector {
onStreamFinished: {
try {
root.groups = Binds.group(JSON.parse(text));
root.status = "";
} catch (e) {
root.status = "hyprctl binds -j gave nothing readable";
}
}
}
}
// A config with no visible window exits, reporting nothing. This 1x1
// click-through window holds the shell open while the overlay is hidden.
PanelWindow {
anchors { top: true; left: true }
implicitWidth: 1
implicitHeight: 1
color: "transparent"
exclusionMode: ExclusionMode.Ignore
mask: Region {}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
}
LazyLoader {
active: root.open
PanelWindow {
screen: root.screenObj
anchors { top: true; left: true; right: true; bottom: true }
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "quickshell-keybinds"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
// Keys reach a focused item, not a window.
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: root.close()
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.base.r, Theme.base.g, Theme.base.b, 0.82)
MouseArea {
anchors.fill: parent
onClicked: root.close()
}
Text {
anchors.centerIn: parent
visible: root.status !== ""
text: root.status
color: Theme.text
font.family: Theme.fontFamily
font.pixelSize: 22
}
// The left fifth is the macropad; the list centres in the
// rest.
Item {
id: listArea
x: parent.width * 0.2
width: parent.width * 0.8
height: parent.height
}
PadPanel {
width: parent.width * 0.2
height: parent.height
visible: root.status === ""
actions: root.padActions
}
// Groups fill a column down to the screen's height, then
// start the next one, so a long group never leaves a row
// of short ones sitting on empty space. The width is left
// to the content, which is what centres the block.
// ponytail: a single group taller than the screen clips;
// split it in keybindings.lua if one ever gets there.
Flow {
anchors.centerIn: listArea
height: parent.height * 0.9
flow: Flow.TopToBottom
spacing: 28
visible: root.status === ""
Repeater {
model: root.groups
Column {
required property var modelData
width: 440
spacing: 4
Text {
text: modelData.name
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: 18
font.bold: true
bottomPadding: 6
}
Repeater {
model: modelData.rows
// Fixed widths summing to the column's:
// a Row sizes to its children.
Row {
required property var modelData
spacing: 12
Text {
width: 240
elide: Text.ElideLeft
text: modelData.keys
color: Theme.subtext
font.family: Theme.fontFamily
font.pixelSize: 14
}
Text {
width: 188
elide: Text.ElideRight
text: modelData.label
color: Theme.text
font.family: Theme.fontFamily
font.pixelSize: 14
}
}
}
}
}
}
}
}
}
}
}
|