summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-12 18:00:54 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-01-12 18:00:54 +1100
commitb4aca729ddae0526b66822698db7066cb09e1682 (patch)
tree2a406cca4cfc616dd22ce7c1be61cc20d5db85bc /modules
parentInitial commit (diff)
downloadcaelestia-shell-b4aca729ddae0526b66822698db7066cb09e1682.tar.gz
caelestia-shell-b4aca729ddae0526b66822698db7066cb09e1682.tar.bz2
caelestia-shell-b4aca729ddae0526b66822698db7066cb09e1682.zip
bar
Diffstat (limited to 'modules')
-rw-r--r--modules/bar.tsx283
1 files changed, 283 insertions, 0 deletions
diff --git a/modules/bar.tsx b/modules/bar.tsx
new file mode 100644
index 0000000..1db5e82
--- /dev/null
+++ b/modules/bar.tsx
@@ -0,0 +1,283 @@
+import { GLib, register, Variable } from "astal";
+import { bind, kebabify } from "astal/binding";
+import { App, Astal, astalify, Gdk, Gtk, type ConstructProps } from "astal/gtk3";
+import AstalHyprland from "gi://AstalHyprland";
+import AstalNotifd from "gi://AstalNotifd";
+import AstalTray from "gi://AstalTray";
+import Players from "../services/players";
+import { getAppCategoryIcon } from "../utils/icons";
+import { ellipsize } from "../utils/strings";
+import { osIcon } from "../utils/system";
+import { setupCustomTooltip } from "../utils/widgets";
+
+const hyprland = AstalHyprland.get_default();
+
+const wsPerGroup = 10;
+
+const hookFocusedClientProp = (
+ self: any, // Ugh why is there no base Widget type
+ prop: keyof AstalHyprland.Client,
+ callback: (c: AstalHyprland.Client | null) => void
+) => {
+ let id: number | null = null;
+ let lastClient: AstalHyprland.Client | null = null;
+ self.hook(hyprland, "notify::focused-client", () => {
+ if (id) lastClient?.disconnect(id);
+ lastClient = hyprland.focusedClient; // Can be null
+ id = lastClient?.connect(`notify::${kebabify(prop)}`, () => callback(lastClient));
+ callback(lastClient);
+ });
+ self.connect("destroy", () => id && lastClient?.disconnect(id));
+ callback(lastClient);
+};
+
+const OSIcon = () => <label className="module os-icon" label={osIcon} />;
+
+const ActiveWindow = () => (
+ <box
+ hasTooltip
+ className="module active-window"
+ setup={self => {
+ const title = Variable(hyprland.focusedClient?.title ?? "");
+ hookFocusedClientProp(self, "title", c => title.set(c?.title ?? ""));
+
+ const window = setupCustomTooltip(self, bind(title));
+ if (window) {
+ self.hook(title, (_, v) => !v && window.hide());
+ self.hook(window, "map", () => !title.get() && window.hide());
+ }
+ }}
+ >
+ <label
+ className="icon"
+ setup={self =>
+ hookFocusedClientProp(self, "class", c => {
+ self.label = c ? getAppCategoryIcon(c.class) : "desktop_windows";
+ })
+ }
+ />
+ <label
+ setup={self => hookFocusedClientProp(self, "title", c => (self.label = c ? ellipsize(c.title) : "Desktop"))}
+ />
+ </box>
+);
+
+const MediaPlaying = () => {
+ const players = Players.get_default();
+ const getLabel = (fallback = "") =>
+ players.lastPlayer ? `${players.lastPlayer.title} - ${players.lastPlayer.artist}` : fallback;
+ return (
+ <box
+ className="module media-playing"
+ setup={self => {
+ const label = Variable(getLabel());
+ players.hookLastPlayer(self, ["notify::title", "notify::artist"], () => label.set(getLabel()));
+ setupCustomTooltip(self, bind(label));
+ }}
+ >
+ <icon
+ setup={self =>
+ players.hookLastPlayer(self, "notify::entry", () => {
+ const icon = `caelestia-${players.lastPlayer?.entry}-symbolic`;
+ self.icon = players.lastPlayer
+ ? Astal.Icon.lookup_icon(icon)
+ ? icon
+ : "caelestia-media-generic-symbolic"
+ : "caelestia-media-none-symbolic";
+ })
+ }
+ />
+ <label
+ setup={self =>
+ players.hookLastPlayer(self, ["notify::title", "notify::artist"], () => {
+ self.label = getLabel("No media");
+ })
+ }
+ />
+ </box>
+ );
+};
+
+const Workspace = ({ idx }: { idx: number }) => {
+ let wsId = Math.floor((hyprland.focusedWorkspace.id - 1) / wsPerGroup) * wsPerGroup + idx;
+ return (
+ <button
+ halign={Gtk.Align.CENTER}
+ valign={Gtk.Align.CENTER}
+ onClicked={() => hyprland.dispatch("workspace", String(wsId))}
+ setup={self => {
+ const update = () => {
+ self.toggleClassName(
+ "occupied",
+ hyprland.clients.some(c => c.workspace.id === wsId)
+ );
+ self.toggleClassName("focused", hyprland.focusedWorkspace.id === wsId);
+ };
+
+ self.hook(hyprland, "notify::focused-workspace", () => {
+ wsId = Math.floor((hyprland.focusedWorkspace.id - 1) / wsPerGroup) * wsPerGroup + idx;
+ update();
+ });
+ self.hook(hyprland, "client-added", update);
+ self.hook(hyprland, "client-moved", update);
+ self.hook(hyprland, "client-removed", update);
+
+ update();
+ }}
+ />
+ );
+};
+
+const Workspaces = () => (
+ <eventbox
+ onScroll={(_, event) => {
+ const activeWs = hyprland.focusedClient?.workspace.name;
+ if (activeWs?.startsWith("special:")) hyprland.dispatch("togglespecialworkspace", activeWs.slice(8));
+ else if (event.delta_y > 0 || hyprland.focusedWorkspace.id > 1)
+ hyprland.dispatch("workspace", (event.delta_y < 0 ? "-" : "+") + 1);
+ }}
+ >
+ <box className="module workspaces">
+ {Array.from({ length: wsPerGroup }).map((_, idx) => (
+ <Workspace idx={idx + 1} /> // Start from 1
+ ))}
+ </box>
+ </eventbox>
+);
+
+@register()
+class TrayItemMenu extends astalify(Gtk.Menu) {
+ constructor(props: ConstructProps<TrayItemMenu, Gtk.Menu.ConstructorProps> & { item: AstalTray.TrayItem }) {
+ const { item, ...sProps } = props;
+ super(sProps as any);
+
+ this.hook(item, "notify::menu-model", () => this.bind_model(item.menuModel, null, true));
+ this.hook(item, "notify::action-group", () => this.insert_action_group("dbusmenu", item.actionGroup));
+ this.bind_model(item.menuModel, null, true);
+ this.insert_action_group("dbusmenu", item.actionGroup);
+ }
+}
+
+const TrayItem = (item: AstalTray.TrayItem) => {
+ const menu = (<TrayItemMenu item={item} />) as TrayItemMenu;
+ return (
+ <button
+ onClick={(self, event) => {
+ if (event.button === Astal.MouseButton.PRIMARY) {
+ if (item.isMenu) menu.popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null);
+ else item.activate(0, 0);
+ } else if (event.button === Astal.MouseButton.SECONDARY)
+ menu.popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null);
+ }}
+ onScroll={(_, event) => {
+ if (event.delta_x !== 0) item.scroll(event.delta_x, "horizontal");
+ if (event.delta_y !== 0) item.scroll(event.delta_y, "vertical");
+ }}
+ onDestroy={() => menu.destroy()}
+ setup={self => setupCustomTooltip(self, bind(item, "tooltipMarkup"))}
+ >
+ <icon halign={Gtk.Align.CENTER} gicon={bind(item, "gicon")} />
+ </button>
+ );
+};
+
+const Tray = () => <box className="module tray">{bind(AstalTray.get_default(), "items").as(i => i.map(TrayItem))}</box>;
+
+const Notifications = () => {
+ const unreadCount = Variable(0);
+ return (
+ <box
+ className="module notifications"
+ setup={self =>
+ setupCustomTooltip(
+ self,
+ bind(unreadCount).as(n => n + " unread notifications")
+ )
+ }
+ >
+ <label className="icon" label="info" />
+ <label
+ label="0"
+ setup={self => {
+ const notifd = AstalNotifd.get_default();
+ let notifsOpen = false;
+ let unread = new Set<number>();
+
+ 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);
+ }
+ }
+ });
+ }}
+ />
+ </box>
+ );
+};
+
+const DateTime = () => (
+ <box className="module date-time">
+ <label className="icon" label="calendar_month" />
+ <label
+ setup={self => {
+ const pollVar = Variable(null).poll(5000, () => {
+ self.label = GLib.DateTime.new_now_local().format("%d/%m/%y %R") ?? new Date().toLocaleString();
+ return null;
+ });
+ self.connect("destroy", () => pollVar.drop());
+ }}
+ />
+ </box>
+);
+
+const Power = () => (
+ <button
+ className="module power"
+ label="power_settings_new"
+ cursor="pointer"
+ onClicked={() => {
+ // TODO: Power menu
+ }}
+ />
+);
+
+export default ({ monitor }: { monitor: AstalHyprland.Monitor }) => (
+ <window
+ namespace="bar"
+ monitor={monitor.id}
+ anchor={Astal.WindowAnchor.TOP}
+ exclusivity={Astal.Exclusivity.EXCLUSIVE}
+ visible
+ >
+ <centerbox className="bar" css={"min-width: " + monitor.width * 0.8 + "px;"}>
+ <box halign={Gtk.Align.START}>
+ <OSIcon />
+ <ActiveWindow />
+ <MediaPlaying />
+ </box>
+ <Workspaces />
+ <box halign={Gtk.Align.END}>
+ <Tray />
+ <Notifications />
+ <DateTime />
+ <Power />
+ </box>
+ </centerbox>
+ </window>
+);