/* * dotfiles - handler.ts * * Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License * https://janishutz.com, development@janishutz.com * * */ import { App, Astal, Gdk } from "astal/gtk4" import Notifd from "gi://AstalNotifd"; import Notification from "./notifications"; import { timeout, Variable } from "astal" // ─────────────────────────────────────────────────────────────────── // Config // ─────────────────────────────────────────────────────────────────── const TIMEOUT_DELAY = 5000; let isRunning = false; let notificationMenuOpen = false; interface NotificationDetails { notification: Notifd.Notification; backendID: number; notifdID: number; } // ─────────────────────────────────────────────────────────────────── // ╭───────────────────────────────────────────────╮ // │ Handler │ // ╰───────────────────────────────────────────────╯ // ─────────────────────────────────────────────────────────────────── let ShownNotifications: Variable = Variable( [] ); let Notifications: NotificationDetails[] = []; const notifd = Notifd.get_default(); notifd.ignoreTimeout = true; /** * Delete a notification by its internal ID * @param index The notifd ID of the notification */ const deleteNotification = ( index: number ): void => { hideNotification( index ); Notifications.splice( index, 1 ); if ( Notifications.length === 0 ) { notificationMenuOpen = false; } } // ─────────────────────────────────────────────────────────────────── /** * Delete a notification by notifd id * @param id The notifd ID of the notification */ const deleteNotificationByNotificationID = ( id: number ): void => { const index = findNotificationByNotificationID( id ); if ( index > -1 ) { deleteNotification( index ); } } // ─────────────────────────────────────────────────────────────────── /** * Find the internal ID from the notifd id for a notification (helper function) * @param id The notifd ID of the notification * @returns The internal ID or -1 if not found */ const findNotificationByNotificationID = ( id: number ): number => { // Find index in Notifications array for (let index = 0; index < Notifications.length; index++) { if ( Notifications[ index ].notifdID === id ) { return index; } } return -1; } // ─────────────────────────────────────────────────────────────────── /** * Add a notification to the notification handler * @param id The notifd ID of the notification */ const addNotification = ( id: number ): void => { const currIndex = Notifications.length; Notifications.push( { notifdID: id, backendID: currIndex, notification: notifd.get_notification( id ) } ); showNotification( currIndex ); } // ─────────────────────────────────────────────────────────────────── /** * Start the notifd runner and handle notifications. */ const hookToNotificationDaemon = (): void => { if ( isRunning ) { printerr( '[ Notifications ] Error: Already running' ); return; } isRunning = true; notifd.connect( 'notified', ( _, id ) => { addNotification( id ); } ); notifd.connect( 'resolved', ( _, id ) => { deleteNotificationByNotificationID( id ); } ); } // ─────────────────────────────────────────────────────────────────── /** * Show a notification. It will stay on screen (regardless of removeAgain passed in), if * critical urgency * @param id The internal id (index in Notifications array) * @param removeAgain = true If to remove the notification from the screen again automatically */ const showNotification = ( id: number, removeAgain: boolean = true ) => { // Add notification to UI for display const not = [...ShownNotifications.get()].reverse(); not.push( id ); ShownNotifications.set( not.reverse() ); // Set delay to remove the notification again if ( removeAgain && Notifications[ id ].notification.get_urgency() !== Notifd.Urgency.CRITICAL ) { timeout( TIMEOUT_DELAY, () => { hideNotification( id ); } ); } } // ─────────────────────────────────────────────────────────────────── /** * Stop displaying notification * @param id The internal id (index in the Notifications array) */ const hideNotification = ( id: number ) => { if ( !notificationMenuOpen ) { const not = [...ShownNotifications.get()]; not.splice( not.indexOf( id ), 1 ); ShownNotifications.set( not ); } } // ─────────────────────────────────────────────────────────────────── /** * Open the notification menu. Called by toggleNotificationMenu too */ const openNotificationMenu = () => { // Simply show all notifications notificationMenuOpen = true; const not = []; for (let index = 0; index < Notifications.length; index++) { not.push( index ); } ShownNotifications.set( not.reverse() ); } // ─────────────────────────────────────────────────────────────────── /** * Close the notification menu. Called by toggleNotificationMenu too */ const closeNotificationMenu = () => { // Hide all notifications notificationMenuOpen = true; ShownNotifications.set( [] ); } // ─────────────────────────────────────────────────────────────────── /** * Toggle the notification menu (i.e. show all notifications) */ const toggleNotificationMenu = (): string => { if ( notificationMenuOpen ) { closeNotificationMenu(); return 'Toggle notification menu closed'; } else { openNotificationMenu(); return 'Toggled notification menu open'; } } // ─────────────────────────────────────────────────────────────────── /** * Delete all notifications */ const clearAllNotifications = () => { Notifications = []; ShownNotifications.set( [] ); // TODO: Hiding for each individual deleteNotification notificationMenuOpen = false; } // ─────────────────────────────────────────────────────────────────── /** * Delete the newest notifications */ const clearNewestNotifications = () => { const not = [...ShownNotifications.get()]; not.splice( 0, 1 ); ShownNotifications.set( not ); Notifications.splice( Notifications.length - 1, 1 ); } // ─────────────────────────────────────────────────────────────────── // ╭───────────────────────────────────────────────╮ // │ User Interface │ // ╰───────────────────────────────────────────────╯ // ─────────────────────────────────────────────────────────────────── const startNotificationHandler = (gdkmonitor: Gdk.Monitor) => { const { TOP, RIGHT } = Astal.WindowAnchor hookToNotificationDaemon(); return list.length > 0 )} application={App}> {ShownNotifications( list => list.map( i => { // i is index in ShownNotifications array return } ) ) } } const cliHandler = ( args: string[] ): string => { if ( args[ 1 ] == 'show' ) { openNotificationMenu(); return 'Showing all open notifications'; } else if ( args[ 1 ] == 'hide' ) { closeNotificationMenu(); return 'Hid all notifications'; } else if ( args[ 1 ] == 'clear' ) { clearAllNotifications(); return 'Cleared all notifications'; } else if ( args[ 1 ] == 'clear-newest' ) { clearNewestNotifications(); return 'Cleared newest notification'; } else if ( args[ 1 ] == 'toggle' ) { return toggleNotificationMenu(); } else if ( args[ 1 ] == 'list' ){ if ( Notifications.length > 0 ) { let list = 'Currently unviewed notifications: '; for (let index = 0; index < Notifications.length; index++) { const element = Notifications[index]; list += `\n - (${element.notifdID}) ${element.notification.get_app_name()}: ${element.notification.get_summary()}`; } return list; } else { return 'No currently unviewed notifications' } } else { return 'Unknown command!'; } } export default { startNotificationHandler, cliHandler }