summaryrefslogtreecommitdiff
path: root/src/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/sidebar/dashboard.tsx123
-rw-r--r--src/modules/sidebar/index.tsx34
-rw-r--r--src/modules/sidebar/modules/hwresources.tsx67
-rw-r--r--src/modules/sidebar/modules/notifications.tsx70
4 files changed, 294 insertions, 0 deletions
diff --git a/src/modules/sidebar/dashboard.tsx b/src/modules/sidebar/dashboard.tsx
new file mode 100644
index 0000000..b7d03d0
--- /dev/null
+++ b/src/modules/sidebar/dashboard.tsx
@@ -0,0 +1,123 @@
+import Players from "@/services/players";
+import { osIcon, osId } from "@/utils/system";
+import Slider from "@/widgets/slider";
+import { bind, GLib, monitorFile, Variable } from "astal";
+import { Gtk } from "astal/gtk3";
+import AstalMpris from "gi://AstalMpris";
+import Notifications from "./modules/notifications";
+
+const lengthStr = (length: number) =>
+ `${Math.floor(length / 60)}:${Math.floor(length % 60)
+ .toString()
+ .padStart(2, "0")}`;
+
+const FaceFallback = () => (
+ <label
+ setup={self => {
+ const name = GLib.get_real_name();
+ if (name !== "Unknown")
+ self.label = name
+ .split(" ")
+ .map(s => s[0].toUpperCase())
+ .join("");
+ else {
+ self.label = "";
+ self.xalign = 0.44;
+ }
+ }}
+ />
+);
+
+const User = () => {
+ const uptime = Variable("").poll(5000, "uptime -p");
+ const hasFace = Variable(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS));
+ monitorFile(HOME + "/.face", () => hasFace.set(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS)));
+
+ return (
+ <box className="user">
+ <box
+ homogeneous
+ className="face"
+ setup={self => {
+ self.css = `background-image: url("${HOME}/.face");`;
+ monitorFile(HOME + "/.face", () => (self.css = `background-image: url("${HOME}/.face");`));
+ }}
+ onDestroy={() => hasFace.drop()}
+ >
+ {bind(hasFace).as(h => (h ? <box visible={false} /> : <FaceFallback />))}
+ </box>
+ <box vertical hexpand valign={Gtk.Align.CENTER} className="details">
+ <label xalign={0} className="name" label={`${osIcon} ${GLib.get_user_name()}`} />
+ <label xalign={0} label={(GLib.getenv("XDG_CURRENT_DESKTOP") ?? osId).toUpperCase()} />
+ <label truncate xalign={0} className="uptime" label={bind(uptime)} onDestroy={() => uptime.drop()} />
+ </box>
+ </box>
+ );
+};
+
+const QuickToggles = () => <box></box>;
+
+const Media = ({ player }: { player: AstalMpris.Player }) => {
+ const position = Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l);
+
+ return (
+ <box className="media" onDestroy={() => position.drop()}>
+ <box
+ homogeneous
+ className="cover-art"
+ css={bind(player, "coverArt").as(a => `background-image: url("${a}");`)}
+ >
+ {bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.31} label="" />))}
+ </box>
+ <box vertical className="details">
+ <label truncate className="title" label={bind(player, "title")} />
+ <label truncate className="artist" label={bind(player, "artist")} />
+ <box hexpand className="controls">
+ <button
+ hexpand
+ sensitive={bind(player, "canGoPrevious")}
+ cursor="pointer"
+ onClicked={() => player.next()}
+ label="󰒮"
+ />
+ <button
+ hexpand
+ sensitive={bind(player, "canControl")}
+ cursor="pointer"
+ onClicked={() => player.play_pause()}
+ label={bind(player, "playbackStatus").as(s =>
+ s === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"
+ )}
+ />
+ <button
+ hexpand
+ sensitive={bind(player, "canGoNext")}
+ cursor="pointer"
+ onClicked={() => player.next()}
+ label="󰒭"
+ />
+ </box>
+ <Slider value={bind(position)} />
+ <box className="time">
+ <label label={bind(player, "position").as(lengthStr)} />
+ <box hexpand />
+ <label label={bind(player, "length").as(lengthStr)} />
+ </box>
+ </box>
+ </box>
+ );
+};
+
+const Today = () => <box></box>;
+
+export default () => (
+ <box vertical className="pane dashboard" name="dashboard">
+ <User />
+ <box className="separator" />
+ {bind(Players.get_default(), "lastPlayer").as(p => (
+ <Media player={p} />
+ ))}
+ <box className="separator" />
+ <Notifications />
+ </box>
+);
diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx
new file mode 100644
index 0000000..3b62d82
--- /dev/null
+++ b/src/modules/sidebar/index.tsx
@@ -0,0 +1,34 @@
+import type { Monitor } from "@/services/monitors";
+import { bind, register, Variable } from "astal";
+import { App, Astal, Gtk, Widget } from "astal/gtk3";
+import Dashboard from "./dashboard";
+
+@register()
+export default class SideBar extends Widget.Window {
+ readonly shown: Variable<string> = Variable("dashboard");
+
+ constructor({ monitor }: { monitor: Monitor }) {
+ super({
+ application: App,
+ name: "sidebar",
+ namespace: "caelestia-sidebar",
+ monitor: monitor.id,
+ anchor: Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
+ exclusivity: Astal.Exclusivity.EXCLUSIVE,
+ // visible: false,
+ });
+
+ this.add(
+ <box vertical className="sidebar">
+ <stack
+ vexpand
+ transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
+ transitionDuration={200}
+ shown={bind(this.shown)}
+ >
+ <Dashboard />
+ </stack>
+ </box>
+ );
+ }
+}
diff --git a/src/modules/sidebar/modules/hwresources.tsx b/src/modules/sidebar/modules/hwresources.tsx
new file mode 100644
index 0000000..768d8bd
--- /dev/null
+++ b/src/modules/sidebar/modules/hwresources.tsx
@@ -0,0 +1,67 @@
+import Cpu from "@/services/cpu";
+import Gpu from "@/services/gpu";
+import Memory from "@/services/memory";
+import Storage from "@/services/storage";
+import Slider from "@/widgets/slider";
+import { bind, type Binding } from "astal";
+import { Gtk, type Widget } from "astal/gtk3";
+
+const fmt = (bytes: number, pow: number) => +(bytes / 1024 ** pow).toFixed(2);
+const format = ({ total, used }: { total: number; used: number }) => {
+ if (total >= 1024 ** 4) return `${fmt(used, 4)}/${fmt(total, 4)} TiB`;
+ if (total >= 1024 ** 3) return `${fmt(used, 3)}/${fmt(total, 3)} GiB`;
+ if (total >= 1024 ** 2) return `${fmt(used, 2)}/${fmt(total, 2)} MiB`;
+ if (total >= 1024) return `${fmt(used, 1)}/${fmt(total, 1)} KiB`;
+ return `${used}/${total} B`;
+};
+
+const Resource = ({
+ icon,
+ name,
+ value,
+ labelSetup,
+}: {
+ icon: string;
+ name: string;
+ value: Binding<number>;
+ labelSetup?: (self: Widget.Label) => void;
+}) => (
+ <box vertical className={`resource ${name}`}>
+ <box className="inner">
+ <label label={icon} />
+ <Slider value={value.as(v => v / 100)} />
+ </box>
+ <label halign={Gtk.Align.END} label={labelSetup ? "" : value.as(v => `${+v.toFixed(2)}%`)} setup={labelSetup} />
+ </box>
+);
+
+export default () => (
+ <box vertical className="hw-resources">
+ {Gpu.get_default().available && <Resource icon="󰢮" name="gpu" value={bind(Gpu.get_default(), "usage")} />}
+ <Resource icon="" name="cpu" value={bind(Cpu.get_default(), "usage")} />
+ <Resource
+ icon=""
+ name="memory"
+ value={bind(Memory.get_default(), "usage")}
+ labelSetup={self => {
+ const mem = Memory.get_default();
+ const update = () => (self.label = format(mem));
+ self.hook(mem, "notify::used", update);
+ self.hook(mem, "notify::total", update);
+ update();
+ }}
+ />
+ <Resource
+ icon="󰋊"
+ name="storage"
+ value={bind(Storage.get_default(), "usage")}
+ labelSetup={self => {
+ const storage = Storage.get_default();
+ const update = () => (self.label = format(storage));
+ self.hook(storage, "notify::used", update);
+ self.hook(storage, "notify::total", update);
+ update();
+ }}
+ />
+ </box>
+);
diff --git a/src/modules/sidebar/modules/notifications.tsx b/src/modules/sidebar/modules/notifications.tsx
new file mode 100644
index 0000000..eb8f0aa
--- /dev/null
+++ b/src/modules/sidebar/modules/notifications.tsx
@@ -0,0 +1,70 @@
+import Notification from "@/widgets/notification";
+import { bind } from "astal";
+import { Astal, Gtk } from "astal/gtk3";
+import AstalNotifd from "gi://AstalNotifd";
+
+const List = () => (
+ <box
+ vertical
+ valign={Gtk.Align.START}
+ className="list"
+ setup={self => {
+ const notifd = AstalNotifd.get_default();
+ const map = new Map<number, Notification>();
+
+ const addNotification = (notification: AstalNotifd.Notification) => {
+ const notif = (<Notification notification={notification} compact />) as Notification;
+ notif.connect("destroy", () => map.get(notification.id) === notif && map.delete(notification.id));
+ map.get(notification.id)?.destroyWithAnims();
+ map.set(notification.id, notif);
+
+ const widget = (
+ <eventbox
+ // Dismiss on middle click
+ onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
+ setup={self => self.hook(notif, "destroy", () => self.destroy())}
+ >
+ {notif}
+ </eventbox>
+ );
+
+ self.pack_end(widget, false, false, 0);
+ };
+
+ notifd
+ .get_notifications()
+ .sort((a, b) => a.time - b.time)
+ .forEach(addNotification);
+
+ self.hook(notifd, "notified", (_, id) => addNotification(notifd.get_notification(id)));
+ self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
+ }}
+ />
+);
+
+export default () => (
+ <box vertical className="notifications">
+ <box className="header-bar">
+ <label
+ label={bind(AstalNotifd.get_default(), "notifications").as(
+ n => `${n.length} notification${n.length === 1 ? "" : "s"}`
+ )}
+ />
+ <box hexpand />
+ <button
+ className={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "enabled" : ""))}
+ cursor="pointer"
+ onClicked={() => (AstalNotifd.get_default().dontDisturb = !AstalNotifd.get_default().dontDisturb)}
+ label="󰂛 Silence"
+ />
+ <button
+ cursor="pointer"
+ onClicked={() => AstalNotifd.get_default().notifications.forEach(n => n.dismiss())}
+ label="󰎟 Clear"
+ />
+ </box>
+ <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
+ <List />
+ </scrollable>
+ </box>
+);