Krolyxon
Commit 2358cd9

Branches

Refs →

aurora2358cd9

aurora: nothing os redesign, phase 3 (wallpaper looks, home screen, picker)

aurora draws the wallpaper itself (Back/WallView, glyph's wall shader): mono dots, colour
dots or photo, wave transitions; hyprpaper is no longer started, hyprpaper.conf stays the
record. Desktop is glyph's home: Doto clock, greeting, bento widgets. Wallpaper picker with
full-screen live preview and look switch. New appearance settings.
Author
krolyxon
Date
2026-10-28 20:30 +0530
Parent
8ab0f85
Commit
2358cd9f5d513b94431b7f4bf66212d051bd9bdd

+792 −233

14 files changed

  1. M .config/hypr/hyprland.lua +1 −1
  2. A .config/quickshell/aurora/Back.qml +48 −0
  3. M .config/quickshell/aurora/CatLoop.qml +3 −2
  4. M .config/quickshell/aurora/Cava.qml +2 −1
  5. M .config/quickshell/aurora/Desktop.qml +353 −68
  6. A .config/quickshell/aurora/Orb.qml +26 −0
  7. M .config/quickshell/aurora/SetAppearance.qml +23 −0
  8. M .config/quickshell/aurora/SetShell.qml +2 −2
  9. M .config/quickshell/aurora/Settings.qml +18 −1
  10. M .config/quickshell/aurora/Wall.qml +7 −9
  11. M .config/quickshell/aurora/WallPicker.qml +208 −147
  12. A .config/quickshell/aurora/WallView.qml +93 −0
  13. M .config/quickshell/aurora/shell.qml +6 −0
  14. M .local/bin/setwall +2 −2
M .config/hypr/hyprland.lua View
@@ -30,7 +30,7 @@ hl.on("hyprland.start", function()
3030 hl.exec_cmd("gammastep")
3131 hl.exec_cmd("hypridle")
3232 -- hl.exec_cmd("blueman-applet")
33− hl.exec_cmd("hyprpaper") -- wallpaper is set by aurora (Wall.qml), it reads hyprpaper.conf
33+ -- no hyprpaper: aurora draws the wallpaper itself (Back.qml / WallView.qml)
3434 -- polkit agent: aurora (Agents.qml); it starts hyprpolkitagent itself if it can't register
3535 end)
3636
A .config/quickshell/aurora/Back.qml View
@@ -0,0 +1,48 @@
1+import QtQuick
2+import Quickshell
3+import Quickshell.Wayland
4+import Quickshell.Hyprland
5+
6+// The desktop backdrop of one monitor (Background layer, under the home screen): the wallpaper in Settings.wallLook
7+// (WallView), and without one a dot grid with the breathing halftone orb. A window of its own: the home screen above it
8+// redraws often (clock colon, rings, cava) while this full-screen shader only redraws for the wave or a look change.
9+// aurora draws the wallpaper itself; hyprpaper isn't used any more (Wall.qml).
10+// After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), home/Back.qml.
11+PanelWindow {
12+ id: win
13+ required property var modelData
14+ screen: modelData
15+
16+ anchors { top: true; bottom: true; left: true; right: true }
17+ exclusionMode: ExclusionMode.Ignore
18+ color: Theme.bg
19+ mask: Region {}
20+ WlrLayershell.layer: WlrLayer.Background
21+ WlrLayershell.namespace: "aurora-back"
22+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
23+
24+ readonly property var hmon: Hyprland.monitorFor(modelData)
25+ readonly property bool home: hmon && hmon.activeWorkspace ? hmon.activeWorkspace.toplevels.values.length === 0 : true
26+
27+ property real phase: 0
28+ Timer { running: win.home && orb.visible; interval: 100; repeat: true; onTriggered: win.phase += 0.1 }
29+
30+ Image {
31+ visible: Settings.dotGrid && !wall.opaque
32+ anchors.fill: parent
33+ fillMode: Image.Tile
34+ source: "data:image/svg+xml," + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><circle cx="16" cy="16" r="1.5" fill="#' + Theme.hex(Theme.s2) + '"/></svg>')
35+ }
36+ Orb {
37+ id: orb
38+ visible: !wall.opaque
39+ x: win.width - 720
40+ y: win.height - 580
41+ phase: win.phase
42+ }
43+ WallView {
44+ id: wall
45+ anchors.fill: parent
46+ path: Wall.current
47+ }
48+}
M .config/quickshell/aurora/CatLoop.qml View
@@ -4,7 +4,8 @@ import QtQuick
44 // falls asleep after 4 idle cycles. Needs the "Waycat" font you already use.
55 Item {
66 id: cat
7− implicitWidth: 40
7+ implicitWidth: Math.max(40, size * 2.6)
8+ property int size: 14 // the bar uses 14; the home screen draws it big
89
910 property int idx: 0
1011 property bool sleeping: false
@@ -18,7 +19,7 @@ Item {
1819 text: cat.frames[cat.idx] || ""
1920 color: Theme.fg
2021 font.family: "Waycat"
21− font.pixelSize: 14
22+ font.pixelSize: cat.size
2223 font.bold: true
2324 }
2425
M .config/quickshell/aurora/Cava.qml View
@@ -9,7 +9,8 @@ import Quickshell.Services.Mpris
99 Singleton {
1010 id: root
1111 readonly property int bars: Math.max(8, Settings.vizBars)
12− readonly property bool wanted: Settings.desktopViz
12+ // the home screen's cava widget (round Glyph matrix)
13+ readonly property bool wanted: Settings.desktopClock && Settings.desktopViz && Settings.homeWidgets.indexOf("cava") >= 0
1314 property bool available: false
1415 property var values: [] // 0..1 per bar
1516
M .config/quickshell/aurora/Desktop.qml View
@@ -1,17 +1,21 @@
11 import QtQuick
22 import Quickshell
33 import Quickshell.Wayland
4−import Quickshell.Services.Mpris
4+import Quickshell.Hyprland
5+import Quickshell.Bluetooth
6+import Quickshell.Services.UPower
57
6−// Desktop layer (per monitor): clock, date, system line, now playing, cava visualizer along the bottom.
7−// Sits on the Bottom layer: above hyprpaper, below windows, never takes input.
8+// The home screen of one monitor (Bottom layer: above the wallpaper, under windows, never takes input):
9+// a 300px Doto clock with a blinking colon, the date, a greeting, a meta line, and a bento of widgets
10+// (Settings.homeWidgets, 3 x 3 cells of 220px; wide ones span cells, what doesn't fit is left out).
11+// Laid out on a 1920 x 1080 canvas scaled to the screen. Clocks blink and the cover spins only while the workspace is empty.
12+// After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), home/Home.qml.
813 PanelWindow {
914 id: win
1015 required property var modelData
1116 screen: modelData
12− readonly property bool showClock: Settings.desktopClock
13− readonly property bool showViz: Settings.desktopViz
1417
18+ visible: Settings.desktopClock
1519 anchors { top: true; bottom: true; left: true; right: true }
1620 exclusionMode: ExclusionMode.Ignore
1721 color: "transparent"
@@ -20,84 +24,365 @@ PanelWindow {
2024 WlrLayershell.namespace: "aurora-desktop"
2125 WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
2226
27+ readonly property var hmon: Hyprland.monitorFor(modelData)
28+ readonly property bool home: hmon && hmon.activeWorkspace ? hmon.activeWorkspace.toplevels.values.length === 0 : true
29+
2330 SystemClock { id: clock; precision: SystemClock.Minutes }
31+ readonly property var en: Qt.locale("en_US")
2432
25− function pick() {
26− var l = Mpris.players.values, first = null
27− for (var i = 0; i < l.length; i++) {
28− if (l[i].isPlaying) return l[i]
29− if (first === null) first = l[i]
30− }
31− return first
33+ // blinking colon: two frames a second, not an animation on every vsync
34+ property bool dim: false
35+ Timer { running: win.home && Settings.blink && win.visible; interval: 500; repeat: true; onTriggered: win.dim = !win.dim }
36+
37+ readonly property var player: Media.player
38+ function findBuds() {
39+ var l = Bluetooth.devices.values
40+ for (var i = 0; i < l.length; i++) if (l[i].connected && l[i].batteryAvailable) return l[i]
41+ return null
42+ }
43+ readonly property var buds: findBuds()
44+ readonly property var bat: UPower.displayDevice
45+ readonly property real batPct: bat && bat.isPresent ? (bat.percentage > 1 ? bat.percentage / 100 : bat.percentage) : 0
46+
47+ function week(d) { // ISO week
48+ var t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))
49+ t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7))
50+ return Math.ceil(((t - Date.UTC(t.getUTCFullYear(), 0, 1)) / 86400000 + 1) / 7)
51+ }
52+ readonly property string hello: {
53+ var h = clock.date.getHours()
54+ return h < 5 ? "Good night" : h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening"
55+ }
56+ // widget id -> width in cells
57+ readonly property var kinds: ({ calendar: 1, cpu: 1, ram: 1, disk: 1, battery: 1, gpu: 1, player: 2, cava: 1, cat: 1, system: 3 })
58+ function shows(id) {
59+ if (id === "cava") return Settings.desktopViz && Cava.available
60+ if (id === "battery") return bat !== null && bat.isPresent
61+ if (id === "gpu") return Gpu.mode !== ""
62+ return kinds[id] !== undefined
3263 }
33− readonly property var player: pick()
3464
35− Column {
36− visible: win.showClock
37− x: 56
38− y: Theme.barHeight + 48
39− spacing: 2
65+ // the 1920 x 1080 canvas, scaled to the screen
66+ Item {
67+ id: stage
68+ readonly property real s: Math.min(win.width / 1920, win.height / 1080)
69+ width: win.width / s
70+ height: win.height / s
71+ scale: s
72+ transformOrigin: Item.TopLeft
4073
74+ Row {
75+ x: 182
76+ y: 150
77+ Num { text: Theme.hh(clock.date); font.pixelSize: 300 }
78+ Num { text: ":"; font.pixelSize: 300; opacity: Settings.blink && win.dim ? 0.2 : 1 }
79+ Num { text: Qt.formatDateTime(clock.date, "mm"); font.pixelSize: 300 }
80+ }
81+ Lb {
82+ x: 190
83+ y: 448
84+ px: 14
85+ text: win.en.toString(clock.date, "dddd · d MMMM") + " · week " + win.week(clock.date)
86+ }
4187 Text {
42− text: Qt.formatDateTime(clock.date, "HH:mm")
88+ x: 188
89+ y: 560
90+ textFormat: Text.StyledText
91+ text: win.hello + ",<br><font color=\"" + Theme.grey + "\">" + (Settings.name || Quickshell.env("USER") || "") + ".</font>"
92+ font.family: Theme.ui
93+ font.pixelSize: 54
94+ font.weight: Font.Light
95+ font.letterSpacing: -1
96+ lineHeight: 1.1
4397 color: Theme.fg
44− font.family: Theme.font
45− font.pixelSize: 96
46− font.bold: true
4798 }
4899 Row {
49− spacing: 10
50− Rectangle { width: 4; height: 22; color: Theme.accent; anchors.verticalCenter: parent.verticalCenter }
51− Text {
52− text: Qt.formatDateTime(clock.date, "dddd, d MMMM").toUpperCase()
53− color: Theme.accent
54− font.family: Theme.font
55− font.pixelSize: 18
56− font.bold: true
57− font.letterSpacing: 2
100+ x: 190
101+ y: 716
102+ spacing: 28
103+ Meta { icon: Theme.iBell; text: Notifs.count + (Notifs.count === 1 ? " notification" : " notifications") }
104+ Meta { visible: win.buds !== null; icon: Theme.iHeadphones; text: win.buds ? (win.buds.name + " · " + Math.round(win.buds.battery * 100) + "%") : "" }
105+ Meta { visible: win.player !== null && win.player.isPlaying; icon: Theme.iMusic; text: win.player ? (win.player.trackTitle || "") : "" }
106+ }
107+
108+ // bento: left to right, a widget that doesn't fit the row starts the next one, more than 3 rows are left out
109+ Repeater {
110+ model: {
111+ var out = [], r = 0, c = 0, l = Settings.homeWidgets
112+ for (var i = 0; i < l.length; i++) {
113+ var id = l[i], w = win.kinds[id]
114+ if (!w || !win.shows(id)) continue
115+ if (c + w > 3) { r++; c = 0 }
116+ if (r > 2) break
117+ out.push({ id: id, x: 1140 + c * 240, y: 150 + r * 240 })
118+ c += w
119+ }
120+ return out
121+ }
122+ delegate: Loader {
123+ required property var modelData
124+ x: modelData.x
125+ y: modelData.y
126+ sourceComponent: ({ calendar: calendarW, cpu: cpuW, ram: ramW, disk: diskW, battery: batteryW, gpu: gpuW,
127+ player: playerW, cava: cavaW, cat: catW, system: systemW })[modelData.id] || null
58128 }
59129 }
60− Item { width: 1; height: 10 }
61− Text {
62− text: Theme.iChip + " " + Math.round(SysStats.cpu * 100) + "% "
63− + Theme.iMem + " " + SysStats.memUsed.toFixed(1) + "/" + SysStats.memTotal.toFixed(1) + "G "
64− + Theme.iClock + " up " + Theme.fmtTime(SysStats.uptime)
65− color: Theme.dim
66− font.family: Theme.font
67− font.pixelSize: Theme.fontSize + 1
130+
131+ Component {
132+ id: calendarW
133+ W {
134+ Column {
135+ x: 24
136+ y: 24
137+ width: parent.width - 48
138+ Lb { text: win.en.monthName(clock.date.getMonth()) }
139+ Num { topPadding: lead + 8; text: clock.date.getDate(); font.pixelSize: 118 }
140+ Row {
141+ width: parent.width
142+ topPadding: 14
143+ Repeater {
144+ model: ["M", "T", "W", "T", "F", "S", "S"]
145+ Text {
146+ required property string modelData
147+ required property int index
148+ readonly property bool today: (clock.date.getDay() + 6) % 7 === index
149+ width: parent.width / 7
150+ horizontalAlignment: Text.AlignHCenter
151+ text: modelData
152+ font.family: Theme.mono
153+ font.pixelSize: 11
154+ color: today ? Theme.fill : Theme.grey
155+ Rectangle {
156+ visible: parent.today
157+ anchors.bottom: parent.bottom
158+ anchors.horizontalCenter: parent.horizontalCenter
159+ width: 10
160+ height: 2
161+ color: Theme.fill
162+ }
163+ }
164+ }
165+ }
166+ }
167+ }
68168 }
69− Text {
70− visible: win.player !== null && (win.player.trackTitle || "") !== ""
71− text: win.player ? (win.player.isPlaying ? Theme.iMusic : Theme.iPause) + " " + win.player.trackTitle
72− + (win.player.trackArtist ? " · " + win.player.trackArtist : "") : ""
73− color: win.player && win.player.isPlaying ? Theme.fg : Theme.dim
74− font.family: Theme.font
75− font.pixelSize: Theme.fontSize + 1
169+ Component { id: cpuW; RingW { f: SysStats.cpu; value: Math.round(SysStats.cpu * 100); label: "cpu %" } }
170+ Component {
171+ id: ramW
172+ RingW {
173+ f: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0
174+ value: Math.round(f * 100)
175+ label: "ram · " + SysStats.memUsed.toFixed(1) + "g"
176+ }
76177 }
77− }
78−
79− // visualizer: bottom-up bars across the whole width
80− Row {
81− visible: win.showViz && Cava.available
82− anchors.bottom: parent.bottom
83− anchors.left: parent.left
84− anchors.right: parent.right
85− height: 180
86− spacing: 3
87− Repeater {
88− model: Cava.bars
89− delegate: Rectangle {
90− required property int index
91− readonly property real v: index < Cava.values.length ? Cava.values[index] : 0
92− anchors.bottom: parent.bottom
93− width: (win.width - (Cava.bars - 1) * 3) / Cava.bars
94− height: Math.max(0, v * parent.height)
95− opacity: 0.85
96− gradient: Gradient {
97− GradientStop { position: 0; color: Theme.accent }
98− GradientStop { position: 1; color: Theme.accentDim }
178+ Component {
179+ id: diskW
180+ RingW {
181+ readonly property var d: SysStats.disks.length ? SysStats.disks[0] : null
182+ f: d ? d.used / d.size : 0
183+ value: Math.round(f * 100)
184+ label: "disk %"
185+ }
186+ }
187+ Component {
188+ id: batteryW
189+ RingW {
190+ readonly property bool charging: win.bat && win.bat.state === UPowerDeviceState.Charging
191+ f: win.batPct
192+ value: Math.round(win.batPct * 100)
193+ label: charging ? "charging" : "battery"
194+ on: !charging && win.batPct <= Settings.batLow / 100 ? Theme.err : Theme.fill
195+ }
196+ }
197+ Component {
198+ id: gpuW
199+ W {
200+ round: true
201+ Column {
202+ anchors.centerIn: parent
203+ spacing: 14
204+ Icon { anchors.horizontalCenter: parent.horizontalCenter; text: Theme.iGpu; font.pixelSize: 56; color: Theme.fill }
205+ Lb { anchors.horizontalCenter: parent.horizontalCenter; text: "gpu · " + Gpu.mode }
206+ }
207+ }
208+ }
209+ // the player: the cover as dots in a circle, spinning while it plays
210+ Component {
211+ id: playerW
212+ W {
213+ span: 2
214+ Item {
215+ id: art
216+ x: 28
217+ anchors.verticalCenter: parent.verticalCenter
218+ width: 164
219+ height: 164
220+ property real turn: 0
221+ Timer {
222+ running: win.home && Media.playing
223+ interval: 33
224+ repeat: true
225+ onTriggered: art.turn = (art.turn + 360 / 14 / 30) % 360
226+ }
227+ DotArt { anchors.fill: parent; rotation: art.turn; url: win.player ? win.player.trackArtUrl : ""; step: 6 }
228+ }
229+ // MPRIS doesn't push the position: poke it while the progress shows
230+ Timer {
231+ running: win.home && Media.playing && Media.seekable
232+ interval: 1000
233+ repeat: true
234+ onTriggered: win.player.positionChanged()
235+ }
236+ Column {
237+ anchors.left: art.right
238+ anchors.leftMargin: 26
239+ anchors.right: parent.right
240+ anchors.rightMargin: 28
241+ anchors.verticalCenter: parent.verticalCenter
242+ Lb { width: parent.width; elide: Text.ElideRight; text: "now playing · " + (win.player ? win.player.identity : "silence") }
243+ Text {
244+ width: parent.width
245+ topPadding: 10
246+ elide: Text.ElideRight
247+ text: win.player ? (win.player.trackTitle || "Nothing on") : "Nothing on"
248+ font.family: Theme.ui
249+ font.pixelSize: 26
250+ font.weight: Font.DemiBold
251+ color: Theme.fg
252+ }
253+ Text {
254+ width: parent.width
255+ elide: Text.ElideRight
256+ text: win.player ? (win.player.trackArtist || "") : ""
257+ font.family: Theme.ui
258+ font.pixelSize: 15
259+ color: Theme.fgDim
260+ }
261+ Item { width: 1; height: 18 }
262+ Seg {
263+ width: parent.width
264+ height: 6
265+ n: 24
266+ f: win.player && win.player.lengthSupported && win.player.length > 0 ? win.player.position / win.player.length : 0
267+ }
268+ Item { width: 1; height: 16 }
269+ PlayControls { size: 22; play: 52 }
270+ }
271+ }
272+ }
273+ // cava as a round Glyph Matrix: bass in the middle, highs to the sides, columns grow up and down from the middle
274+ Component {
275+ id: cavaW
276+ W {
277+ round: true
278+ Grid {
279+ anchors.centerIn: parent
280+ columns: 15
281+ spacing: 4
282+ Repeater {
283+ model: 15 * 15
284+ Rectangle {
285+ required property int index
286+ readonly property int cx: index % 15 - 7
287+ readonly property int cy: Math.floor(index / 15) - 7
288+ readonly property int half: Math.floor(Math.sqrt(7.5 * 7.5 - cx * cx)) // half a column inside the circle
289+ readonly property int n: Cava.values.length
290+ readonly property real v: n ? Cava.values[Math.round(Math.abs(cx) / 7 * (n * 0.6 - 1))] || 0 : 0
291+ width: 8
292+ height: 8
293+ radius: 4
294+ // outside the circle: transparent, not invisible (Grid would close the gap)
295+ color: Math.abs(cy) > half ? "transparent" : Math.abs(cy) < v * (half + 1) ? Theme.fill : Theme.s3
296+ }
297+ }
298+ }
299+ }
300+ }
301+ // the cat from the bar, big and as LED dots
302+ Component {
303+ id: catW
304+ W {
305+ CatLoop {
306+ anchors.centerIn: parent
307+ width: 180
308+ height: 180
309+ size: 120
310+ layer.enabled: true
311+ layer.effect: DotMask { step: 5 }
312+ }
313+ }
314+ }
315+ // system: numbers, and the cpu history as a dot matrix
316+ Component {
317+ id: systemW
318+ W {
319+ span: 3
320+ Grid {
321+ x: 34
322+ anchors.verticalCenter: parent.verticalCenter
323+ columns: 2
324+ columnSpacing: 10
325+ rowSpacing: 22
326+ Stat { label: "cpu"; value: Math.round(SysStats.cpu * 100) + "%" }
327+ Stat { label: "mem"; value: SysStats.memUsed.toFixed(1) + "G" }
328+ Stat { label: "up"; value: Math.floor(SysStats.uptime / 3600) + ":" + ("0" + Math.floor(SysStats.uptime / 60) % 60).slice(-2) }
329+ Stat { label: "disk"; value: SysStats.disks.length ? Math.round(SysStats.disks[0].used / SysStats.disks[0].size * 100) + "%" : "-" }
330+ }
331+ Column {
332+ x: 344
333+ anchors.verticalCenter: parent.verticalCenter
334+ width: parent.width - x - 34
335+ spacing: 12
336+ Lb { text: "cpu · 40 s" }
337+ Spark { width: parent.width; height: 92; slots: 34; values: SysStats.history }
99338 }
100339 }
101340 }
102341 }
342+
343+ // a bento cell: a square with radius 40, or a circle
344+ component W: Rectangle {
345+ property int span: 1
346+ property bool round: false
347+ width: span * 220 + (span - 1) * 20
348+ height: 220
349+ radius: round ? 110 : 40
350+ color: Theme.s1
351+ border.width: 1
352+ border.color: Theme.s2
353+ }
354+ // round widget: a ring of dots lit by f, the number and a label inside
355+ component RingW: Rectangle {
356+ id: rw
357+ property real f
358+ property string value
359+ property string label
360+ property color on: Theme.fill
361+ width: 220
362+ height: 220
363+ radius: 110
364+ color: Theme.s1
365+ border.width: 1
366+ border.color: Theme.s2
367+ Ring { anchors.fill: parent; anchors.margins: 12; f: rw.f; on: rw.on }
368+ Column {
369+ anchors.centerIn: parent
370+ Num { anchors.horizontalCenter: parent.horizontalCenter; text: rw.value; font.pixelSize: 72 }
371+ Lb { anchors.horizontalCenter: parent.horizontalCenter; topPadding: 8; text: rw.label }
372+ }
373+ }
374+ component Meta: Row {
375+ property string icon
376+ property string text
377+ spacing: 10
378+ Icon { anchors.verticalCenter: parent.verticalCenter; text: parent.icon; font.pixelSize: 16 }
379+ Lb { anchors.verticalCenter: parent.verticalCenter; text: parent.text }
380+ }
381+ component Stat: Column {
382+ property string label
383+ property string value
384+ width: 130
385+ Lb { text: parent.label }
386+ Num { topPadding: lead + 6; text: parent.value }
387+ }
103388 }
A .config/quickshell/aurora/Orb.qml View
@@ -0,0 +1,26 @@
1+import QtQuick
2+import QtQuick.Shapes
3+
4+// The halftone orb on an empty desktop (no wallpaper): equal dots, a radial gradient sets their brightness.
5+// phase (s) = a slow breath. Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Orb.qml.
6+Shape {
7+ property real phase: 0
8+
9+ width: 1100
10+ height: 1100
11+ opacity: 0.18 + 0.06 * Math.sin(phase * 2 * Math.PI / 8)
12+ layer.enabled: true
13+ layer.textureSize: Qt.size(320, 320) // a smooth gradient: the dots sample their cell centre, more pixels are wasted
14+ layer.effect: DotMask { step: 14 }
15+
16+ ShapePath {
17+ strokeWidth: -1
18+ fillGradient: RadialGradient {
19+ centerX: 352; centerY: 330; centerRadius: 730
20+ focalX: 352; focalY: 330
21+ GradientStop { position: 0; color: Theme.fill }
22+ GradientStop { position: 1; color: Theme.fade(Theme.fill, 0) }
23+ }
24+ PathAngleArc { centerX: 550; centerY: 550; radiusX: 550; radiusY: 550; startAngle: 0; sweepAngle: 360 }
25+ }
26+}
M .config/quickshell/aurora/SetAppearance.qml View
@@ -63,6 +63,29 @@ Flickable {
6363 }
6464 }
6565
66+ SectionLabel { text: "wallpaper look" }
67+ Choice {
68+ label: "drawn as"
69+ options: [{ value: "dots", label: "mono dots" }, { value: "colour", label: "colour dots" }, { value: "photo", label: "photo" }]
70+ value: Settings.wallLook
71+ onPicked: v => Settings.wallLook = v
72+ }
73+ Knob { label: "dot size"; from: 6; to: 24; value: Settings.wallPitch; unit: "px"; onMoved: v => Settings.wallPitch = v }
74+ Knob { label: "brightness"; from: 20; to: 100; value: Settings.wallBright; unit: "%"; onMoved: v => Settings.wallBright = v }
75+ Switch { label: "dot grid"; hint: "on the desktop when there is no wallpaper"; checked: Settings.dotGrid; onToggled: on => Settings.dotGrid = on }
76+
77+ SectionLabel { text: "home screen" }
78+ Field { label: "name"; value: Settings.name; placeholder: "greeting name (empty = your user name)"; onSubmitted: v => Settings.name = v }
79+ Switch { label: "24-hour clock"; checked: Settings.clock24; onToggled: on => Settings.clock24 = on }
80+ Switch { label: "blinking colon"; checked: Settings.blink; onToggled: on => Settings.blink = on }
81+ Switch { label: "animations"; hint: "panels slide and fade, dots light up in waves"; checked: Settings.motion; onToggled: on => Settings.motion = on }
82+ Choice {
83+ label: "app icons"
84+ options: [{ value: "dots", label: "dots" }, { value: "mono", label: "mono" }, { value: "colour", label: "colour" }]
85+ value: Settings.appIcons
86+ onPicked: v => Settings.appIcons = v
87+ }
88+
6689 SectionLabel { text: "colours" }
6790 Text {
6891 Layout.fillWidth: true
M .config/quickshell/aurora/SetShell.qml View
@@ -15,8 +15,8 @@ Flickable {
1515 spacing: 8
1616
1717 SectionLabel { text: "desktop" }
18− Switch { label: "clock"; hint: "big clock + stats on the wallpaper"; checked: Settings.desktopClock; onToggled: on => Settings.desktopClock = on }
19− Switch { label: "visualizer"; hint: "cava bars while music plays"; checked: Settings.desktopViz; onToggled: on => Settings.desktopViz = on }
18+ Switch { label: "clock"; hint: "home screen: Doto clock, greeting, widgets"; checked: Settings.desktopClock; onToggled: on => Settings.desktopClock = on }
19+ Switch { label: "visualizer"; hint: "the round cava matrix widget while music plays"; checked: Settings.desktopViz; onToggled: on => Settings.desktopViz = on }
2020 Knob { label: "visualizer bars"; from: 16; to: 128; value: Settings.vizBars; onMoved: v => Settings.vizBars = v }
2121
2222 SectionLabel { text: "dock" }
M .config/quickshell/aurora/Settings.qml View
@@ -18,7 +18,9 @@ Singleton {
1818 idleDim: 150, idleOff: 330, idleLock: 600, idleSuspend: 0, charTone: 0, portalAutoOpen: true,
1919 chargeMode: "off", batLow: 15, batCritical: 5, batCriticalAction: "none",
2020 privacyIndicators: true, powerAuto: false, powerAc: "balanced", powerBat: "saver",
21− motion: true, clock24: true, appIcons: "dots", barSize: 40
21+ motion: true, clock24: true, appIcons: "dots", barSize: 40,
22+ wallLook: "dots", wallPitch: 10, wallBright: 85, dotGrid: true,
23+ name: "", blink: true, homeWidgets: ["calendar", "cpu", "ram", "player", "cava", "system"]
2224 })
2325
2426 property alias fontSize: a.fontSize
@@ -55,6 +57,13 @@ Singleton {
5557 property alias clock24: a.clock24
5658 property alias appIcons: a.appIcons
5759 property alias barSize: a.barSize
60+ property alias wallLook: a.wallLook
61+ property alias wallPitch: a.wallPitch
62+ property alias wallBright: a.wallBright
63+ property alias dotGrid: a.dotGrid
64+ property alias name: a.name
65+ property alias blink: a.blink
66+ property alias homeWidgets: a.homeWidgets
5867
5968 function reset() {
6069 for (var k in defaults) a[k] = defaults[k]
@@ -113,6 +122,14 @@ Singleton {
113122 property bool clock24: true
114123 property string appIcons: "dots" // dots | mono | colour: how app icons are drawn
115124 property int barSize: 40 // height of the bar pill (Doto clock from 40 up)
125+ property string wallLook: "dots" // dots | colour | photo: how aurora draws the wallpaper (WallView)
126+ property int wallPitch: 10 // dot step, px
127+ property int wallBright: 85 // %, from the background to the full image
128+ property bool dotGrid: true // dot grid on an empty desktop
129+ property string name: "" // home screen greeting, empty = $USER
130+ property bool blink: true // the home clock's colon blinks
131+ // home screen bento, in order: calendar cpu ram disk battery gpu player cava cat system (Desktop.qml)
132+ property list<string> homeWidgets: ["calendar", "cpu", "ram", "player", "cava", "system"]
116133 }
117134 }
118135 // first run: create the file once the directory exists
M .config/quickshell/aurora/Wall.qml View
@@ -3,9 +3,9 @@ import QtQuick
33 import Quickshell
44 import Quickshell.Io
55
6−// Wallpapers: folder listing, cached thumbnails, and applying them. aurora owns the wallpaper:
7−// apply() writes hyprpaper.conf, switches hyprpaper live, runs matugen (all colours, terminal included; Theme then
8−// recolours itself). recolor() re-runs just matugen when the scheme/contrast setting changes.
6+// Wallpapers: folder listing, cached thumbnails, and applying them. aurora owns the wallpaper and draws it itself
7+// (Back.qml / WallView.qml, look = Settings.wallLook): apply() records the path in hyprpaper.conf (only as the record,
8+// hyprpaper isn't run any more) and runs matugen (all colours, terminal included; Theme then recolours itself). recolor() re-runs just matugen when the scheme/contrast setting changes.
99 // ~/.local/bin/setwall is only a CLI front-end for the IPC calls (wallrandom / wall / wallset).
1010 Singleton {
1111 id: root
@@ -19,7 +19,7 @@ Singleton {
1919 property var folders: [] // subfolder names; "" = everything under rootDir
2020 property var files: [] // absolute paths of the current folder
2121 property var ready: ({}) // md5(path) -> true once its thumbnail exists
22− property string current: "" // wallpaper hyprpaper is showing
22+ property string current: "" // the wallpaper on screen
2323 property bool loading: false
2424 property bool pendingRandom: false // random() asked before the folder was listed
2525 property bool checkedColours: false // matugen output checked once at startup
@@ -41,13 +41,11 @@ Singleton {
4141 "-regextype", "posix-extended", "-iregex", ".*\\.(png|jpe?g|webp)"]
4242 listProc.running = true
4343 }
44− // hyprpaper.conf is the source of truth for the current wallpaper (hyprpaper reads it on start).
45− // If hyprpaper isn't running, let Hyprland start it so it doesn't live in aurora's service cgroup.
44+ // hyprpaper.conf is the record of the current wallpaper (kept in hyprpaper's format, so it still works if
45+ // hyprpaper is ever started again)
4646 readonly property string applyJob:
4747 'p="$1"; c="$2"; ' +
4848 'printf "splash = false\\nwallpaper {\\n monitor =\\n path = %s\\n}\\n" "$p" > "$c"; ' +
49− 'if pgrep -x hyprpaper >/dev/null; then hyprctl hyprpaper wallpaper ",$p"; ' +
50− 'else hyprctl dispatch \'hl.dsp.exec_cmd("hyprpaper")\'; fi; ' +
5149 'matugen image "$p" -m dark -t "$3" --contrast="$4" --prefer saturation -q'
5250 function apply(path) {
5351 if (!path) return
@@ -151,7 +149,7 @@ Singleton {
151149 }
152150 }
153151
154− // current wallpaper, straight from hyprpaper.conf. None yet (fresh install): pick a random one.
152+ // current wallpaper, from its record in hyprpaper.conf. None yet (fresh install): pick a random one.
155153 FileView {
156154 path: root.conf
157155 watchChanges: true
M .config/quickshell/aurora/WallPicker.qml View
@@ -1,38 +1,44 @@
11 import QtQuick
22 import QtQuick.Layouts
3+import QtQuick.Effects
34 import Quickshell
45 import Quickshell.Wayland
5−import Quickshell.Hyprland
66
7−// Wallpaper picker: folder chips, filter, thumbnail grid. Vim-style: the grid has the keyboard
8−// (hjkl/arrows move, g/G first/last, ctrl+d/u page), `/` starts searching (enter keeps the filter,
9−// esc clears it). Enter / double-click applies (stays open so you can try a few), Esc / q or click
10−// outside closes. Tab / Shift+Tab cycles folders.
7+// Wallpaper picker: the whole screen previews the selected wallpaper in the current look (no matugen until you apply),
8+// a card at the bottom holds folder chips, the look (mono dots / colour dots / photo), the filter and the thumbnails.
9+// Vim-style: the grid has the keyboard (hjkl/arrows move, g/G first/last, ctrl+d/u page), `/` searches (enter keeps the
10+// filter, esc clears it), `v` cycles the look, Tab / Shift+Tab the folders. Enter / double-click applies (stays open to
11+// try a few), Esc / q or a click on the preview closes. After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), wall/Walls.qml.
1112 PanelWindow {
1213 id: win
1314 property bool opened: false
1415 signal closeRequested()
1516
16− visible: opened
17− implicitWidth: screen ? Math.round(screen.width * 0.7) : 1200
18− implicitHeight: screen ? Math.round(screen.height * 0.7) : 700
19− exclusiveZone: 0
17+ property real t: opened ? 1 : 0
18+ Behavior on t { NumberAnimation { duration: Theme.t; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } }
19+
20+ visible: t > 0
21+ anchors { top: true; bottom: true; left: true; right: true }
22+ exclusionMode: ExclusionMode.Ignore
2023 color: "transparent"
2124 WlrLayershell.layer: WlrLayer.Overlay
2225 WlrLayershell.namespace: "aurora-wallpaper"
2326 WlrLayershell.keyboardFocus: opened ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
2427
25− HyprlandFocusGrab {
26− windows: [win]
27− active: win.opened
28− onCleared: win.closeRequested()
29− }
30−
28+ property bool pendingSel: false // just opened: select the current wallpaper once the folder is listed
3129 onOpenedChanged: {
32− if (!opened) return
30+ if (!opened) { previewPath = ""; return }
3331 filter.text = ""
32+ pendingSel = true
3433 Wall.refresh()
3534 grid.forceActiveFocus()
35+ selT.restart()
36+ }
37+ function selectCurrent() {
38+ if (!pendingSel || Wall.loading || !shown.length) return
39+ pendingSel = false
40+ var i = shown.indexOf(Wall.current)
41+ if (i >= 0) { grid.currentIndex = i; grid.positionViewAtIndex(i, GridView.Center) }
3642 }
3743
3844 // search mode = the filter box has the keyboard (entered with `/` or by clicking it)
@@ -46,6 +52,8 @@ PanelWindow {
4652 function page(d) {
4753 grid.currentIndex = Math.max(0, Math.min(shown.length - 1, grid.currentIndex + d * grid.perPage))
4854 }
55+ readonly property var looks: ["dots", "colour", "photo"]
56+ function cycleLook() { Settings.wallLook = looks[(looks.indexOf(Settings.wallLook) + 1) % looks.length] }
4957
5058 readonly property var shown: {
5159 var q = filter.text.toLowerCase(), out = [], l = Wall.files
@@ -53,6 +61,13 @@ PanelWindow {
5361 if (q === "" || l[i].substring(l[i].lastIndexOf("/") + 1).toLowerCase().indexOf(q) >= 0) out.push(l[i])
5462 return out
5563 }
64+ // after the grid has taken the new model (replacing it resets its currentIndex to 0)
65+ Timer { id: selT; interval: 60; onTriggered: win.selectCurrent() }
66+ onShownChanged: if (pendingSel) selT.restart()
67+ Connections {
68+ target: Wall
69+ function onLoadingChanged() { if (win.pendingSel) selT.restart() }
70+ }
5671 readonly property var chips: [""].concat(Wall.folders)
5772
5873 function cycleFolder(d) {
@@ -61,123 +76,148 @@ PanelWindow {
6176 }
6277 function name(p) { return p.substring(p.lastIndexOf("/") + 1) }
6378
79+ // the preview follows the selection after a short pause (flipping fast with hjkl doesn't load every image)
80+ property string previewPath: ""
81+ readonly property string selected: opened && grid.currentIndex >= 0 && grid.currentIndex < shown.length ? shown[grid.currentIndex] : ""
82+ onSelectedChanged: previewT.restart()
83+ Timer { id: previewT; interval: 220; onTriggered: win.previewPath = win.selected || Wall.current }
84+
85+ // ---- the preview, full screen ----
6486 Rectangle {
6587 anchors.fill: parent
66− color: Theme.bgA
67− border.width: 2
68− border.color: Theme.accent
88+ color: Theme.bg
89+ opacity: win.t
90+ WallView {
91+ anchors.fill: parent
92+ path: win.previewPath !== "" ? win.previewPath : Wall.current
93+ }
94+ MouseArea { anchors.fill: parent; onClicked: win.closeRequested() }
95+ Lb {
96+ x: 40
97+ y: 32
98+ px: 11
99+ color: Theme.fg
100+ text: "preview · " + win.name(win.previewPath !== "" ? win.previewPath : Wall.current)
101+ + (win.previewPath !== "" && win.previewPath !== Wall.current ? " · ↵ apply" : " · on now")
102+ }
103+ }
104+
105+ // ---- the card ----
106+ Rectangle {
107+ id: card
108+ anchors.horizontalCenter: parent.horizontalCenter
109+ anchors.bottom: parent.bottom
110+ anchors.bottomMargin: 24 - 24 * (1 - win.t)
111+ width: Math.min(parent.width - 48, 1480)
112+ height: Math.round(parent.height * 0.46)
113+ opacity: win.t
114+ radius: 40
115+ color: Qt.rgba(Theme.s1.r, Theme.s1.g, Theme.s1.b, Theme.panelAlpha)
116+ border.width: 1
117+ border.color: Theme.s2
118+ MouseArea { anchors.fill: parent } // clicks on the card don't close
69119
70120 ColumnLayout {
71121 anchors.fill: parent
72− anchors.margins: 12
73− spacing: 10
122+ anchors.margins: 24
123+ spacing: 14
74124
75− // folder chips
76− Flow {
125+ RowLayout {
77126 Layout.fillWidth: true
78− spacing: 4
79− Repeater {
80− model: win.chips
81− delegate: Rectangle {
82− id: chip
83− required property var modelData
84− readonly property bool current: Wall.folder === modelData
85− width: cl.implicitWidth + 18
86− height: 24
87− color: current ? Theme.accent : (cm.containsMouse ? Theme.surfaceHi : "transparent")
88− border.width: 1
89− border.color: current ? Theme.accent : Theme.line
90− Text {
91− id: cl
92− anchors.centerIn: parent
93− text: chip.modelData === "" ? "all" : chip.modelData
94− color: chip.current ? Theme.bg : Theme.fg
95− font.family: Theme.font
96− font.pixelSize: Theme.fontSize - 1
97− font.bold: chip.current
98− }
99− MouseArea {
100− id: cm
101− anchors.fill: parent
102− hoverEnabled: true
103− cursorShape: Qt.PointingHandCursor
104− onClicked: Wall.listFolder(chip.modelData)
127+ spacing: 10
128+ Flow {
129+ Layout.fillWidth: true
130+ spacing: 2
131+ Repeater {
132+ model: win.chips
133+ delegate: Chip {
134+ required property var modelData
135+ text: modelData === "" ? "all" : modelData
136+ selected: Wall.folder === modelData
137+ onClicked: Wall.listFolder(modelData)
105138 }
106139 }
107140 }
141+ Segmented {
142+ Layout.preferredWidth: 330
143+ implicitHeight: 44
144+ options: [{ value: "dots", label: "mono" }, { value: "colour", label: "colour" }, { value: "photo", label: "photo" }]
145+ value: Settings.wallLook
146+ onPicked: v => Settings.wallLook = v
147+ }
148+ IconBtn { implicitHeight: 44; icon: Theme.iShuffle; label: "random"; onClicked: Wall.random() }
149+ IconBtn { implicitHeight: 44; icon: Theme.iClose; onClicked: win.closeRequested() }
108150 }
109151
110− // filter + actions
111− RowLayout {
152+ // filter: a pill with the live caret
153+ Rectangle {
112154 Layout.fillWidth: true
113− spacing: 6
114− Rectangle {
115− Layout.fillWidth: true
116− implicitHeight: 30
117− color: Theme.surface
118− border.width: 1
119− border.color: filter.activeFocus ? Theme.accent : Theme.line
120− Text {
121− anchors.left: parent.left
122− anchors.leftMargin: 10
123− anchors.verticalCenter: parent.verticalCenter
124− text: win.searching ? "/" : Theme.iSearch
125− color: win.searching ? Theme.accent : Theme.dim
126− font.family: Theme.font
127− font.pixelSize: Theme.fontSize
128− font.bold: win.searching
155+ implicitHeight: 48
156+ radius: height / 2
157+ color: Theme.s2
158+ border.width: win.searching ? 1 : 0
159+ border.color: Theme.fill
160+ Icon {
161+ id: lens
162+ x: 20
163+ anchors.verticalCenter: parent.verticalCenter
164+ text: Theme.iSearch
165+ color: win.searching ? Theme.fg : Theme.grey
166+ font.pixelSize: 18
167+ }
168+ TextInput {
169+ id: filter
170+ anchors.left: lens.right
171+ anchors.leftMargin: 14
172+ anchors.right: count.left
173+ anchors.rightMargin: 12
174+ anchors.verticalCenter: parent.verticalCenter
175+ color: Theme.fg
176+ selectionColor: Theme.fill
177+ selectedTextColor: Theme.fillFg
178+ font.family: Theme.ui
179+ font.pixelSize: 16
180+ clip: true
181+ cursorDelegate: Rectangle {
182+ width: 2
183+ color: Theme.live
184+ visible: filter.activeFocus
185+ Timer { running: filter.activeFocus; interval: 500; repeat: true; onTriggered: parent.opacity = 1 - parent.opacity }
186+ }
187+ onTextChanged: grid.currentIndex = 0
188+ // like vim's `/`: enter keeps the filter and goes back to the grid, esc drops it
189+ Keys.onEscapePressed: win.endSearch(true)
190+ Keys.onReturnPressed: win.endSearch(false)
191+ Keys.onEnterPressed: win.endSearch(false)
192+ Keys.onUpPressed: grid.moveCurrentIndexUp()
193+ Keys.onDownPressed: grid.moveCurrentIndexDown()
194+ Keys.onTabPressed: win.cycleFolder(1)
195+ Keys.onBacktabPressed: win.cycleFolder(-1)
196+ // ctrl+hjkl moves through the results without leaving the search
197+ Keys.onPressed: event => {
198+ if (!(event.modifiers & Qt.ControlModifier)) return
199+ event.accepted = true
200+ if (event.key === Qt.Key_H) grid.moveCurrentIndexLeft()
201+ else if (event.key === Qt.Key_J) grid.moveCurrentIndexDown()
202+ else if (event.key === Qt.Key_K) grid.moveCurrentIndexUp()
203+ else if (event.key === Qt.Key_L) grid.moveCurrentIndexRight()
204+ else event.accepted = false
129205 }
130206 Text {
131− anchors.left: parent.left
132− anchors.leftMargin: 32
133− anchors.verticalCenter: parent.verticalCenter
134207 visible: filter.text === "" && !win.searching
135− text: "press / to search"
136− color: Theme.dim
137− font.family: Theme.font
138− font.pixelSize: Theme.fontSize
139− }
140− TextInput {
141− id: filter
142− anchors.fill: parent
143− anchors.leftMargin: 32
144− anchors.rightMargin: 10
145− verticalAlignment: TextInput.AlignVCenter
146− color: Theme.fg
147− selectionColor: Theme.accent
148− selectedTextColor: Theme.bg
149− font.family: Theme.font
150− font.pixelSize: Theme.fontSize
151− clip: true
152− onTextChanged: grid.currentIndex = 0
153− // like vim's `/`: enter keeps the filter and goes back to the grid, esc drops it
154− Keys.onEscapePressed: win.endSearch(true)
155− Keys.onReturnPressed: win.endSearch(false)
156− Keys.onEnterPressed: win.endSearch(false)
157− Keys.onUpPressed: grid.moveCurrentIndexUp()
158− Keys.onDownPressed: grid.moveCurrentIndexDown()
159− Keys.onTabPressed: win.cycleFolder(1)
160− Keys.onBacktabPressed: win.cycleFolder(-1)
161− // ctrl+hjkl moves through the results without leaving the search
162− Keys.onPressed: event => {
163− if (!(event.modifiers & Qt.ControlModifier)) return
164− event.accepted = true
165− if (event.key === Qt.Key_H) grid.moveCurrentIndexLeft()
166− else if (event.key === Qt.Key_J) grid.moveCurrentIndexDown()
167− else if (event.key === Qt.Key_K) grid.moveCurrentIndexUp()
168− else if (event.key === Qt.Key_L) grid.moveCurrentIndexRight()
169− else event.accepted = false
170− }
208+ anchors.verticalCenter: parent.verticalCenter
209+ text: "Press / to search wallpapers"
210+ font: filter.font
211+ color: Theme.mute
171212 }
172213 }
173− Text {
174− text: Wall.loading ? "..." : win.shown.length + " / " + Wall.files.length
175− color: Theme.dim
176− font.family: Theme.font
177− font.pixelSize: Theme.fontSize - 1
214+ Lb {
215+ id: count
216+ anchors.right: parent.right
217+ anchors.rightMargin: 22
218+ anchors.verticalCenter: parent.verticalCenter
219+ text: Wall.loading ? "…" : win.shown.length + " / " + Wall.files.length
178220 }
179− IconBtn { icon: Theme.iShuffle; label: "random"; onClicked: Wall.random() }
180− IconBtn { icon: Theme.iClose; onClicked: win.closeRequested() }
181221 }
182222
183223 GridView {
@@ -185,10 +225,10 @@ PanelWindow {
185225 Layout.fillWidth: true
186226 Layout.fillHeight: true
187227 clip: true
188− readonly property int cols: Math.max(1, Math.floor(width / 240))
228+ readonly property int cols: Math.max(1, Math.floor(width / 230))
189229 readonly property int perPage: cols * Math.max(1, Math.floor(height / cellHeight))
190230 cellWidth: Math.floor(width / cols)
191− cellHeight: Math.round(cellWidth * 9 / 16) + 22
231+ cellHeight: Math.round(cellWidth * 9 / 16) + 30
192232 model: win.shown
193233 currentIndex: 0
194234 boundsBehavior: Flickable.StopAtBounds
@@ -216,6 +256,7 @@ PanelWindow {
216256 else if (event.text === "l") grid.moveCurrentIndexRight()
217257 else if (event.text === "g") grid.currentIndex = 0
218258 else if (event.text === "G") grid.currentIndex = win.shown.length - 1
259+ else if (event.text === "v") win.cycleLook()
219260 else if (event.text === "q") win.closeRequested()
220261 else event.accepted = false
221262 }
@@ -231,43 +272,65 @@ PanelWindow {
231272 height: grid.cellHeight
232273
233274 Rectangle {
234− anchors.fill: parent
235− anchors.margins: 4
236− color: Theme.surface
237− border.width: cell.selected || cell.isCurrent ? 2 : 1
238− border.color: cell.selected ? Theme.fg : (cell.isCurrent ? Theme.accent : Theme.line)
239−
275+ id: frame
276+ x: 6
277+ y: 6
278+ width: parent.width - 12
279+ height: parent.height - 36
280+ radius: 22
281+ color: Theme.s2
282+ border.width: cell.selected ? 2 : 0
283+ border.color: Theme.fill
240284 Image {
241285 id: img
242286 anchors.fill: parent
243− anchors.margins: 2
244− anchors.bottomMargin: 20
287+ anchors.margins: cell.selected ? 5 : 0
245288 fillMode: Image.PreserveAspectCrop
246289 asynchronous: true
247290 cache: false
248291 sourceSize.width: 320
249292 sourceSize.height: 180
250293 source: Wall.ready[cell.hash] ? "file://" + Wall.thumbDir + "/" + cell.hash + ".jpg" : ""
294+ layer.enabled: true
295+ layer.effect: MultiEffect { maskEnabled: true; maskSource: mask }
296+ }
297+ Rectangle {
298+ id: mask
299+ width: img.width
300+ height: img.height
301+ radius: cell.selected ? 17 : 22
302+ visible: false
303+ layer.enabled: true
251304 }
252− Text {
253− anchors.centerIn: img
305+ Icon {
306+ anchors.centerIn: parent
254307 visible: img.status !== Image.Ready
255308 text: Theme.iImage
256− color: Theme.dim
257− font.family: Theme.font
309+ color: Theme.mute
258310 font.pixelSize: 22
259311 }
260− Text {
261− anchors.left: parent.left
262− anchors.right: parent.right
263− anchors.bottom: parent.bottom
264− anchors.margins: 4
265− height: 14
266− text: (cell.isCurrent ? Theme.iCheck + " " : "") + win.name(cell.modelData)
312+ }
313+ Row {
314+ anchors.left: frame.left
315+ anchors.leftMargin: 8
316+ anchors.right: frame.right
317+ anchors.top: frame.bottom
318+ anchors.topMargin: 8
319+ spacing: 8
320+ Rectangle {
321+ visible: cell.isCurrent
322+ anchors.verticalCenter: parent.verticalCenter
323+ width: 8
324+ height: 8
325+ radius: 4
326+ color: Theme.live
327+ }
328+ Lb {
329+ width: parent.width - (cell.isCurrent ? 16 : 0)
267330 elide: Text.ElideMiddle
268− color: cell.isCurrent ? Theme.accent : Theme.dim
269− font.family: Theme.font
270− font.pixelSize: Theme.fontSize - 2
331+ px: 10
332+ text: win.name(cell.modelData)
333+ color: cell.selected ? Theme.fg : Theme.grey
271334 }
272335 }
273336 MouseArea {
@@ -279,15 +342,13 @@ PanelWindow {
279342 }
280343 }
281344
282− Text {
283− Layout.fillWidth: true
284− text: (win.searching ? "-- SEARCH -- enter done esc clear ↑↓ move "
285− : "hjkl move / search enter apply tab folder g/G first/last esc close ")
286− + Theme.iImage + " " + win.name(Wall.current)
287− elide: Text.ElideRight
288− color: Theme.dim
289− font.family: Theme.font
290− font.pixelSize: Theme.fontSize - 2
345+ Row {
346+ spacing: 30
347+ Repeater {
348+ model: win.searching ? ["↵ done", "esc clear", "↑↓ move"]
349+ : ["hjkl move", "/ search", "↵ apply", "v look", "tab folder", "g/G first/last", "esc close"]
350+ Lb { required property string modelData; text: modelData; px: 10 }
351+ }
291352 }
292353 }
293354 }
A .config/quickshell/aurora/WallView.qml View
@@ -0,0 +1,93 @@
1+import QtQuick
2+
3+// The wallpaper, drawn by shaders/wall.frag: look "dots" = a matrix of LED dots in the theme's tone (bg..fg),
4+// "colour" = dots in the image's own colours, "photo" = the plain image. A new path arrives as a wave from `origin`;
5+// two images take turns (the shown one and the incoming one), the old one is unloaded after the wave.
6+// An empty path = transparent (the dot grid shows through). `opaque` = the image covers everything.
7+// Ported from glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), components/Wall.qml.
8+Item {
9+ id: root
10+
11+ property string path
12+ property string look: Settings.wallLook
13+ property real pitch: Settings.wallPitch
14+ property real bright: Settings.wallBright / 100
15+ property point origin: Qt.point(width / 2, height / 2)
16+ property bool animate: Theme.motion
17+ readonly property bool opaque: fx.hasA > 0 && !run.running
18+
19+ property bool flip: false // which one is on screen: false = ia, true = ib
20+ readonly property Image cur: flip ? ib : ia
21+ readonly property Image next: flip ? ia : ib
22+
23+ function crop(img) {
24+ var ia = img.implicitWidth / Math.max(1, img.implicitHeight), sa = width / Math.max(1, height)
25+ return ia > sa ? Qt.size(sa / ia, 1) : Qt.size(1, ia / sa)
26+ }
27+ function url(p) { return p ? "file://" + p.split("/").map(encodeURIComponent).join("/") : "" }
28+
29+ // ---- change: shown = the path on screen, coming = the one loading ----
30+ property string shown: ""
31+ property string coming: ""
32+ property bool pending: false
33+ readonly property bool sized: width > 0 && height > 0 // no loading before the size is known (sourceSize 0 = full decode)
34+
35+ onSizedChanged: change()
36+ onPathChanged: change()
37+ function change() {
38+ if (!sized) return
39+ if (run.running) { run.stop(); swap() } // flipping faster than the wave: finish the current one now
40+ if (path === shown) { pending = false; next.source = ""; return }
41+ coming = path
42+ pending = true
43+ next.source = url(path)
44+ check()
45+ }
46+ function check() {
47+ if (!pending || next.status === Image.Loading) return
48+ pending = false // an error ends up transparent, like an empty path
49+ if (animate) run.restart()
50+ else swap()
51+ }
52+ function swap() {
53+ flip = !flip
54+ shown = coming
55+ fx.wave = 0
56+ next.source = "" // now the old one
57+ }
58+
59+ NumberAnimation { id: run; target: fx; property: "wave"; from: 0; to: 1; duration: 1500; easing.type: Easing.InOutSine; onFinished: root.swap() }
60+
61+ // the textures are siblings of the effect, not children, and opacity 0 rather than invisible: the scene graph
62+ // doesn't update the texture of an invisible Image. sourceSize is logical pixels; Qt multiplies by the DPR itself
63+ Image { id: ia; opacity: 0; asynchronous: true; cache: false; fillMode: Image.PreserveAspectCrop; sourceSize: Qt.size(root.width, root.height); onStatusChanged: if (root.next === ia) root.check() }
64+ Image { id: ib; opacity: 0; asynchronous: true; cache: false; fillMode: Image.PreserveAspectCrop; sourceSize: ia.sourceSize; onStatusChanged: if (root.next === ib) root.check() }
65+
66+ ShaderEffect {
67+ id: fx
68+ anchors.fill: parent
69+ visible: hasA > 0 || hasB > 0 || run.running
70+
71+ // ---- uniforms ----
72+ readonly property size size: Qt.size(width, height)
73+ readonly property point origin: root.origin
74+ readonly property var srcA: root.cur
75+ readonly property var srcB: root.next
76+ readonly property size cropA: root.crop(root.cur)
77+ readonly property size cropB: root.crop(root.next)
78+ readonly property real hasA: root.cur.status === Image.Ready ? 1 : 0
79+ readonly property real hasB: root.next.status === Image.Ready ? 1 : 0
80+ readonly property color lo: Theme.bg
81+ readonly property color hi: Theme.fg
82+ readonly property color bg: Theme.bg
83+ readonly property real pitch: root.pitch
84+ readonly property real bright: root.bright
85+ property real dots: root.look === "photo" ? 0 : 1
86+ property real mono: root.look === "dots" ? 1 : 0
87+ property real wave: 0
88+ Behavior on dots { enabled: Theme.motion; NumberAnimation { duration: 700; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } }
89+ Behavior on mono { enabled: Theme.motion; NumberAnimation { duration: 700 } }
90+
91+ fragmentShader: Qt.resolvedUrl("shaders/wall.frag.qsb")
92+ }
93+}
M .config/quickshell/aurora/shell.qml View
@@ -91,6 +91,12 @@ ShellRoot {
9191 // keep every pipewire node bound so volume / mute / properties are readable
9292 PwObjectTracker { objects: Pipewire.nodes.values }
9393
94+ // the wallpaper (Background layer), then the home screen on top of it (Bottom layer)
95+ Variants {
96+ model: Quickshell.screens
97+ Back {}
98+ }
99+
94100 Variants {
95101 model: Quickshell.screens
96102 Desktop {}
M .local/bin/setwall View
@@ -1,6 +1,6 @@
11 #!/bin/sh
2−# Set the wallpaper through aurora. aurora owns the wallpaper: it writes hyprpaper.conf,
3−# switches hyprpaper live and runs matugen (colours). This is just the CLI front-end for its IPC.
2+# Set the wallpaper through aurora. aurora owns the wallpaper: it draws it itself (mono dots, colour dots or photo,
3+# see its settings), records it in hyprpaper.conf and runs matugen (colours). This is just the CLI front-end for its IPC.
44 #
55 # setwall random wallpaper from the folder selected in aurora's picker
66 # setwall -m open (toggle) aurora's wallpaper picker