- 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>
160 lines
4.8 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|