From f2bdddb9b6fa1ca1dce306336b9f91f3dc1733f4 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Sat, 26 Apr 2025 09:49:03 +0200 Subject: [PATCH] [AGS] Add new notifications setup --- README.md | 7 + .../components/notifications-opt/README.md | 6 + .../components/notifications-opt/main.tsx | 32 +++++ .../notifications-opt/modules/Icon.tsx | 25 ++++ .../modules/Notification.tsx | 126 ++++++++++++++++++ config/astal/util/notifd.ts | 83 ++++++++++++ 6 files changed, 279 insertions(+) create mode 100644 README.md create mode 100644 config/astal/components/notifications-opt/README.md create mode 100644 config/astal/components/notifications-opt/main.tsx create mode 100644 config/astal/components/notifications-opt/modules/Icon.tsx create mode 100644 config/astal/components/notifications-opt/modules/Notification.tsx create mode 100644 config/astal/util/notifd.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..236fce6 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# janishutz Hyprland + + +## Setting up to develop +Clone this repo locally. For `config/astal` and `config/ags`, you will want to run `ags types -d .` in every directory where there is a `app.ts` file as well as `mkdir node_modules && cd node_modules && ln -sf /usr/share/astal/gjs/ ./astal` to prepare for development. + +The `config/ags` directory contains gtk3 config for astal, whereas in `config/astal`, gtk4 configs can be found. All modules but for the notifications are written in Gtk 4. diff --git a/config/astal/components/notifications-opt/README.md b/config/astal/components/notifications-opt/README.md new file mode 100644 index 0000000..c61b718 --- /dev/null +++ b/config/astal/components/notifications-opt/README.md @@ -0,0 +1,6 @@ +# Source +This has been copied from [matshell](https://github.com/Neurarian/matshell) + +It is not yet used, as it has not been adapted yet to feature a notification history. + +Potentially, a notification centre will be added to make this here work better. Styling is also missing diff --git a/config/astal/components/notifications-opt/main.tsx b/config/astal/components/notifications-opt/main.tsx new file mode 100644 index 0000000..2b86cbb --- /dev/null +++ b/config/astal/components/notifications-opt/main.tsx @@ -0,0 +1,32 @@ +import { Astal } from "astal/gtk4"; +import Notifd from "gi://AstalNotifd"; +import Hyprland from "gi://AstalHyprland"; +import { bind } from "astal"; +import { NotificationWidget } from "./modules/Notification"; +import { hyprToGdk } from "../../util/hyprland"; + +export default function Notifications() { + const notifd = Notifd.get_default(); + const hyprland = Hyprland.get_default(); + const { TOP, RIGHT } = Astal.WindowAnchor; + + return ( + hyprToGdk(focused), + )} + anchor={TOP | RIGHT} + visible={bind(notifd, "notifications").as( + (notifications) => notifications.length > 0, + )} + child={ + + {bind(notifd, "notifications").as((notifications) => + notifications.map((n) => ), + )} + + } + /> + ); +} diff --git a/config/astal/components/notifications-opt/modules/Icon.tsx b/config/astal/components/notifications-opt/modules/Icon.tsx new file mode 100644 index 0000000..4ef4e45 --- /dev/null +++ b/config/astal/components/notifications-opt/modules/Icon.tsx @@ -0,0 +1,25 @@ +import { Gtk } from "astal/gtk4"; +import Notifd from "gi://AstalNotifd"; +import { fileExists, isIcon } from "../../../util/notifd"; + + +export function NotificationIcon(notification: Notifd.Notification) { + if (notification.image || notification.appIcon || notification.desktopEntry) { + const icon = notification.image || notification.appIcon || notification.desktopEntry; + if (fileExists(icon)) { + return ( + + + + ); + } else if (isIcon(icon)) { + return ( + + + + ); + } + } + return null; +} + diff --git a/config/astal/components/notifications-opt/modules/Notification.tsx b/config/astal/components/notifications-opt/modules/Notification.tsx new file mode 100644 index 0000000..b533ea3 --- /dev/null +++ b/config/astal/components/notifications-opt/modules/Notification.tsx @@ -0,0 +1,126 @@ +import { bind } from "astal"; +import { Gtk } from "astal/gtk4"; +import Notifd from "gi://AstalNotifd"; +import { NotificationIcon } from "./Icon"; +import { createTimeoutManager } from "../../../util/notifd"; + +export function NotificationWidget({ + notification, +}: { + notification: Notifd.Notification; +}) { + const { START, CENTER, END } = Gtk.Align; + const actions = notification.actions || []; + const TIMEOUT_DELAY = 3000; + + // Keep track of notification validity + const notifd = Notifd.get_default(); + const timeoutManager = createTimeoutManager( + () => notification.dismiss(), + TIMEOUT_DELAY, + ); + return ( + { + // Set up timeout + timeoutManager.setupTimeout(); + const clickGesture = Gtk.GestureClick.new(); + clickGesture.set_button(0); // 0 means any button + clickGesture.connect("pressed", (gesture, _) => { + try { + // Get which button was pressed (1=left, 2=middle, 3=right) + const button = gesture.get_current_button(); + + if (button === 1) { + // PRIMARY/LEFT + if (actions.length > 0) n.invoke(actions[0]); + } else if (button === 2) { + // MIDDLE + notifd.notifications?.forEach((n) => { + n.dismiss(); + }); + } else if (button === 3) { + // SECONDARY/RIGHT + notification.dismiss(); + } + } catch (error) { + console.error(error); + } + }); + self.add_controller(clickGesture); + + self.connect("unrealize", () => { + timeoutManager.cleanup(); + }); + }} + onHoverEnter={timeoutManager.handleHover} + onHoverLeave={timeoutManager.handleHoverLost} + vertical + vexpand={false} + cssClasses={["notification", `${urgency(notification)}`]} + name={notification.id.toString()} + > + + + + + + {NotificationIcon(notification)} + + + + + {actions.length > 0 && ( + + {actions.map(({ label, action }) => ( + + ))} + + )} + + ); +} diff --git a/config/astal/util/notifd.ts b/config/astal/util/notifd.ts new file mode 100644 index 0000000..55884ab --- /dev/null +++ b/config/astal/util/notifd.ts @@ -0,0 +1,83 @@ +// ┌ ┐ +// │ From https://github.com/Neurarian/matshell │ +// └ ┘ + +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);