Krolyxon
Commit b32fedf

Branches

Refs →

aurora.configquickshellauroraPowerMenu.qml

228 lines · 9.1 KB · QML

1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Wayland
5import Quickshell.Hyprland
6
7// Session / power actions (was the fuzzel `sysact` script). Type to fuzzy filter, Up/Down/Tab pick,
8// Enter runs, Esc closes. Destructive ones need a second Enter/click ("sure?").
9PanelWindow {
10 id: win
11 property bool opened: false
12 signal closeRequested()
13
14 visible: opened
15 implicitWidth: 300
16 // fixed to the full list so the centred window doesn't jump while filtering
17 implicitHeight: 24 + 36 + actions.length * 32 + 22
18 exclusiveZone: 0
19 color: "transparent"
20 WlrLayershell.layer: WlrLayer.Overlay
21 WlrLayershell.namespace: "aurora-power"
22 WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
23
24 HyprlandFocusGrab {
25 windows: [win]
26 active: win.opened
27 onCleared: win.closeRequested()
28 }
29
30 readonly property var actions: [
31 { id: "lock", icon: Theme.iLock, label: "lock", confirm: false },
32 { id: "leave", icon: Theme.iLogout, label: "leave Hyprland", confirm: true },
33 { id: "renew", icon: Theme.iRefresh, label: "renew Hyprland", confirm: false },
34 { id: "hibernate", icon: Theme.iHibernate, label: "hibernate", confirm: true },
35 { id: "reboot", icon: Theme.iReboot, label: "reboot", confirm: true },
36 { id: "shutdown", icon: Theme.iPower, label: "shutdown", confirm: true },
37 { id: "sleep", icon: Theme.iSleep, label: "sleep", confirm: false },
38 { id: "display", icon: Theme.iMonitor, label: "display off", confirm: false }
39 ]
40
41 property int current: 0
42 property string armed: ""
43 Timer { id: disarm; interval: 3000; onTriggered: win.armed = "" }
44
45 onOpenedChanged: {
46 if (!opened) return
47 filter.text = ""
48 current = 0
49 armed = ""
50 filter.forceActiveFocus()
51 }
52
53 // fuzzel-style: query chars must appear in order; prefix > word start > contiguous > scattered
54 function score(label, q) {
55 if (q === "") return 1
56 var s = label.toLowerCase()
57 if (s.indexOf(q) === 0) return 1000
58 var w = s.indexOf(" " + q)
59 if (w >= 0) return 500
60 if (s.indexOf(q) >= 0) return 300
61 var pos = 0, gaps = 0
62 for (var i = 0; i < q.length; i++) {
63 var f = s.indexOf(q[i], pos)
64 if (f < 0) return 0
65 gaps += f - pos
66 pos = f + 1
67 }
68 return 100 - gaps
69 }
70 readonly property var shown: {
71 var q = filter.text.toLowerCase().trim(), out = []
72 for (var i = 0; i < actions.length; i++) {
73 var sc = score(actions[i].label, q)
74 if (sc > 0) out.push({ a: actions[i], sc: sc, i: i })
75 }
76 out.sort(function (x, y) { return y.sc - x.sc || x.i - y.i })
77 var res = []
78 for (var j = 0; j < out.length; j++) res.push(out[j].a)
79 return res
80 }
81
82 function trigger(i) {
83 var a = shown[i]
84 if (!a) return
85 current = i
86 if (a.confirm && armed !== a.id) { armed = a.id; disarm.restart(); return }
87 armed = ""
88 closeRequested()
89 run(a.id)
90 }
91 function run(id) {
92 switch (id) {
93 case "lock": Quickshell.execDetached(["loginctl", "lock-session"]); break
94 case "leave": Hypr.logout(); break
95 case "renew": Quickshell.execDetached(["hyprctl", "reload"]); break
96 case "hibernate": Quickshell.execDetached(["systemctl", "hibernate", "-i"]); break
97 case "reboot": Quickshell.execDetached(["systemctl", "reboot", "-i"]); break
98 case "shutdown": Quickshell.execDetached(["systemctl", "poweroff", "-i"]); break
99 case "sleep": Quickshell.execDetached(["systemctl", "suspend", "-i"]); break
100 case "display": dpmsDelay.restart(); break // let the Enter key-up pass first, it would wake the screen
101 }
102 }
103 Timer {
104 id: dpmsDelay
105 interval: 400
106 onTriggered: Quickshell.execDetached(["hyprctl", "dispatch", 'hl.dsp.dpms({action="off"})'])
107 }
108
109 Rectangle {
110 anchors.fill: parent
111 color: Theme.bgA
112 border.width: 2
113 border.color: Theme.accent
114
115 ColumnLayout {
116 id: col
117 anchors.fill: parent
118 anchors.margins: 12
119 spacing: 2
120
121 Rectangle {
122 Layout.fillWidth: true
123 Layout.bottomMargin: 6
124 implicitHeight: 30
125 color: Theme.surface
126 border.width: 1
127 border.color: filter.activeFocus ? Theme.accent : Theme.line
128 Text {
129 anchors.left: parent.left
130 anchors.leftMargin: 10
131 anchors.verticalCenter: parent.verticalCenter
132 text: Theme.iPower
133 color: Theme.dim
134 font.family: Theme.font
135 font.pixelSize: Theme.fontSize
136 }
137 TextInput {
138 id: filter
139 anchors.fill: parent
140 anchors.leftMargin: 32
141 anchors.rightMargin: 10
142 verticalAlignment: TextInput.AlignVCenter
143 color: Theme.fg
144 selectionColor: Theme.accent
145 selectedTextColor: Theme.bg
146 font.family: Theme.font
147 font.pixelSize: Theme.fontSize
148 clip: true
149 onTextChanged: { win.current = 0; win.armed = "" }
150 Keys.onEscapePressed: win.closeRequested()
151 Keys.onReturnPressed: win.trigger(win.current)
152 Keys.onEnterPressed: win.trigger(win.current)
153 Keys.onPressed: event => {
154 var n = win.shown.length
155 if (!n) return
156 if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab) {
157 win.current = (win.current + 1) % n; win.armed = ""; event.accepted = true
158 } else if (event.key === Qt.Key_Up || event.key === Qt.Key_Backtab) {
159 win.current = (win.current + n - 1) % n; win.armed = ""; event.accepted = true
160 }
161 }
162 }
163 }
164
165 Repeater {
166 model: win.shown
167 delegate: Rectangle {
168 id: row
169 required property var modelData
170 required property int index
171 readonly property bool selected: win.current === index
172 readonly property bool isArmed: win.armed === modelData.id
173 Layout.fillWidth: true
174 implicitHeight: 30
175 color: isArmed ? Theme.err : (selected ? Theme.surfaceHi : (rm.containsMouse ? Theme.surface : "transparent"))
176 border.width: selected ? 1 : 0
177 border.color: isArmed ? Theme.err : Theme.accent
178
179 Rectangle { // selection marker, like ClipWindow
180 visible: row.selected
181 width: 2
182 height: parent.height
183 color: row.isArmed ? Theme.bg : Theme.accent
184 }
185 Text {
186 id: ico
187 anchors.left: parent.left
188 anchors.leftMargin: 12
189 anchors.verticalCenter: parent.verticalCenter
190 width: 18
191 text: row.modelData.icon
192 color: row.isArmed ? Theme.bg : Theme.accent
193 font.family: Theme.font
194 font.pixelSize: Theme.fontSize + 2
195 }
196 Text {
197 anchors.left: ico.right
198 anchors.leftMargin: 10
199 anchors.verticalCenter: parent.verticalCenter
200 text: row.isArmed ? row.modelData.label + " sure?" : row.modelData.label
201 color: row.isArmed ? Theme.bg : Theme.fg
202 font.family: Theme.font
203 font.pixelSize: Theme.fontSize
204 }
205 MouseArea {
206 id: rm
207 anchors.fill: parent
208 hoverEnabled: true
209 cursorShape: Qt.PointingHandCursor
210 onEntered: if (win.current !== row.index) { win.current = row.index; win.armed = "" }
211 onClicked: win.trigger(row.index)
212 }
213 }
214 }
215
216 Item { Layout.fillHeight: true }
217
218 Text {
219 Layout.fillWidth: true
220 Layout.topMargin: 6
221 text: win.shown.length ? "type filter enter run esc close" : "no match"
222 color: win.shown.length ? Theme.dim : Theme.err
223 font.family: Theme.font
224 font.pixelSize: Theme.fontSize - 2
225 }
226 }
227 }
228}