pragma Singleton // Curated shell feedback. This is deliberately not a notification store: one // event is visible, at most four wait behind it, and expired entries vanish. import Quickshell import Quickshell.Hyprland import QtQuick Singleton { id: root readonly property int ambientPriority: 10 readonly property int importantPriority: 50 readonly property int criticalPriority: 90 property var activeEvent: null property var pendingEvents: [] property int sequence: 0 readonly property bool active: root.activeEvent !== null readonly property int queueLength: root.pendingEvents.length signal eventPublished(var event) signal eventDismissed(string key) Timer { id: expiryTimer repeat: false onTriggered: root.dismiss() } function publish(candidate: var): bool { const event = root.normalize(candidate); if (!event) return false; // DND is allowed to quiet device ambience, but it must never hide a // privacy transition or the result of something the user initiated. if (Notifs.doNotDisturb && event.priority < root.importantPriority) return false; if (root.activeEvent && root.activeEvent.key === event.key) { root.activeEvent = event; root.armExpiry(); root.eventPublished(event); return true; } const withoutEquivalent = root.pendingEvents.filter(item => item.key !== event.key); if (!root.activeEvent || event.priority > root.activeEvent.priority) { if (root.activeEvent) withoutEquivalent.push(root.activeEvent); root.pendingEvents = root.trimQueue(withoutEquivalent); root.activeEvent = event; root.armExpiry(); } else { withoutEquivalent.push(event); root.pendingEvents = root.trimQueue(withoutEquivalent); } root.eventPublished(event); return true; } function dismiss(): void { if (!root.activeEvent) return; const dismissedKey = root.activeEvent.key; expiryTimer.stop(); root.activeEvent = null; root.eventDismissed(dismissedKey); root.advance(); } function invoke(): void { if (!root.activeEvent) return; const action = root.activeEvent.actionId; const data = root.activeEvent.actionData; root.dismiss(); if (action === "focus-workspace") { FocusSession.activateWorkspace(); } else if (action === "open-workspace") { const workspaceId = Number(data); if (Number.isInteger(workspaceId) && workspaceId > 0) Hyprland.dispatch(`hl.dsp.focus({ workspace = ${workspaceId} })`); } else if (action === "open-activity") { ShellState.open("activity"); } else if (action === "open-path" && data) { Quickshell.execDetached(["xdg-open", data]); } } function reset(): void { expiryTimer.stop(); root.activeEvent = null; root.pendingEvents = []; } function normalize(candidate: var): var { if (!candidate || !candidate.key || !candidate.title) return null; root.sequence++; const priority = Number(candidate.priority ?? root.ambientPriority); const duration = Number(candidate.durationMs ?? 3200); return { key: String(candidate.key), icon: String(candidate.icon ?? "dialog-information-symbolic"), glyph: String(candidate.glyph ?? ""), title: String(candidate.title), detail: String(candidate.detail ?? ""), tone: String(candidate.tone ?? "accent"), priority: Number.isFinite(priority) ? priority : root.ambientPriority, durationMs: Math.max(1200, Number.isFinite(duration) ? duration : 3200), actionId: String(candidate.actionId ?? ""), actionData: String(candidate.actionData ?? ""), monitorName: String(candidate.monitorName ?? ""), sequence: root.sequence }; } function trimQueue(events: var): var { const sorted = events.slice().sort((a, b) => { if (a.priority !== b.priority) return b.priority - a.priority; return a.sequence - b.sequence; }); return sorted.slice(0, 4); } function advance(): void { if (root.pendingEvents.length === 0) return; const next = root.pendingEvents[0]; root.pendingEvents = root.pendingEvents.slice(1); root.activeEvent = next; root.armExpiry(); } function armExpiry(): void { expiryTimer.stop(); if (!root.activeEvent) return; expiryTimer.interval = root.activeEvent.durationMs; expiryTimer.start(); } }