Krolyxon
Commit b390172

Branches

Refs →

aurorab390172

aurora: settings app display, power, keybinds and about pages

Idle writes ~/.config/aurora/hypridle.conf (sourced by hypridle.conf) and
restarts hypridle through Hyprland's exec. Night light toggles gammastep.
Battery and GPU blocks moved out of SysPage into shared components.
Author
krolyxon
Date
2026-10-20 00:52 +0530
Parent
e5c3c69
Commit
b3901727ec789a08909e608e69f6ecc50f402d92

+824 −110

14 files changed

  1. M .config/hypr/hypridle.conf +3 −21
  2. A .config/quickshell/aurora/BatteryInfo.qml +50 −0
  3. M .config/quickshell/aurora/Field.qml +2 −0
  4. A .config/quickshell/aurora/GpuSwitch.qml +45 −0
  5. A .config/quickshell/aurora/Idle.qml +61 −0
  6. A .config/quickshell/aurora/SetAbout.qml +117 −0
  7. A .config/quickshell/aurora/SetDisplay.qml +273 −0
  8. A .config/quickshell/aurora/SetKeys.qml +150 −0
  9. A .config/quickshell/aurora/SetPower.qml +81 −0
  10. M .config/quickshell/aurora/Settings.qml +10 −1
  11. M .config/quickshell/aurora/SettingsApp.qml +13 −1
  12. M .config/quickshell/aurora/SysPage.qml +2 −87
  13. M .config/quickshell/aurora/Toggles.qml +15 −0
  14. M .config/quickshell/aurora/shell.qml +2 −0
M .config/hypr/hypridle.conf View
@@ -4,24 +4,6 @@ general {
44 after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({action="on"})' # to avoid having to press a key twice to turn on the display.
55 }
66
7−listener {
8− timeout = 150 # 2.5min.
9− on-timeout = brightnessctl -s set 5 # set monitor backlight to minimum, avoid 0 on OLED monitor.
10− on-resume = brightnessctl -r # monitor backlight restore.
11−}
12−
13−listener {
14− timeout = 600 # 10min
15− on-timeout = loginctl lock-session # lock screen when timeout has passed
16−}
17−
18−listener {
19− timeout = 330 # 5.5min
20− on-timeout = hyprctl dispatch 'hl.dsp.dpms({action="off"})' # screen off when timeout has passed
21− on-resume = hyprctl dispatch 'hl.dsp.dpms({action="on"})' && brightnessctl -r # screen on when activity is detected after timeout has fired.
22−}
23−
24−# listener {
25−# timeout = 1800 # 30min
26−# on-timeout = systemctl suspend # suspend pc
27−# }
7+# Timeouts (dim / screen off / lock / suspend) are set in aurora's settings app > Power,
8+# which writes this file and restarts hypridle (Idle.qml). Defaults: dim 2.5min, off 5.5min, lock 10min.
9+source = ~/.config/aurora/hypridle.conf
A .config/quickshell/aurora/BatteryInfo.qml View
@@ -0,0 +1,50 @@
1+import QtQuick
2+import QtQuick.Layouts
3+import Quickshell.Services.UPower
4+
5+// battery block: charge bar, time left / to full, power draw, health (SysPage, settings app > Power)
6+ColumnLayout {
7+ id: root
8+ readonly property var bat: UPower.displayDevice
9+ readonly property bool hasBat: bat !== null && bat.isPresent
10+ readonly property real batPct: hasBat ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0
11+ readonly property bool charging: hasBat && bat.state === UPowerDeviceState.Charging
12+ function stateText() {
13+ if (!hasBat) return ""
14+ switch (bat.state) {
15+ case UPowerDeviceState.Charging: return "charging"
16+ case UPowerDeviceState.Discharging: return "discharging"
17+ case UPowerDeviceState.FullyCharged: return "full"
18+ default: return "idle"
19+ }
20+ }
21+ visible: hasBat
22+ Layout.fillWidth: true
23+ spacing: 8
24+ SectionLabel { text: "battery" }
25+ StatBar {
26+ label: "charge"
27+ frac: root.batPct
28+ fill: root.batPct <= 0.15 && !root.charging ? Theme.err : Theme.accent
29+ valueText: Math.round(root.batPct * 100) + "% " + root.stateText()
30+ }
31+ Text {
32+ Layout.fillWidth: true
33+ wrapMode: Text.Wrap
34+ color: Theme.dim
35+ font.family: Theme.font
36+ font.pixelSize: Theme.fontSize - 1
37+ lineHeight: 1.3
38+ text: {
39+ if (!root.hasBat) return ""
40+ var lines = []
41+ var t = root.charging ? root.bat.timeToFull : root.bat.timeToEmpty
42+ lines.push((root.charging ? "time to full " : "time left ") + Theme.fmtTime(t))
43+ if (root.bat.changeRate !== undefined)
44+ lines.push("power draw " + Math.abs(root.bat.changeRate).toFixed(1) + " W")
45+ if (root.bat.healthSupported)
46+ lines.push("health " + Math.round(root.bat.healthPercentage) + "%")
47+ return lines.join("\n")
48+ }
49+ }
50+}
M .config/quickshell/aurora/Field.qml View
@@ -8,6 +8,7 @@ RowLayout {
88 property string value: ""
99 property string placeholder: ""
1010 signal submitted(string v)
11+ signal edited(string v) // every keystroke (live filters)
1112 Layout.fillWidth: true
1213 spacing: 8
1314 Text {
@@ -37,6 +38,7 @@ RowLayout {
3738 selectedTextColor: Theme.bg
3839 font.family: Theme.font
3940 font.pixelSize: Theme.fontSize
41+ onTextEdited: f.edited(text)
4042 onAccepted: { if (text !== f.value) f.submitted(text); focus = false }
4143 onActiveFocusChanged: if (!activeFocus && text !== f.value) f.submitted(text)
4244 Keys.onEscapePressed: e => { text = f.value; focus = false; e.accepted = true }
A .config/quickshell/aurora/GpuSwitch.qml View
@@ -0,0 +1,45 @@
1+import QtQuick
2+import QtQuick.Layouts
3+
4+// envycontrol mode buttons with the two-click arm (SysPage, settings app > Power)
5+ColumnLayout {
6+ id: root
7+ // two-click arm before switching (needs a reboot to apply)
8+ property string armedGpu: ""
9+ Timer { id: disarm; interval: 3000; onTriggered: root.armedGpu = "" }
10+ function gpuClick(m) {
11+ if (Gpu.mode === m || Gpu.busy) return
12+ if (armedGpu !== m) { armedGpu = m; disarm.restart(); return }
13+ armedGpu = ""
14+ Gpu.set(m)
15+ }
16+ visible: Gpu.mode !== ""
17+ Layout.fillWidth: true
18+ spacing: 8
19+ SectionLabel { text: "graphics · envycontrol" }
20+ RowLayout {
21+ Layout.fillWidth: true
22+ spacing: 6
23+ Repeater {
24+ model: ["integrated", "hybrid", "nvidia"]
25+ delegate: IconBtn {
26+ required property string modelData
27+ Layout.fillWidth: true
28+ icon: Theme.iGpu
29+ label: root.armedGpu === modelData ? "sure?" : modelData
30+ active: Gpu.mode === modelData
31+ opacity: Gpu.busy ? 0.5 : 1
32+ onClicked: root.gpuClick(modelData)
33+ }
34+ }
35+ }
36+ Text {
37+ visible: Gpu.pending !== "" || Gpu.message !== "" || Gpu.busy
38+ Layout.fillWidth: true
39+ wrapMode: Text.Wrap
40+ color: Gpu.message !== "" ? Theme.err : Theme.accent
41+ font.family: Theme.font
42+ font.pixelSize: Theme.fontSize - 2
43+ text: Gpu.busy ? "switching…" : (Gpu.message !== "" ? Gpu.message : "queued: " + Gpu.pending + " · reboot to apply")
44+ }
45+}
A .config/quickshell/aurora/Idle.qml View
@@ -0,0 +1,61 @@
1+pragma Singleton
2+import QtQuick
3+import Quickshell
4+import Quickshell.Io
5+
6+// hypridle timeouts from the settings app. The repo's hypridle.conf keeps `general {}` and sources
7+// ~/.config/aurora/hypridle.conf, which this writes (also on startup when it's missing: hypridle errors
8+// on a missing source). hypridle is restarted through Hyprland's exec so it lives outside aurora's
9+// systemd unit (an aurora restart would kill it otherwise). Seconds, 0 = never.
10+Singleton {
11+ id: root
12+ readonly property string file: Quickshell.env("HOME") + "/.config/aurora/hypridle.conf"
13+ readonly property int dim: Settings.idleDim
14+ readonly property int screenOff: Settings.idleOff
15+ readonly property int lock: Settings.idleLock
16+ readonly property int suspend: Settings.idleSuspend
17+
18+ function setTimeout(key, v) { Settings[key] = Math.max(0, Math.round(v)) }
19+ // any change (slider, Settings.reset(), hand edit of settings.json) rewrites the file
20+ onDimChanged: writeTimer.restart()
21+ onScreenOffChanged: writeTimer.restart()
22+ onLockChanged: writeTimer.restart()
23+ onSuspendChanged: writeTimer.restart()
24+
25+ function render() {
26+ var off = "hyprctl dispatch 'hl.dsp.dpms({action=\"off\"})'"
27+ var on = "hyprctl dispatch 'hl.dsp.dpms({action=\"on\"})'"
28+ var l = ["# generated by aurora's settings app (Idle.qml); edit there, not here", ""]
29+ function listener(t, timeout, resume) {
30+ if (t <= 0) return
31+ l.push("listener {", " timeout = " + t, " on-timeout = " + timeout)
32+ if (resume) l.push(" on-resume = " + resume)
33+ l.push("}", "")
34+ }
35+ listener(dim, "brightnessctl -s set 5", "brightnessctl -r") // dim the panel, restore on activity
36+ listener(screenOff, off, on + " && brightnessctl -r")
37+ listener(lock, "loginctl lock-session", "")
38+ listener(suspend, "systemctl suspend", "")
39+ return l.join("\n")
40+ }
41+ function write() {
42+ fv.setText(render())
43+ restartTimer.restart()
44+ }
45+
46+ Timer { id: writeTimer; interval: 800; onTriggered: root.write() }
47+ Timer {
48+ id: restartTimer
49+ interval: 300
50+ onTriggered: Quickshell.execDetached(["sh", "-c",
51+ "pkill -x hypridle; for i in 1 2 3 4 5 6 7 8 9 10; do pgrep -x hypridle >/dev/null || break; sleep 0.1; done; " +
52+ "hyprctl dispatch 'hl.dsp.exec_cmd(\"hypridle\")' >/dev/null"])
53+ }
54+
55+ FileView {
56+ id: fv
57+ path: root.file
58+ printErrors: false
59+ onLoadFailed: err => { if (err === FileViewError.FileNotFound) root.write() }
60+ }
61+}
A .config/quickshell/aurora/SetAbout.qml View
@@ -0,0 +1,117 @@
1+import QtQuick
2+import QtQuick.Layouts
3+import Quickshell
4+import Quickshell.Io
5+
6+// Settings app > About: what this machine is (one shell probe on open) + live memory / disks from SysStats.
7+Flickable {
8+ id: flick
9+ clip: true
10+ contentWidth: width
11+ contentHeight: col.implicitHeight
12+ boundsBehavior: Flickable.StopAtBounds
13+
14+ property var info: [] // [[label, value]]
15+ function size(v) { return v >= 1000 ? (v / 1024).toFixed(1) + "T" : (v >= 100 ? Math.round(v) : v.toFixed(1)) + "G" }
16+
17+ Process {
18+ running: true
19+ command: ["sh", "-c", [
20+ '. /etc/os-release 2>/dev/null; printf "system\\t%s\\n" "${PRETTY_NAME:-Linux}"',
21+ 'printf "kernel\\t%s\\n" "$(uname -r)"',
22+ 'printf "host\\t%s\\n" "$(cat /etc/hostname 2>/dev/null || uname -n)"',
23+ 'printf "machine\\t%s\\n" "$(cat /sys/class/dmi/id/product_version 2>/dev/null) ($(cat /sys/class/dmi/id/product_name 2>/dev/null))"',
24+ 'printf "cpu\\t%s (%s threads)\\n" "$(sed -n "s/^model name[^:]*: //p" /proc/cpuinfo | head -1)" "$(nproc)"',
25+ 'command -v lspci >/dev/null && lspci | sed -n "s/.*\\(VGA\\|3D\\)[^:]*: //p" | sed "s/ (rev .*//" | while read -r g; do printf "gpu\\t%s\\n" "$g"; done',
26+ 'printf "hyprland\\t%s\\n" "$(hyprctl version -j 2>/dev/null | sed -n "s/.*\\"tag\\": \\"\\([^\\"]*\\)\\".*/\\1/p")"',
27+ 'printf "quickshell\\t%s\\n" "$(qs --version 2>/dev/null | head -1 | sed "s/^Quickshell //; s/ (.*//")"',
28+ 'printf "shell\\t%s\\n" "$(basename "${SHELL:-sh}")"',
29+ 'command -v pacman >/dev/null && printf "packages\\t%s (pacman)\\n" "$(pacman -Qq | wc -l)"'
30+ ].join("; ")]
31+ stdout: StdioCollector {
32+ onStreamFinished: {
33+ var out = [], l = text.split("\n")
34+ for (var i = 0; i < l.length; i++) {
35+ var t = l[i].indexOf("\t")
36+ if (t > 0) out.push([l[i].slice(0, t), l[i].slice(t + 1)])
37+ }
38+ flick.info = out
39+ }
40+ }
41+ }
42+
43+ ColumnLayout {
44+ id: col
45+ width: flick.width
46+ spacing: 8
47+
48+ RowLayout {
49+ spacing: 14
50+ Text {
51+ text: Theme.iArch
52+ color: Theme.accent
53+ font.family: Theme.font
54+ font.pixelSize: 56
55+ }
56+ ColumnLayout {
57+ spacing: 2
58+ Text {
59+ text: flick.info.length ? flick.info[0][1] : ""
60+ color: Theme.fg
61+ font.family: Theme.font
62+ font.pixelSize: Theme.fontSize + 6
63+ font.bold: true
64+ }
65+ Text {
66+ text: Quickshell.env("USER") + "@" + (flick.info.length > 2 ? flick.info[2][1] : "")
67+ + " · up " + Theme.fmtTime(SysStats.uptime)
68+ color: Theme.dim
69+ font.family: Theme.font
70+ font.pixelSize: Theme.fontSize
71+ }
72+ }
73+ }
74+
75+ SectionLabel { text: "system"; Layout.topMargin: 6 }
76+ Repeater {
77+ model: flick.info
78+ delegate: RowLayout {
79+ required property var modelData
80+ Layout.fillWidth: true
81+ spacing: 8
82+ Text {
83+ Layout.preferredWidth: 150
84+ text: modelData[0]
85+ color: Theme.accent
86+ font.family: Theme.font
87+ font.pixelSize: Theme.fontSize
88+ }
89+ Text {
90+ Layout.fillWidth: true
91+ text: modelData[1]
92+ color: Theme.fg
93+ elide: Text.ElideRight
94+ font.family: Theme.font
95+ font.pixelSize: Theme.fontSize
96+ }
97+ }
98+ }
99+
100+ SectionLabel { text: "memory & storage"; Layout.topMargin: 6 }
101+ StatBar {
102+ label: "memory"
103+ frac: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0
104+ valueText: flick.size(SysStats.memUsed) + " / " + flick.size(SysStats.memTotal)
105+ }
106+ Repeater {
107+ model: SysStats.disks
108+ delegate: StatBar {
109+ required property var modelData
110+ label: modelData.label
111+ frac: modelData.used / modelData.size
112+ fill: frac > 0.9 ? Theme.err : Theme.accent
113+ valueText: flick.size(modelData.used) + " / " + flick.size(modelData.size)
114+ }
115+ }
116+ }
117+}
A .config/quickshell/aurora/SetDisplay.qml View
@@ -0,0 +1,273 @@
1+import QtQuick
2+import QtQuick.Layouts
3+import Quickshell
4+import Quickshell.Io
5+
6+// Settings app > Display: monitors (live, with a keep/revert countdown), kanshi profiles, brightness, night light.
7+// Monitor changes are runtime only: kanshi re-applies its profile on hotplug, persistent layouts live in its config.
8+Flickable {
9+ id: flick
10+ clip: true
11+ contentWidth: width
12+ contentHeight: col.implicitHeight
13+ boundsBehavior: Flickable.StopAtBounds
14+
15+ readonly property string kanshiFile: Quickshell.env("HOME") + "/.config/kanshi/config"
16+ property var monitors: [] // hyprctl monitors all -j
17+ property var revertSpec: null // spec to restore if the change isn't kept
18+ property int countdown: 0
19+ property string message: ""
20+
21+ function refresh() { monProc.running = true }
22+ Component.onCompleted: refresh()
23+
24+ // monitor -> its current spec
25+ function specOf(m) {
26+ if (m.disabled) return { output: m.name, disabled: true }
27+ return { output: m.name, mode: m.width + "x" + m.height + "@" + m.refreshRate.toFixed(2),
28+ position: m.x + "x" + m.y, scale: m.scale }
29+ }
30+ // only characters a monitor name / mode / position can contain, so nothing can break out of the Lua string
31+ function q(s) { return '"' + String(s).replace(/[^A-Za-z0-9@x._\-]/g, "") + '"' }
32+ function lua(spec) {
33+ if (spec.disabled) return "hl.monitor({ output = " + q(spec.output) + ", disabled = true })"
34+ return "hl.monitor({ output = " + q(spec.output) + ", mode = " + q(spec.mode) + ", position = "
35+ + q(spec.position) + ", scale = " + Number(spec.scale) + " })"
36+ }
37+ function apply(m, change) {
38+ var spec
39+ if (change.disabled) spec = { output: m.name, disabled: true }
40+ else if (m.disabled) spec = { output: m.name, mode: "preferred", position: "auto", scale: 1 }
41+ else {
42+ spec = specOf(m)
43+ for (var k in change) spec[k] = change[k]
44+ }
45+ revertSpec = specOf(m)
46+ run(lua(spec))
47+ countdown = 15
48+ tick.restart()
49+ }
50+ function keep() { revertSpec = null; countdown = 0; tick.stop() }
51+ function revert() {
52+ if (revertSpec) run(lua(revertSpec))
53+ keep()
54+ }
55+ function run(code) {
56+ evalProc.command = ["hyprctl", "eval", code]
57+ evalProc.running = true
58+ }
59+ function enabledCount() {
60+ var n = 0
61+ for (var i = 0; i < monitors.length; i++) if (!monitors[i].disabled) n++
62+ return n
63+ }
64+
65+ Timer {
66+ id: tick
67+ interval: 1000
68+ repeat: true
69+ onTriggered: { if (--flick.countdown <= 0) flick.revert() }
70+ }
71+ Process {
72+ id: monProc
73+ command: ["hyprctl", "monitors", "all", "-j"]
74+ stdout: StdioCollector {
75+ onStreamFinished: { try { flick.monitors = JSON.parse(text) } catch (e) {} }
76+ }
77+ }
78+ Process {
79+ id: evalProc
80+ stdout: StdioCollector {
81+ onStreamFinished: {
82+ var t = text.trim()
83+ flick.message = t === "" || t === "ok" ? "" : t
84+ refreshLater.restart()
85+ }
86+ }
87+ }
88+ Timer { id: refreshLater; interval: 600; onTriggered: flick.refresh() }
89+
90+ // kanshi profiles: `profile NAME {` lines
91+ property var profiles: []
92+ FileView {
93+ path: flick.kanshiFile
94+ printErrors: false
95+ onLoaded: {
96+ var out = [], re = /^\s*profile\s+(\S+)\s*\{/gm, mm
97+ var t = text()
98+ while ((mm = re.exec(t)) !== null) out.push(mm[1])
99+ flick.profiles = out
100+ }
101+ }
102+
103+ ColumnLayout {
104+ id: col
105+ width: flick.width
106+ spacing: 8
107+
108+ // keep / revert bar after a monitor change
109+ Rectangle {
110+ Layout.fillWidth: true
111+ visible: flick.countdown > 0
112+ implicitHeight: 40
113+ color: Theme.surface
114+ border.width: 1
115+ border.color: Theme.accent
116+ RowLayout {
117+ anchors.fill: parent
118+ anchors.margins: 6
119+ Text {
120+ Layout.fillWidth: true
121+ text: "keep this display setup? reverting in " + flick.countdown + "s"
122+ color: Theme.fg
123+ font.family: Theme.font
124+ font.pixelSize: Theme.fontSize
125+ }
126+ IconBtn { icon: Theme.iCheck; label: "keep"; active: true; onClicked: flick.keep() }
127+ IconBtn { icon: Theme.iRefresh; label: "revert"; onClicked: flick.revert() }
128+ }
129+ }
130+ Text {
131+ Layout.fillWidth: true
132+ visible: flick.message !== ""
133+ text: flick.message
134+ color: Theme.err
135+ wrapMode: Text.WordWrap
136+ font.family: Theme.font
137+ font.pixelSize: Theme.fontSize - 1
138+ }
139+
140+ Repeater {
141+ model: flick.monitors
142+ delegate: ColumnLayout {
143+ id: mon
144+ required property var modelData
145+ readonly property var m: modelData
146+ readonly property string curMode: m.width + "x" + m.height
147+ Layout.fillWidth: true
148+ spacing: 6
149+
150+ SectionLabel { text: mon.m.name }
151+ Text {
152+ Layout.fillWidth: true
153+ text: mon.m.description + (mon.m.disabled ? " · disabled"
154+ : " · " + mon.curMode + " @ " + mon.m.refreshRate.toFixed(2) + "Hz · scale " + mon.m.scale
155+ + " · at " + mon.m.x + "," + mon.m.y)
156+ color: Theme.dim
157+ elide: Text.ElideRight
158+ font.family: Theme.font
159+ font.pixelSize: Theme.fontSize - 1
160+ }
161+ Switch {
162+ label: "enabled"
163+ hint: !mon.m.disabled && flick.enabledCount() <= 1 ? "the only active monitor can't be turned off" : ""
164+ checked: !mon.m.disabled
165+ onToggled: on => {
166+ if (!on && flick.enabledCount() <= 1) return
167+ flick.apply(mon.m, { disabled: !on })
168+ }
169+ }
170+ Text {
171+ visible: !mon.m.disabled
172+ text: "mode"
173+ color: Theme.fg
174+ font.family: Theme.font
175+ font.pixelSize: Theme.fontSize
176+ }
177+ Flow {
178+ Layout.fillWidth: true
179+ visible: !mon.m.disabled
180+ spacing: 4
181+ Repeater {
182+ model: mon.m.availableModes
183+ delegate: IconBtn {
184+ required property string modelData
185+ readonly property string mode: modelData.replace("Hz", "")
186+ readonly property real hz: parseFloat(mode.split("@")[1])
187+ implicitHeight: 24
188+ label: mode
189+ active: mode.split("@")[0] === mon.curMode && Math.abs(hz - mon.m.refreshRate) < 0.02
190+ onClicked: if (!active) flick.apply(mon.m, { mode: mode })
191+ }
192+ }
193+ }
194+ Choice {
195+ visible: !mon.m.disabled
196+ label: "scale"
197+ options: [{ value: 1, label: "100%" }, { value: 1.25, label: "125%" }, { value: 1.5, label: "150%" },
198+ { value: 1.75, label: "175%" }, { value: 2, label: "200%" }]
199+ value: Math.round(mon.m.scale * 100) / 100
200+ onPicked: x => flick.apply(mon.m, { scale: x })
201+ }
202+ Field {
203+ visible: !mon.m.disabled
204+ label: "position"
205+ placeholder: "1920x0"
206+ value: mon.m.x + "x" + mon.m.y
207+ onSubmitted: x => {
208+ if (/^-?\d+x-?\d+$/.test(x.trim())) flick.apply(mon.m, { position: x.trim() })
209+ else flick.message = "position must look like 1920x0"
210+ }
211+ }
212+ }
213+ }
214+
215+ SectionLabel { text: "profiles (kanshi)" }
216+ Text {
217+ Layout.fillWidth: true
218+ text: "changes above are temporary: kanshi re-applies its profile when monitors are plugged in."
219+ color: Theme.dim
220+ wrapMode: Text.WordWrap
221+ font.family: Theme.font
222+ font.pixelSize: Theme.fontSize - 1
223+ }
224+ Flow {
225+ Layout.fillWidth: true
226+ spacing: 4
227+ Repeater {
228+ model: flick.profiles
229+ delegate: IconBtn {
230+ required property string modelData
231+ implicitHeight: 26
232+ icon: Theme.iMonitor
233+ label: modelData
234+ onClicked: {
235+ Quickshell.execDetached(["kanshictl", "switch", modelData])
236+ refreshLater.restart()
237+ }
238+ }
239+ }
240+ IconBtn {
241+ implicitHeight: 26
242+ icon: Theme.iConsole
243+ label: "edit config"
244+ onClicked: Quickshell.execDetached(["sh", "-c", 'exec foot -e "${EDITOR:-nvim}" "$1"', "sh", flick.kanshiFile])
245+ }
246+ }
247+
248+ SectionLabel { text: "brightness & colour" }
249+ Knob {
250+ visible: Bright.available
251+ label: "laptop panel"
252+ unit: "%"
253+ from: 1
254+ to: 100
255+ value: Bright.percent
256+ onMoved: x => Bright.setPercent(x)
257+ }
258+ Switch {
259+ visible: Toggles.hasNightLight
260+ label: "night light"
261+ hint: "gammastep's warm colours (its schedule comes from ~/.config/gammastep)"
262+ checked: Toggles.nightLight
263+ onToggled: on => Toggles.setNightLight(on)
264+ }
265+ Text {
266+ visible: !Toggles.hasNightLight
267+ text: "night light: gammastep isn't running"
268+ color: Theme.dim
269+ font.family: Theme.font
270+ font.pixelSize: Theme.fontSize - 1
271+ }
272+ }
273+}
A .config/quickshell/aurora/SetKeys.qml View
@@ -0,0 +1,150 @@
1+import QtQuick
2+import QtQuick.Layouts
3+import Quickshell
4+import Quickshell.Io
5+
6+// Settings app > Keybinds: read-only, searchable list parsed from ~/.config/hypr/source/keybinds.lua
7+// (`hyprctl binds` only shows "__lua <n>" for Lua binds). Comments above binds become the groups.
8+ColumnLayout {
9+ id: page
10+ spacing: 8
11+ readonly property string file: Quickshell.env("HOME") + "/.config/hypr/source/keybinds.lua"
12+ property var binds: [] // [{group, keys, action}]
13+ property string query: ""
14+
15+ // split the argument list of a call at top-level commas (strings and nested brackets respected)
16+ function splitArgs(s) {
17+ var out = [], depth = 0, q = "", cur = ""
18+ for (var i = 0; i < s.length; i++) {
19+ var c = s[i]
20+ if (q !== "") {
21+ cur += c
22+ if (c === q && s[i - 1] !== "\\") q = ""
23+ continue
24+ }
25+ if (c === '"' || c === "'") { q = c; cur += c; continue }
26+ if (c === "(" || c === "{" || c === "[") depth++
27+ if (c === ")" || c === "}" || c === "]") depth--
28+ if (c === "," && depth === 0) { out.push(cur.trim()); cur = ""; continue }
29+ cur += c
30+ }
31+ if (cur.trim() !== "") out.push(cur.trim())
32+ return out
33+ }
34+ function keys(expr) {
35+ if (/\.\.\s*n\b/.test(expr)) return "SUPER + 1..0" // the workspace loop
36+ var k = expr.replace(/mainMod/g, '"SUPER"').replace(/\.\./g, "").replace(/["']/g, "")
37+ return k.split("+").map(function (x) { x = x.trim(); return x.length === 1 ? x.toUpperCase() : x }).filter(function (x) { return x !== "" })
38+ .join(" + ").replace(/mouse:272/, "left-drag").replace(/mouse:273/, "right-drag")
39+ .replace(/mouse:274/, "middle-drag").replace(/mouse_down/, "scroll down").replace(/mouse_up/, "scroll up")
40+ }
41+ function action(expr, opts) {
42+ var d = /desc(?:ription)?\s*=\s*"([^"]*)"/.exec(opts || "")
43+ if (d) return d[1]
44+ var e = /exec_cmd\(([\s\S]*)\)\s*$/.exec(expr)
45+ if (e) return e[1].replace(/\\"/g, "\u0001").replace(/"/g, "").replace(/\u0001/g, '"') // terminal .. " -e x" -> terminal -e x
46+ .replace(/\s*\.\.\s*/g, " ").trim()
47+ if (/^function/.test(expr)) return "lua function"
48+ return expr.replace(/^hl\.dsp\./, "").replace(/["{}]/g, "").replace(/\s*=\s*/g, "=")
49+ .replace(/\(\s*/g, " ").replace(/\s*\)/g, "").replace(/\s+/g, " ").trim()
50+ }
51+ function parse(t) {
52+ var lines = t.split("\n"), out = [], group = "", buf = "", depth = 0
53+ for (var i = 0; i < lines.length; i++) {
54+ var l = lines[i], tr = l.trim()
55+ if (buf === "") {
56+ var cm = /^--+\s*(.*?)\s*-*$/.exec(tr)
57+ if (cm && cm[1] !== "" && !/^bind\s*=/.test(cm[1]) && cm[1].indexOf("http") < 0 && !/^-+$/.test(cm[1])) group = cm[1].replace(/^-+\s*/, "")
58+ if (tr.indexOf("hl.bind(") !== 0) continue
59+ }
60+ buf += l + "\n"
61+ depth += (l.match(/\(/g) || []).length - (l.match(/\)/g) || []).length
62+ if (depth > 0) continue
63+ var inner = buf.slice(buf.indexOf("hl.bind(") + 8, buf.lastIndexOf(")"))
64+ var a = splitArgs(inner)
65+ if (a.length >= 2) out.push({ group: group, keys: keys(a[0]), action: action(a[1], a[2]) })
66+ buf = ""
67+ depth = 0
68+ }
69+ binds = out
70+ }
71+
72+ readonly property var shown: {
73+ var q = query.toLowerCase(), out = [], last = null
74+ for (var i = 0; i < binds.length; i++) {
75+ var b = binds[i]
76+ if (q !== "" && (b.keys + " " + b.action + " " + b.group).toLowerCase().indexOf(q) < 0) continue
77+ if (b.group !== last) { out.push({ header: b.group }); last = b.group } // group header row
78+ out.push(b)
79+ }
80+ return out
81+ }
82+
83+ FileView {
84+ path: page.file
85+ watchChanges: true
86+ onFileChanged: reload()
87+ onLoaded: page.parse(text())
88+ }
89+
90+ Field {
91+ id: search
92+ label: Theme.iSearch + " filter"
93+ placeholder: "key, command or group"
94+ onEdited: v => page.query = v
95+ }
96+
97+ ListView {
98+ Layout.fillWidth: true
99+ Layout.fillHeight: true
100+ clip: true
101+ spacing: 2
102+ model: page.shown
103+ boundsBehavior: Flickable.StopAtBounds
104+ delegate: RowLayout {
105+ required property var modelData
106+ readonly property bool isHeader: modelData.header !== undefined
107+ width: ListView.view.width
108+ spacing: 10
109+ SectionLabel {
110+ visible: parent.isHeader
111+ Layout.topMargin: 6
112+ text: parent.isHeader ? parent.modelData.header : ""
113+ }
114+ Rectangle {
115+ visible: !parent.isHeader
116+ Layout.preferredWidth: 220
117+ implicitHeight: 24
118+ color: Theme.surface
119+ border.width: 1
120+ border.color: Theme.line
121+ Text {
122+ anchors.fill: parent
123+ anchors.leftMargin: 6
124+ verticalAlignment: Text.AlignVCenter
125+ text: modelData.keys || ""
126+ color: Theme.accent
127+ elide: Text.ElideRight
128+ font.family: Theme.font
129+ font.pixelSize: Theme.fontSize - 1
130+ font.bold: true
131+ }
132+ }
133+ Text {
134+ visible: !parent.isHeader
135+ Layout.fillWidth: true
136+ text: modelData.action || ""
137+ color: Theme.fg
138+ elide: Text.ElideRight
139+ font.family: Theme.font
140+ font.pixelSize: Theme.fontSize - 1
141+ }
142+ }
143+ }
144+ Text {
145+ text: page.binds.length + " binds · " + page.file.replace(Quickshell.env("HOME"), "~")
146+ color: Theme.dim
147+ font.family: Theme.font
148+ font.pixelSize: Theme.fontSize - 2
149+ }
150+}
A .config/quickshell/aurora/SetPower.qml View
@@ -0,0 +1,81 @@
1+import QtQuick
2+import QtQuick.Layouts
3+
4+// Settings app > Power: battery, graphics mode, idle timeouts (hypridle via Idle) and caffeine.
5+Flickable {
6+ id: flick
7+ clip: true
8+ contentWidth: width
9+ contentHeight: col.implicitHeight
10+ boundsBehavior: Flickable.StopAtBounds
11+
12+ // seconds -> "2m 30s" / "never"
13+ function dur(s) {
14+ if (s <= 0) return "never"
15+ var m = Math.floor(s / 60), r = s % 60
16+ return m > 0 ? m + "m" + (r ? " " + r + "s" : "") : r + "s"
17+ }
18+
19+ // one timeout row: slider in 30s steps (0 = never) + readable value
20+ component Timeout: RowLayout {
21+ id: t
22+ property string label: ""
23+ property string key: ""
24+ property int value: 0
25+ property int max: 1800
26+ Layout.fillWidth: true
27+ spacing: 8
28+ Text {
29+ Layout.preferredWidth: 150
30+ text: t.label
31+ color: Theme.fg
32+ font.family: Theme.font
33+ font.pixelSize: Theme.fontSize
34+ }
35+ BarSlider {
36+ Layout.fillWidth: true
37+ from: 0
38+ to: t.max
39+ step: 30 / t.max
40+ value: t.value
41+ onMoved: v => Idle.setTimeout(t.key, Math.round(v / 30) * 30)
42+ }
43+ Text {
44+ Layout.preferredWidth: 70
45+ horizontalAlignment: Text.AlignRight
46+ text: flick.dur(t.value)
47+ color: t.value > 0 ? Theme.fg : Theme.dim
48+ font.family: Theme.font
49+ font.pixelSize: Theme.fontSize
50+ }
51+ }
52+
53+ ColumnLayout {
54+ id: col
55+ width: flick.width
56+ spacing: 8
57+
58+ BatteryInfo {}
59+ GpuSwitch { Layout.topMargin: 6 }
60+
61+ SectionLabel { text: "when idle"; Layout.topMargin: 6 }
62+ Timeout { label: "dim the screen"; key: "idleDim"; value: Idle.dim }
63+ Timeout { label: "screen off"; key: "idleOff"; value: Idle.screenOff }
64+ Timeout { label: "lock"; key: "idleLock"; value: Idle.lock }
65+ Timeout { label: "suspend"; key: "idleSuspend"; value: Idle.suspend; max: 3600 }
66+ Switch {
67+ label: "caffeine"
68+ hint: "keep the screen awake until turned off (not saved)"
69+ checked: Toggles.caffeine
70+ onToggled: on => Toggles.caffeine = on
71+ }
72+ Text {
73+ Layout.fillWidth: true
74+ text: "saved to ~/.config/aurora/hypridle.conf, hypridle restarts on change"
75+ color: Theme.dim
76+ elide: Text.ElideLeft
77+ font.family: Theme.font
78+ font.pixelSize: Theme.fontSize - 2
79+ }
80+ }
81+}
M .config/quickshell/aurora/Settings.qml View
@@ -14,7 +14,8 @@ Singleton {
1414 fontSize: 12, wsFontSize: 11, barHeight: 22, barAlpha: 1.0, panelAlpha: 0.88, accentIndex: 14,
1515 osdTimeout: 1500, desktopClock: true, desktopViz: true, vizBars: 64,
1616 dock: true, dockPinned: ["foot", "librewolf", "pcmanfm", "obsidian"], wallFolder: "dusky-dark",
17− notifSound: true, notifVolume: 0.7
17+ notifSound: true, notifVolume: 0.7,
18+ idleDim: 150, idleOff: 330, idleLock: 600, idleSuspend: 0
1819 })
1920
2021 property alias fontSize: a.fontSize
@@ -32,6 +33,10 @@ Singleton {
3233 property alias wallFolder: a.wallFolder
3334 property alias notifSound: a.notifSound
3435 property alias notifVolume: a.notifVolume
36+ property alias idleDim: a.idleDim
37+ property alias idleOff: a.idleOff
38+ property alias idleLock: a.idleLock
39+ property alias idleSuspend: a.idleSuspend
3540
3641 function reset() {
3742 for (var k in defaults) a[k] = defaults[k]
@@ -71,6 +76,10 @@ Singleton {
7176 property string wallFolder: "dusky-dark"
7277 property bool notifSound: true
7378 property real notifVolume: 0.7
79+ property int idleDim: 150 // hypridle timeouts in seconds, 0 = never (Idle.qml)
80+ property int idleOff: 330
81+ property int idleLock: 600
82+ property int idleSuspend: 0
7483 }
7584 }
7685 // first run: create the file once the directory exists
M .config/quickshell/aurora/SettingsApp.qml View
@@ -28,7 +28,11 @@ FloatingWindow {
2828 { id: "appearance", icon: Theme.iPalette, name: "appearance", group: "look" },
2929 { id: "shell", icon: Theme.iDock, name: "shell", group: "look" },
3030 { id: "windows", icon: Theme.iWindow, name: "windows", group: "hyprland" },
31− { id: "input", icon: Theme.iKeyboard, name: "input", group: "hyprland" }
31+ { id: "input", icon: Theme.iMouse, name: "input", group: "hyprland" },
32+ { id: "keybinds", icon: Theme.iKeyboard, name: "keybinds", group: "hyprland" },
33+ { id: "display", icon: Theme.iMonitor, name: "display", group: "system" },
34+ { id: "power", icon: Theme.iPower, name: "power", group: "system" },
35+ { id: "about", icon: Theme.iArch, name: "about", group: "system" }
3236 ]
3337 readonly property int index: {
3438 for (var i = 0; i < sections.length; i++) if (sections[i].id === section) return i
@@ -140,6 +144,10 @@ FloatingWindow {
140144 case "shell": return shellC
141145 case "windows": return windowsC
142146 case "input": return inputC
147+ case "keybinds": return keysC
148+ case "display": return displayC
149+ case "power": return powerC
150+ case "about": return aboutC
143151 default: return netC
144152 }
145153 }
@@ -155,4 +163,8 @@ FloatingWindow {
155163 Component { id: shellC; SetShell {} }
156164 Component { id: windowsC; SetWindows {} }
157165 Component { id: inputC; SetInput {} }
166+ Component { id: keysC; SetKeys {} }
167+ Component { id: displayC; SetDisplay {} }
168+ Component { id: powerC; SetPower {} }
169+ Component { id: aboutC; SetAbout {} }
158170 }
M .config/quickshell/aurora/SysPage.qml View
@@ -9,29 +9,6 @@ Flickable {
99 contentHeight: col.implicitHeight
1010 boundsBehavior: Flickable.StopAtBounds
1111
12− readonly property var bat: UPower.displayDevice
13− readonly property bool hasBat: bat !== null && bat.isPresent
14− readonly property real batPct: hasBat ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0
15− readonly property bool charging: hasBat && bat.state === UPowerDeviceState.Charging
16−
17− property string armedGpu: ""
18− Timer { id: disarm; interval: 3000; onTriggered: flick.armedGpu = "" }
19−
20− function gpuClick(m) {
21− if (Gpu.mode === m || Gpu.busy) return
22− if (armedGpu !== m) { armedGpu = m; disarm.restart(); return }
23− armedGpu = ""
24− Gpu.set(m)
25− }
26− function stateText() {
27− if (!hasBat) return ""
28− switch (bat.state) {
29− case UPowerDeviceState.Charging: return "charging"
30− case UPowerDeviceState.Discharging: return "discharging"
31− case UPowerDeviceState.FullyCharged: return "full"
32− default: return "idle"
33− }
34− }
3512 // GiB -> "812G" / "1.8T"
3613 function size(v) { return v >= 1000 ? (v / 1024).toFixed(1) + "T" : (v >= 100 ? Math.round(v) : v.toFixed(1)) + "G" }
3714
@@ -75,38 +52,7 @@ Flickable {
7552 }
7653 }
7754
78− ColumnLayout {
79− visible: flick.hasBat
80− Layout.fillWidth: true
81− Layout.topMargin: 6
82− spacing: 8
83− SectionLabel { text: "battery" }
84− StatBar {
85− label: "charge"
86− frac: flick.batPct
87− fill: flick.batPct <= 0.15 && !flick.charging ? Theme.err : Theme.accent
88− valueText: Math.round(flick.batPct * 100) + "% " + flick.stateText()
89− }
90− Text {
91− Layout.fillWidth: true
92− wrapMode: Text.Wrap
93− color: Theme.dim
94− font.family: Theme.font
95− font.pixelSize: Theme.fontSize - 1
96− lineHeight: 1.3
97− text: {
98− if (!flick.hasBat) return ""
99− var lines = []
100− var t = flick.charging ? flick.bat.timeToFull : flick.bat.timeToEmpty
101− lines.push((flick.charging ? "time to full " : "time left ") + Theme.fmtTime(t))
102− if (flick.bat.changeRate !== undefined)
103− lines.push("power draw " + Math.abs(flick.bat.changeRate).toFixed(1) + " W")
104− if (flick.bat.healthSupported)
105− lines.push("health " + Math.round(flick.bat.healthPercentage) + "%")
106− return lines.join("\n")
107− }
108− }
109− }
55+ BatteryInfo { Layout.topMargin: 6 }
11056
11157 ColumnLayout {
11258 visible: PowerProfiles.hasPerformanceProfile !== undefined
@@ -142,38 +88,7 @@ Flickable {
14288 }
14389 }
14490
145− ColumnLayout {
146− visible: Gpu.mode !== ""
147− Layout.fillWidth: true
148− Layout.topMargin: 6
149− spacing: 8
150− SectionLabel { text: "graphics · envycontrol" }
151− RowLayout {
152− Layout.fillWidth: true
153− spacing: 6
154− Repeater {
155− model: ["integrated", "hybrid", "nvidia"]
156− delegate: IconBtn {
157− required property string modelData
158− Layout.fillWidth: true
159− icon: Theme.iGpu
160− label: flick.armedGpu === modelData ? "sure?" : modelData
161− active: Gpu.mode === modelData
162− opacity: Gpu.busy ? 0.5 : 1
163− onClicked: flick.gpuClick(modelData)
164− }
165− }
166− }
167− Text {
168− visible: Gpu.pending !== "" || Gpu.message !== "" || Gpu.busy
169− Layout.fillWidth: true
170− wrapMode: Text.Wrap
171− color: Gpu.message !== "" ? Theme.err : Theme.accent
172− font.family: Theme.font
173− font.pixelSize: Theme.fontSize - 2
174− text: Gpu.busy ? "switching…" : (Gpu.message !== "" ? Gpu.message : "queued: " + Gpu.pending + " · reboot to apply")
175− }
176− }
91+ GpuSwitch { Layout.topMargin: 6 }
17792
17893 Text {
17994 Layout.topMargin: 10
M .config/quickshell/aurora/Toggles.qml View
@@ -29,6 +29,21 @@ Singleton {
2929 airplane = on
3030 }
3131
32+ // ---- night light: gammastep (started by Hyprland) toggles on SIGUSR1. It starts enabled, and it
33+ // can't be asked, so the state is tracked here; hasNightLight = gammastep was running at startup.
34+ property bool hasNightLight: false
35+ property bool nightLight: true
36+ function setNightLight(on) {
37+ if (!hasNightLight || on === nightLight) return
38+ Quickshell.execDetached(["pkill", "-USR1", "-x", "gammastep"])
39+ nightLight = on
40+ }
41+ Process {
42+ command: ["pgrep", "-x", "gammastep"]
43+ running: true
44+ onExited: code => root.hasNightLight = code === 0
45+ }
46+
3247 // ---- colour picker: hyprpicker copies the hex itself, we just show it as a notification
3348 property bool hasPicker: false
3449 function pickColour() { if (!picker.running) picker.running = true }
M .config/quickshell/aurora/shell.qml View
@@ -19,6 +19,8 @@ ShellRoot {
1919 property string overlay: ""
2020 property bool barShown: true // `bar` IPC (SUPER+B); not persisted, a restart shows it again
2121 property var overlayScreen: focusedScreen()
22+ // touch singletons that must start with the shell (Idle creates ~/.config/aurora/hypridle.conf)
23+ readonly property string idleFile: Idle.file
2224 readonly property var tabNames: ["home", "audio", "net", "bt", "sys", "notif"]
2325 // settings app (a normal window): open state + shown section
2426 property bool settingsOpen: false