Files
dotfiles/dot_config/quickshell/notifications/NotificationDaemon.qml
s0wlz (Matthias Puchstein) c5f7162ebb quickshell: add initial bar config with per-monitor workspaces
- Vertical bar on DP-2 with rounded-square pills throughout
- Per-monitor workspace groups sorted by screen x position, with
  Nerd Font icons for named workspaces and apex-neon red active indicator
- Bar layout: datetime+weather top, workspaces centered, gamemode+media+notif+system bottom
- Popouts anchor to triggering icon (top-right for datetime/weather, bottom-right for media/notif/system)
- Lock command switched from hyprlock to swaylock
- Hyprland blur/ignore_alpha layerrules for quickshell namespace

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 20:00:54 +02:00

160 lines
4.8 KiB
QML

import Quickshell
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
// 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
}
}
}
}
}
}
}
}