Files
dotfiles/dot_config/quickshell/notifications/NotificationDaemon.qml
T
mpuchstein 8b9c38ab48 hypr: unify lockscreen and login screen via hyprlogin
Replace nwg-hello with hyprlogin as the greetd greeter. Both hyprlock
and hyprlogin now share a single apex-neon theme fragment
(.chezmoitemplates/apex-neon-lock.conf), giving an identical look on
lock and login.

- Add system/ staging tree for /etc files (not auto-applied by chezmoi)
- Add system/install-greeter.sh to render templates and sudo-install to /etc
- Add apex-neon shared fragment: palette, font, animations, input-field,
  clock/date bubbles, weather/location bubbles
- Add dot_config/hypr/themes/apex-neon-lock.conf.tmpl ($HOME copy for hyprlock)
- Rewrite hyprlock.conf.tmpl to source the shared fragment; move
  notification bubble here (hyprlock-only, requires quickshell)
- Add Quickshell IpcHandler so `qs ipc call notifications count` works
- Session fixed to hyprland-uwsm.desktop; greetd config targets greetd.conf
2026-05-29 17:19:32 +02:00

167 lines
5.1 KiB
QML

import Quickshell
import Quickshell.Io
import Quickshell.Services.Notifications
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
import "../shared" as Shared
Scope {
id: root
// Expose for NC popout
property alias trackedNotifications: server.trackedNotifications
property bool dnd: false
// External access (used by hyprlock notification widget via `qs ipc call notifications count`)
IpcHandler {
target: "notifications"
function count(): int { return server.trackedNotifications.values.length }
}
// Toast IDs currently showing as popups (capped to avoid overflow)
readonly property int maxToasts: 4
property var toastIds: []
property var timestamps: ({}) // notification id → arrival epoch ms
NotificationServer {
id: server
keepOnReload: true
bodySupported: true
bodyMarkupSupported: false
actionsSupported: true
imageSupported: true
persistenceSupported: true
onNotification: notification => {
notification.tracked = true;
root.timestamps[notification.id] = Date.now();
root.timestampsChanged();
// Suppress toasts in DnD mode (still tracked for history)
if (root.dnd) return;
// Add to toast list, evict oldest if at capacity
let ids = root.toastIds.slice();
if (ids.length >= root.maxToasts) {
let evicted = ids.shift();
delete toastTimer.pending[evicted];
}
ids.push(notification.id);
root.toastIds = ids;
// Schedule toast removal (not notification removal)
let timeout = notification.expireTimeout > 0 ? notification.expireTimeout * 1000 : 5000;
if (notification.urgency !== NotificationUrgency.Critical) {
toastTimer.createTimer(notification.id, timeout);
}
}
}
// Toast timer manager
QtObject {
id: toastTimer
property var pending: ({})
function createTimer(id, timeout) {
pending[id] = Date.now() + timeout;
}
}
Timer {
interval: 500
running: root.toastIds.length > 0
repeat: true
onTriggered: {
let now = Date.now();
let changed = false;
let ids = root.toastIds.slice();
for (let id in toastTimer.pending) {
if (now >= toastTimer.pending[id]) {
let idx = ids.indexOf(parseInt(id));
if (idx >= 0) { ids.splice(idx, 1); changed = true; }
delete toastTimer.pending[id];
}
}
if (changed) root.toastIds = ids;
}
}
// Prune stale timestamps to prevent unbounded growth
Timer {
interval: 60000
running: true
repeat: true
onTriggered: {
let tracked = server.trackedNotifications;
let live = new Set();
for (let i = 0; i < tracked.values.length; i++)
live.add(tracked.values[i].id);
let ts = root.timestamps;
let pruned = false;
for (let id in ts) {
if (!live.has(parseInt(id))) { delete ts[id]; pruned = true; }
}
if (pruned) root.timestampsChanged();
}
}
// Toast popup window — shows only active toasts
Variants {
model: Quickshell.screens
delegate: Component {
PanelWindow {
required property var modelData
screen: modelData
WlrLayershell.namespace: "quickshell:notifications"
surfaceFormat { opaque: false }
visible: modelData.name === Shared.Config.monitor && root.toastIds.length > 0
anchors {
top: true
right: true
}
exclusionMode: ExclusionMode.Ignore
implicitWidth: 380
implicitHeight: toastColumn.implicitHeight + 20
color: "transparent"
margins {
right: Shared.Theme.barWidth + 12
top: 8
}
ColumnLayout {
id: toastColumn
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 10
anchors.rightMargin: 10
width: 360
spacing: 8
Repeater {
model: server.trackedNotifications
Loader {
required property var modelData
active: root.toastIds.indexOf(modelData.id) >= 0
visible: active
Layout.fillWidth: true
sourceComponent: NotificationPopup {
notification: modelData
}
}
}
}
}
}
}
}