[Build] Start refactor

This commit is contained in:
2026-02-02 16:01:56 +01:00
parent c38907ec39
commit 10a5c775be
561 changed files with 1936094 additions and 13878 deletions

View File

@@ -0,0 +1,299 @@
/*
* 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
}

View File

@@ -0,0 +1,12 @@
import { Gtk, Gdk } from "astal/gtk4";
import { GLib } from "astal";
export const isIcon = (icon: string) => {
const display = Gdk.Display.get_default();
if (!display) return false;
const iconTheme = Gtk.IconTheme.get_for_display(display);
return iconTheme.has_icon(icon);
};
export const fileExists = (path: string) =>
GLib.file_test(path, GLib.FileTest.EXISTS);

View File

@@ -0,0 +1,24 @@
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { fileExists, isIcon } from "./helper";
export function NotificationIcon(notification: Notifd.Notification) {
if ( notification.image || notification.appIcon || notification.desktopEntry) {
const icon = notification.image || notification.appIcon || notification.desktopEntry;
if (fileExists(icon)) {
return (
<box expand={false} valign={Gtk.Align.CENTER}>
<image file={icon} />
</box>
);
} else if (isIcon(icon)) {
return (
<box expand={false} valign={Gtk.Align.CENTER}>
<image iconName={icon} />
</box>
);
}
}
return null;
}

View File

@@ -0,0 +1,124 @@
@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.NotificationHandler {
all: unset;
}
box.notification {
&:first-child {
margin-top: 1rem;
}
&:last-child {
margin-bottom: 1rem;
}
& {
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 {
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;
}
}
}
}

View File

@@ -0,0 +1,113 @@
// From astal examples
import { bind, GLib } from "astal";
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { NotificationIcon } from "./icon";
// import Pango from "gi://Pango?version=1.0"
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) => void;
notification: Notifd.Notification;
id: number;
};
export default function Notification(props: Props) {
const { notification: n, id: id, delete: del } = props;
const { START, CENTER, END } = Gtk.Align;
return (
<box vertical cssClasses={["notification", `${urgency(n)}`]}>
<box cssClasses={["header"]}>
{n.appIcon || n.desktopEntry ? (
<Gtk.Image
cssClasses={["app-icon"]}
visible={Boolean(n.appIcon || n.desktopEntry)}
iconName={n.appIcon || n.desktopEntry}
/>
) : (
<image iconName={"window-close-symbolic"}></image>
)}
<label
cssClasses={["app-name"]}
halign={START}
// ellipsize={Pango.EllipsizeMode.END}
label={n.appName || "Unknown"}
/>
<label
cssClasses={["time"]}
hexpand
halign={END}
label={time(n.time)}
/>
<button
onClicked={() => {
del(id);
}}
child={<image iconName="window-close-symbolic" />}
></button>
</box>
<Gtk.Separator visible />
<box cssClasses={["content"]}>
<box
cssClasses={["image"]}
visible={Boolean(NotificationIcon(n))}
halign={CENTER}
valign={CENTER}
vexpand={true}
>
{NotificationIcon(n)}
</box>
<box vertical>
<label
cssClasses={["summary"]}
halign={START}
xalign={0}
useMarkup
label={bind(n, "summary")}
// ellipsize={Pango.EllipsizeMode.END}
/>
{n.body && (
<label
cssClasses={["body"]}
valign={CENTER}
wrap={true}
maxWidthChars={50}
label={bind(n, "body")}
/>
)}
</box>
</box>
{n.get_actions().length > 0 ? (
<box cssClasses={["actions"]}>
{n.get_actions().map(({ label, id }) => (
<button hexpand onClicked={() => n.invoke(id)}>
<label label={label} halign={CENTER} hexpand />
</button>
))}
</box>
) : (
<box></box>
)}
</box>
);
}