feat: migrate to ags v3

This commit is contained in:
tux
2025-09-24 16:39:34 +05:30
parent 6437cad620
commit 9649ab0b6e
26 changed files with 330 additions and 451 deletions

View File

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

View File

@@ -1,28 +1,49 @@
import { Astal, Gdk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { bind } from "astal";
import { NotificationWidget } from "./notification";
import { For, createState, onCleanup } from "ags";
import { Astal, Gdk, Gtk } from "ags/gtk4";
import AstalNotifd from "gi://AstalNotifd";
import { Notification } from "./notification";
export const WINDOW_NAME = "notifications";
export const Notifications = (gdkmonitor: Gdk.Monitor) => {
const notifd = Notifd.get_default();
const notifd = AstalNotifd.get_default();
const { TOP, RIGHT } = Astal.WindowAnchor;
const [notifications, setNotifications] = createState(
new Array<AstalNotifd.Notification>(),
);
const notifiedHandler = notifd.connect("notified", (_, id, replaced) => {
const notification = notifd.get_notification(id);
if (replaced && notifications.get().some((n) => n.id === id)) {
setNotifications((ns) => ns.map((n) => (n.id === id ? notification : n)));
} else {
setNotifications((ns) => [notification, ...ns]);
}
});
const resolvedHandler = notifd.connect("resolved", (_, id) => {
setNotifications((ns) => ns.filter((n) => n.id !== id));
});
onCleanup(() => {
notifd.disconnect(notifiedHandler);
notifd.disconnect(resolvedHandler);
});
return (
<window
$={(self) => onCleanup(() => self.destroy())}
name={WINDOW_NAME}
cssClasses={["notifications"]}
gdkmonitor={gdkmonitor}
visible={notifications((ns) => ns.length > 0)}
anchor={TOP | RIGHT}
visible={bind(notifd, "notifications").as(
(notifications) => notifications.length > 0,
)}
>
<box vertical={true} spacing={10}>
{bind(notifd, "notifications").as((notifications) =>
notifications.map((n) => <NotificationWidget n={n} />),
)}
<box orientation={Gtk.Orientation.VERTICAL}>
<For each={notifications}>{(n) => <Notification n={n} />}</For>
</box>
</window>
);

View File

@@ -1,79 +0,0 @@
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);

View File

@@ -1,53 +1,112 @@
import { bind } from "astal";
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { urgency, createTimeoutManager } from "./notifd";
import { Gdk, Gtk } from "ags/gtk4";
import Adw from "gi://Adw";
import AstalNotifd from "gi://AstalNotifd";
import GLib from "gi://GLib";
import Pango from "gi://Pango";
export const NotificationWidget = ({ n }: { n: Notifd.Notification }) => {
const { START, CENTER } = Gtk.Align;
const actions = n.actions || [];
const TIMEOUT_DELAY = 3000;
const isIcon = (icon?: string | null) => {
const iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!);
return icon && iconTheme.has_icon(icon);
};
// Keep track of notification validity
const timeoutManager = createTimeoutManager(() => n.dismiss(), TIMEOUT_DELAY);
const fileExists = (path: string) => {
return GLib.file_test(path, GLib.FileTest.EXISTS);
};
const time = (time: number, format = "%H:%M") => {
return GLib.DateTime.new_from_unix_local(time).format(format)!;
};
const urgency = (n: AstalNotifd.Notification) => {
const { LOW, NORMAL, CRITICAL } = AstalNotifd.Urgency;
switch (n.urgency) {
case LOW:
return "low";
case CRITICAL:
return "critical";
case NORMAL:
default:
return "normal";
}
};
export const Notification = ({ n }: { n: AstalNotifd.Notification }) => {
return (
<box
setup={(self) => {
// Set up timeout
timeoutManager.setupTimeout();
self.connect("unrealize", () => {
timeoutManager.cleanup();
});
}}
cssClasses={["notification", `${urgency(n)}`]}
name={n.id.toString()}
spacing={10}
>
<box vertical cssClasses={["text"]} spacing={10}>
<label
cssClasses={["title"]}
label={bind(n, "summary")}
halign={START}
/>
{n.body && (
<label cssClasses={["body"]} label={bind(n, "body")} halign={START} />
<Adw.Clamp maximumSize={400}>
<box
widthRequest={400}
class={`notification ${urgency(n)}`}
orientation={Gtk.Orientation.VERTICAL}
>
<box class="header">
{(n.appIcon || isIcon(n.desktopEntry)) && (
<image
class="app-icon"
visible={Boolean(n.appIcon || n.desktopEntry)}
iconName={n.appIcon || n.desktopEntry}
/>
)}
<label
class="app-name"
halign={Gtk.Align.START}
ellipsize={Pango.EllipsizeMode.END}
label={n.appName || "Unknown"}
/>
<label
class="time"
hexpand
halign={Gtk.Align.END}
label={time(n.time)}
/>
<button onClicked={() => n.dismiss()}>
<image iconName="window-close-symbolic" />
</button>
</box>
<Gtk.Separator visible />
<box class="content">
{n.image && fileExists(n.image) && (
<image valign={Gtk.Align.START} class="image" file={n.image} />
)}
{n.image && isIcon(n.image) && (
<box valign={Gtk.Align.START} class="icon-image">
<image
iconName={n.image}
halign={Gtk.Align.CENTER}
valign={Gtk.Align.CENTER}
/>
</box>
)}
<box orientation={Gtk.Orientation.VERTICAL}>
<label
class="summary"
halign={Gtk.Align.START}
xalign={0}
label={n.summary}
ellipsize={Pango.EllipsizeMode.END}
/>
{n.body && (
<label
class="body"
wrap
useMarkup
halign={Gtk.Align.START}
xalign={0}
justify={Gtk.Justification.FILL}
label={n.body}
/>
)}
</box>
</box>
{n.actions.length > 0 && (
<box class="actions">
{n.actions.map(({ label, id }) => (
<button hexpand onClicked={() => n.invoke(id)}>
<label label={label} halign={Gtk.Align.CENTER} hexpand />
</button>
))}
</box>
)}
</box>
{actions.length > 0 && (
<box cssClasses={["actions"]}>
{actions.map(({ label, id }) => (
<button
hexpand
cssClasses={["action-button"]}
onClicked={() => n.invoke(id)}
>
<label label={label} halign={CENTER} hexpand />
</button>
))}
</box>
)}
</box>
</Adw.Clamp>
);
};