diff options
author | 2025-03-18 16:47:58 +0300 | |
---|---|---|
committer | 2025-03-18 16:50:29 +0300 | |
commit | 63742be30723555ab54c7b11136dfaf6f83d0c2e (patch) | |
tree | 2abb956786f56eb41bc37fac464448fc53225de1 /widget/notifications | |
parent | feat: dynamic workspace updating (diff) | |
download | ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.gz ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.bz2 ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.lz ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.xz ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.tar.zst ags-config-63742be30723555ab54c7b11136dfaf6f83d0c2e.zip |
feat: add notifications widget
Diffstat (limited to '')
-rw-r--r-- | widget/notifications/Notifications.scss | 141 | ||||
-rw-r--r-- | widget/notifications/Notifications.tsx | 108 |
2 files changed, 249 insertions, 0 deletions
diff --git a/widget/notifications/Notifications.scss b/widget/notifications/Notifications.scss new file mode 100644 index 0000000..5762df1 --- /dev/null +++ b/widget/notifications/Notifications.scss @@ -0,0 +1,141 @@ +$rosewater: #f5e0dc; +$flamingo: #f2cdcd; +$pink: #f5c2e7; +$mauve: #cba6f7; +$red: #f38ba8; +$maroon: #eba0ac; +$peach: #fab387; +$yellow: #f9e2af; +$green: #a6e3a1; +$teal: #94e2d5; +$sky: #89dceb; +$sapphire: #74c7ec; +$blue: #89b4fa; +$lavender: #b4befe; + +$text: #cdd6f4; +$subtext1: #bac2de; +$subtext0: #a6adc8; + +$overlay2: #9399b2; +$overlay1: #7f849c; +$overlay0: #6c7086; + +$surface2: #585b70; +$surface1: #45475a; +$surface0: #313244; + +$base: #1e1e2e; +$mantle: #181825; +$crust: #11111b; + +window.Notifications { + box.Notification { + margin: .25rem 1rem; + border: 1px solid $blue; + border-radius: 5px; + min-width: 300px; + + background-color: $base; + + separator { + background: $blue; + } + + &.low { + border-color: $overlay1; + + separator { + background: $overlay1; + } + } + + &.critical { + border-color: $red; + + separator { + background: $red; + } + } + + &:first-child { + margin-top: 1rem; + } + + &:last-child { + margin-bottom: 1rem; + } + + box.Header { + label.Application { + margin-top: .25rem; + margin-left: .5rem; + + color: $text; + + font-weight: bold; + } + + label.Time { + margin: .25rem .25rem 0 1rem; + + color: $subtext1; + } + + button.Close { + margin: 0; + border: 0; + border-radius: 0; + padding: 5px; + min-width: 0; + min-height: 0; + + background: none; + + image { + background-color: $red; + border-radius: 50%; + color: $red; + } + } + } + + box.Contents { + margin: .5rem; + + label.Summary { + color: $text; + + font-weight: bold; + } + + label.Body { + color: $subtext1; + } + } + + box.Actions { + button { + margin: 0 .25rem; + border: 0; + border-radius: 0; + padding: 5px; + min-width: 0; + min-height: 0; + + background: none; + color: $text; + + font-weight: bold; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + } + } +} diff --git a/widget/notifications/Notifications.tsx b/widget/notifications/Notifications.tsx new file mode 100644 index 0000000..6708b96 --- /dev/null +++ b/widget/notifications/Notifications.tsx @@ -0,0 +1,108 @@ +import { GLib, Variable } from "astal"; +import { bind, Subscribable } from "astal/binding"; +import { Astal, Gdk, Gtk } from "astal/gtk4"; +import AstalNotifd from "gi://AstalNotifd?version=0.1"; + +const notificationTimeout: number = 5000; + +class NotificationHandler implements Subscribable<Gtk.Widget[]> { + private notifications: Variable<Gtk.Widget[]> = Variable([]); + private notificationsMap: Map<number, Gtk.Widget> = new Map(); + + constructor() { + const notifd = AstalNotifd.get_default(); + + notifd.connect("notified", (_source, id, _replaced) => { + const n = notifd.get_notification(id); + this.create(n); + setTimeout(() => this.remove(id), notificationTimeout); + }); + + notifd.connect("resolved", (_source, id, _reason) => { + this.remove(id); + }); + } + + private rerender() { + this.notifications.set([...this.notificationsMap.values()].reverse()); + } + + private create(n: AstalNotifd.Notification) { + const notification = Notification(n); + this.notificationsMap.get(n.id)?.emit("destroy"); + this.notificationsMap.set(n.id, notification); + this.rerender(); + } + + private remove(id: number) { + this.notificationsMap.get(id)?.emit("destroy"); + this.notificationsMap.delete(id); + this.rerender(); + } + + subscribe(callback: (value: Gtk.Widget[]) => void): () => void { + return this.notifications.subscribe(callback); + } + + get(): Gtk.Widget[] { + return this.notifications.get(); + } +} + +function getUrgencyClass(n: AstalNotifd.Notification): string { + switch (n.urgency) { + case AstalNotifd.Urgency.LOW: + return "low"; + case AstalNotifd.Urgency.CRITICAL: + return "critical"; + case AstalNotifd.Urgency.NORMAL: + default: + return "normal"; + } +} + +function Notification(n: AstalNotifd.Notification) { + const appName = n.appName; + const time = GLib.DateTime.new_from_unix_local(n.time); + + return <box cssClasses={["Notification", getUrgencyClass(n)]} vertical> + <box cssClasses={["Header"]}> + <label cssClasses={["Application"]} halign={Gtk.Align.START} label={appName || "Unknown"} /> + <label cssClasses={["Time"]} hexpand halign={Gtk.Align.END} label={time.format("%I:%M:%S %p")!} /> + <button cssClasses={["Close"]} onClicked={() => n.dismiss()}><image iconName="window-close-symbolic" /></button> + </box> + <Gtk.Separator visible /> + <box cssClasses={["Contents"]}> + <box vertical> + <label cssClasses={["Summary"]} halign={Gtk.Align.START} label={n.summary} /> + <label cssClasses={["Body"]} useMarkup wrap maxWidthChars={0} justify={Gtk.Justification.FILL} label={n.body} /> + </box> + </box> + + {n.get_actions().length > 0 && <> + <Gtk.Separator visible /> + <box cssClasses={["Actions"]}> + {n.get_actions().map(({ label, id }) => <button hexpand onClicked={() => n.invoke(id)}><label label={label} /></button>)} + </box> + </>} + </box>; +} + +export default function(gdkmonitor: Gdk.Monitor) { + const { TOP, RIGHT } = Astal.WindowAnchor; + const notifications = new NotificationHandler(); + + return <window + // NOTE: is required due to last notification being displayed all the time + visible={bind(notifications).as(v => v.length != 0)} + + name={"Notifications"} + cssClasses={["Notifications"]} + gdkmonitor={gdkmonitor} + exclusivity={Astal.Exclusivity.EXCLUSIVE} + anchor={TOP | RIGHT}> + <box vertical> + {bind(notifications)} + </box> + </window> +} |