1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
// The balloon stack, bottom-right of DP-1, over conky.
Scope {
id: root
// A tick drives both expiry and the end of a snooze without waiting for a
// file change. 250ms keeps ronema's -t 1 near-instant.
property double now: Date.now()
Timer {
interval: 250
running: true
repeat: true
onTriggered: root.now = Date.now()
}
// The Repeater model is Notify.queue itself, so its identity only changes
// when the daemon rewrites the file, not on the 250ms tick. Expiry and
// snooze are the balloon's own business: the tick reassigns `now`, each
// balloon re-evaluates its visible binding and leaves the column at
// expiry. Rebuilding the model on the tick would recreate every delegate
// (reloading icons, resetting hover) four times a second.
readonly property var live: Notify.queue || []
// The window border gradient, from the same file Hyprland reads, so the
// balloons wear the windows' border. udt-accent writes it as Hyprland
// colours, `rgb(rrggbb)` or `rgba(rrggbbaa)`; Qt wants `#aarrggbb`. The
// fallback is the triple look_and_feel.lua falls back to.
property var borderColors: ["#c6a0f6", "#b7bdf8", "#8aadf4"]
FileView {
path: `${Quickshell.env("HOME")}/.cache/udt/udt-border.lua`
watchChanges: true
onFileChanged: reload()
onLoaded: {
const re = /rgba?\(([0-9a-fA-F]{6})([0-9a-fA-F]{2})?\)/g;
const out = [];
let m;
while ((m = re.exec(text())) !== null) out.push("#" + (m[2] ?? "") + m[1]);
if (out.length > 0) root.borderColors = out;
}
}
// Hyprland's borderangle is speed 30, linear, loop, which reads as 3s a
// turn, but on a balloon that size looked slow next to the windows, so
// the turn is tuned by eye rather than taken from the config. One angle for every balloon, so they turn together like the
// windows do, and only while the stack is mapped.
property real borderAngle: 0
NumberAnimation on borderAngle {
from: 0
to: 2 * Math.PI
duration: 1500
loops: Animation.Infinite
running: win.visible
}
PanelWindow {
id: win
// From the queue, never from layout: a hidden window stops polishing,
// and Column computes implicitHeight during polish, so deriving
// visibility from it would never turn true again after the first
// hide. Queue membership is a plain property the binding re-reads
// whether or not the window is mapped, so a later notification
// re-maps it. Expired entries that stay in the queue keep a
// zero-height window mapped, which is harmless.
visible: root.live.length > 0
screen: Quickshell.screens.find(s => s.name === "DP-1") ?? Quickshell.screens[0]
anchors { bottom: true; right: true }
margins { bottom: 12; right: 12 }
implicitWidth: 340
implicitHeight: column.implicitHeight
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "quickshell-notifications"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
Column {
id: column
width: parent.width
anchors { bottom: parent.bottom; right: parent.right }
spacing: 8
Repeater {
model: root.live
NotificationBalloon {
notification: modelData
now: root.now
borderColors: root.borderColors
borderAngle: root.borderAngle
}
}
}
}
}
|