Files
Panama/tests/quickshell/NotificationAppRulesHarness.qml
T

274 lines
11 KiB
QML

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
});
}
// A command carried as data in the `panama-exec` hint -- the mechanism
// the escalation ladder rides. Three facts in one pass: a notification
// without the hint carries nothing (which is almost all of them), one
// with it carries exactly what was sent, and the entry goes when the
// notification does rather than outliving it in the model.
function execHint(): string {
root.reset();
const plain = root.notification(20, "org.exec.App.desktop", "Exec App");
Notifs.handleNotification(plain);
const carrying = root.notification(21, "org.exec.App.desktop", "Exec App");
carrying.hints = { "panama-exec": " panama-agent-crash 41283 kitty " };
Notifs.handleNotification(carrying);
const carried = Notifs.execCommand(carrying);
carrying.dismiss();
return JSON.stringify({
plain: Notifs.execCommand(plain),
carried: carried,
afterDismiss: Notifs.execCommand(carrying)
});
}
// 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
});
}
}
}