import Quickshell import Quickshell.Io import Quickshell.Services.Notifications import QtQuick import qs.config import qs.services ShellRoot { id: root // A stand-in for a served notification. `urgency` and `expireTimeout` are // spelled out rather than left undefined: the effective-urgency override // and the banner-duration choice both read them, so a fixture that omits // them would be testing the undefined path instead of the ordinary one. function notification(idValue: int, desktopEntryValue: string, appNameValue: string): var { const closeHandlers = []; return { id: idValue, desktopEntry: desktopEntryValue, appName: appNameValue, appIcon: "", urgency: NotificationUrgency.Normal, expireTimeout: -1, hints: ({}), transient: false, lastGeneration: false, tracked: false, dismissed: false, closed: { connect: callback => closeHandlers.push(callback) }, dismiss: function() { this.dismissed = true; for (const callback of closeHandlers) callback(); } }; } function resetNotifications(): void { Notifs.history = []; Notifs.popups = []; Notifs.unreadCount = 0; Notifs.doNotDisturb = false; } function reset(): void { root.resetNotifications(); Notifs.fallbackAppRules = {}; Notifs.rememberedApplications = {}; DesktopPreferences.set("notificationAppRules", {}); DesktopPreferences.set("criticalBreaksThrough", false); } // One notification through the whole delivery path, reported as the four // counts that distinguish every outcome from every other: muted, filed // quietly, held by Do Not Disturb, or shown. function deliver(n: var): var { Notifs.handleNotification(n); return { tracked: n.tracked, history: Notifs.history.length, popups: Notifs.popups.length, unread: Notifs.unreadCount }; } function ruleKeys(): var { return Object.keys(DesktopPreferences.get("notificationAppRules")); } IpcHandler { target: "notification-app-rules-test" function exercise(): string { root.reset(); const signal = root.notification(1, "org.signal.Signal.desktop", "Signal"); Notifs.handleNotification(signal); const appId = Notifs.notificationAppId(signal); const initialRules = DesktopPreferences.get("notificationAppRules"); const fallback = root.notification(6, "", "Fallback Terminal"); Notifs.handleNotification(fallback); const fallbackId = Notifs.notificationAppId(fallback); const fallbackApplication = Notifs.applications.find(app => app.id === fallbackId); root.resetNotifications(); Notifs.setAppRule(appId, { enabled: false }); const muted = root.notification(2, "org.signal.Signal.desktop", "Signal"); Notifs.handleNotification(muted); const mutedResult = { tracked: muted.tracked, history: Notifs.history.length, popups: Notifs.popups.length, unread: Notifs.unreadCount }; root.reset(); Notifs.doNotDisturb = true; const dnd = root.notification(3, "org.signal.Signal.desktop", "Signal"); Notifs.handleNotification(dnd); return JSON.stringify({ appId: appId, initialRules: initialRules, fallback: { id: fallbackId, application: fallbackApplication }, muted: mutedResult, dnd: { tracked: dnd.tracked, history: Notifs.history.length, popups: Notifs.popups.length, unread: Notifs.unreadCount } }); } function persist(): string { root.reset(); const notification = root.notification(5, "org.persist.App.desktop", "Persist"); Notifs.handleNotification(notification); Notifs.setAppRule("org.persist.App.desktop", { enabled: false }); return JSON.stringify(DesktopPreferences.get("notificationAppRules")); } function restored(): string { return JSON.stringify(DesktopPreferences.get("notificationAppRules")); } function applications(): string { return JSON.stringify(Notifs.applications); } // display: "history" is not muting. It has to reach history and the // unread count and stop short of the banner, which is the one // combination of the four counts that says so. function historyOnly(): string { root.reset(); const seed = root.notification(10, "org.history.App.desktop", "History App"); Notifs.handleNotification(seed); const appId = Notifs.notificationAppId(seed); root.resetNotifications(); Notifs.setAppRule(appId, { display: "history" }); return JSON.stringify(root.deliver( root.notification(11, "org.history.App.desktop", "History App"))); } // Forgetting removes the key rather than resetting its fields, so the // next notification writes a fresh default rule and the row returns. function forgetting(): string { root.reset(); const seed = root.notification(12, "org.forget.App.desktop", "Forget App"); Notifs.handleNotification(seed); const appId = Notifs.notificationAppId(seed); Notifs.setAppRule(appId, { sound: false, enabled: false }); const before = root.ruleKeys().indexOf(appId) >= 0; Notifs.forgetApp(appId); const after = root.ruleKeys().indexOf(appId) >= 0; // Forgetting something with no rule is a no-op, not a write. const forgotUnknown = Notifs.forgetApp("org.never.Seen.desktop"); root.resetNotifications(); Notifs.handleNotification( root.notification(13, "org.forget.App.desktop", "Forget App")); return JSON.stringify({ before: before, after: after, returned: root.ruleKeys().indexOf(appId) >= 0, soundAfterReturn: Notifs.appRule(appId).sound, forgotUnknown: forgotUnknown }); } // The override is one answer shared by the bell, the banner duration, // the breakthrough gate and the card. Only the duration is observable // from here, and it is the one that would silently keep the old value. function urgency(): string { root.reset(); const n = root.notification(14, "org.urgent.App.desktop", "Urgent App"); Notifs.handleNotification(n); const appId = Notifs.notificationAppId(n); const claimed = Notifs.effectiveUrgency(n) === NotificationUrgency.Normal; const normalTimeout = Notifs.notificationTimeoutMs(n) === Settings.notificationTimeoutMs; Notifs.setAppRule(appId, { urgency: "critical" }); const escalated = Notifs.effectiveUrgency(n) === NotificationUrgency.Critical; const escalatedTimeout = Notifs.notificationTimeoutMs(n) === Settings.notificationTimeoutCriticalMs; Notifs.setAppRule(appId, { urgency: "low" }); const lowered = Notifs.effectiveUrgency(n) === NotificationUrgency.Low; return JSON.stringify({ claimed: claimed, escalated: escalated, lowered: lowered, normalTimeout: normalTimeout, escalatedTimeout: escalatedTimeout }); } // Do Not Disturb with its second exception. Off, everything is held; // on, exactly the effective-critical notifications get through -- and // an application told to be treated as critical is one of them. function breakthrough(): string { root.reset(); const seed = root.notification(15, "org.critical.App.desktop", "Critical App"); Notifs.handleNotification(seed); const appId = Notifs.notificationAppId(seed); function attempt(idValue, claimedUrgency) { root.resetNotifications(); Notifs.doNotDisturb = true; const n = root.notification(idValue, "org.critical.App.desktop", "Critical App"); n.urgency = claimedUrgency; Notifs.handleNotification(n); return Notifs.popups.length; } DesktopPreferences.set("criticalBreaksThrough", false); const held = attempt(16, NotificationUrgency.Critical); DesktopPreferences.set("criticalBreaksThrough", true); const through = attempt(17, NotificationUrgency.Critical); const ordinary = attempt(18, NotificationUrgency.Normal); Notifs.setAppRule(appId, { urgency: "critical" }); const escalated = attempt(19, NotificationUrgency.Normal); DesktopPreferences.set("criticalBreaksThrough", false); Notifs.doNotDisturb = false; return JSON.stringify({ held: held, through: through, ordinary: ordinary, escalated: escalated }); } } }