auroraeff182a
aurora: pending updates indicator, gesture settings
+156 −3
9 files changed
- M .config/quickshell/aurora/Bar.qml +11 −0
- M .config/quickshell/aurora/HyprConf.qml +1 −0
- M .config/quickshell/aurora/SetInput.qml +5 −1
- M .config/quickshell/aurora/SetShell.qml +2 −0
- M .config/quickshell/aurora/Settings.qml +3 −1
- M .config/quickshell/aurora/SysPage.qml +45 −1
- M .config/quickshell/aurora/Theme.qml +1 −0
- A .config/quickshell/aurora/Updates.qml +87 −0
- M .config/quickshell/aurora/shell.qml +1 −0
| @@ -128,6 +128,17 @@ PanelWindow { | |||
| 128 | 128 | underline: Theme.err | |
| 129 | 129 | } | |
| 130 | 130 | ||
| 131 | + | BarPill { // left click: list in the sys tab, right click: upgrade in foot | |
| 132 | + | visible: bar.monFocused && Settings.updatesPill && Updates.count > 0 | |
| 133 | + | icon: Theme.iUpdate | |
| 134 | + | text: String(Updates.count) | |
| 135 | + | tint: Updates.upgrading ? Theme.dim : Theme.fg | |
| 136 | + | onActivated: button => { | |
| 137 | + | if (button === Qt.RightButton) Updates.upgrade() | |
| 138 | + | else bar.togglePanel("sys", bar.modelData) | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | ||
| 131 | 142 | BarPill { | |
| 132 | 143 | visible: Toggles.caffeine || Toggles.airplane | |
| 133 | 144 | icon: (Toggles.caffeine ? Theme.iCoffee : "") + (Toggles.caffeine && Toggles.airplane ? " " : "") | |
| @@ -31,6 +31,7 @@ Singleton { | |||
| 31 | 31 | "input:left_handed": "bool", | |
| 32 | 32 | "input:touchpad:natural_scroll": "bool", "input:touchpad:tap_to_click": "bool", | |
| 33 | 33 | "input:touchpad:disable_while_typing": "bool", "input:touchpad:scroll_factor": "float", | |
| 34 | + | "gestures:workspace_swipe_invert": "bool", "gestures:workspace_swipe_distance": "int", | |
| 34 | 35 | "cursor:inactive_timeout": "int", "cursor:hide_on_key_press": "bool" | |
| 35 | 36 | }) | |
| 36 | 37 | ||
| @@ -57,6 +57,10 @@ Flickable { | |||
| 57 | 57 | Switch { label: "disable while typing"; checked: flick.v("input:touchpad:disable_while_typing", false); onToggled: on => HyprConf.set("input:touchpad:disable_while_typing", on) } | |
| 58 | 58 | Knob { label: "scroll speed"; from: 0.1; to: 3; decimals: 2; value: flick.v("input:touchpad:scroll_factor", 1); onMoved: x => HyprConf.set("input:touchpad:scroll_factor", x) } | |
| 59 | 59 | ||
| 60 | + | SectionLabel { text: "gestures" } | |
| 61 | + | Switch { label: "invert swipe"; hint: "reverse the 3-finger workspace swipe"; checked: flick.v("gestures:workspace_swipe_invert", true); onToggled: on => HyprConf.set("gestures:workspace_swipe_invert", on) } | |
| 62 | + | Knob { label: "swipe distance"; unit: "px"; from: 100; to: 1000; value: flick.v("gestures:workspace_swipe_distance", 300); onMoved: x => HyprConf.set("gestures:workspace_swipe_distance", Math.round(x / 10) * 10) } | |
| 63 | + | ||
| 60 | 64 | SectionLabel { text: "cursor" } | |
| 61 | 65 | Knob { label: "hide when idle"; unit: "s"; from: 0; to: 30; value: flick.v("cursor:inactive_timeout", 0); onMoved: x => HyprConf.set("cursor:inactive_timeout", x) } | |
| 62 | 66 | Switch { label: "hide while typing"; checked: flick.v("cursor:hide_on_key_press", false); onToggled: on => HyprConf.set("cursor:hide_on_key_press", on) } | |
| @@ -72,7 +76,7 @@ Flickable { | |||
| 72 | 76 | font.family: Theme.font | |
| 73 | 77 | font.pixelSize: Theme.fontSize - 2 | |
| 74 | 78 | } | |
| 75 | − | IconBtn { icon: Theme.iRefresh; label: "reset to config"; onClicked: HyprConf.resetAll(["input:", "cursor:"]) } | |
| 79 | + | IconBtn { icon: Theme.iRefresh; label: "reset to config"; onClicked: HyprConf.resetAll(["input:", "cursor:", "gestures:"]) } | |
| 76 | 80 | } | |
| 77 | 81 | } | |
| 78 | 82 | } | |
| @@ -49,7 +49,9 @@ Flickable { | |||
| 49 | 49 | SectionLabel { text: "network" } | |
| 50 | 50 | Switch { label: "captive portals"; hint: "open the login page in the browser when a network needs one"; checked: Settings.portalAutoOpen; onToggled: on => Settings.portalAutoOpen = on } | |
| 51 | 51 | ||
| 52 | + | SectionLabel { text: "bar" } | |
| 52 | 53 | Switch { label: "privacy indicators"; hint: "show in the bar when an app records the mic, uses the camera or shares the screen"; checked: Settings.privacyIndicators; onToggled: on => Settings.privacyIndicators = on } | |
| 54 | + | Switch { label: "pending updates"; hint: "show the pacman + AUR update count in the bar"; checked: Settings.updatesPill; onToggled: on => Settings.updatesPill = on } | |
| 53 | 55 | ||
| 54 | 56 | SectionLabel { text: "notifications" } | |
| 55 | 57 | Switch { label: "do not disturb"; hint: "hide popups and mute the sound (critical ones still show)"; checked: Notifs.dnd; onToggled: on => Notifs.dnd = on } | |
| @@ -17,7 +17,7 @@ Singleton { | |||
| 17 | 17 | notifSound: true, notifVolume: 0.7, | |
| 18 | 18 | idleDim: 150, idleOff: 330, idleLock: 600, idleSuspend: 0, charTone: 0, portalAutoOpen: true, | |
| 19 | 19 | chargeMode: "off", batLow: 15, batCritical: 5, batCriticalAction: "none", | |
| 20 | − | privacyIndicators: true | |
| 20 | + | privacyIndicators: true, updatesPill: true | |
| 21 | 21 | }) | |
| 22 | 22 | ||
| 23 | 23 | property alias fontSize: a.fontSize | |
| @@ -46,6 +46,7 @@ Singleton { | |||
| 46 | 46 | property alias batCritical: a.batCritical | |
| 47 | 47 | property alias batCriticalAction: a.batCriticalAction | |
| 48 | 48 | property alias privacyIndicators: a.privacyIndicators | |
| 49 | + | property alias updatesPill: a.updatesPill | |
| 49 | 50 | ||
| 50 | 51 | function reset() { | |
| 51 | 52 | for (var k in defaults) a[k] = defaults[k] | |
| @@ -96,6 +97,7 @@ Singleton { | |||
| 96 | 97 | property int batCritical: 5 | |
| 97 | 98 | property string batCriticalAction: "none" // none | suspend | poweroff | |
| 98 | 99 | property bool privacyIndicators: true // mic/camera/screen-share icons in the bar (Privacy.qml) | |
| 100 | + | property bool updatesPill: true // pending package updates in the bar (Updates.qml) | |
| 99 | 101 | } | |
| 100 | 102 | } | |
| 101 | 103 | // first run: create the file once the directory exists | |
| @@ -13,7 +13,7 @@ Flickable { | |||
| 13 | 13 | function size(v) { return v >= 1000 ? (v / 1024).toFixed(1) + "T" : (v >= 100 ? Math.round(v) : v.toFixed(1)) + "G" } | |
| 14 | 14 | ||
| 15 | 15 | // pick up drives plugged in since the last poll | |
| 16 | − | onVisibleChanged: if (visible) SysStats.refreshDisks() | |
| 16 | + | onVisibleChanged: if (visible) { SysStats.refreshDisks(); Updates.check(false) } | |
| 17 | 17 | ||
| 18 | 18 | ColumnLayout { | |
| 19 | 19 | id: col | |
| @@ -79,6 +79,50 @@ Flickable { | |||
| 79 | 79 | } | |
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | + | // pending pacman + AUR updates (Updates) | |
| 83 | + | SectionLabel { text: "updates"; Layout.topMargin: 6; visible: Updates.available } | |
| 84 | + | RowLayout { | |
| 85 | + | visible: Updates.available | |
| 86 | + | Layout.fillWidth: true | |
| 87 | + | spacing: 6 | |
| 88 | + | Text { | |
| 89 | + | Layout.fillWidth: true | |
| 90 | + | text: Updates.checking ? "checking..." | |
| 91 | + | : Updates.error !== "" ? Updates.error | |
| 92 | + | : (Updates.count === 0 ? "up to date" : Updates.repo + " repo, " + Updates.aur + " aur") | |
| 93 | + | + (Updates.lastCheck.getTime() > 0 ? " · " + Qt.formatDateTime(Updates.lastCheck, "HH:mm") : "") | |
| 94 | + | color: Updates.error !== "" ? Theme.err : Theme.fg | |
| 95 | + | font.family: Theme.font | |
| 96 | + | font.pixelSize: Theme.fontSize | |
| 97 | + | elide: Text.ElideRight | |
| 98 | + | } | |
| 99 | + | IconBtn { icon: Theme.iRefresh; label: "check"; onClicked: Updates.check(true) } | |
| 100 | + | IconBtn { | |
| 101 | + | visible: Updates.count > 0 | |
| 102 | + | icon: Theme.iUpdate | |
| 103 | + | label: Updates.upgrading ? "running" : "upgrade" | |
| 104 | + | active: Updates.upgrading | |
| 105 | + | onClicked: Updates.upgrade() | |
| 106 | + | } | |
| 107 | + | } | |
| 108 | + | Repeater { | |
| 109 | + | model: Updates.available ? Updates.list.slice(0, 6) : [] | |
| 110 | + | delegate: ListRow { | |
| 111 | + | required property var modelData | |
| 112 | + | icon: Theme.iUpdate | |
| 113 | + | title: modelData.name | |
| 114 | + | subtitle: modelData.from + " → " + modelData.to | |
| 115 | + | trailing: modelData.aur ? "aur" : "" | |
| 116 | + | } | |
| 117 | + | } | |
| 118 | + | Text { | |
| 119 | + | visible: Updates.available && Updates.list.length > 6 | |
| 120 | + | text: "+ " + (Updates.list.length - 6) + " more" | |
| 121 | + | color: Theme.dim | |
| 122 | + | font.family: Theme.font | |
| 123 | + | font.pixelSize: Theme.fontSize - 2 | |
| 124 | + | } | |
| 125 | + | ||
| 82 | 126 | BatteryInfo { Layout.topMargin: 6 } | |
| 83 | 127 | ||
| 84 | 128 | ColumnLayout { | |
| @@ -173,6 +173,7 @@ Singleton { | |||
| 173 | 173 | readonly property string iDisk: cp(0xF02CA) | |
| 174 | 174 | readonly property string iUsb: cp(0xF129B) | |
| 175 | 175 | readonly property string iEject: cp(0xF01EA) | |
| 176 | + | readonly property string iUpdate: cp(0xF03D5) // package up | |
| 176 | 177 | readonly property string iFolderOpen: cp(0xF0770) | |
| 177 | 178 | readonly property string iGpu: cp(0xF08AE) | |
| 178 | 179 | readonly property string iLeaf: cp(0xF032A) | |
| @@ -0,0 +1,87 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Io | |
| 5 | + | ||
| 6 | + | // pending package updates: checkupdates (pacman-contrib, syncs a private db copy) + paru -Qua (AUR). | |
| 7 | + | // Checked a minute after start, then every 2h; the sys tab asks for a fresh one when opened (check(false) | |
| 8 | + | // skips it if the last one is under 10 min old). upgrade() runs paru in foot, started through Hyprland so | |
| 9 | + | // restarting aurora can't kill a pacman transaction; the terminal calls the `updates` IPC when it closes. | |
| 10 | + | Singleton { | |
| 11 | + | id: root | |
| 12 | + | property bool available: false // checkupdates installed | |
| 13 | + | property bool checking: false | |
| 14 | + | property bool upgrading: false | |
| 15 | + | property var list: [] // [{name, from, to, aur}], repo packages first | |
| 16 | + | property int repo: 0 | |
| 17 | + | property int aur: 0 | |
| 18 | + | readonly property int count: repo + aur | |
| 19 | + | property date lastCheck: new Date(0) | |
| 20 | + | property string error: "" // checkupdates failed (offline, db locked...): the old list is kept | |
| 21 | + | ||
| 22 | + | function check(force) { | |
| 23 | + | if (!available || checking) return | |
| 24 | + | if (!force && Date.now() - lastCheck.getTime() < 600000) return | |
| 25 | + | checking = true | |
| 26 | + | checkProc.running = true | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | function upgrade() { | |
| 30 | + | if (upgrading) return | |
| 31 | + | upgrading = true | |
| 32 | + | upgradeGuard.restart() | |
| 33 | + | Quickshell.execDetached(["hyprctl", "dispatch", "hl.dsp.exec_cmd([[foot -T 'system upgrade' -e sh -c " | |
| 34 | + | + "'paru; printf \"\\ndone, press enter to close\"; read _; qs -c aurora ipc call shell updates']])"]) | |
| 35 | + | } | |
| 36 | + | // called by the upgrade terminal when it closes (and usable by hand after updating elsewhere) | |
| 37 | + | function finished() { | |
| 38 | + | upgrading = false | |
| 39 | + | upgradeGuard.stop() | |
| 40 | + | check(true) | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // "name old -> new [ignored]" lines; @repo <exit code> separates the two lists | |
| 44 | + | function parse(text) { | |
| 45 | + | var out = [], nr = 0, na = 0, isAur = false, code = 0 | |
| 46 | + | var lines = text.split("\n") | |
| 47 | + | for (var i = 0; i < lines.length; i++) { | |
| 48 | + | var l = lines[i].trim() | |
| 49 | + | var c = l.match(/^@repo (\d+)$/) | |
| 50 | + | if (c) { code = parseInt(c[1]); isAur = true; continue } | |
| 51 | + | var m = l.match(/^(\S+)\s+(\S+)\s+->\s+(\S+)/) | |
| 52 | + | if (!m || l.indexOf("[ignored]") >= 0) continue | |
| 53 | + | out.push({ name: m[1], from: m[2], to: m[3], aur: isAur }) | |
| 54 | + | if (isAur) na++; else nr++ | |
| 55 | + | } | |
| 56 | + | // checkupdates: 0 = updates, 2 = none, anything else = failed | |
| 57 | + | if (code !== 0 && code !== 2) { | |
| 58 | + | error = "checkupdates failed (" + code + ")" | |
| 59 | + | out = list.filter(function (p) { return !p.aur }).concat(out.filter(function (p) { return p.aur })) | |
| 60 | + | nr = repo | |
| 61 | + | } else { | |
| 62 | + | error = "" | |
| 63 | + | } | |
| 64 | + | list = out | |
| 65 | + | repo = nr | |
| 66 | + | aur = na | |
| 67 | + | lastCheck = new Date() | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | Process { | |
| 71 | + | command: ["sh", "-c", "command -v checkupdates"] | |
| 72 | + | running: true | |
| 73 | + | onExited: code => root.available = code === 0 | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | Process { | |
| 77 | + | id: checkProc | |
| 78 | + | command: ["sh", "-c", 'checkupdates 2>/dev/null; echo "@repo $?"; command -v paru >/dev/null && paru -Qua 2>/dev/null; true'] | |
| 79 | + | stdout: StdioCollector { onStreamFinished: { root.parse(text); root.checking = false } } | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // the terminal may be closed with the window's X, skipping the IPC call | |
| 83 | + | Timer { id: upgradeGuard; interval: 3600000; onTriggered: root.upgrading = false } | |
| 84 | + | ||
| 85 | + | Timer { interval: 60000; running: root.available; onTriggered: root.check(true) } | |
| 86 | + | Timer { interval: 7200000; running: root.available; repeat: true; onTriggered: root.check(true) } | |
| 87 | + | } | |
| @@ -219,5 +219,6 @@ ShellRoot { | |||
| 219 | 219 | function caffeine(): void { Toggles.caffeine = !Toggles.caffeine } | |
| 220 | 220 | function portal(): void { Net.signIn() } // open the captive portal login page (or say we're online) | |
| 221 | 221 | function bright(dir: string): void { Bright.step(dir === "down" ? -1 : 1) } // up | down | |
| 222 | + | function updates(): void { Updates.finished() } // re-check pending updates (the upgrade terminal calls it) | |
| 222 | 223 | } | |
| 223 | 224 | } | |