/* * dotfiles - handler.ts * * Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License * https://janishutz.com, development@janishutz.com * * */ import { 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: number[] = []; const ShownNotificationsCount: Variable = Variable( 0 ); 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 ); } // ─────────────────────────────────────────────────────────────────── /** * 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 => { print( '[ Notifications ] Notification with id ' + id + ' added.' ); 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 ShownNotifications.reverse().push( id ); ShownNotifications.reverse(); ShownNotificationsCount.set( ShownNotifications.length ); // 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 ) { ShownNotifications.splice( ShownNotifications.indexOf( id ), 1 ); } } // ─────────────────────────────────────────────────────────────────── /** * Open the notification menu. Called by toggleNotificationMenu too */ const openNotificationMenu = () => { // Simply show all notifications notificationMenuOpen = true; const ShownNotifications = []; for (let index = 0; index < Notifications.length; index++) { ShownNotifications.push( index ); } } // ─────────────────────────────────────────────────────────────────── /** * Close the notification menu. Called by toggleNotificationMenu too */ const closeNotificationMenu = () => { // Hide all notifications notificationMenuOpen = true; ShownNotifications = []; ShownNotificationsCount.set( 0 ); } // ─────────────────────────────────────────────────────────────────── /** * Toggle the notification menu (i.e. show all notifications) */ const toggleNotificationMenu = () => { if ( notificationMenuOpen ) { closeNotificationMenu(); } else { openNotificationMenu(); } } // ─────────────────────────────────────────────────────────────────── /** * Delete all notifications */ const clearAllNotifications = () => { Notifications = []; ShownNotifications = []; ShownNotificationsCount.set( 0 ); } // ─────────────────────────────────────────────────────────────────── /** * Delete the newest notifications */ const clearNewestNotifications = () => { ShownNotifications.splice( 0, 1 ); Notifications.splice( Notifications.length - 1, 1 ); } // ─────────────────────────────────────────────────────────────────── // ╭───────────────────────────────────────────────╮ // │ User Interface │ // ╰───────────────────────────────────────────────╯ // ─────────────────────────────────────────────────────────────────── const startNotificationHandler = (gdkmonitor: Gdk.Monitor) => { const { TOP, RIGHT } = Astal.WindowAnchor hookToNotificationDaemon(); const range = (n: number) => [...Array(n).keys()]; return {ShownNotificationsCount( n => range( n ).map( i => { print( 'Rendering' ); // 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' ) { toggleNotificationMenu(); return 'Toggled notifications'; } else { return 'Unknown command!'; } } export default { startNotificationHandler, cliHandler }