[AGS] Add new notifications setup
This commit is contained in:
parent
e19a1179d5
commit
f2bdddb9b6
7
README.md
Normal file
7
README.md
Normal 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.
|
6
config/astal/components/notifications-opt/README.md
Normal file
6
config/astal/components/notifications-opt/README.md
Normal 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
|
32
config/astal/components/notifications-opt/main.tsx
Normal file
32
config/astal/components/notifications-opt/main.tsx
Normal 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>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
25
config/astal/components/notifications-opt/modules/Icon.tsx
Normal file
25
config/astal/components/notifications-opt/modules/Icon.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
83
config/astal/util/notifd.ts
Normal file
83
config/astal/util/notifd.ts
Normal 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);
|
Loading…
x
Reference in New Issue
Block a user