diff --git a/modules/home/ags/app/app.ts b/modules/home/ags/app/app.ts
index 83217ef..ee918e0 100644
--- a/modules/home/ags/app/app.ts
+++ b/modules/home/ags/app/app.ts
@@ -1,10 +1,12 @@
import { App } from "astal/gtk3"
import style from "./style.scss"
import Bar from "./widget/Bar"
+import Notifications from "./widget/Notifications"
App.start({
css: style,
main() {
App.get_monitors().map(Bar)
+ Notifications(App.get_monitors()[0])
},
})
diff --git a/modules/home/ags/app/notif-config.ts b/modules/home/ags/app/notif-config.ts
new file mode 100644
index 0000000..0e93fea
--- /dev/null
+++ b/modules/home/ags/app/notif-config.ts
@@ -0,0 +1 @@
+export const MAX_TIMEOUT = 10000
diff --git a/modules/home/ags/app/style.scss b/modules/home/ags/app/style.scss
index cdee348..f8ced17 100644
--- a/modules/home/ags/app/style.scss
+++ b/modules/home/ags/app/style.scss
@@ -193,3 +193,66 @@
.power-row:hover {
background: transparentize($base05, 0.88);
}
+
+.notifications {
+ .notif {
+ background: $base01;
+ border-radius: 10px;
+ padding: 8px 12px;
+ color: $base05;
+
+ &:hover {
+ background: transparentize($base05, 0.88);
+ }
+
+ &.critical {
+ border: 1px solid $base08;
+ }
+ }
+
+ .notif-header {
+ margin-bottom: 4px;
+
+ .notif-app-name {
+ font-weight: bold;
+ margin: 0 6px;
+ }
+
+ .notif-time {
+ font-size: 0.85em;
+ opacity: 0.7;
+ margin-right: 4px;
+ }
+
+ .notif-close {
+ border-radius: 6px;
+ padding: 0 2px;
+ }
+ }
+
+ .notif-content {
+ .notif-image {
+ margin-right: 8px;
+ }
+
+ .notif-summary {
+ font-weight: bold;
+ }
+
+ .notif-text {
+ font-size: 0.9em;
+ opacity: 0.85;
+ }
+ }
+
+ .notif-action {
+ background: $base02;
+ border-radius: 6px;
+ margin-top: 6px;
+ padding: 2px 8px;
+
+ &:hover {
+ background: $base03;
+ }
+ }
+}
diff --git a/modules/home/ags/app/widget/Notifications.tsx b/modules/home/ags/app/widget/Notifications.tsx
new file mode 100644
index 0000000..a60281c
--- /dev/null
+++ b/modules/home/ags/app/widget/Notifications.tsx
@@ -0,0 +1,128 @@
+import GLib from "gi://GLib"
+import { App, Astal, Gdk, Gtk } from "astal/gtk3"
+import Notifd from "gi://AstalNotifd"
+import { bind } from "astal"
+import { MAX_TIMEOUT } from "../notif-config"
+
+const notifd = Notifd.get_default()
+
+notifd.default_timeout = MAX_TIMEOUT
+
+// notifications restored from the daemon's cache on startup are stale
+for (const n of notifd.notifications) {
+ n.dismiss()
+}
+
+// the daemon honors app-requested timeouts, so enforce our own cap for
+// notifications that ask for more (or none at all). critical notifications
+// are exempt and stay until dismissed.
+notifd.connect("notified", (_, id: number) => {
+ const n = notifd.get_notification(id)
+ if (!n || n.urgency === Notifd.Urgency.CRITICAL) return
+
+ if (n.expire_timeout <= 0 || n.expire_timeout > MAX_TIMEOUT) {
+ GLib.timeout_add(GLib.PRIORITY_DEFAULT, MAX_TIMEOUT, () => {
+ if (notifd.notifications.includes(n)) n.dismiss()
+ return GLib.SOURCE_REMOVE
+ })
+ }
+})
+
+function formatTime(n: Notifd.Notification) {
+ return GLib.DateTime.new_from_unix_local(n.time).format("%H:%M")!
+}
+
+function NotificationCard(n: Notifd.Notification) {
+ const critical = n.urgency === Notifd.Urgency.CRITICAL
+
+ return (
+
+ )
+}
+
+export default function Notifications(gdkmonitor: Gdk.Monitor) {
+ const { TOP, RIGHT } = Astal.WindowAnchor
+
+ return (
+ ns.length > 0)}
+ >
+
+ {bind(notifd, "notifications").as((ns) =>
+ [...ns]
+ .sort((a, b) => b.time - a.time)
+ .map((n) => NotificationCard(n)),
+ )}
+
+
+ )
+}
diff --git a/modules/home/ags/default.nix b/modules/home/ags/default.nix
index bebe858..dced158 100644
--- a/modules/home/ags/default.nix
+++ b/modules/home/ags/default.nix
@@ -39,6 +39,10 @@ let
)
);
+ notifConfig = pkgs.writeText "notif-config.ts" ''
+ export const MAX_TIMEOUT = ${toString cfg.timeout};
+ '';
+
astalDeps =
with pkgs.astal;
[
@@ -48,6 +52,7 @@ let
network
battery
wireplumber
+ notifd
]
++ [ pkgs.networkmanager ];
@@ -59,6 +64,7 @@ let
dependencies = astalDeps;
postUnpack = ''
cp ${palette} "$sourceRoot/palette.scss"
+ cp ${notifConfig} "$sourceRoot/notif-config.ts"
'';
};
in
diff --git a/modules/options/ags.nix b/modules/options/ags.nix
index 5e74ccd..fc119cf 100644
--- a/modules/options/ags.nix
+++ b/modules/options/ags.nix
@@ -9,5 +9,14 @@ with lib;
default = false;
description = "Enable the AGS status bar";
};
+
+ timeout = mkOption {
+ type = types.ints.positive;
+ default = 10000;
+ description = ''
+ Maximum duration notification popups stay on screen, in milliseconds.
+ Critical notifications are exempt and stay until dismissed.
+ '';
+ };
};
}