Build the Panama Hyprland desktop
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
pragma Singleton
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// The freedesktop notification server, plus the two lists the UI renders:
|
||||
//
|
||||
// popups — what Toasts.qml is currently showing (transient, timed)
|
||||
// history — GNOME's message tray, what NotificationCenter.qml shows
|
||||
//
|
||||
// A notification lives exactly as long as `tracked` is true, so history holds
|
||||
// the *live* objects rather than copies: that keeps actions and inline replies
|
||||
// working from the tray, which is what GNOME does. The cost is that a
|
||||
// notification an app closes itself (progress bars, "download finished"
|
||||
// replacing "downloading") disappears from history too — correct behaviour,
|
||||
// but the reason history is not append-only.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import qs.config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property alias server: server
|
||||
|
||||
// Live tracked set, straight from the server. Mostly useful for counting;
|
||||
// the UI wants `popups` / `history`, which are ordered newest-first.
|
||||
readonly property alias active: server.trackedNotifications
|
||||
|
||||
property var history: []
|
||||
property var popups: []
|
||||
|
||||
// Suppresses toasts entirely. Notifications still reach history.
|
||||
property bool doNotDisturb: false
|
||||
|
||||
// Cleared when the notification centre is opened. The bar binds to this.
|
||||
property int unreadCount: 0
|
||||
|
||||
// Arrival times, keyed by notification id — the protocol carries no
|
||||
// timestamp. Deliberately formatted once at arrival rather than shown as
|
||||
// "5 minutes ago", which would need a clock ticking behind every card.
|
||||
readonly property var arrivals: ({})
|
||||
|
||||
function timeText(id: int): string {
|
||||
const at = root.arrivals[id];
|
||||
return at ? Qt.formatDateTime(at, Settings.use24Hour ? "HH:mm" : "h:mm AP") : "";
|
||||
}
|
||||
|
||||
readonly property bool hasNotifications: root.history.length > 0
|
||||
|
||||
// history grouped by app, in most-recent-app-first order — the shape
|
||||
// NotificationCenter.qml renders directly.
|
||||
readonly property var groups: {
|
||||
const out = [];
|
||||
const byApp = {};
|
||||
for (const n of root.history) {
|
||||
const key = n.appName || "Notifications";
|
||||
let group = byApp[key];
|
||||
if (!group) {
|
||||
group = {
|
||||
app: key,
|
||||
icon: n.appIcon,
|
||||
desktopEntry: n.desktopEntry,
|
||||
items: []
|
||||
};
|
||||
byApp[key] = group;
|
||||
out.push(group);
|
||||
}
|
||||
group.items.push(n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
keepOnReload: true
|
||||
persistenceSupported: true
|
||||
bodySupported: true
|
||||
bodyMarkupSupported: true
|
||||
bodyImagesSupported: true
|
||||
imageSupported: true
|
||||
actionsSupported: true
|
||||
actionIconsSupported: true
|
||||
inlineReplySupported: true
|
||||
|
||||
onNotification: notification => {
|
||||
// Replayed from before a shell reload. Letting these through would
|
||||
// re-toast and re-list everything on every edit, so they are left
|
||||
// untracked and allowed to die.
|
||||
if (notification.lastGeneration)
|
||||
return;
|
||||
|
||||
// Without this the object is destroyed the instant this returns.
|
||||
notification.tracked = true;
|
||||
root.arrivals[notification.id] = new Date();
|
||||
|
||||
// The object may go away at any time (app-side close, dismiss()).
|
||||
// Drop our references synchronously when it does.
|
||||
notification.closed.connect(() => root.forget(notification));
|
||||
|
||||
// `transient` is the volume-OSD case: show it, never file it.
|
||||
if (!notification.transient) {
|
||||
root.pushHistory(notification);
|
||||
root.unreadCount += 1;
|
||||
}
|
||||
|
||||
if (!root.doNotDisturb)
|
||||
root.popups = [notification].concat(root.popups);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mutation ────────────────────────────────────────────────────────────
|
||||
|
||||
function pushHistory(n: Notification): void {
|
||||
const next = [n].concat(root.history);
|
||||
|
||||
// Anything past the cap is released, otherwise it stays tracked
|
||||
// forever and the server's set grows without bound.
|
||||
const evicted = next.splice(Settings.notificationHistoryLimit);
|
||||
root.history = next;
|
||||
for (const old of evicted)
|
||||
old.dismiss();
|
||||
}
|
||||
|
||||
// Hide a toast without filing the notification away as read. Mirrors
|
||||
// GNOME: closing a banner leaves it in the tray.
|
||||
function dropPopup(n: Notification): void {
|
||||
const next = root.popups.filter(x => x !== n);
|
||||
if (next.length === root.popups.length)
|
||||
return;
|
||||
root.popups = next;
|
||||
|
||||
// Transients were never in history, so nothing else holds them.
|
||||
if (n.transient)
|
||||
n.dismiss();
|
||||
}
|
||||
|
||||
function dismiss(n: Notification): void {
|
||||
n.dismiss();
|
||||
}
|
||||
|
||||
function dismissAll(): void {
|
||||
// Copy first: dismiss() re-enters through forget() and rewrites both
|
||||
// lists while we iterate.
|
||||
const all = root.history.slice();
|
||||
root.history = [];
|
||||
root.popups = [];
|
||||
for (const n of all)
|
||||
n.dismiss();
|
||||
root.unreadCount = 0;
|
||||
}
|
||||
|
||||
function dismissApp(appName: string): void {
|
||||
const doomed = root.history.filter(n => (n.appName || "Notifications") === appName);
|
||||
root.history = root.history.filter(n => doomed.indexOf(n) === -1);
|
||||
root.popups = root.popups.filter(n => doomed.indexOf(n) === -1);
|
||||
for (const n of doomed)
|
||||
n.dismiss();
|
||||
}
|
||||
|
||||
function markAllRead(): void {
|
||||
root.unreadCount = 0;
|
||||
}
|
||||
|
||||
// Called from the `closed` signal — the object is on its way out, so this
|
||||
// only ever removes references, never touches the notification.
|
||||
function forget(n: Notification): void {
|
||||
delete root.arrivals[n.id];
|
||||
if (root.history.indexOf(n) !== -1)
|
||||
root.history = root.history.filter(x => x !== n);
|
||||
if (root.popups.indexOf(n) !== -1)
|
||||
root.popups = root.popups.filter(x => x !== n);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user