Krolyxon
Commit 6a3e480

Branches

Refs →

aurora.configquickshellauroraCalPopup.qml

591 lines · 25.0 KB · QML

1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Wayland
5import Quickshell.Hyprland
6
7// Calendar dashboard under the bar clock (top centre): one card, sections split by dotted lines:
8// month calendar | weather (now, next 4 days, daylight) | system (cpu/mem/gpu rings, top processes, threads) | net | media.
9// Calendar: Monday first, ISO week numbers, today in a primary circle. Scroll over it / arrows change the month,
10// up / down the year, space = back to today, Esc closes. SysStats samples its details only while this is open.
11// After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), cal/Cal.qml and deck/Deck.qml.
12PanelWindow {
13 id: win
14 property bool opened: false
15 signal closeRequested()
16
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 // under the clock: top centre, or next to it (left edge, centred) when the bar is vertical
22 readonly property bool side: Theme.barVertical
23 anchors.top: !side
24 anchors.left: side
25 margins.top: 6
26 margins.left: 8
27 implicitWidth: card.width + (side ? 20 : 0) // room for the slide
28 implicitHeight: card.height + (side ? 0 : 20)
29 exclusiveZone: 0
30 color: "transparent"
31 mask: Region { item: card }
32 WlrLayershell.layer: WlrLayer.Overlay
33 WlrLayershell.namespace: "aurora-calendar"
34 WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
35
36 HyprlandFocusGrab {
37 windows: [win]
38 active: win.opened
39 onCleared: win.closeRequested()
40 }
41
42 Binding { target: SysStats; property: "detailed"; value: win.opened }
43
44 SystemClock { id: clock; precision: SystemClock.Minutes }
45 readonly property var en: Qt.locale("en_US")
46 readonly property var now: clock.date
47 readonly property real h: calCol.implicitHeight // every section is as tall as the calendar
48
49 property int year: 0
50 property int month: 0 // 0..11
51 function today() {
52 var d = new Date()
53 year = d.getFullYear()
54 month = d.getMonth()
55 }
56 function shift(n) {
57 var m = month + n
58 year += Math.floor(m / 12)
59 month = ((m % 12) + 12) % 12
60 }
61 onOpenedChanged: if (opened) { today(); Weather.poke() }
62
63 function isoWeek(d) {
64 var t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()))
65 var day = t.getUTCDay() || 7
66 t.setUTCDate(t.getUTCDate() + 4 - day)
67 var y0 = new Date(Date.UTC(t.getUTCFullYear(), 0, 1))
68 return Math.ceil(((t - y0) / 86400000 + 1) / 7)
69 }
70 // 6 rows x 7 days starting on the Monday on/before the 1st
71 readonly property var days: {
72 var first = new Date(year, month, 1)
73 var start = new Date(year, month, 1 - ((first.getDay() + 6) % 7))
74 var out = []
75 for (var i = 0; i < 42; i++)
76 out.push(new Date(start.getFullYear(), start.getMonth(), start.getDate() + i))
77 return out
78 }
79 function hm(d) { return Theme.hh(d) + ":" + Qt.formatDateTime(d, "mm") }
80
81 // weather of today (sunrise/sunset) and the next days
82 readonly property var wToday: Weather.dayOf(now)
83 readonly property var wNext: Weather.after(now, 4)
84
85 // MPRIS doesn't push position updates, so poke it while playing
86 Timer {
87 running: win.visible && Media.playing && Media.seekable
88 interval: 500
89 repeat: true
90 onTriggered: Media.player.positionChanged()
91 }
92
93 Rectangle {
94 id: card
95 x: win.side ? -16 * (1 - win.t) : 0
96 y: win.side ? 0 : -16 * (1 - win.t)
97 opacity: win.t
98 width: row.width + 52
99 height: row.height + 48
100 radius: 36
101 color: Theme.fade(Theme.s1, Theme.panelAlpha)
102 border.width: 1
103 border.color: Theme.s2
104 focus: true
105 Keys.onEscapePressed: win.closeRequested()
106 Keys.onLeftPressed: win.shift(-1)
107 Keys.onRightPressed: win.shift(1)
108 Keys.onUpPressed: win.shift(-12)
109 Keys.onDownPressed: win.shift(12)
110 Keys.onSpacePressed: win.today()
111
112 Row {
113 id: row
114 x: 26
115 y: 24
116 spacing: 22
117
118 // ---------------- calendar ----------------
119 Item {
120 width: calCol.implicitWidth
121 height: calCol.implicitHeight
122 MouseArea {
123 anchors.fill: parent
124 acceptedButtons: Qt.NoButton
125 onWheel: w => win.shift(w.angleDelta.y > 0 ? -1 : 1)
126 }
127 ColumnLayout {
128 id: calCol
129 width: 288
130 spacing: 0
131
132 Lb {
133 Layout.leftMargin: 6
134 px: 10
135 text: win.en.toString(win.now, "dddd · d MMMM") + " · week " + win.isoWeek(win.now)
136 }
137 RowLayout {
138 Layout.fillWidth: true
139 Layout.topMargin: 12
140 Layout.leftMargin: 6
141 spacing: 10
142 Num {
143 text: win.en.standaloneMonthName(win.month, Locale.ShortFormat)
144 font.pixelSize: 44
145 MouseArea {
146 anchors.fill: parent
147 cursorShape: Qt.PointingHandCursor
148 onClicked: win.today()
149 }
150 }
151 Lb {
152 Layout.alignment: Qt.AlignVCenter
153 text: win.year
154 }
155 Item { Layout.fillWidth: true }
156 IconBtn { implicitHeight: 36; icon: Theme.iChevronLeft; onClicked: win.shift(-1) }
157 IconBtn { implicitHeight: 36; icon: Theme.iChevron; onClicked: win.shift(1) }
158 }
159 Grid {
160 id: grid
161 Layout.fillWidth: true
162 Layout.topMargin: 12
163 columns: 8
164 readonly property real cw: calCol.width / 8
165
166 Repeater {
167 model: ["wk", "m", "t", "w", "t", "f", "s", "s"]
168 delegate: Lb {
169 required property var modelData
170 required property int index
171 width: grid.cw
172 height: 24
173 horizontalAlignment: Text.AlignHCenter
174 verticalAlignment: Text.AlignVCenter
175 text: modelData
176 px: 10
177 color: index === 0 ? Theme.mute : Theme.grey
178 }
179 }
180 Repeater {
181 model: 48
182 delegate: Item {
183 id: cell
184 required property int index
185 readonly property bool isWeek: index % 8 === 0
186 // week-number cells use that row's Monday
187 readonly property var d: win.days[Math.floor(index / 8) * 7 + Math.max(0, (index % 8) - 1)]
188 readonly property bool inMonth: d.getMonth() === win.month
189 readonly property bool isToday: !isWeek && d.toDateString() === win.now.toDateString()
190 readonly property bool weekend: (index % 8) >= 6
191 width: grid.cw
192 height: 34
193 Rectangle {
194 visible: cell.isToday
195 anchors.centerIn: parent
196 width: 30
197 height: 30
198 radius: 15
199 color: Theme.fill
200 }
201 Text {
202 anchors.centerIn: parent
203 text: cell.isWeek ? win.isoWeek(cell.d) : cell.d.getDate()
204 color: cell.isToday ? Theme.fillFg
205 : cell.isWeek ? Theme.mute
206 : !cell.inMonth ? Theme.mute
207 : cell.weekend ? Theme.grey : Theme.fg
208 font.family: Theme.mono
209 font.pixelSize: cell.isWeek ? 10 : 13
210 font.bold: cell.isToday
211 }
212 }
213 }
214 }
215 }
216 }
217
218 Sep { h: win.h }
219
220 // ---------------- weather ----------------
221 ColumnLayout {
222 width: 208
223 height: win.h
224 spacing: 0
225
226 Lb {
227 Layout.fillWidth: true
228 elide: Text.ElideRight
229 px: 10
230 color: Weather.error !== "" && !Weather.ok ? Theme.err : Theme.grey
231 text: !Weather.ok ? (Weather.busy ? "looking outside" : Weather.error || "no weather")
232 : (Weather.place !== "" ? Weather.place + " · " : "") + Weather.name(Weather.now.code)
233 }
234 RowLayout {
235 Layout.topMargin: 14
236 spacing: 12
237 Icon {
238 text: Weather.ok ? Weather.icon(Weather.now.code, Weather.now.day) : Theme.iCloudy
239 font.pixelSize: 46
240 opacity: Weather.ok ? 1 : 0.4
241 layer.enabled: true
242 layer.effect: DotMask { step: 3 }
243 }
244 Num {
245 text: Weather.ok ? Math.round(Weather.now.temp) + "°" : "--"
246 font.pixelSize: 52
247 }
248 }
249 Lb {
250 Layout.topMargin: 12
251 visible: Weather.ok
252 px: 10
253 text: Weather.ok ? "feels " + Math.round(Weather.now.feels) + "° · " + Math.round(Weather.now.wind) + " km/h" : ""
254 }
255 Lb {
256 Layout.topMargin: 4
257 visible: Weather.ok
258 px: 10
259 text: Weather.ok ? "humid " + Math.round(Weather.now.humidity) + "% · rain " + Math.round(Weather.now.rain || 0) + "%" : ""
260 }
261 Item { Layout.fillHeight: true }
262 Row {
263 spacing: 8
264 Repeater {
265 model: win.wNext
266 Column {
267 required property var modelData
268 width: 46
269 Lb {
270 anchors.horizontalCenter: parent.horizontalCenter
271 px: 10
272 text: win.en.toString(modelData.date, "ddd")
273 }
274 Icon {
275 anchors.horizontalCenter: parent.horizontalCenter
276 topPadding: lead + 6
277 text: Weather.icon(modelData.code, true)
278 font.pixelSize: 24
279 layer.enabled: true
280 layer.effect: DotMask { step: 2 }
281 }
282 Lb {
283 anchors.horizontalCenter: parent.horizontalCenter
284 topPadding: 6
285 px: 11
286 color: Theme.fg
287 text: Math.round(modelData.max) + "°"
288 }
289 Lb {
290 anchors.horizontalCenter: parent.horizontalCenter
291 px: 10
292 text: Math.round(modelData.min) + "°"
293 }
294 }
295 }
296 }
297 // daylight: lit from sunrise to now
298 Seg {
299 Layout.fillWidth: true
300 Layout.topMargin: 16
301 Layout.preferredHeight: 6
302 n: 24
303 f: win.wToday ? (win.now - win.wToday.rise) / (win.wToday.set - win.wToday.rise) : 0
304 }
305 RowLayout {
306 Layout.fillWidth: true
307 Layout.topMargin: 10
308 Meta { icon: Theme.iSunrise; text: win.wToday ? win.hm(win.wToday.rise) : "--:--" }
309 Item { Layout.fillWidth: true }
310 Meta { icon: Theme.iSunset; text: win.wToday ? win.hm(win.wToday.set) : "--:--" }
311 }
312 }
313
314 Sep { h: win.h }
315
316 // ---------------- system ----------------
317 ColumnLayout {
318 width: 320
319 height: win.h
320 spacing: 0
321
322 RowLayout {
323 Layout.fillWidth: true
324 spacing: 0
325 Gauge {
326 on: win.opened
327 f: SysStats.cpu
328 label: "cpu" + (SysStats.cpuTemp > 0 ? " · " + Math.round(SysStats.cpuTemp) + "°" : "")
329 }
330 Item { Layout.fillWidth: true }
331 Gauge {
332 on: win.opened
333 f: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0
334 label: "mem · " + SysStats.memUsed.toFixed(1) + "g"
335 }
336 Item { Layout.fillWidth: true }
337 // the dGPU if there is one (asleep = idle, never woken for this), else the root disk
338 Gauge {
339 readonly property var disk: SysStats.disks.length ? SysStats.disks[0] : null
340 on: win.opened
341 f: SysStats.gpuDev !== "" ? SysStats.gpu : disk ? disk.used / disk.size : 0
342 label: SysStats.gpuDev === "" ? "disk · " + (disk ? disk.label : "")
343 : SysStats.gpuAwake ? "gpu · " + Math.round(SysStats.gpuTemp) + "°" : "gpu · asleep"
344 }
345 }
346 Item { Layout.fillHeight: true }
347 RowLayout {
348 Layout.fillWidth: true
349 spacing: 16
350 ColumnLayout {
351 Layout.preferredWidth: 160
352 Layout.alignment: Qt.AlignTop
353 spacing: 5
354 Lb { px: 10; text: "processes" }
355 Repeater {
356 model: SysStats.procs
357 RowLayout {
358 required property var modelData
359 spacing: 6
360 Lb {
361 Layout.preferredWidth: 78
362 elide: Text.ElideRight
363 px: 10
364 color: Theme.fg
365 text: modelData[0]
366 }
367 Lb {
368 Layout.preferredWidth: 34
369 horizontalAlignment: Text.AlignRight
370 px: 10
371 text: Math.round(modelData[1]) + "%"
372 }
373 Seg {
374 Layout.preferredWidth: 36
375 Layout.preferredHeight: 5
376 n: 6
377 spacing: 3
378 f: modelData[1] / 100
379 }
380 }
381 }
382 }
383 ColumnLayout {
384 Layout.fillWidth: true
385 Layout.alignment: Qt.AlignTop
386 spacing: 8
387 Lb {
388 px: 10
389 text: "threads" + (SysStats.cpuFreq > 0 ? " · " + SysStats.cpuFreq.toFixed(1) + " ghz" : "")
390 }
391 // a column per thread: the top dot is the level, the ones below it trail
392 Grid {
393 id: cores
394 readonly property int n: Math.max(1, SysStats.cores.length)
395 readonly property real d: (width - (n - 1) * spacing) / n
396 Layout.fillWidth: true
397 columns: n
398 spacing: 2
399 Repeater {
400 model: cores.n * 8
401 Rectangle {
402 required property int index
403 readonly property int up: 8 - Math.floor(index / cores.n) // 8 top .. 1 bottom
404 readonly property int lvl: Math.round(1 + 7 * (SysStats.cores[index % cores.n] || 0))
405 width: cores.d
406 height: cores.d
407 radius: cores.d / 2
408 color: up === lvl ? Theme.fill : up < lvl ? Theme.fade(Theme.fill, 0.4) : Theme.s3
409 }
410 }
411 }
412 }
413 }
414 }
415
416 Sep { h: win.h }
417
418 // ---------------- net ----------------
419 ColumnLayout {
420 width: 180
421 height: win.h
422 spacing: 0
423
424 Lb {
425 Layout.fillWidth: true
426 elide: Text.ElideRight
427 px: 10
428 text: "net · " + (Net.connected !== "" ? Net.connected : SysStats.iface !== "" ? SysStats.iface : "offline")
429 }
430 Speed { Layout.topMargin: 16; icon: Theme.iArrowDown; b: SysStats.rx }
431 Speed { Layout.topMargin: 10; icon: Theme.iArrowUp; b: SysStats.tx }
432 Item { Layout.fillHeight: true }
433 // the last 40s: download grows up from the middle, upload (dimmer) down
434 Grid {
435 id: hist
436 readonly property var pts: SysStats.netHistory
437 readonly property real peak: {
438 var p = 10240
439 for (var i = 0; i < pts.length; i++) p = Math.max(p, pts[i][0], pts[i][1])
440 return p
441 }
442 readonly property real d: (width - 19 * spacing) / 20
443 Layout.fillWidth: true
444 columns: 20
445 spacing: 3
446 Repeater {
447 model: 20 * 10
448 Rectangle {
449 required property int index
450 readonly property int r: Math.floor(index / 20) // 0..9 top to bottom
451 readonly property bool up: r >= 5
452 readonly property var p: hist.pts[index % 20 - 20 + hist.pts.length] || [0, 0]
453 readonly property real v: up ? p[1] : p[0]
454 readonly property bool lit: v > 0 && (up ? r - 4 : 5 - r) <= Math.ceil(5 * v / hist.peak)
455 width: hist.d
456 height: hist.d
457 radius: hist.d / 2
458 color: lit ? (up ? Theme.grey : Theme.fill) : Theme.s3
459 }
460 }
461 }
462 }
463
464 Sep { h: win.h; visible: Media.player !== null }
465
466 // ---------------- media ----------------
467 ColumnLayout {
468 visible: Media.player !== null
469 width: 250
470 height: win.h
471 spacing: 0
472
473 Lb {
474 Layout.fillWidth: true
475 elide: Text.ElideRight
476 px: 10
477 text: Media.player ? Media.player.identity : ""
478 }
479 RowLayout {
480 Layout.fillWidth: true
481 Layout.topMargin: 16
482 spacing: 14
483 // the cover as dots, turning while it plays
484 DotArt {
485 Layout.preferredWidth: 84
486 Layout.preferredHeight: 84
487 step: 5
488 url: Media.player ? Media.player.trackArtUrl : ""
489 RotationAnimation on rotation {
490 from: 0
491 to: 360
492 duration: 14000
493 loops: Animation.Infinite
494 running: win.visible && Theme.motion
495 paused: running && !Media.playing
496 }
497 }
498 ColumnLayout {
499 Layout.fillWidth: true
500 spacing: 2
501 Text {
502 Layout.fillWidth: true
503 wrapMode: Text.Wrap
504 maximumLineCount: 2
505 elide: Text.ElideRight
506 text: Media.player && Media.player.trackTitle ? Media.player.trackTitle : "nothing on"
507 color: Theme.fg
508 font.family: Theme.ui
509 font.pixelSize: 17
510 font.weight: Font.DemiBold
511 }
512 Text {
513 Layout.fillWidth: true
514 elide: Text.ElideRight
515 text: Media.player ? Media.player.trackArtist : ""
516 color: Theme.fgDim
517 font.family: Theme.ui
518 font.pixelSize: 13
519 }
520 }
521 }
522 Item { Layout.fillHeight: true }
523 Seg {
524 Layout.fillWidth: true
525 Layout.preferredHeight: 6
526 n: 20
527 f: Media.seekable ? Media.player.position / Media.player.length : 0
528 }
529 RowLayout {
530 Layout.fillWidth: true
531 Layout.topMargin: 8
532 Lb { px: 10; text: Media.seekable ? Media.fmt(Media.player.position) : "" }
533 Item { Layout.fillWidth: true }
534 Lb { px: 10; text: Media.seekable ? Media.fmt(Media.player.length) : "" }
535 }
536 PlayControls {
537 Layout.alignment: Qt.AlignHCenter
538 Layout.topMargin: 12
539 size: 20
540 play: 48
541 }
542 }
543 }
544 }
545
546 // dotted line between sections (inline components can't see the file's ids: everything comes in as properties)
547 component Sep: Column {
548 id: sep
549 property real h
550 spacing: 7
551 Repeater {
552 model: Math.floor((sep.h + 7) / 10)
553 Rectangle { width: 3; height: 3; radius: 1.5; color: Theme.s3 }
554 }
555 }
556 // dot ring with the percentage inside and a label under it; lights in a wave when the popup opens
557 component Gauge: Column {
558 id: g
559 property real f
560 property string label
561 property bool on
562 spacing: 8
563 Item {
564 anchors.horizontalCenter: parent.horizontalCenter
565 width: 88
566 height: 88
567 Ring { anchors.fill: parent; anchors.margins: 2; n: 32; dot: 4; f: g.on ? g.f : 0 }
568 Num { anchors.centerIn: parent; text: Math.round(g.f * 100); font.pixelSize: 30 }
569 }
570 Lb { anchors.horizontalCenter: parent.horizontalCenter; px: 10; text: g.label }
571 }
572 // arrow, Doto number, unit
573 component Speed: RowLayout {
574 id: sp
575 property string icon
576 property real b
577 readonly property var v: SysStats.rate(b)
578 spacing: 8
579 Icon { text: sp.icon; font.pixelSize: 18 }
580 Num { text: sp.v[0]; font.pixelSize: 44 }
581 Lb { px: 10; text: sp.v[1] }
582 }
583 component Meta: RowLayout {
584 id: m
585 property string icon
586 property string text
587 spacing: 8
588 Icon { text: m.icon; font.pixelSize: 16; color: Theme.grey }
589 Lb { px: 10; text: m.text }
590 }
591}