diff --git a/flake.nix b/flake.nix
index bf60847..c135a9d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -36,6 +36,7 @@
battery
tray
network
+ notifd
];
};
in
@@ -88,6 +89,7 @@
battery
tray
network
+ notifd
];
})
];
diff --git a/style.scss b/style.scss
index 583224a..8785068 100644
--- a/style.scss
+++ b/style.scss
@@ -15,3 +15,6 @@ $font-size: 1rem;
// app-launcher
@import "./widgets/app-launcher/style.scss";
+
+// notifications
+@import "./widgets/notifications/style.scss";
diff --git a/widgets/notifications/icon.tsx b/widgets/notifications/icon.tsx
new file mode 100644
index 0000000..8f6aa13
--- /dev/null
+++ b/widgets/notifications/icon.tsx
@@ -0,0 +1,21 @@
+import { Gtk } from "astal/gtk4";
+import Notifd from "gi://AstalNotifd";
+import { fileExists, isIcon } from "./notifd";
+
+export const Icon = (notification: Notifd.Notification) => {
+ const icon =
+ notification.image || notification.appIcon || notification.desktopEntry;
+ if (!icon) return null;
+ if (fileExists(icon))
+ return (
+
+
+
+ );
+ else if (isIcon(icon))
+ return (
+
+
+
+ );
+};
diff --git a/widgets/notifications/index.tsx b/widgets/notifications/index.tsx
new file mode 100644
index 0000000..9f30a79
--- /dev/null
+++ b/widgets/notifications/index.tsx
@@ -0,0 +1,29 @@
+import { Astal, Gdk } from "astal/gtk4";
+import Notifd from "gi://AstalNotifd";
+import { bind } from "astal";
+import { NotificationWidget } from "./notification";
+
+export const WINDOW_NAME = "notifications";
+
+export const Notifications = (gdkmonitor: Gdk.Monitor) => {
+ const notifd = Notifd.get_default();
+ const { TOP, RIGHT } = Astal.WindowAnchor;
+
+ return (
+ notifications.length > 0,
+ )}
+ >
+
+ {bind(notifd, "notifications").as((notifications) =>
+ notifications.map((n) => ),
+ )}
+
+
+ );
+};
diff --git a/widgets/notifications/notifd.ts b/widgets/notifications/notifd.ts
new file mode 100644
index 0000000..42d0129
--- /dev/null
+++ b/widgets/notifications/notifd.ts
@@ -0,0 +1,79 @@
+import Notifd from "gi://AstalNotifd";
+import { GLib } from "astal";
+import { Gtk, Gdk } from "astal/gtk4";
+
+type TimeoutManager = {
+ setupTimeout: () => void;
+ clearTimeout: () => void;
+ handleHover: () => void;
+ handleHoverLost: () => void;
+ cleanup: () => void;
+};
+
+export const createTimeoutManager = (
+ dismissCallback: () => void,
+ timeoutDelay: number,
+): TimeoutManager => {
+ let isHovered = false;
+ let timeoutId: number | null = null;
+
+ const clearTimeout = () => {
+ if (timeoutId !== null) {
+ GLib.source_remove(timeoutId);
+ timeoutId = null;
+ }
+ };
+
+ const setupTimeout = () => {
+ clearTimeout();
+
+ if (!isHovered) {
+ timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeoutDelay, () => {
+ clearTimeout();
+ dismissCallback();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ };
+
+ return {
+ setupTimeout,
+ clearTimeout,
+ handleHover: () => {
+ isHovered = true;
+ clearTimeout();
+ },
+ handleHoverLost: () => {
+ isHovered = false;
+ setupTimeout();
+ },
+ cleanup: clearTimeout,
+ };
+};
+
+export const time = (time: number, format = "%H:%M") =>
+ GLib.DateTime.new_from_unix_local(time).format(format)!;
+
+export const urgency = (notification: Notifd.Notification) => {
+ const { LOW, NORMAL, CRITICAL } = Notifd.Urgency;
+
+ switch (notification.urgency) {
+ case LOW:
+ return "low";
+ case CRITICAL:
+ return "critical";
+ case NORMAL:
+ default:
+ return "normal";
+ }
+};
+
+export const isIcon = (icon: string) => {
+ const display = Gdk.Display.get_default();
+ if (!display) return false;
+ const iconTheme = Gtk.IconTheme.get_for_display(display);
+ return iconTheme.has_icon(icon);
+};
+
+export const fileExists = (path: string) =>
+ GLib.file_test(path, GLib.FileTest.EXISTS);
diff --git a/widgets/notifications/notification.tsx b/widgets/notifications/notification.tsx
new file mode 100644
index 0000000..b9478b2
--- /dev/null
+++ b/widgets/notifications/notification.tsx
@@ -0,0 +1,53 @@
+import { bind } from "astal";
+import { Gtk } from "astal/gtk4";
+import Notifd from "gi://AstalNotifd";
+import { urgency, createTimeoutManager } from "./notifd";
+
+export const NotificationWidget = ({ n }: { n: Notifd.Notification }) => {
+ const { START, CENTER } = Gtk.Align;
+ const actions = n.actions || [];
+ const TIMEOUT_DELAY = 3000;
+
+ // Keep track of notification validity
+ const timeoutManager = createTimeoutManager(() => n.dismiss(), TIMEOUT_DELAY);
+
+ return (
+ {
+ // Set up timeout
+ timeoutManager.setupTimeout();
+
+ self.connect("unrealize", () => {
+ timeoutManager.cleanup();
+ });
+ }}
+ cssClasses={["notification", `${urgency(n)}`]}
+ name={n.id.toString()}
+ spacing={10}
+ >
+
+
+ {n.body && (
+
+ )}
+
+ {actions.length > 0 && (
+
+ {actions.map(({ label, id }) => (
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/widgets/notifications/style.scss b/widgets/notifications/style.scss
new file mode 100644
index 0000000..2950214
--- /dev/null
+++ b/widgets/notifications/style.scss
@@ -0,0 +1,30 @@
+@use "sass:math";
+@use "sass:list";
+
+window.notifications {
+ margin: 10px;
+
+ .notification {
+ background: $bg-color;
+ padding: 20px;
+ border-radius: $rounded;
+ min-width: 20rem;
+ border: 1px solid transparent;
+
+ &.critical {
+ border: 1px solid rgba(red, 0.5);
+ }
+
+ &.low {
+ border: 1px solid rgba(yellow, 0.5);
+ }
+
+ .text {
+ color: rgba($fg-color, 0.8);
+
+ .title {
+ font-weight: 600;
+ }
+ }
+ }
+}
diff --git a/windows.ts b/windows.ts
index f82ce5c..348029c 100644
--- a/windows.ts
+++ b/windows.ts
@@ -1,4 +1,5 @@
import { Bar } from "./widgets/bar";
import { AppLauncher } from "./widgets/app-launcher";
+import { Notifications } from "./widgets/notifications";
-export default [Bar, AppLauncher];
+export default [Bar, AppLauncher, Notifications];