diff options
author | 2025-03-18 16:47:58 +0300 | |
---|---|---|
committer | 2025-03-18 16:50:29 +0300 | |
commit | 63742be30723555ab54c7b11136dfaf6f83d0c2e (patch) | |
tree | 2abb956786f56eb41bc37fac464448fc53225de1 | |
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
-rw-r--r-- | app.ts | 2 | ||||
-rw-r--r-- | style.scss | 88 | ||||
-rw-r--r-- | widget/bar/Bar.scss | 80 | ||||
-rw-r--r-- | widget/notifications/Notifications.scss | 141 | ||||
-rw-r--r-- | widget/notifications/Notifications.tsx | 108 |
5 files changed, 333 insertions, 86 deletions
@@ -3,9 +3,11 @@ import { App } from "astal/gtk4" import style from "./style.scss" import Bar from "./widget/bar/Bar" +import Notifications from "./widget/notifications/Notifications" const windows = [ Bar, + Notifications, ]; App.start({ @@ -2,89 +2,5 @@ // $fg-color: #{"@theme_fg_color"}; // $bg-color: #{"@theme_bg_color"}; -window.Bar { - border: none; - box-shadow: none; - background-color: #000; - color: #fff; - - >box { - padding: 0; - } -} - -box.Workspaces { - padding-left: 3px; - - >button { - min-width: 10px; - min-height: 10px; - border-radius: 5px; - - border: none; - margin: 5px 2px; - padding: 0; - background: #ccc; - - transition: min-width .1s ease; - - &.active { - min-width: 25px; - } - - &.focused { - background: #AD49E1; - } - } -} - -box.Tray { - margin-right: 5px; - - button { - margin: 0; - padding: 1px; - border: none; - border-radius: 0; - min-width: 10px; - min-height: 10px; - - background: #000; - } -} - -box.AudioVolume { - margin-right: 20px; - min-width: 140px; - - image { - margin-right: 5px; - } - - scale { - margin: 0; - padding: 0; - } - - trough { - border-radius: 20px; - } - - highlight { - border-radius: 20px; - min-height: 10px; - } - - slider { - margin: 0; - min-height: 0; - min-width: 0; - opacity: 0; - } -} - - -window.Notifications { - background-color: #000; - color: #fff; -} +@use "./widget/bar/Bar"; +@use "./widget/notifications/Notifications"; diff --git a/widget/bar/Bar.scss b/widget/bar/Bar.scss new file mode 100644 index 0000000..f57d7a7 --- /dev/null +++ b/widget/bar/Bar.scss @@ -0,0 +1,80 @@ +window.Bar { + border: none; + box-shadow: none; + background-color: #000; + color: #fff; + + >box { + padding: 0; + } +} + +box.Workspaces { + padding-left: 3px; + + >button { + min-width: 10px; + min-height: 10px; + border-radius: 5px; + + border: none; + margin: 5px 2px; + padding: 0; + background: #ccc; + + transition: min-width .1s ease; + + &.active { + min-width: 25px; + } + + &.focused { + background: #AD49E1; + } + } +} + +box.Tray { + margin-right: 5px; + + button { + margin: 0; + padding: 1px; + border: none; + border-radius: 0; + min-width: 10px; + min-height: 10px; + + background: #000; + } +} + +box.AudioVolume { + margin-right: 20px; + min-width: 140px; + + image { + margin-right: 5px; + } + + scale { + margin: 0; + padding: 0; + } + + trough { + border-radius: 20px; + } + + highlight { + border-radius: 20px; + min-height: 10px; + } + + slider { + margin: 0; + min-height: 0; + min-width: 0; + opacity: 0; + } +} 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> +} |