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.
+824 −110
14 files changed
- M .config/hypr/hypridle.conf +3 −21
- A .config/quickshell/aurora/BatteryInfo.qml +50 −0
- M .config/quickshell/aurora/Field.qml +2 −0
- A .config/quickshell/aurora/GpuSwitch.qml +45 −0
- A .config/quickshell/aurora/Idle.qml +61 −0
- A .config/quickshell/aurora/SetAbout.qml +117 −0
- A .config/quickshell/aurora/SetDisplay.qml +273 −0
- A .config/quickshell/aurora/SetKeys.qml +150 −0
- A .config/quickshell/aurora/SetPower.qml +81 −0
- M .config/quickshell/aurora/Settings.qml +10 −1
- M .config/quickshell/aurora/SettingsApp.qml +13 −1
- M .config/quickshell/aurora/SysPage.qml +2 −87
- M .config/quickshell/aurora/Toggles.qml +15 −0
- M .config/quickshell/aurora/shell.qml +2 −0
| @@ -4,24 +4,6 @@ general { | |||
| 4 | 4 | after_sleep_cmd = hyprctl dispatch 'hl.dsp.dpms({action="on"})' # to avoid having to press a key twice to turn on the display. | |
| 5 | 5 | } | |
| 6 | 6 | ||
| 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 | |
| @@ -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 | + | } | |
| @@ -8,6 +8,7 @@ RowLayout { | |||
| 8 | 8 | property string value: "" | |
| 9 | 9 | property string placeholder: "" | |
| 10 | 10 | signal submitted(string v) | |
| 11 | + | signal edited(string v) // every keystroke (live filters) | |
| 11 | 12 | Layout.fillWidth: true | |
| 12 | 13 | spacing: 8 | |
| 13 | 14 | Text { | |
| @@ -37,6 +38,7 @@ RowLayout { | |||
| 37 | 38 | selectedTextColor: Theme.bg | |
| 38 | 39 | font.family: Theme.font | |
| 39 | 40 | font.pixelSize: Theme.fontSize | |
| 41 | + | onTextEdited: f.edited(text) | |
| 40 | 42 | onAccepted: { if (text !== f.value) f.submitted(text); focus = false } | |
| 41 | 43 | onActiveFocusChanged: if (!activeFocus && text !== f.value) f.submitted(text) | |
| 42 | 44 | Keys.onEscapePressed: e => { text = f.value; focus = false; e.accepted = true } | |
| @@ -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 | + | } | |
| @@ -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 | + | } | |
| @@ -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 | + | } | |
| @@ -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 | + | } | |
| @@ -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 | + | } | |
| @@ -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 | + | } | |
| @@ -14,7 +14,8 @@ Singleton { | |||
| 14 | 14 | fontSize: 12, wsFontSize: 11, barHeight: 22, barAlpha: 1.0, panelAlpha: 0.88, accentIndex: 14, | |
| 15 | 15 | osdTimeout: 1500, desktopClock: true, desktopViz: true, vizBars: 64, | |
| 16 | 16 | 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 | |
| 18 | 19 | }) | |
| 19 | 20 | ||
| 20 | 21 | property alias fontSize: a.fontSize | |
| @@ -32,6 +33,10 @@ Singleton { | |||
| 32 | 33 | property alias wallFolder: a.wallFolder | |
| 33 | 34 | property alias notifSound: a.notifSound | |
| 34 | 35 | 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 | |
| 35 | 40 | ||
| 36 | 41 | function reset() { | |
| 37 | 42 | for (var k in defaults) a[k] = defaults[k] | |
| @@ -71,6 +76,10 @@ Singleton { | |||
| 71 | 76 | property string wallFolder: "dusky-dark" | |
| 72 | 77 | property bool notifSound: true | |
| 73 | 78 | 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 | |
| 74 | 83 | } | |
| 75 | 84 | } | |
| 76 | 85 | // first run: create the file once the directory exists | |
| @@ -28,7 +28,11 @@ FloatingWindow { | |||
| 28 | 28 | { id: "appearance", icon: Theme.iPalette, name: "appearance", group: "look" }, | |
| 29 | 29 | { id: "shell", icon: Theme.iDock, name: "shell", group: "look" }, | |
| 30 | 30 | { 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" } | |
| 32 | 36 | ] | |
| 33 | 37 | readonly property int index: { | |
| 34 | 38 | for (var i = 0; i < sections.length; i++) if (sections[i].id === section) return i | |
| @@ -140,6 +144,10 @@ FloatingWindow { | |||
| 140 | 144 | case "shell": return shellC | |
| 141 | 145 | case "windows": return windowsC | |
| 142 | 146 | case "input": return inputC | |
| 147 | + | case "keybinds": return keysC | |
| 148 | + | case "display": return displayC | |
| 149 | + | case "power": return powerC | |
| 150 | + | case "about": return aboutC | |
| 143 | 151 | default: return netC | |
| 144 | 152 | } | |
| 145 | 153 | } | |
| @@ -155,4 +163,8 @@ FloatingWindow { | |||
| 155 | 163 | Component { id: shellC; SetShell {} } | |
| 156 | 164 | Component { id: windowsC; SetWindows {} } | |
| 157 | 165 | Component { id: inputC; SetInput {} } | |
| 166 | + | Component { id: keysC; SetKeys {} } | |
| 167 | + | Component { id: displayC; SetDisplay {} } | |
| 168 | + | Component { id: powerC; SetPower {} } | |
| 169 | + | Component { id: aboutC; SetAbout {} } | |
| 158 | 170 | } | |
| @@ -9,29 +9,6 @@ Flickable { | |||
| 9 | 9 | contentHeight: col.implicitHeight | |
| 10 | 10 | boundsBehavior: Flickable.StopAtBounds | |
| 11 | 11 | ||
| 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 | − | } | |
| 35 | 12 | // GiB -> "812G" / "1.8T" | |
| 36 | 13 | function size(v) { return v >= 1000 ? (v / 1024).toFixed(1) + "T" : (v >= 100 ? Math.round(v) : v.toFixed(1)) + "G" } | |
| 37 | 14 | ||
| @@ -75,38 +52,7 @@ Flickable { | |||
| 75 | 52 | } | |
| 76 | 53 | } | |
| 77 | 54 | ||
| 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 } | |
| 110 | 56 | ||
| 111 | 57 | ColumnLayout { | |
| 112 | 58 | visible: PowerProfiles.hasPerformanceProfile !== undefined | |
| @@ -142,38 +88,7 @@ Flickable { | |||
| 142 | 88 | } | |
| 143 | 89 | } | |
| 144 | 90 | ||
| 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 } | |
| 177 | 92 | ||
| 178 | 93 | Text { | |
| 179 | 94 | Layout.topMargin: 10 | |
| @@ -29,6 +29,21 @@ Singleton { | |||
| 29 | 29 | airplane = on | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 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 | + | ||
| 32 | 47 | // ---- colour picker: hyprpicker copies the hex itself, we just show it as a notification | |
| 33 | 48 | property bool hasPicker: false | |
| 34 | 49 | function pickColour() { if (!picker.running) picker.running = true } | |
| @@ -19,6 +19,8 @@ ShellRoot { | |||
| 19 | 19 | property string overlay: "" | |
| 20 | 20 | property bool barShown: true // `bar` IPC (SUPER+B); not persisted, a restart shows it again | |
| 21 | 21 | 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 | |
| 22 | 24 | readonly property var tabNames: ["home", "audio", "net", "bt", "sys", "notif"] | |
| 23 | 25 | // settings app (a normal window): open state + shown section | |
| 24 | 26 | property bool settingsOpen: false | |