274 lines
11 KiB
QML
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
|
|
});
|
|
}
|
|
}
|
|
}
|