300 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			300 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /*
 | |
| *               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<number[]> = 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 <window
 | |
|         cssClasses={["NotificationHandler"]}
 | |
|         gdkmonitor={gdkmonitor}
 | |
|         exclusivity={Astal.Exclusivity.EXCLUSIVE}
 | |
|         anchor={TOP | RIGHT}
 | |
|         visible={ShownNotifications( list => list.length > 0 )}
 | |
|         application={App}>
 | |
|         <box vertical>
 | |
|             {ShownNotifications( list => list.map( i => {
 | |
|                 // i is index in ShownNotifications array
 | |
|                 return <Notification id={i} delete={deleteNotification} notification={Notifications[ i ].notification}></Notification>
 | |
|             } ) ) }
 | |
|         </box>
 | |
|     </window>
 | |
| }
 | |
| 
 | |
| 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
 | |
| }
 |