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
 | 
						|
}
 |