[AGS] Add new notifications setup

This commit is contained in:
Janis Hutz 2025-04-26 09:49:03 +02:00
parent e19a1179d5
commit f2bdddb9b6
6 changed files with 279 additions and 0 deletions

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# janishutz Hyprland
## Setting up to develop
Clone this repo locally. For `config/astal` and `config/ags`, you will want to run `ags types -d .` in every directory where there is a `app.ts` file as well as `mkdir node_modules && cd node_modules && ln -sf /usr/share/astal/gjs/ ./astal` to prepare for development.
The `config/ags` directory contains gtk3 config for astal, whereas in `config/astal`, gtk4 configs can be found. All modules but for the notifications are written in Gtk 4.

View File

@ -0,0 +1,6 @@
# Source
This has been copied from [matshell](https://github.com/Neurarian/matshell)
It is not yet used, as it has not been adapted yet to feature a notification history.
Potentially, a notification centre will be added to make this here work better. Styling is also missing

View File

@ -0,0 +1,32 @@
import { Astal } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import Hyprland from "gi://AstalHyprland";
import { bind } from "astal";
import { NotificationWidget } from "./modules/Notification";
import { hyprToGdk } from "../../util/hyprland";
export default function Notifications() {
const notifd = Notifd.get_default();
const hyprland = Hyprland.get_default();
const { TOP, RIGHT } = Astal.WindowAnchor;
return (
<window
name="notifications"
gdkmonitor={bind(hyprland, "focusedMonitor").as(
(focused: Hyprland.Monitor) => hyprToGdk(focused),
)}
anchor={TOP | RIGHT}
visible={bind(notifd, "notifications").as(
(notifications) => notifications.length > 0,
)}
child={
<box vertical={true} cssClasses={["notifications"]}>
{bind(notifd, "notifications").as((notifications) =>
notifications.map((n) => <NotificationWidget notification={n} />),
)}
</box>
}
/>
);
}

View File

@ -0,0 +1,25 @@
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { fileExists, isIcon } from "../../../util/notifd";
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,126 @@
import { bind } from "astal";
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { NotificationIcon } from "./Icon";
import { createTimeoutManager } from "../../../util/notifd";
export function NotificationWidget({
notification,
}: {
notification: Notifd.Notification;
}) {
const { START, CENTER, END } = Gtk.Align;
const actions = notification.actions || [];
const TIMEOUT_DELAY = 3000;
// Keep track of notification validity
const notifd = Notifd.get_default();
const timeoutManager = createTimeoutManager(
() => notification.dismiss(),
TIMEOUT_DELAY,
);
return (
<box
setup={(self) => {
// Set up timeout
timeoutManager.setupTimeout();
const clickGesture = Gtk.GestureClick.new();
clickGesture.set_button(0); // 0 means any button
clickGesture.connect("pressed", (gesture, _) => {
try {
// Get which button was pressed (1=left, 2=middle, 3=right)
const button = gesture.get_current_button();
if (button === 1) {
// PRIMARY/LEFT
if (actions.length > 0) n.invoke(actions[0]);
} else if (button === 2) {
// MIDDLE
notifd.notifications?.forEach((n) => {
n.dismiss();
});
} else if (button === 3) {
// SECONDARY/RIGHT
notification.dismiss();
}
} catch (error) {
console.error(error);
}
});
self.add_controller(clickGesture);
self.connect("unrealize", () => {
timeoutManager.cleanup();
});
}}
onHoverEnter={timeoutManager.handleHover}
onHoverLeave={timeoutManager.handleHoverLost}
vertical
vexpand={false}
cssClasses={["notification", `${urgency(notification)}`]}
name={notification.id.toString()}
>
<box cssClasses={["header"]}>
<label
cssClasses={["app-name"]}
halign={CENTER}
label={bind(notification, "app_name")}
/>
<label
cssClasses={["time"]}
hexpand
halign={END}
label={time(notification.time)}
/>
</box>
<Gtk.Separator />
<box cssClasses={["content"]}>
<box
cssClasses={["thumb"]}
visible={Boolean(NotificationIcon(notification))}
halign={CENTER}
valign={CENTER}
vexpand={true}
>
{NotificationIcon(notification)}
</box>
<box
vertical
cssClasses={["text-content"]}
hexpand={true}
halign={CENTER}
valign={CENTER}
>
<label
cssClasses={["title"]}
valign={CENTER}
wrap={false}
label={bind(notification, "summary")}
/>
{notification.body && (
<label
cssClasses={["body"]}
valign={CENTER}
wrap={true}
maxWidthChars={50}
label={bind(notification, "body")}
/>
)}
</box>
</box>
{actions.length > 0 && (
<box cssClasses={["actions"]}>
{actions.map(({ label, action }) => (
<button
hexpand
cssClasses={["action-button"]}
onClicked={() => notification.invoke(action)}
>
<label label={label} halign={CENTER} hexpand />
</button>
))}
</box>
)}
</box>
);
}

View File

@ -0,0 +1,83 @@
// ┌ ┐
// │ From https://github.com/Neurarian/matshell │
// └ ┘
import Notifd from "gi://AstalNotifd";
import { GLib } from "astal";
import { Gtk, Gdk } from "astal/gtk4";
type TimeoutManager = {
setupTimeout: () => void;
clearTimeout: () => void;
handleHover: () => void;
handleHoverLost: () => void;
cleanup: () => void;
};
export const createTimeoutManager = (
dismissCallback: () => void,
timeoutDelay: number,
): TimeoutManager => {
let isHovered = false;
let timeoutId: number | null = null;
const clearTimeout = () => {
if (timeoutId !== null) {
GLib.source_remove(timeoutId);
timeoutId = null;
}
};
const setupTimeout = () => {
clearTimeout();
if (!isHovered) {
timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeoutDelay, () => {
clearTimeout();
dismissCallback();
return GLib.SOURCE_REMOVE;
});
}
};
return {
setupTimeout,
clearTimeout,
handleHover: () => {
isHovered = true;
clearTimeout();
},
handleHoverLost: () => {
isHovered = false;
setupTimeout();
},
cleanup: clearTimeout,
};
};
export const time = (time: number, format = "%H:%M") =>
GLib.DateTime.new_from_unix_local(time).format(format)!;
export const urgency = (notification: Notifd.Notification) => {
const { LOW, NORMAL, CRITICAL } = Notifd.Urgency;
switch (notification.urgency) {
case LOW:
return "low";
case CRITICAL:
return "critical";
case NORMAL:
default:
return "normal";
}
};
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);