152 lines
4.9 KiB
QML
152 lines
4.9 KiB
QML
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();
|
|
}
|
|
}
|