Krolyxon
Commit e1c1567

Branches

Refs →

aurorae1c1567

aurora: add wallpaper picker (thumbnail grid, applies via setwall)

Author
krolyxon
Date
2026-10-17 14:57 +0530
Parent
5f6633a
Commit
e1c15679ea684f4c498ee4b66fd95669ec7d1a22

+409 −1

6 files changed

  1. M .config/quickshell/aurora/ControlPanel.qml +5 −1
  2. M .config/quickshell/aurora/HomePage.qml +20 −0
  3. M .config/quickshell/aurora/Theme.qml +2 −0
  4. A .config/quickshell/aurora/Wall.qml +127 −0
  5. A .config/quickshell/aurora/WallPicker.qml +237 −0
  6. M .config/quickshell/aurora/shell.qml +18 −0
M .config/quickshell/aurora/ControlPanel.qml View
@@ -10,6 +10,7 @@ PanelWindow {
1010 property int tab: 0
1111 signal tabRequested(int index)
1212 signal closeRequested()
13+ signal overlayRequested(string name)
1314
1415 visible: opened
1516 anchors { top: true; right: true }
@@ -103,7 +104,10 @@ PanelWindow {
103104 Layout.fillHeight: true
104105 currentIndex: win.tab
105106
106− HomePage { onCloseRequested: win.closeRequested() }
107+ HomePage {
108+ onCloseRequested: win.closeRequested()
109+ onOverlayRequested: name => win.overlayRequested(name)
110+ }
107111 AudioPage {}
108112 NetPage { active: win.opened && win.tab === 2 }
109113 BtPage { active: win.opened && win.tab === 3 }
M .config/quickshell/aurora/HomePage.qml View
@@ -8,6 +8,7 @@ ColumnLayout {
88 id: page
99 spacing: 10
1010 signal closeRequested()
11+ signal overlayRequested(string name)
1112
1213 property string armed: ""
1314 Timer { id: disarm; interval: 3000; onTriggered: page.armed = "" }
@@ -95,6 +96,25 @@ ColumnLayout {
9596 }
9697 }
9798
99+ SectionLabel { text: "desktop" }
100+
101+ RowLayout {
102+ Layout.fillWidth: true
103+ spacing: 6
104+ IconBtn {
105+ Layout.fillWidth: true
106+ icon: Theme.iImage
107+ label: "wallpaper"
108+ onClicked: page.overlayRequested("wall")
109+ }
110+ IconBtn {
111+ Layout.fillWidth: true
112+ icon: Theme.iShuffle
113+ label: "random"
114+ onClicked: Wall.random()
115+ }
116+ }
117+
98118 SectionLabel { text: "capture" }
99119
100120 RowLayout {
M .config/quickshell/aurora/Theme.qml View
@@ -149,6 +149,8 @@ Singleton {
149149 readonly property string iEthernet: cp(0xF0200)
150150 readonly property string iPlug: cp(0xF06A5)
151151 readonly property string iBatCharge: cp(0xF0084)
152+ readonly property string iShuffle: cp(0xF049D)
153+ readonly property string iImage: cp(0xF02E9)
152154
153155 // workspace icons: the exact Font Awesome glyphs from your waybar config.
154156 // 1 browser, 2 code, 3 book, 4 cog, 5 chat, 6 file, 7 folder, 8 terminal, 9 flask, 10 image
A .config/quickshell/aurora/Wall.qml View
@@ -0,0 +1,127 @@
1+pragma Singleton
2+import QtQuick
3+import Quickshell
4+import Quickshell.Io
5+
6+// Wallpapers: folder listing, cached thumbnails, apply through ~/.local/bin/setwall
7+// (setwall writes hyprpaper.conf, switches hyprpaper live and runs `wal -i`; Theme then recolours itself).
8+Singleton {
9+ id: root
10+
11+ readonly property string home: Quickshell.env("HOME")
12+ property string rootDir: home + "/pix/wallpapers"
13+ property string folder: "dusky-dark" // same default as setwall's WALL_DIR
14+ readonly property string setwall: home + "/.local/bin/setwall"
15+ readonly property string thumbDir: home + "/.cache/aurora/thumbs"
16+
17+ property var folders: [] // subfolder names; "" = everything under rootDir
18+ property var files: [] // absolute paths of the current folder
19+ property var ready: ({}) // md5(path) -> true once its thumbnail exists
20+ property string current: "" // wallpaper hyprpaper is showing
21+ property bool loading: false
22+
23+ function dirOf(name) { return name === "" ? rootDir : rootDir + "/" + name }
24+ function thumbOf(path) { return thumbDir + "/" + Qt.md5(path) + ".jpg" }
25+
26+ function refresh() {
27+ dirsProc.running = true
28+ listFolder(folder)
29+ }
30+ function listFolder(name) {
31+ folder = name
32+ thumbProc.running = false
33+ listProc.running = false
34+ loading = true
35+ listProc.command = ["find", dirOf(name), "-type", "f", "-not", "-path", "*/.*",
36+ "-regextype", "posix-extended", "-iregex", ".*\\.(png|jpe?g|webp)"]
37+ listProc.running = true
38+ }
39+ function apply(path) {
40+ if (!path) return
41+ current = path
42+ Quickshell.execDetached([setwall, "-p", path])
43+ }
44+ // random from the folder shown last; before the first listing let setwall pick (its own WALL_DIR)
45+ function random() {
46+ if (files.length) apply(files[Math.floor(Math.random() * files.length)])
47+ else Quickshell.execDetached([setwall])
48+ }
49+
50+ Process {
51+ id: dirsProc
52+ command: ["find", root.rootDir, "-mindepth", "1", "-maxdepth", "1", "-type", "d", "-not", "-name", ".*"]
53+ stdout: StdioCollector {
54+ onStreamFinished: {
55+ var out = [], l = text.split("\n")
56+ for (var i = 0; i < l.length; i++) {
57+ var s = l[i].trim()
58+ if (s !== "") out.push(s.substring(s.lastIndexOf("/") + 1))
59+ }
60+ out.sort()
61+ root.folders = out
62+ }
63+ }
64+ }
65+
66+ Process {
67+ id: listProc
68+ stdout: StdioCollector {
69+ onStreamFinished: {
70+ var out = [], l = text.split("\n")
71+ for (var i = 0; i < l.length; i++) if (l[i] !== "") out.push(l[i])
72+ out.sort()
73+ root.files = out
74+ root.loading = false
75+ root.makeThumbs()
76+ }
77+ }
78+ }
79+
80+ // ---- thumbnails ----
81+ // one background job per folder: 4 parallel `magick` runs, prints each hash when its thumb exists
82+ readonly property string thumbJob:
83+ 'f="$1"; h=$(printf %s "$f" | md5sum | cut -c1-32); t="$0/$h.jpg"; ' +
84+ 'if [ ! -f "$t" ]; then ' +
85+ ' magick "$f[0]" -thumbnail 320x180^ -gravity center -extent 320x180 -quality 82 "$t.part.jpg" 2>/dev/null ' +
86+ ' && mv "$t.part.jpg" "$t"; ' +
87+ 'fi; [ -f "$t" ] && echo "$h"'
88+ function makeThumbs() {
89+ if (!files.length) return
90+ thumbProc.command = ["sh", "-c",
91+ 'j="$1"; d="$2"; shift 2; mkdir -p "$d"; printf "%s\\0" "$@" | xargs -0 -n1 -P4 sh -c "$j" "$d"',
92+ "sh", thumbJob, thumbDir].concat(files)
93+ thumbProc.running = true
94+ }
95+ property var pendingReady: ({})
96+ Timer {
97+ id: flush
98+ interval: 250
99+ onTriggered: {
100+ var r = root.ready
101+ for (var k in root.pendingReady) r[k] = true
102+ root.pendingReady = {}
103+ root.ready = r
104+ root.readyChanged()
105+ }
106+ }
107+ Process {
108+ id: thumbProc
109+ stdout: SplitParser {
110+ onRead: line => {
111+ root.pendingReady[line.trim()] = true
112+ if (!flush.running) flush.start()
113+ }
114+ }
115+ }
116+
117+ // current wallpaper, straight from the config setwall writes
118+ FileView {
119+ path: root.home + "/.config/hypr/hyprpaper.conf"
120+ watchChanges: true
121+ onFileChanged: reload()
122+ onLoaded: {
123+ var m = text().match(/^\s*path\s*=\s*(.+)$/m)
124+ if (m) root.current = m[1].trim()
125+ }
126+ }
127+}
A .config/quickshell/aurora/WallPicker.qml View
@@ -0,0 +1,237 @@
1+import QtQuick
2+import QtQuick.Layouts
3+import Quickshell
4+import Quickshell.Wayland
5+import Quickshell.Hyprland
6+
7+// Wallpaper picker: folder chips, filter, thumbnail grid. Enter / double-click applies (stays open
8+// so you can try a few), Esc or click outside closes. Tab / Shift+Tab cycles folders.
9+PanelWindow {
10+ id: win
11+ property bool opened: false
12+ signal closeRequested()
13+
14+ visible: opened
15+ implicitWidth: screen ? Math.round(screen.width * 0.7) : 1200
16+ implicitHeight: screen ? Math.round(screen.height * 0.7) : 700
17+ exclusiveZone: 0
18+ color: "transparent"
19+ WlrLayershell.layer: WlrLayer.Overlay
20+ WlrLayershell.namespace: "aurora-wallpaper"
21+ WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
22+
23+ HyprlandFocusGrab {
24+ windows: [win]
25+ active: win.opened
26+ onCleared: win.closeRequested()
27+ }
28+
29+ onOpenedChanged: {
30+ if (!opened) return
31+ filter.text = ""
32+ Wall.refresh()
33+ filter.forceActiveFocus()
34+ }
35+
36+ readonly property var shown: {
37+ var q = filter.text.toLowerCase(), out = [], l = Wall.files
38+ for (var i = 0; i < l.length; i++)
39+ if (q === "" || l[i].substring(l[i].lastIndexOf("/") + 1).toLowerCase().indexOf(q) >= 0) out.push(l[i])
40+ return out
41+ }
42+ readonly property var chips: [""].concat(Wall.folders)
43+
44+ function cycleFolder(d) {
45+ var i = chips.indexOf(Wall.folder)
46+ Wall.listFolder(chips[(i + d + chips.length) % chips.length])
47+ }
48+ function name(p) { return p.substring(p.lastIndexOf("/") + 1) }
49+
50+ Rectangle {
51+ anchors.fill: parent
52+ color: Theme.bgA
53+ border.width: 2
54+ border.color: Theme.accent
55+
56+ ColumnLayout {
57+ anchors.fill: parent
58+ anchors.margins: 12
59+ spacing: 10
60+
61+ // folder chips
62+ Flow {
63+ Layout.fillWidth: true
64+ spacing: 4
65+ Repeater {
66+ model: win.chips
67+ delegate: Rectangle {
68+ id: chip
69+ required property var modelData
70+ readonly property bool current: Wall.folder === modelData
71+ width: cl.implicitWidth + 18
72+ height: 24
73+ color: current ? Theme.accent : (cm.containsMouse ? Theme.surfaceHi : "transparent")
74+ border.width: 1
75+ border.color: current ? Theme.accent : Theme.line
76+ Text {
77+ id: cl
78+ anchors.centerIn: parent
79+ text: chip.modelData === "" ? "all" : chip.modelData
80+ color: chip.current ? Theme.bg : Theme.fg
81+ font.family: Theme.font
82+ font.pixelSize: Theme.fontSize - 1
83+ font.bold: chip.current
84+ }
85+ MouseArea {
86+ id: cm
87+ anchors.fill: parent
88+ hoverEnabled: true
89+ cursorShape: Qt.PointingHandCursor
90+ onClicked: Wall.listFolder(chip.modelData)
91+ }
92+ }
93+ }
94+ }
95+
96+ // filter + actions
97+ RowLayout {
98+ Layout.fillWidth: true
99+ spacing: 6
100+ Rectangle {
101+ Layout.fillWidth: true
102+ implicitHeight: 30
103+ color: Theme.surface
104+ border.width: 1
105+ border.color: filter.activeFocus ? Theme.accent : Theme.line
106+ Text {
107+ anchors.left: parent.left
108+ anchors.leftMargin: 10
109+ anchors.verticalCenter: parent.verticalCenter
110+ text: Theme.iSearch
111+ color: Theme.dim
112+ font.family: Theme.font
113+ font.pixelSize: Theme.fontSize
114+ }
115+ TextInput {
116+ id: filter
117+ anchors.fill: parent
118+ anchors.leftMargin: 32
119+ anchors.rightMargin: 10
120+ verticalAlignment: TextInput.AlignVCenter
121+ color: Theme.fg
122+ selectionColor: Theme.accent
123+ selectedTextColor: Theme.bg
124+ font.family: Theme.font
125+ font.pixelSize: Theme.fontSize
126+ clip: true
127+ onTextChanged: grid.currentIndex = 0
128+ Keys.onEscapePressed: win.closeRequested()
129+ Keys.onReturnPressed: if (grid.currentIndex >= 0) Wall.apply(win.shown[grid.currentIndex])
130+ Keys.onEnterPressed: if (grid.currentIndex >= 0) Wall.apply(win.shown[grid.currentIndex])
131+ Keys.onLeftPressed: event => { if (text === "") grid.moveCurrentIndexLeft(); else event.accepted = false }
132+ Keys.onRightPressed: event => { if (text === "") grid.moveCurrentIndexRight(); else event.accepted = false }
133+ Keys.onUpPressed: grid.moveCurrentIndexUp()
134+ Keys.onDownPressed: grid.moveCurrentIndexDown()
135+ Keys.onTabPressed: win.cycleFolder(1)
136+ Keys.onBacktabPressed: win.cycleFolder(-1)
137+ Keys.onPressed: event => {
138+ if (event.key === Qt.Key_PageDown) { grid.currentIndex = Math.min(win.shown.length - 1, grid.currentIndex + grid.perPage); event.accepted = true }
139+ else if (event.key === Qt.Key_PageUp) { grid.currentIndex = Math.max(0, grid.currentIndex - grid.perPage); event.accepted = true }
140+ }
141+ }
142+ }
143+ Text {
144+ text: Wall.loading ? "..." : win.shown.length + " / " + Wall.files.length
145+ color: Theme.dim
146+ font.family: Theme.font
147+ font.pixelSize: Theme.fontSize - 1
148+ }
149+ IconBtn { icon: Theme.iShuffle; label: "random"; onClicked: Wall.random() }
150+ IconBtn { icon: Theme.iClose; onClicked: win.closeRequested() }
151+ }
152+
153+ GridView {
154+ id: grid
155+ Layout.fillWidth: true
156+ Layout.fillHeight: true
157+ clip: true
158+ readonly property int cols: Math.max(1, Math.floor(width / 240))
159+ readonly property int perPage: cols * Math.max(1, Math.floor(height / cellHeight))
160+ cellWidth: Math.floor(width / cols)
161+ cellHeight: Math.round(cellWidth * 9 / 16) + 22
162+ model: win.shown
163+ currentIndex: 0
164+ boundsBehavior: Flickable.StopAtBounds
165+ highlightMoveDuration: 0
166+ cacheBuffer: cellHeight * 2
167+
168+ delegate: Item {
169+ id: cell
170+ required property var modelData
171+ required property int index
172+ readonly property string hash: Qt.md5(modelData)
173+ readonly property bool isCurrent: Wall.current === modelData
174+ readonly property bool selected: grid.currentIndex === index
175+ width: grid.cellWidth
176+ height: grid.cellHeight
177+
178+ Rectangle {
179+ anchors.fill: parent
180+ anchors.margins: 4
181+ color: Theme.surface
182+ border.width: cell.selected || cell.isCurrent ? 2 : 1
183+ border.color: cell.selected ? Theme.fg : (cell.isCurrent ? Theme.accent : Theme.line)
184+
185+ Image {
186+ id: img
187+ anchors.fill: parent
188+ anchors.margins: 2
189+ anchors.bottomMargin: 20
190+ fillMode: Image.PreserveAspectCrop
191+ asynchronous: true
192+ cache: false
193+ sourceSize.width: 320
194+ sourceSize.height: 180
195+ source: Wall.ready[cell.hash] ? "file://" + Wall.thumbDir + "/" + cell.hash + ".jpg" : ""
196+ }
197+ Text {
198+ anchors.centerIn: img
199+ visible: img.status !== Image.Ready
200+ text: Theme.iImage
201+ color: Theme.dim
202+ font.family: Theme.font
203+ font.pixelSize: 22
204+ }
205+ Text {
206+ anchors.left: parent.left
207+ anchors.right: parent.right
208+ anchors.bottom: parent.bottom
209+ anchors.margins: 4
210+ height: 14
211+ text: (cell.isCurrent ? Theme.iCheck + " " : "") + win.name(cell.modelData)
212+ elide: Text.ElideMiddle
213+ color: cell.isCurrent ? Theme.accent : Theme.dim
214+ font.family: Theme.font
215+ font.pixelSize: Theme.fontSize - 2
216+ }
217+ }
218+ MouseArea {
219+ anchors.fill: parent
220+ cursorShape: Qt.PointingHandCursor
221+ onClicked: grid.currentIndex = cell.index
222+ onDoubleClicked: Wall.apply(cell.modelData)
223+ }
224+ }
225+ }
226+
227+ Text {
228+ Layout.fillWidth: true
229+ text: "enter apply tab folder arrows move esc close " + Theme.iImage + " " + win.name(Wall.current)
230+ elide: Text.ElideRight
231+ color: Theme.dim
232+ font.family: Theme.font
233+ font.pixelSize: Theme.fontSize - 2
234+ }
235+ }
236+ }
237+}
M .config/quickshell/aurora/shell.qml View
@@ -15,6 +15,9 @@ ShellRoot {
1515 property string lastScreen: ""
1616 property var panelScreen: focusedScreen()
1717 readonly property var focusedScr: focusedScreen()
18+ // centered overlay windows (wallpaper picker, clipboard, ...): one at a time, on the focused monitor
19+ property string overlay: ""
20+ property var overlayScreen: focusedScreen()
1821 readonly property var tabNames: ["home", "audio", "net", "bt", "sys", "notif"]
1922
2023 // screen of the monitor that currently has focus (falls back to the first screen)
@@ -31,6 +34,12 @@ ShellRoot {
3134 lastTab = tabNames[tab]
3235 lastScreen = panelScreen ? panelScreen.name : ""
3336 }
37+ function toggleOverlay(name) {
38+ if (overlay === name) { overlay = ""; return }
39+ close()
40+ overlayScreen = focusedScreen()
41+ overlay = name
42+ }
3443 function openTab(name) {
3544 var i = tabNames.indexOf(name)
3645 if (i >= 0) tab = i
@@ -64,6 +73,13 @@ ShellRoot {
6473 tab: root.tab
6574 onTabRequested: i => root.tab = i
6675 onCloseRequested: root.close()
76+ onOverlayRequested: name => { root.close(); root.toggleOverlay(name) }
77+ }
78+
79+ WallPicker {
80+ screen: root.overlayScreen
81+ opened: root.overlay === "wall"
82+ onCloseRequested: root.overlay = ""
6783 }
6884
6985 NotifPopups { screen: root.focusedScr; suppressed: root.opened }
@@ -88,6 +104,8 @@ ShellRoot {
88104 function dnd(): void { Notifs.dnd = !Notifs.dnd }
89105 function shot(mode: string): void { root.close(); Shot.take(mode) } // region | window | output | ocr
90106 function shotsave(): void { Shot.save = !Shot.save }
107+ function wall(): void { root.toggleOverlay("wall") }
108+ function wallrandom(): void { Wall.random() }
91109 function bright(dir: string): void { Bright.step(dir === "down" ? -1 : 1) } // up | down
92110 }
93111 }