summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar Mora Unie Youer <[email protected]>2025-03-18 16:47:58 +0300
committerLibravatar Mora Unie Youer <[email protected]>2025-03-18 16:50:29 +0300
commit63742be30723555ab54c7b11136dfaf6f83d0c2e (patch)
tree2abb956786f56eb41bc37fac464448fc53225de1
parentfeat: dynamic workspace updating (diff)
downloadags-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.ts2
-rw-r--r--style.scss88
-rw-r--r--widget/bar/Bar.scss80
-rw-r--r--widget/notifications/Notifications.scss141
-rw-r--r--widget/notifications/Notifications.tsx108
5 files changed, 333 insertions, 86 deletions
diff --git a/app.ts b/app.ts
index 4e6b532..2f7cae3 100644
--- a/app.ts
+++ b/app.ts
@@ -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({
diff --git a/style.scss b/style.scss
index bbc7078..d45eaa5 100644
--- a/style.scss
+++ b/style.scss
@@ -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>
+}