mirror of
https://github.com/zephrynis/nix-flake.git
synced 2026-02-18 20:21:53 +00:00
- Add complete colorshell v2.0.3 configuration to home/ags-config/ - Disable runner plugin and NightLight tile (incompatible with NixOS) - Customize SCSS with full opacity (no transparency) - Add dark pale blue color scheme in home/pywal-colors/ - Configure Papirus-Dark icon theme via home-manager - Make ~/.config/ags/ immutable and managed by Nix store - Auto-deploy pywal colors to ~/.cache/wal/colors.json All AGS configuration is now reproducible and version controlled.
362 lines
12 KiB
TypeScript
362 lines
12 KiB
TypeScript
import { execAsync } from "ags/process";
|
|
import { generalConfig } from "../config";
|
|
import { onCleanup } from "ags";
|
|
import GObject, { getter, ParamSpec, property, register, signal } from "ags/gobject";
|
|
|
|
import AstalNotifd from "gi://AstalNotifd";
|
|
import GLib from "gi://GLib?version=2.0";
|
|
|
|
|
|
export type HistoryNotification = {
|
|
id: number;
|
|
appName: string;
|
|
body: string;
|
|
summary: string;
|
|
urgency: AstalNotifd.Urgency;
|
|
appIcon?: string;
|
|
time: number;
|
|
image?: string;
|
|
}
|
|
|
|
export class NotificationTimeout {
|
|
#source?: GLib.Source;
|
|
#args?: Array<any>;
|
|
#millis: number;
|
|
#lastRemained!: number;
|
|
|
|
readonly callback: () => void;
|
|
get millis(): number { return this.#millis; }
|
|
get remaining(): number { return this.source!.get_time() }
|
|
get lastRemained(): number { return this.#lastRemained; }
|
|
get running(): boolean { return Boolean(this.source?.is_destroyed()); }
|
|
get source(): GLib.Source|undefined { return this.#source; }
|
|
|
|
constructor(millis: number, callback: () => void, start: boolean = true, ...args: Array<any>) {
|
|
this.#millis = millis;
|
|
this.callback = callback;
|
|
this.#args = args;
|
|
|
|
if(!start) return;
|
|
this.start();
|
|
}
|
|
|
|
cancel(): void {
|
|
// use lastRemained to calculate on what time the user hold the notification, so it
|
|
// can be released by the remaining time (works like a timeout "pause")
|
|
this.#lastRemained = Math.floor(Math.max(this.#source!.get_ready_time() - GLib.get_monotonic_time()) / 1000);
|
|
this.#source?.destroy();
|
|
this.#source?.unref();
|
|
this.#source = undefined;
|
|
}
|
|
|
|
start(newMillis?: number): GLib.Source {
|
|
if(this.running)
|
|
throw new Error("Notifications: Can't start a new counter if it's already running!");
|
|
|
|
if(newMillis !== undefined)
|
|
this.#millis = newMillis;
|
|
|
|
this.#source = setTimeout(
|
|
this.callback,
|
|
this.#millis,
|
|
this.#args
|
|
);
|
|
|
|
this.#lastRemained = Math.floor(Math.max(this.#source!.get_ready_time() - GLib.get_monotonic_time()) / 1000);
|
|
|
|
return this.#source;
|
|
}
|
|
};
|
|
|
|
@register({ GTypeName: "Notifications" })
|
|
export class Notifications extends GObject.Object {
|
|
private static instance: (Notifications|null) = null;
|
|
|
|
declare $signals: GObject.Object.SignalSignatures & {
|
|
"history-added": (notification: HistoryNotification) => void;
|
|
"history-removed": (notificationId: number) => void;
|
|
"history-cleared": () => void;
|
|
"notification-added": (notification: AstalNotifd.Notification) => void;
|
|
"notification-removed": (notificationId: number) => void;
|
|
"notification-replaced": (notificationId: number) => void;
|
|
};
|
|
|
|
#notifications = new Map<number, [AstalNotifd.Notification, NotificationTimeout]>();
|
|
#history: Array<HistoryNotification> = [];
|
|
#connections: Array<number> = [];
|
|
|
|
@getter(Array<AstalNotifd.Notification>)
|
|
public get notifications() {
|
|
return [...this.#notifications.values()].map(([n]) => n);
|
|
};
|
|
|
|
@getter(Array<HistoryNotification>)
|
|
public get history() { return this.#history };
|
|
|
|
@getter(Array<AstalNotifd.Notification>)
|
|
public get notificationsOnHold() {
|
|
return [...this.#notifications.values()].filter(([_, s]) =>
|
|
typeof s === "undefined"
|
|
).map(([n]) => n);
|
|
}
|
|
|
|
@property(Number)
|
|
public historyLimit: number = 10;
|
|
|
|
/** skip notifications directly to notification history */
|
|
@property(Boolean)
|
|
public ignoreNotifications: boolean = false;
|
|
|
|
|
|
@signal(AstalNotifd.Notification) notificationAdded(_notification: AstalNotifd.Notification) {};
|
|
@signal(Number) notificationRemoved(_id: number) {};
|
|
@signal(Object as unknown as ParamSpec<HistoryNotification>) historyAdded(_notification: Object) {};
|
|
@signal() historyCleared() {};
|
|
@signal(Number) historyRemoved(_id: number) {};
|
|
@signal(Number) notificationReplaced(_id: number) {};
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.#connections.push(
|
|
AstalNotifd.get_default().connect("notified", (notifd, id) => {
|
|
const notification = notifd.get_notification(id);
|
|
|
|
if(this.getNotifd().dontDisturb || this.ignoreNotifications) {
|
|
this.addHistory(notification, () => notification.dismiss());
|
|
return;
|
|
}
|
|
|
|
this.addNotification(notification, this.getNotificationTimeout(notification) > 0);
|
|
}),
|
|
|
|
AstalNotifd.get_default().connect("resolved", (notifd, id, _reason) => {
|
|
this.removeNotification(id);
|
|
this.addHistory(notifd.get_notification(id));
|
|
})
|
|
);
|
|
|
|
onCleanup(() => {
|
|
this.#connections.map(id =>
|
|
AstalNotifd.get_default().disconnect(id));
|
|
});
|
|
}
|
|
|
|
public static getDefault(): Notifications {
|
|
if(!this.instance)
|
|
this.instance = new Notifications();
|
|
|
|
return this.instance;
|
|
}
|
|
|
|
public async sendNotification(props: {
|
|
urgency?: AstalNotifd.Urgency;
|
|
appName?: string;
|
|
image?: string;
|
|
summary: string;
|
|
body?: string;
|
|
replaceId?: number;
|
|
actions?: Array<{
|
|
id?: (string|number);
|
|
text: string;
|
|
onAction?: () => void
|
|
}>
|
|
}): Promise<{
|
|
id?: (string|number);
|
|
text: string;
|
|
onAction?: () => void
|
|
}|null|void> {
|
|
|
|
return await execAsync([
|
|
"notify-send",
|
|
...(props.urgency ? [
|
|
"-u", this.getUrgencyString(props.urgency)
|
|
] : []), ...(props.appName ? [
|
|
"-a", props.appName
|
|
] : []), ...(props.image ? [
|
|
"-i", props.image
|
|
] : []), ...(props.actions ? props.actions.map((action) =>
|
|
[ "-A", action.text ]
|
|
).flat(2) : []), ...(props.replaceId ? [
|
|
"-r", props.replaceId.toString()
|
|
] : []), props.summary, props.body ? props.body : ""
|
|
]).then((stdout) => {
|
|
stdout = stdout.trim();
|
|
if(!stdout) {
|
|
if(props.actions && props.actions.length > 0)
|
|
return null;
|
|
|
|
return;
|
|
}
|
|
|
|
if(props.actions && props.actions.length > 0) {
|
|
const action = props.actions[Number.parseInt(stdout)];
|
|
action?.onAction?.();
|
|
|
|
return action ?? undefined;
|
|
}
|
|
}).catch((err: Error) => {
|
|
console.error(`Notifications: Couldn't send notification! Is the daemon running? Stderr:\n${
|
|
err.message ? `${err.message}\n` : ""}Stack: ${err.stack}`);
|
|
});
|
|
}
|
|
|
|
public getUrgencyString(urgency: AstalNotifd.Notification|AstalNotifd.Urgency) {
|
|
switch((urgency instanceof AstalNotifd.Notification) ?
|
|
urgency.urgency : urgency) {
|
|
|
|
case AstalNotifd.Urgency.LOW:
|
|
return "low";
|
|
case AstalNotifd.Urgency.CRITICAL:
|
|
return "critical";
|
|
}
|
|
|
|
return "normal";
|
|
}
|
|
|
|
private addHistory(notif: AstalNotifd.Notification, onAdded?: (notif: AstalNotifd.Notification) => void): void {
|
|
if(!notif) return;
|
|
|
|
this.#history.length === this.historyLimit &&
|
|
this.removeHistory(this.#history[this.#history.length - 1]);
|
|
|
|
this.#history.map((notifb, i) =>
|
|
notifb.id === notif.id && this.#history.splice(i, 1));
|
|
|
|
this.#history.unshift({
|
|
id: notif.id,
|
|
appName: notif.app_name,
|
|
body: notif.body,
|
|
summary: notif.summary,
|
|
urgency: notif.urgency,
|
|
appIcon: notif.app_icon,
|
|
time: notif.time,
|
|
image: notif.image ? notif.image : undefined
|
|
} as HistoryNotification);
|
|
|
|
this.notify("history");
|
|
this.emit("history-added", this.#history[0]);
|
|
onAdded?.(notif);
|
|
}
|
|
|
|
public async clearHistory(): Promise<void> {
|
|
this.#history.reverse().map((notif) => {
|
|
this.#history = this.history.filter((n) => n.id !== notif.id);
|
|
this.emit("history-removed", notif.id);
|
|
});
|
|
|
|
this.emit("history-cleared");
|
|
this.notify("history");
|
|
}
|
|
|
|
public removeHistory(notif: (HistoryNotification|number)): void {
|
|
const notifId = (typeof notif === "number") ? notif : notif.id;
|
|
this.#history = this.#history.filter((item: HistoryNotification) =>
|
|
item.id !== notifId);
|
|
|
|
this.notify("history");
|
|
this.emit("history-removed", notifId);
|
|
}
|
|
|
|
private addNotification(
|
|
notif: AstalNotifd.Notification,
|
|
removeOnTimeout: boolean = true,
|
|
onTimeoutEnd?: () => void
|
|
): void {
|
|
|
|
const replaced = this.#notifications.has(notif.id);
|
|
const notifTimeout = this.getNotificationTimeout(notif);
|
|
const onEnd = () => {
|
|
removeOnTimeout && this.removeNotification(notif);
|
|
onTimeoutEnd?.();
|
|
}
|
|
|
|
// destroy timer of replaced notification(if there's any)
|
|
if(replaced) {
|
|
const data = this.#notifications.get(notif.id)!;
|
|
(data?.[1] instanceof NotificationTimeout) &&
|
|
data[1].cancel();
|
|
}
|
|
|
|
this.#notifications.set(notif.id, [
|
|
notif,
|
|
new NotificationTimeout(notifTimeout, onEnd, notifTimeout > 0)
|
|
]);
|
|
|
|
replaced && this.emit("notification-replaced", notif.id);
|
|
|
|
this.notify("notifications");
|
|
this.emit("notification-added", notif);
|
|
|
|
if(notifTimeout <= 0) onEnd?.();
|
|
}
|
|
|
|
public getNotificationTimeout(notif: AstalNotifd.Notification): number {
|
|
return generalConfig.getProperty(
|
|
`notifications.timeout_${this.getUrgencyString(notif.urgency)}`,
|
|
"number"
|
|
);
|
|
}
|
|
|
|
public removeNotification(notif: (AstalNotifd.Notification|number), addToHistory: boolean = true): void {
|
|
notif = typeof notif === "number" ?
|
|
this.#notifications.get(notif)?.[0]!
|
|
: notif;
|
|
|
|
if(!notif) return;
|
|
|
|
const timeout = this.#notifications.get(notif.id)![1];
|
|
timeout.running && timeout.cancel();
|
|
|
|
this.#notifications.delete(notif.id);
|
|
addToHistory && this.addHistory(notif);
|
|
|
|
notif.dismiss();
|
|
this.notify("notifications");
|
|
this.emit("notification-removed", notif.id);
|
|
}
|
|
|
|
public holdNotification(notif: AstalNotifd.Notification|number): void {
|
|
const id = typeof notif === "number" ? notif : notif.id;
|
|
const data = this.#notifications.get(id);
|
|
|
|
if(!data) return;
|
|
|
|
data[1].cancel();
|
|
this.notify("notifications-on-hold");
|
|
}
|
|
|
|
public releaseNotification(notif: AstalNotifd.Notification|number): void {
|
|
const id = typeof notif === "number" ? notif : notif.id;
|
|
const data = this.#notifications.get(id);
|
|
|
|
if(!data) return;
|
|
data[1].start(data[1].lastRemained);
|
|
|
|
this.notify("notifications-on-hold");
|
|
}
|
|
|
|
public toggleDoNotDisturb(value?: boolean): boolean {
|
|
value = value ?? !AstalNotifd.get_default().dontDisturb;
|
|
AstalNotifd.get_default().dontDisturb = value;
|
|
|
|
return value;
|
|
}
|
|
|
|
public getNotifd(): AstalNotifd.Notifd { return AstalNotifd.get_default(); }
|
|
|
|
public emit<Signal extends keyof typeof this.$signals>(
|
|
signal: Signal, ...args: Parameters<(typeof this.$signals)[Signal]>
|
|
): void {
|
|
super.emit(signal, ...args);
|
|
}
|
|
|
|
public connect<Signal extends keyof typeof this.$signals>(
|
|
signal: Signal,
|
|
callback: (self: typeof this, ...params: Parameters<(typeof this.$signals)[Signal]>) =>
|
|
ReturnType<(typeof this.$signals)[Signal]>
|
|
): number {
|
|
return super.connect(signal, callback);
|
|
}
|
|
}
|