[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