summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app.tsx2
-rw-r--r--scss/_lib.scss2
-rw-r--r--scss/bar.scss2
-rw-r--r--scss/notifications.scss101
-rw-r--r--scss/notifpopups.scss84
-rw-r--r--scss/widgets.scss78
-rw-r--r--src/modules/bar.tsx77
-rw-r--r--src/modules/notifications.tsx84
-rw-r--r--src/modules/notifpopups.tsx10
-rw-r--r--src/widgets/notification.tsx11
-rw-r--r--src/widgets/popupwindow.ts5
-rw-r--r--style.scss1
12 files changed, 302 insertions, 155 deletions
diff --git a/app.tsx b/app.tsx
index 31e7ed1..708532f 100644
--- a/app.tsx
+++ b/app.tsx
@@ -2,6 +2,7 @@ import { execAsync, GLib, writeFileAsync } from "astal";
import { App } from "astal/gtk3";
import Bar from "./src/modules/bar";
import Launcher from "./src/modules/launcher";
+import Notifications from "./src/modules/notifications";
import NotifPopups from "./src/modules/notifpopups";
import Osds from "./src/modules/osds";
import Monitors from "./src/services/monitors";
@@ -24,6 +25,7 @@ App.start({
<NotifPopups />;
<Osds />;
Monitors.get_default().forEach(m => <Bar monitor={m} />);
+ <Notifications />;
console.log("Caelestia started");
},
diff --git a/scss/_lib.scss b/scss/_lib.scss
index e12140f..d2ad3d0 100644
--- a/scss/_lib.scss
+++ b/scss/_lib.scss
@@ -15,7 +15,7 @@ $scale: 0.068rem;
border: s($width) $style color.change($colour, $alpha: $alpha);
}
-@mixin shadow($colour: scheme.$mantle, $alpha: 0.4, $x: 2, $y: 3, $blur: 8, $spread: 0) {
+@mixin shadow($colour: black, $alpha: 0.4, $x: 2, $y: 3, $blur: 8, $spread: 0) {
box-shadow: s($x) s($y) s($blur) s($spread) color.change($colour, $alpha: $alpha);
}
diff --git a/scss/bar.scss b/scss/bar.scss
index ab75b00..1d73319 100644
--- a/scss/bar.scss
+++ b/scss/bar.scss
@@ -92,7 +92,7 @@
color: scheme.$blue;
}
- .notifications {
+ .unread {
color: scheme.$mauve;
}
diff --git a/scss/notifications.scss b/scss/notifications.scss
new file mode 100644
index 0000000..51f1a0b
--- /dev/null
+++ b/scss/notifications.scss
@@ -0,0 +1,101 @@
+@use "sass:color";
+@use "scheme";
+@use "lib";
+@use "font";
+
+@mixin popup($accent) {
+ .separator {
+ background-color: $accent;
+ }
+
+ .image {
+ @include lib.border($accent, 0.05);
+ }
+}
+
+.notifications {
+ @include lib.rounded(8);
+ @include lib.border(scheme.$mauve, 0.4, 2);
+ @include lib.shadow;
+
+ min-width: lib.s(400);
+ min-height: lib.s(600);
+ background-color: scheme.$base;
+ color: scheme.$mauve;
+ padding: lib.s(10) lib.s(12);
+
+ .header {
+ @include font.mono;
+ @include lib.spacing(8);
+
+ padding: 0 lib.s(5);
+ margin-bottom: lib.s(8);
+
+ button {
+ @include lib.rounded(5);
+ @include lib.element-decel;
+
+ padding: lib.s(3) lib.s(8);
+
+ &:hover,
+ &:focus {
+ background-color: scheme.$surface0;
+ }
+
+ &:active {
+ background-color: scheme.$surface1;
+ }
+
+ &.enabled {
+ background-color: scheme.$mauve;
+ color: scheme.$base;
+
+ &:hover,
+ &:focus {
+ background-color: color.mix(scheme.$mauve, scheme.$base, 80%);
+ }
+
+ &:active {
+ background-color: color.mix(scheme.$mauve, scheme.$base, 70%);
+ }
+ }
+ }
+ }
+
+ .notification {
+ .wrapper {
+ padding-bottom: lib.s(10);
+ }
+
+ .inner {
+ background-color: color.mix(scheme.$surface0, scheme.$base, 70%);
+
+ &.low {
+ @include popup(scheme.$overlay0);
+ }
+
+ &.normal {
+ @include lib.border(scheme.$lavender, 0.3);
+ @include popup(scheme.$lavender);
+ }
+
+ &.critical {
+ @include lib.border(scheme.$red, 0.5);
+ @include popup(scheme.$red);
+ }
+ }
+
+ .actions > * {
+ background-color: scheme.$surface1;
+
+ &:hover,
+ &:focus {
+ background-color: scheme.$surface2;
+ }
+
+ &:active {
+ background-color: scheme.$overlay0;
+ }
+ }
+ }
+}
diff --git a/scss/notifpopups.scss b/scss/notifpopups.scss
index 25e52e1..cec24a4 100644
--- a/scss/notifpopups.scss
+++ b/scss/notifpopups.scss
@@ -21,86 +21,22 @@
min-width: lib.s(410);
padding-left: lib.s(10); // So notifications can overshoot for init animation
- .wrapper {
- padding-top: lib.s(10);
- }
-
.notification {
- @include lib.rounded(8, $tr: 0, $br: 0);
- @include lib.shadow;
- @include font.main;
- @include popup(scheme.$lavender);
-
- background-color: scheme.$base;
- color: scheme.$text;
- padding: lib.s(10) lib.s(12);
-
- @include lib.spacing($vertical: true);
-
- &.low {
- @include popup(scheme.$overlay0);
- }
-
- &.critical {
- @include popup(scheme.$red);
+ .wrapper {
+ padding-top: lib.s(10);
}
- }
-
- .header,
- .content {
- padding: 0 lib.s(5);
- }
-
- .header {
- @include font.mono;
- @include lib.spacing(5);
- }
-
- .content {
- @include lib.spacing(10);
- }
-
- .app-icon {
- font-size: lib.s(18);
- }
-
- .image {
- @include lib.rounded(10);
-
- background-size: cover;
- background-position: center;
- min-width: lib.s(64);
- min-height: lib.s(64);
- margin-top: lib.s(3);
- }
-
- .summary {
- @include font.title;
-
- font-size: lib.s(16);
- }
-
- .body {
- font-size: lib.s(14);
- }
-
- .actions {
- @include lib.spacing;
-
- & > * {
- @include lib.rounded(5);
- @include lib.element-decel;
- padding: lib.s(5) lib.s(10);
- background-color: scheme.$surface0;
+ .inner {
+ @include lib.rounded(8, $tr: 0, $br: 0);
+ @include lib.shadow;
+ @include popup(scheme.$lavender);
- &:hover,
- &:focus {
- background-color: scheme.$surface1;
+ &.low {
+ @include popup(scheme.$overlay0);
}
- &:active {
- background-color: scheme.$surface2;
+ &.critical {
+ @include popup(scheme.$red);
}
}
}
diff --git a/scss/widgets.scss b/scss/widgets.scss
index 39ab490..e54a117 100644
--- a/scss/widgets.scss
+++ b/scss/widgets.scss
@@ -17,6 +17,78 @@ label.icon {
@include font.icon;
}
+.notification {
+ .inner {
+ @include lib.rounded(8);
+ @include font.main;
+
+ background-color: scheme.$base;
+ color: scheme.$text;
+ padding: lib.s(10) lib.s(12);
+
+ @include lib.spacing($vertical: true);
+ }
+
+ .header,
+ .content {
+ padding: 0 lib.s(5);
+ }
+
+ .header {
+ @include font.mono;
+ @include lib.spacing(8);
+ }
+
+ .content {
+ @include lib.spacing(10);
+ }
+
+ .app-icon {
+ font-size: lib.s(18);
+ }
+
+ .image {
+ @include lib.rounded(10);
+
+ background-size: cover;
+ background-position: center;
+ min-width: lib.s(64);
+ min-height: lib.s(64);
+ margin-top: lib.s(3);
+ }
+
+ .summary {
+ @include font.title;
+
+ font-size: lib.s(16);
+ }
+
+ .body {
+ font-size: lib.s(14);
+ }
+
+ .actions {
+ @include lib.spacing;
+
+ & > * {
+ @include lib.rounded(5);
+ @include lib.element-decel;
+
+ padding: lib.s(5) lib.s(10);
+ background-color: scheme.$surface0;
+
+ &:hover,
+ &:focus {
+ background-color: scheme.$surface1;
+ }
+
+ &:active {
+ background-color: scheme.$surface2;
+ }
+ }
+ }
+}
+
separator,
.separator {
@include lib.rounded(2);
@@ -112,9 +184,6 @@ tooltip {
scrollbar {
trough {
- @include lib.rounded(1000);
-
- min-width: lib.s(12);
background-color: transparent;
}
@@ -122,12 +191,13 @@ scrollbar {
@include lib.rounded(1000);
@include lib.element-decel;
- min-width: lib.s(6);
+ min-width: lib.s(3);
min-height: lib.s(30);
background-color: color.change(scheme.$overlay0, $alpha: 0.3);
&:hover,
&:focus {
+ min-width: lib.s(6);
background-color: color.change(scheme.$overlay0, $alpha: 0.4);
}
diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx
index b56d94a..aeb6e42 100644
--- a/src/modules/bar.tsx
+++ b/src/modules/bar.tsx
@@ -15,6 +15,7 @@ import { getAppCategoryIcon } from "../utils/icons";
import { ellipsize } from "../utils/strings";
import { osIcon } from "../utils/system";
import { setupCustomTooltip } from "../utils/widgets";
+import type PopupWindow from "../widgets/popupwindow";
const hyprland = AstalHyprland.get_default();
@@ -385,11 +386,19 @@ const PkgUpdates = () => (
</box>
);
-const Notifications = () => {
+const Unread = () => {
const unreadCount = Variable(0);
return (
- <box
- className="module notifications"
+ <button
+ onClick={(self, event) => {
+ if (event.button === Astal.MouseButton.PRIMARY) {
+ const popup = App.get_window("notifications") as PopupWindow | null;
+ if (popup) {
+ if (popup.visible) popup.hide();
+ else popup.popup_at_widget(self, event);
+ }
+ } else if (event.button === Astal.MouseButton.SECONDARY) unreadCount.set(0);
+ }}
setup={self =>
setupCustomTooltip(
self,
@@ -397,39 +406,37 @@ const Notifications = () => {
)
}
>
- <label className="icon" label="info" />
- <label
- label="0"
- setup={self => {
- const notifd = AstalNotifd.get_default();
- let notifsOpen = false;
- let unread = new Set<number>();
+ <box className="module unread">
+ <label className="icon" label="info" />
+ <label
+ label={bind(unreadCount).as(String)}
+ setup={self => {
+ const notifd = AstalNotifd.get_default();
+ let notifsOpen = false;
+ let unread = new Set<number>();
+ self.hook(unreadCount, (_, u) => u === 0 && unread.clear());
- self.hook(notifd, "notified", (self, id, replaced) => {
- if (!notifsOpen && !replaced) {
- unread.add(id);
- unreadCount.set(unread.size);
- self.label = String(unread.size);
- }
- });
- self.hook(notifd, "resolved", (self, id) => {
- if (unread.delete(id)) {
- unreadCount.set(unread.size);
- self.label = String(unread.size);
- }
- });
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === "notifications") {
- notifsOpen = window.visible;
- if (notifsOpen) {
- unread.clear();
- unreadCount.set(0);
+ self.hook(notifd, "notified", (_, id, replaced) => {
+ if (!notifsOpen && !replaced) {
+ unread.add(id);
+ unreadCount.set(unread.size);
}
- }
- });
- }}
- />
- </box>
+ });
+ self.hook(notifd, "resolved", (_, id) => {
+ if (unread.delete(id)) {
+ unreadCount.set(unread.size);
+ }
+ });
+ self.hook(App, "window-toggled", (_, window) => {
+ if (window.name === "notifications") {
+ notifsOpen = window.visible;
+ if (notifsOpen) unreadCount.set(0);
+ }
+ });
+ }}
+ />
+ </box>
+ </button>
);
};
@@ -491,7 +498,7 @@ export default ({ monitor }: { monitor: Monitor }) => (
<Tray />
<StatusIcons />
<PkgUpdates />
- <Notifications />
+ <Unread />
<DateTime />
<Power />
</box>
diff --git a/src/modules/notifications.tsx b/src/modules/notifications.tsx
index 66188a1..ea98ada 100644
--- a/src/modules/notifications.tsx
+++ b/src/modules/notifications.tsx
@@ -1,6 +1,8 @@
-import { Gtk } from "astal/gtk3";
+import { bind } from "astal";
+import { Astal, Gtk } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
-import { PopupWindow, setupChildClickthrough } from "../utils/widgets";
+import Notification from "../widgets/notification";
+import PopupWindow from "../widgets/popupwindow";
const List = () => (
<box
@@ -9,49 +11,63 @@ const List = () => (
className="list"
setup={self => {
const notifd = AstalNotifd.get_default();
- const map = new Map<number, NotifPopup>();
- self.hook(notifd, "notified", (self, id) => {
- const notification = notifd.get_notification(id);
+ const map = new Map<number, Notification>();
- const popup = (<NotifPopup notification={notification} />) as NotifPopup;
- popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
+ const addNotification = (notification: AstalNotifd.Notification) => {
+ const notif = (<Notification notification={notification} />) as Notification;
+ notif.connect("destroy", () => map.get(notification.id) === notif && map.delete(notification.id));
map.get(notification.id)?.destroyWithAnims();
- map.set(notification.id, popup);
+ map.set(notification.id, notif);
- self.add(
+ self.pack_end(
<eventbox
// Dismiss on middle click
onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
- // Close on hover lost
- onHoverLost={() => popup.destroyWithAnims()}
>
- {popup}
- </eventbox>
+ {notif}
+ </eventbox>,
+ false,
+ false,
+ 0
);
+ };
- // Limit number of popups
- if (config.maxPopups > 0 && self.children.length > config.maxPopups)
- map.values().next().value?.destroyWithAnims();
- });
- self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
+ notifd
+ .get_notifications()
+ .sort((a, b) => a.time - b.time)
+ .forEach(addNotification);
- // Change input region to child region so can click through empty space
- setupChildClickthrough(self);
+ self.hook(notifd, "notified", (_, id) => addNotification(notifd.get_notification(id)));
+ self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
}}
/>
);
-export default class Notifications extends PopupWindow {
- constructor() {
- super({
- name: "notifications",
- child: (
- <box>
- <List />
- </box>
- ),
- });
-
- setupChildClickthrough(self);
- }
-}
+export default () => (
+ <PopupWindow name="notifications">
+ <box vertical className="notifications">
+ <box className="header">
+ <label
+ label={bind(AstalNotifd.get_default(), "notifications").as(
+ n => `${n.length} notification${n.length === 1 ? "" : "s"}`
+ )}
+ />
+ <box hexpand />
+ <button
+ cursor="pointer"
+ onClicked={() => (AstalNotifd.get_default().dontDisturb = !AstalNotifd.get_default().dontDisturb)}
+ label="Silence"
+ className={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "enabled" : ""))}
+ />
+ <button
+ cursor="pointer"
+ onClicked={() => AstalNotifd.get_default().notifications.forEach(n => n.dismiss())}
+ label="Clear"
+ />
+ </box>
+ <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
+ <List />
+ </scrollable>
+ </box>
+ </PopupWindow>
+);
diff --git a/src/modules/notifpopups.tsx b/src/modules/notifpopups.tsx
index 5da3092..9e34549 100644
--- a/src/modules/notifpopups.tsx
+++ b/src/modules/notifpopups.tsx
@@ -1,4 +1,4 @@
-import { Astal, Gtk } from "astal/gtk3";
+import { App, Astal, Gtk } from "astal/gtk3";
import AstalNotifd from "gi://AstalNotifd";
import { notifpopups as config } from "../../config";
import { setupChildClickthrough } from "../utils/widgets";
@@ -16,7 +16,11 @@ export default () => (
setup={self => {
const notifd = AstalNotifd.get_default();
const map = new Map<number, Notification>();
+ let notifsOpen = false;
+
self.hook(notifd, "notified", (self, id) => {
+ if (notifsOpen) return;
+
const notification = notifd.get_notification(id);
const popup = (<Notification popup notification={notification} />) as Notification;
@@ -41,6 +45,10 @@ export default () => (
});
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
+ self.hook(App, "window-toggled", (_, window) => {
+ if (window.name === "notifications") notifsOpen = window.visible;
+ });
+
// Change input region to child region so can click through empty space
setupChildClickthrough(self);
}}
diff --git a/src/widgets/notification.tsx b/src/widgets/notification.tsx
index 0bef5ca..adac831 100644
--- a/src/widgets/notification.tsx
+++ b/src/widgets/notification.tsx
@@ -57,12 +57,16 @@ export default class Notification extends Widget.Box {
#destroyed = false;
constructor({ notification, popup }: { notification: AstalNotifd.Notification; popup?: boolean }) {
- super();
+ super({ className: "notification" });
this.#revealer = (
- <revealer revealChild transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} transitionDuration={150}>
+ <revealer
+ revealChild={popup}
+ transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
+ transitionDuration={150}
+ >
<box className="wrapper">
- <box vertical className={`notification ${urgencyToString(notification.urgency)}`}>
+ <box vertical className={`inner ${urgencyToString(notification.urgency)}`}>
<box className="header">
<AppIcon appIcon={notification.appIcon} desktopEntry={notification.appName} />
<label className="app-name" label={notification.appName ?? "Unknown"} />
@@ -101,6 +105,7 @@ export default class Notification extends Widget.Box {
const width = this.get_preferred_width()[1];
this.css = `margin-left: ${width}px; margin-right: -${width}px;`;
timeout(1, () => {
+ this.#revealer.revealChild = true;
this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`;
});
diff --git a/src/widgets/popupwindow.ts b/src/widgets/popupwindow.ts
index 67aa0ff..9f5192e 100644
--- a/src/widgets/popupwindow.ts
+++ b/src/widgets/popupwindow.ts
@@ -28,10 +28,11 @@ export default class PopupWindow extends Widget.Window {
});
}
- popup_at_widget(widget: JSX.Element, event: Gdk.Event) {
+ popup_at_widget(widget: JSX.Element, event: Gdk.Event | Astal.ClickEvent) {
const { width, height } = widget.get_allocation();
- const [_, x, y] = event.get_coords();
+ const [_, x, y] = event instanceof Gdk.Event ? event.get_coords() : [null, event.x, event.y];
const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
+ this.anchor = Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT;
this.marginLeft = cx + ((width - this.get_preferred_width()[1]) / 2 - x);
this.marginTop = cy + (height - y);
this.show();
diff --git a/style.scss b/style.scss
index 99953a4..38a8906 100644
--- a/style.scss
+++ b/style.scss
@@ -6,6 +6,7 @@
@use "scss/notifpopups";
@use "scss/launcher";
@use "scss/osds";
+@use "scss/notifications";
* {
all: unset; // Remove GTK theme styles