From e93f98d154b76e947761d65ad95394997946ea5d Mon Sep 17 00:00:00 2001 From: tux Date: Fri, 13 Jun 2025 23:12:11 +0530 Subject: [PATCH] feat: add notifications widget --- flake.nix | 2 + style.scss | 3 + widgets/notifications/icon.tsx | 21 +++++++ widgets/notifications/index.tsx | 29 ++++++++++ widgets/notifications/notifd.ts | 79 ++++++++++++++++++++++++++ widgets/notifications/notification.tsx | 53 +++++++++++++++++ widgets/notifications/style.scss | 30 ++++++++++ windows.ts | 3 +- 8 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 widgets/notifications/icon.tsx create mode 100644 widgets/notifications/index.tsx create mode 100644 widgets/notifications/notifd.ts create mode 100644 widgets/notifications/notification.tsx create mode 100644 widgets/notifications/style.scss 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} + > + + + {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];