From e483d7de230da6a20147a0c679ea3cff76627790 Mon Sep 17 00:00:00 2001 From: Janis Hutz Date: Fri, 21 Mar 2025 18:23:44 +0100 Subject: [PATCH] Finish basic notification setup --- config/general/ags/notifications/.gitignore | 2 + config/general/ags/notifications/app.ts | 29 +++ config/general/ags/notifications/env.d.ts | 21 ++ config/general/ags/notifications/handler.ts | 20 -- config/general/ags/notifications/handler.tsx | 184 ++++++++++++++++++ .../ags/notifications/notifications.scss | 0 .../ags/notifications/notifications.tsx | 0 .../notifications/notifications.scss | 125 ++++++++++++ .../notifications/notifications.tsx | 112 +++++++++++ config/general/ags/notifications/package.json | 6 + config/general/ags/notifications/style.scss | 2 + .../general/ags/notifications/tsconfig.json | 14 ++ 12 files changed, 495 insertions(+), 20 deletions(-) create mode 100644 config/general/ags/notifications/.gitignore create mode 100644 config/general/ags/notifications/app.ts create mode 100644 config/general/ags/notifications/env.d.ts delete mode 100644 config/general/ags/notifications/handler.ts create mode 100644 config/general/ags/notifications/handler.tsx delete mode 100644 config/general/ags/notifications/notifications.scss delete mode 100644 config/general/ags/notifications/notifications.tsx create mode 100644 config/general/ags/notifications/notifications/notifications.scss create mode 100644 config/general/ags/notifications/notifications/notifications.tsx create mode 100644 config/general/ags/notifications/package.json create mode 100644 config/general/ags/notifications/style.scss create mode 100644 config/general/ags/notifications/tsconfig.json diff --git a/config/general/ags/notifications/.gitignore b/config/general/ags/notifications/.gitignore new file mode 100644 index 0000000..298eb4d --- /dev/null +++ b/config/general/ags/notifications/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +@girs/ diff --git a/config/general/ags/notifications/app.ts b/config/general/ags/notifications/app.ts new file mode 100644 index 0000000..548b260 --- /dev/null +++ b/config/general/ags/notifications/app.ts @@ -0,0 +1,29 @@ +import { App } from "astal/gtk3" +import style from "./style.scss" + +import not from "./handler" + +App.start({ + instanceName: "notifier", + css: style, + main() { + not.startNotificationHandler( 0, App.get_monitors()[0] ) + }, + requestHandler(request, res) { + if ( request == 'show' ) { + not.openNotificationMenu( 0 ); + res( 'Showing all open notifications' ); + } else if ( request == 'hide' ) { + not.closeNotificationMenu( 0 ); + res( 'Hid all notifications' ); + } else if ( request == 'clear' ) { + not.clearAllNotifications( 0 ); + res( 'Cleared all notifications' ); + } else if ( request == 'clear-newest' ) { + not.clearNewestNotifications( 0 ); + res( 'Cleared newest notification' ); + } else { + res( 'Unknown command!' ); + } + }, +}) diff --git a/config/general/ags/notifications/env.d.ts b/config/general/ags/notifications/env.d.ts new file mode 100644 index 0000000..467c0a4 --- /dev/null +++ b/config/general/ags/notifications/env.d.ts @@ -0,0 +1,21 @@ +declare const SRC: string + +declare module "inline:*" { + const content: string + export default content +} + +declare module "*.scss" { + const content: string + export default content +} + +declare module "*.blp" { + const content: string + export default content +} + +declare module "*.css" { + const content: string + export default content +} diff --git a/config/general/ags/notifications/handler.ts b/config/general/ags/notifications/handler.ts deleted file mode 100644 index 46ac4f5..0000000 --- a/config/general/ags/notifications/handler.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* -* dotfiles - handler.ts -* -* Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License -* https://janishutz.com, development@janishutz.com -* -* -*/ - -// Handle incoming notifications and keep a list that can be consumed by -// other parts of the astal setup - -import Notifd from "gi://AstalNotifd"; -const notifd = Notifd.get_default(); -const notifications: Notifd.Notification[] = []; - -notifd.connect( 'notified', ( _, id ) => { - -} ); - diff --git a/config/general/ags/notifications/handler.tsx b/config/general/ags/notifications/handler.tsx new file mode 100644 index 0000000..ee2ad42 --- /dev/null +++ b/config/general/ags/notifications/handler.tsx @@ -0,0 +1,184 @@ +/* +* dotfiles - handler.ts +* +* Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License +* https://janishutz.com, development@janishutz.com +* +* +*/ + +import { Astal, Gtk, Gdk } from "astal/gtk3" +import Notifd from "gi://AstalNotifd"; +import Notification from "./notifications/notifications"; +import { type Subscribable } from "astal/binding"; +import { Variable, bind, timeout } from "astal" + +// Config +const TIMEOUT_DELAY = 5000; + +class Notifier implements Subscribable { + private display: Map = new Map(); + private notifications: Map = new Map(); + + private notifd: Notifd.Notifd; + private subscriberData: Variable = Variable( [] ); + private instanceID: number; + private menuOpen: boolean; + + /** + * Sets up the notifier + */ + constructor( id: number ) { + this.instanceID = id; + this.menuOpen = false; + this.notifd = Notifd.get_default(); + this.notifd.ignoreTimeout = true; + + this.notifd.connect( 'notified', ( _, id ) => { + this.add( id ); + } ); + + this.notifd.connect( 'resolved', ( _, id ) => { + this.hide( id ); + } ); + } + + private notify () { + this.subscriberData.set( [ ...this.display.values() ].reverse() ); + } + + private add ( id: number ) { + this.notifications.set( id, this.notifd.get_notification( id )! ); + this.show( id ); + } + + /** + * Show an element on screen + * @param id - The id of the element to be shown + */ + private show ( id: number ) { + this.set( id, Notification( { + notification: this.notifications.get( id )!, + onHoverLost: () => this.hide( id ), + setup: () => timeout( TIMEOUT_DELAY, () => { + if ( !this.menuOpen ) { + this.hide( id ); + } + } ), + id: id, + delete: deleteHelper, + instanceID: this.instanceID + } ) ) + } + + /** + * Set a selected widget to be shown + * @param id - The id of the element to be referenced for later + * @param widget - A GTK widget instance + */ + private set ( id: number, widget: Gtk.Widget ) { + this.display.get( id )?.destroy(); + this.display.set( id, widget ); + this.notify(); + } + + /** + * Hide, not delete notification (= send to notification centre) + * @param id - The id of the notification to hide + */ + private hide ( id: number ) { + this.display.get( id )?.destroy(); + this.display.delete( id ); + this.notify(); + } + + /** + * Delete a notification (from notification centre too) + * @param id - The id of the notification to hide + */ + delete ( id: number ) { + this.hide( id ); + this.notifications.get( id )?.dismiss(); + this.notifications.delete( id ); + } + + openNotificationMenu () { + // Show all notifications that have not been cleared + this.menuOpen = true; + this.notifications.forEach( ( _, id ) => { + this.show( id ); + } ) + } + + hideNotifications () { + this.menuOpen = false; + this.notifications.forEach( ( _, id ) => { + this.hide( id ); + } ) + } + + clearAllNotifications () { + this.menuOpen = false; + this.notifications.forEach( ( _, id ) => { + this.delete( id ); + } ) + } + + clearNewestNotification () { + this.delete( [ ...this.notifications.keys() ][0] ); + } + + subscribe(callback: (value: unknown) => void): () => void { + return this.subscriberData.subscribe( callback ); + } + + get() { + return this.subscriberData.get(); + } +} + +const notifiers: Map = new Map(); +const deleteHelper = ( id: number, instanceID: number ) => { + notifiers.get( instanceID )?.delete( id ); +} + +const openNotificationMenu = ( id: number ) => { + notifiers.get( id )?.openNotificationMenu(); +} + +const closeNotificationMenu = ( id: number ) => { + notifiers.get( id )?.hideNotifications(); +} + +const clearAllNotifications = ( id: number ) => { + notifiers.get( id )?.clearAllNotifications(); +} + +const clearNewestNotifications = ( id: number ) => { + notifiers.get( id )?.clearNewestNotification(); +} + +const startNotificationHandler = (id: number, gdkmonitor: Gdk.Monitor) => { + const { TOP, RIGHT } = Astal.WindowAnchor + const notifier: Notifier = new Notifier( id ); + notifiers.set( id, notifier ); + + return + + {bind(notifier)} + + +} + + +export default { + startNotificationHandler, + openNotificationMenu, + closeNotificationMenu, + clearAllNotifications, + clearNewestNotifications +} diff --git a/config/general/ags/notifications/notifications.scss b/config/general/ags/notifications/notifications.scss deleted file mode 100644 index e69de29..0000000 diff --git a/config/general/ags/notifications/notifications.tsx b/config/general/ags/notifications/notifications.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/config/general/ags/notifications/notifications/notifications.scss b/config/general/ags/notifications/notifications/notifications.scss new file mode 100644 index 0000000..a32f08b --- /dev/null +++ b/config/general/ags/notifications/notifications/notifications.scss @@ -0,0 +1,125 @@ +@use "sass:string"; + +@function gtkalpha($c, $a) { + @return string.unquote("alpha(#{$c},#{$a})"); +} + +// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss +$fg-color: #{"@theme_fg_color"}; +$bg-color: #{"@theme_bg_color"}; +$error: red; + +window.NotificationPopups { + all: unset; +} + +eventbox.Notification { + + &:first-child>box { + margin-top: 1rem; + } + + &:last-child>box { + margin-bottom: 1rem; + } + + // eventboxes can not take margins so we style its inner box instead + >box { + min-width: 400px; + border-radius: 13px; + background-color: $bg-color; + margin: .5rem 1rem .5rem 1rem; + box-shadow: 2px 3px 8px 0 gtkalpha(black, .4); + border: 1pt solid gtkalpha($fg-color, .03); + } + + &.critical>box { + border: 1pt solid gtkalpha($error, .4); + + .header { + + .app-name { + color: gtkalpha($error, .8); + + } + + .app-icon { + color: gtkalpha($error, .6); + } + } + } + + .header { + padding: .5rem; + color: gtkalpha($fg-color, 0.5); + + .app-icon { + margin: 0 .4rem; + } + + .app-name { + margin-right: .3rem; + font-weight: bold; + + &:first-child { + margin-left: .4rem; + } + } + + .time { + margin: 0 .4rem; + } + + button { + padding: .2rem; + min-width: 0; + min-height: 0; + } + } + + separator { + margin: 0 .4rem; + background-color: gtkalpha($fg-color, .1); + } + + .content { + margin: 1rem; + margin-top: .5rem; + + .summary { + font-size: 1.2em; + color: $fg-color; + } + + .body { + color: gtkalpha($fg-color, 0.8); + } + + .image { + border: 1px solid gtkalpha($fg-color, .02); + margin-right: .5rem; + border-radius: 9px; + min-width: 100px; + min-height: 100px; + background-size: cover; + background-position: center; + } + } + + .actions { + margin: 1rem; + margin-top: 0; + + button { + margin: 0 .3rem; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } +} diff --git a/config/general/ags/notifications/notifications/notifications.tsx b/config/general/ags/notifications/notifications/notifications.tsx new file mode 100644 index 0000000..de4e7b2 --- /dev/null +++ b/config/general/ags/notifications/notifications/notifications.tsx @@ -0,0 +1,112 @@ +// From astal examples + +import { GLib } from "astal" +import { Gtk, Astal } from "astal/gtk3" +import { type EventBox } from "astal/gtk3/widget" +import Notifd from "gi://AstalNotifd" + +const isIcon = (icon: string) => + !!Astal.Icon.lookup_icon(icon) + +const fileExists = (path: string) => + GLib.file_test(path, GLib.FileTest.EXISTS) + +const time = (time: number, format = "%H:%M") => GLib.DateTime + .new_from_unix_local(time) + .format(format)! + +const urgency = (n: Notifd.Notification) => { + const { LOW, NORMAL, CRITICAL } = Notifd.Urgency + // match operator when? + switch (n.urgency) { + case LOW: return "low" + case CRITICAL: return "critical" + case NORMAL: + default: return "normal" + } +} + +type Props = { + delete( id: number, instanceID: number ): void + setup(self: EventBox): void + onHoverLost(self: EventBox): void + notification: Notifd.Notification + id: number + instanceID: number +} + +export default function Notification(props: Props) { + const { notification: n, onHoverLost, setup, id: id, delete: del, instanceID: instance } = props + const { START, CENTER, END } = Gtk.Align + + return + + + {(n.appIcon || n.desktopEntry) && } + + + + {n.image && fileExists(n.image) && } + {n.image && isIcon(n.image) && + + } + + + + {n.get_actions().length > 0 && + {n.get_actions().map(({ label, id }) => ( + + ))} + } + + +} diff --git a/config/general/ags/notifications/package.json b/config/general/ags/notifications/package.json new file mode 100644 index 0000000..44226f2 --- /dev/null +++ b/config/general/ags/notifications/package.json @@ -0,0 +1,6 @@ +{ + "name": "astal-shell", + "dependencies": { + "astal": "/usr/share/astal/gjs" + } +} diff --git a/config/general/ags/notifications/style.scss b/config/general/ags/notifications/style.scss new file mode 100644 index 0000000..a1c9bb7 --- /dev/null +++ b/config/general/ags/notifications/style.scss @@ -0,0 +1,2 @@ +// Import notification box style +@use "./notifications/notifications.scss" diff --git a/config/general/ags/notifications/tsconfig.json b/config/general/ags/notifications/tsconfig.json new file mode 100644 index 0000000..9471e35 --- /dev/null +++ b/config/general/ags/notifications/tsconfig.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "experimentalDecorators": true, + "strict": true, + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + // "checkJs": true, + // "allowJs": true, + "jsx": "react-jsx", + "jsxImportSource": "astal/gtk3", + } +}