Krolyxon
Commit 4b25a0d

Branches

Refs →

aurora.configquickshellauroraCalPopup.qml

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