summaryrefslogtreecommitdiff
path: root/widget/notifications
diff options
context:
space:
mode:
authorLibravatar Mora Unie Youer <[email protected]>2025-03-18 16:47:58 +0300
committerLibravatar Mora Unie Youer <[email protected]>2025-03-18 16:50:29 +0300
commit63742be30723555ab54c7b11136dfaf6f83d0c2e (patch)
tree2abb956786f56eb41bc37fac464448fc53225de1 /widget/notifications
parentfeat: dynamic workspace updating (diff)
downloadags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.gz
ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.bz2
ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.lz
ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.xz
ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.zst
ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.zip
feat: add notifications widget
Diffstat (limited to '')
-rw-r--r--widget/notifications/Notifications.scss141
-rw-r--r--widget/notifications/Notifications.tsx108
2 files changed, 249 insertions, 0 deletions
diff --git a/widget/notifications/Notifications.scss b/widget/notifications/Notifications.scss
new file mode 100644
index 0000000..5762df1
--- /dev/null
+++ b/widget/notifications/Notifications.scss
@@ -0,0 +1,141 @@
+$rosewater: #f5e0dc;
+$flamingo: #f2cdcd;
+$pink: #f5c2e7;
+$mauve: #cba6f7;
+$red: #f38ba8;
+$maroon: #eba0ac;
+$peach: #fab387;
+$yellow: #f9e2af;
+$green: #a6e3a1;
+$teal: #94e2d5;
+$sky: #89dceb;
+$sapphire: #74c7ec;
+$blue: #89b4fa;
+$lavender: #b4befe;
+
+$text: #cdd6f4;
+$subtext1: #bac2de;
+$subtext0: #a6adc8;
+
+$overlay2: #9399b2;
+$overlay1: #7f849c;
+$overlay0: #6c7086;
+
+$surface2: #585b70;
+$surface1: #45475a;
+$surface0: #313244;
+
+$base: #1e1e2e;
+$mantle: #181825;
+$crust: #11111b;
+
+window.Notifications {
+ box.Notification {
+ margin: .25rem 1rem;
+ border: 1px solid $blue;
+ border-radius: 5px;
+ min-width: 300px;
+
+ background-color: $base;
+
+ separator {
+ background: $blue;
+ }
+
+ &.low {
+ border-color: $overlay1;
+
+ separator {
+ background: $overlay1;
+ }
+ }
+
+ &.critical {
+ border-color: $red;
+
+ separator {
+ background: $red;
+ }
+ }
+
+ &:first-child {
+ margin-top: 1rem;
+ }
+
+ &:last-child {
+ margin-bottom: 1rem;
+ }
+
+ box.Header {
+ label.Application {
+ margin-top: .25rem;
+ margin-left: .5rem;
+
+ color: $text;
+
+ font-weight: bold;
+ }
+
+ label.Time {
+ margin: .25rem .25rem 0 1rem;
+
+ color: $subtext1;
+ }
+
+ button.Close {
+ margin: 0;
+ border: 0;
+ border-radius: 0;
+ padding: 5px;
+ min-width: 0;
+ min-height: 0;
+
+ background: none;
+
+ image {
+ background-color: $red;
+ border-radius: 50%;
+ color: $red;
+ }
+ }
+ }
+
+ box.Contents {
+ margin: .5rem;
+
+ label.Summary {
+ color: $text;
+
+ font-weight: bold;
+ }
+
+ label.Body {
+ color: $subtext1;
+ }
+ }
+
+ box.Actions {
+ button {
+ margin: 0 .25rem;
+ border: 0;
+ border-radius: 0;
+ padding: 5px;
+ min-width: 0;
+ min-height: 0;
+
+ background: none;
+ color: $text;
+
+ font-weight: bold;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/widget/notifications/Notifications.tsx b/widget/notifications/Notifications.tsx
new file mode 100644
index 0000000..6708b96
--- /dev/null
+++ b/widget/notifications/Notifications.tsx
@@ -0,0 +1,108 @@
+import { GLib, Variable } from "astal";
+import { bind, Subscribable } from "astal/binding";
+import { Astal, Gdk, Gtk } from "astal/gtk4";
+import AstalNotifd from "gi://AstalNotifd?version=0.1";
+
+const notificationTimeout: number = 5000;
+
+class NotificationHandler implements Subscribable<Gtk.Widget[]> {
+ private notifications: Variable<Gtk.Widget[]> = Variable([]);
+ private notificationsMap: Map<number, Gtk.Widget> = new Map();
+
+ constructor() {
+ const notifd = AstalNotifd.get_default();
+
+ notifd.connect("notified", (_source, id, _replaced) => {
+ const n = notifd.get_notification(id);
+ this.create(n);
+ setTimeout(() => this.remove(id), notificationTimeout);
+ });
+
+ notifd.connect("resolved", (_source, id, _reason) => {
+ this.remove(id);
+ });
+ }
+
+ private rerender() {
+ this.notifications.set([...this.notificationsMap.values()].reverse());
+ }
+
+ private create(n: AstalNotifd.Notification) {
+ const notification = Notification(n);
+ this.notificationsMap.get(n.id)?.emit("destroy");
+ this.notificationsMap.set(n.id, notification);
+ this.rerender();
+ }
+
+ private remove(id: number) {
+ this.notificationsMap.get(id)?.emit("destroy");
+ this.notificationsMap.delete(id);
+ this.rerender();
+ }
+
+ subscribe(callback: (value: Gtk.Widget[]) => void): () => void {
+ return this.notifications.subscribe(callback);
+ }
+
+ get(): Gtk.Widget[] {
+ return this.notifications.get();
+ }
+}
+
+function getUrgencyClass(n: AstalNotifd.Notification): string {
+ switch (n.urgency) {
+ case AstalNotifd.Urgency.LOW:
+ return "low";
+ case AstalNotifd.Urgency.CRITICAL:
+ return "critical";
+ case AstalNotifd.Urgency.NORMAL:
+ default:
+ return "normal";
+ }
+}
+
+function Notification(n: AstalNotifd.Notification) {
+ const appName = n.appName;
+ const time = GLib.DateTime.new_from_unix_local(n.time);
+
+ return <box cssClasses={["Notification", getUrgencyClass(n)]} vertical>
+ <box cssClasses={["Header"]}>
+ <label cssClasses={["Application"]} halign={Gtk.Align.START} label={appName || "Unknown"} />
+ <label cssClasses={["Time"]} hexpand halign={Gtk.Align.END} label={time.format("%I:%M:%S %p")!} />
+ <button cssClasses={["Close"]} onClicked={() => n.dismiss()}><image iconName="window-close-symbolic" /></button>
+ </box>
+ <Gtk.Separator visible />
+ <box cssClasses={["Contents"]}>
+ <box vertical>
+ <label cssClasses={["Summary"]} halign={Gtk.Align.START} label={n.summary} />
+ <label cssClasses={["Body"]} useMarkup wrap maxWidthChars={0} justify={Gtk.Justification.FILL} label={n.body} />
+ </box>
+ </box>
+
+ {n.get_actions().length > 0 && <>
+ <Gtk.Separator visible />
+ <box cssClasses={["Actions"]}>
+ {n.get_actions().map(({ label, id }) => <button hexpand onClicked={() => n.invoke(id)}><label label={label} /></button>)}
+ </box>
+ </>}
+ </box>;
+}
+
+export default function(gdkmonitor: Gdk.Monitor) {
+ const { TOP, RIGHT } = Astal.WindowAnchor;
+ const notifications = new NotificationHandler();
+
+ return <window
+ // NOTE: is required due to last notification being displayed all the time
+ visible={bind(notifications).as(v => v.length != 0)}
+
+ name={"Notifications"}
+ cssClasses={["Notifications"]}
+ gdkmonitor={gdkmonitor}
+ exclusivity={Astal.Exclusivity.EXCLUSIVE}
+ anchor={TOP | RIGHT}>
+ <box vertical>
+ {bind(notifications)}
+ </box>
+ </window>
+}