summaryrefslogtreecommitdiff
path: root/src/modules
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-04-26 22:36:23 +1000
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-04-26 22:36:23 +1000
commit3c579d0e275cdaf6f2c9589abade94bde7905c82 (patch)
tree4b825dc642cb6eb9a060e54bf8d69288fbee4904 /src/modules
parentschemes: fix (diff)
downloadcaelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.tar.gz
caelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.tar.bz2
caelestia-shell-3c579d0e275cdaf6f2c9589abade94bde7905c82.zip
clean
Remove everything
Diffstat (limited to 'src/modules')
-rw-r--r--src/modules/bar.tsx703
-rw-r--r--src/modules/launcher/actions.tsx522
-rw-r--r--src/modules/launcher/index.tsx144
-rw-r--r--src/modules/launcher/modes.tsx225
-rw-r--r--src/modules/launcher/util.tsx19
-rw-r--r--src/modules/mediadisplay/index.tsx188
-rw-r--r--src/modules/mediadisplay/visualiser.tsx71
-rw-r--r--src/modules/navbar.tsx203
-rw-r--r--src/modules/notifpopups.tsx72
-rw-r--r--src/modules/osds.tsx327
-rw-r--r--src/modules/screencorners.tsx51
-rw-r--r--src/modules/session.tsx44
-rw-r--r--src/modules/sidebar/alerts.tsx11
-rw-r--r--src/modules/sidebar/audio.tsx13
-rw-r--r--src/modules/sidebar/connectivity.tsx10
-rw-r--r--src/modules/sidebar/dashboard.tsx132
-rw-r--r--src/modules/sidebar/index.tsx87
-rw-r--r--src/modules/sidebar/modules/bluetooth.tsx127
-rw-r--r--src/modules/sidebar/modules/calendar.tsx252
-rw-r--r--src/modules/sidebar/modules/deviceselector.tsx126
-rw-r--r--src/modules/sidebar/modules/headlines.tsx204
-rw-r--r--src/modules/sidebar/modules/hwresources.tsx67
-rw-r--r--src/modules/sidebar/modules/media.tsx168
-rw-r--r--src/modules/sidebar/modules/networks.tsx151
-rw-r--r--src/modules/sidebar/modules/news.tsx113
-rw-r--r--src/modules/sidebar/modules/notifications.tsx90
-rw-r--r--src/modules/sidebar/modules/streams.tsx110
-rw-r--r--src/modules/sidebar/modules/upcoming.tsx99
-rw-r--r--src/modules/sidebar/modules/updates.tsx109
-rw-r--r--src/modules/sidebar/packages.tsx11
-rw-r--r--src/modules/sidebar/time.tsx24
31 files changed, 0 insertions, 4473 deletions
diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx
deleted file mode 100644
index c131029..0000000
--- a/src/modules/bar.tsx
+++ /dev/null
@@ -1,703 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Players from "@/services/players";
-import Updates from "@/services/updates";
-import { getAppCategoryIcon } from "@/utils/icons";
-import { bindCurrentTime, osIcon } from "@/utils/system";
-import type { AstalWidget } from "@/utils/types";
-import { setupCustomTooltip } from "@/utils/widgets";
-import ScreenCorner from "@/widgets/screencorner";
-import { execAsync, GLib, Variable } from "astal";
-import { bind, kebabify } from "astal/binding";
-import { App, Astal, Gtk, Widget } from "astal/gtk3";
-import { bar as config } from "config";
-import AstalBattery from "gi://AstalBattery";
-import AstalBluetooth from "gi://AstalBluetooth";
-import AstalHyprland from "gi://AstalHyprland";
-import AstalNetwork from "gi://AstalNetwork";
-import AstalNotifd from "gi://AstalNotifd";
-import AstalTray from "gi://AstalTray";
-import AstalWp from "gi://AstalWp";
-import { switchPane } from "./sidebar";
-
-interface ClassNameProps {
- beforeSpacer: boolean;
- afterSpacer: boolean;
- first: boolean;
- last: boolean;
-}
-
-interface ModuleProps extends ClassNameProps {
- monitor: Monitor;
-}
-
-const hyprland = AstalHyprland.get_default();
-
-const getBatteryIcon = (perc: number) => {
- if (perc < 0.1) return "󰁺";
- if (perc < 0.2) return "󰁻";
- if (perc < 0.3) return "󰁼";
- if (perc < 0.4) return "󰁽";
- if (perc < 0.5) return "󰁾";
- if (perc < 0.6) return "󰁿";
- if (perc < 0.7) return "󰂀";
- if (perc < 0.8) return "󰂁";
- if (perc < 0.9) return "󰂂";
- return "󰁹";
-};
-
-const formatSeconds = (sec: number) => {
- if (sec >= 3600) {
- const hours = Math.floor(sec / 3600);
- let str = `${hours} hour${hours === 1 ? "" : "s"}`;
- const mins = Math.floor((sec % 3600) / 60);
- if (mins > 0) str += ` ${mins} minute${mins === 1 ? "" : "s"}`;
- return str;
- } else if (sec >= 60) {
- const mins = Math.floor(sec / 60);
- return `${mins} minute${mins === 1 ? "" : "s"}`;
- } else return `${sec} second${sec === 1 ? "" : "s"}`;
-};
-
-const hookFocusedClientProp = (
- self: AstalWidget,
- 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 getClassName = ({ beforeSpacer, afterSpacer, first, last }: ClassNameProps) =>
- `${beforeSpacer ? "before-spacer" : ""} ${afterSpacer ? "after-spacer" : ""}` +
- ` ${first ? "first" : ""} ${last ? "last" : ""}`;
-
-const getModule = (module: string) => {
- module = module.toLowerCase();
- if (module === "osicon") return OSIcon;
- if (module === "activewindow") return ActiveWindow;
- if (module === "mediaplaying") return MediaPlaying;
- if (module === "workspaces") return Workspaces;
- if (module === "tray") return Tray;
- if (module === "statusicons") return StatusIcons;
- if (module === "pkgupdates") return PkgUpdates;
- if (module === "notifcount") return NotifCount;
- if (module === "battery") return Battery;
- if (module === "datetime") return DateTime;
- if (module === "power") return Power;
- if (module === "brightnessspacer") return BrightnessSpacer;
- if (module === "volumespacer") return VolumeSpacer;
- return () => null;
-};
-
-const isSpacer = (module?: string) => module?.toLowerCase().endsWith("spacer") ?? false;
-
-const OSIcon = ({ monitor, ...props }: ModuleProps) => (
- <button
- className={`module os-icon ${getClassName(props)}`}
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "dashboard")}
- >
- {osIcon}
- </button>
-);
-
-const ActiveWindow = ({ monitor, ...props }: ModuleProps) => (
- <box
- vertical={bind(config.vertical)}
- className={`module active-window ${getClassName(props)}`}
- setup={self => {
- const title = Variable("");
- const updateTooltip = (c: AstalHyprland.Client | null) =>
- title.set(c?.class && c?.title ? `${c.class}: ${c.title}` : "");
- hookFocusedClientProp(self, "class", updateTooltip);
- hookFocusedClientProp(self, "title", updateTooltip);
- updateTooltip(hyprland.focusedClient);
-
- 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?.class ? getAppCategoryIcon(c.class) : "desktop_windows";
- })
- }
- />
- <label
- truncate
- angle={bind(config.vertical).as(v => (v ? 270 : 0))}
- setup={self => {
- const update = () =>
- (self.label = hyprland.focusedClient?.title ? hyprland.focusedClient.title : "Desktop");
- hookFocusedClientProp(self, "title", update);
- self.hook(config.vertical, update);
- }}
- />
- </box>
-);
-
-const MediaPlaying = ({ monitor, ...props }: ModuleProps) => {
- const players = Players.get_default();
- const getLabel = (fallback = "") =>
- players.lastPlayer ? `${players.lastPlayer.title} - ${players.lastPlayer.artist}` : fallback;
- return (
- <button
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "audio");
- else if (event.button === Astal.MouseButton.SECONDARY) players.lastPlayer?.play_pause();
- else if (event.button === Astal.MouseButton.MIDDLE) players.lastPlayer?.raise();
- }}
- setup={self => {
- const label = Variable(getLabel());
- players.hookLastPlayer(self, ["notify::title", "notify::artist"], () => label.set(getLabel()));
- setupCustomTooltip(self, bind(label));
- }}
- >
- <box vertical={bind(config.vertical)} className={`module media-playing ${getClassName(props)}`}>
- <icon
- setup={self =>
- players.hookLastPlayer(self, "notify::identity", () => {
- const icon = `caelestia-${players.lastPlayer?.identity
- .toLowerCase()
- .replaceAll(" ", "-")}-symbolic`;
- self.icon = players.lastPlayer
- ? Astal.Icon.lookup_icon(icon)
- ? icon
- : "caelestia-media-generic-symbolic"
- : "caelestia-media-none-symbolic";
- })
- }
- />
- <label
- truncate
- angle={bind(config.vertical).as(v => (v ? 270 : 0))}
- setup={self => {
- const update = () => (self.label = getLabel("No media"));
- players.hookLastPlayer(self, ["notify::title", "notify::artist"], update);
- self.hook(config.vertical, update);
- }}
- />
- </box>
- </button>
- );
-};
-
-const Workspace = ({ idx }: { idx: number }) => {
- const wsId = Variable.derive([bind(hyprland, "focusedWorkspace"), config.modules.workspaces.shown], (f, s) =>
- f ? Math.floor((f.id - 1) / s) * s + idx : idx
- );
-
- const label = (
- <label
- css={bind(config.modules.workspaces.xalign).as(a => `margin-left: ${a}px; margin-right: ${-a}px;`)}
- label={bind(config.modules.workspaces.labels).as(l => l[idx - 1] ?? String(idx))}
- />
- );
-
- return (
- <button
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- onClicked={() => hyprland.dispatch("workspace", String(wsId.get()))}
- setup={self => {
- const updateOccupied = () => {
- const occupied = hyprland.clients.some(c => c.workspace?.id === wsId.get());
- self.toggleClassName("occupied", occupied);
- };
- const updateFocused = () => {
- self.toggleClassName("focused", hyprland.focusedWorkspace?.id === wsId.get());
- updateOccupied();
- };
-
- self.hook(hyprland, "client-added", updateOccupied);
- self.hook(hyprland, "client-moved", updateOccupied);
- self.hook(hyprland, "client-removed", updateOccupied);
- self.hook(hyprland, "notify::focused-workspace", updateFocused);
- updateFocused();
- }}
- onDestroy={() => wsId.drop()}
- >
- <box
- visible={bind(config.modules.workspaces.showLabels)}
- vertical={bind(config.vertical)}
- setup={self => {
- const update = () => {
- if (config.modules.workspaces.showWindows.get()) {
- const clients = hyprland.clients.filter(c => c.workspace?.id === wsId.get());
- self.children = [
- label,
- ...clients.map(c => (
- <label className="icon" label={bind(c, "class").as(getAppCategoryIcon)} />
- )),
- ];
- } else self.children = [label];
- };
- self.hook(wsId, update);
- self.hook(hyprland, "client-added", update);
- self.hook(hyprland, "client-moved", update);
- self.hook(hyprland, "client-removed", update);
- update();
- }}
- />
- </button>
- );
-};
-
-const Workspaces = ({ monitor, ...props }: ModuleProps) => {
- const className = Variable.derive(
- [config.modules.workspaces.shown, config.modules.workspaces.showLabels],
- (s, l) => `module workspaces ${s % 2 === 0 ? "even" : "odd"} ${l ? "labels-shown" : ""} ${getClassName(props)}`
- );
-
- return (
- <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 vertical={bind(config.vertical)} className={bind(className)} onDestroy={() => className.drop()}>
- {bind(config.modules.workspaces.shown).as(
- n => Array.from({ length: n }).map((_, idx) => <Workspace idx={idx + 1} />) // Start from 1
- )}
- </box>
- </eventbox>
- );
-};
-
-const TrayItem = (item: AstalTray.TrayItem) => (
- <menubutton
- onButtonPressEvent={(_, event) => event.get_button()[1] === Astal.MouseButton.SECONDARY && item.activate(0, 0)}
- usePopover={false}
- direction={bind(config.vertical).as(v => (v ? Gtk.ArrowType.RIGHT : Gtk.ArrowType.DOWN))}
- menuModel={bind(item, "menuModel")}
- actionGroup={bind(item, "actionGroup").as(a => ["dbusmenu", a])}
- setup={self => setupCustomTooltip(self, bind(item, "tooltipMarkup"))}
- >
- <icon halign={Gtk.Align.CENTER} gicon={bind(item, "gicon")} />
- </menubutton>
-);
-
-const Tray = ({ monitor, ...props }: ModuleProps) => (
- <box
- visible={bind(AstalTray.get_default(), "items").as(i => i.length > 0)}
- vertical={bind(config.vertical)}
- className={`module tray ${getClassName(props)}`}
- >
- {bind(AstalTray.get_default(), "items").as(i => i.map(TrayItem))}
- </box>
-);
-
-const Network = ({ monitor }: { monitor: Monitor }) => (
- <button
- onClick={(_, event) => {
- const network = AstalNetwork.get_default();
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY) network.wifi.enabled = !network.wifi.enabled;
- else if (event.button === Astal.MouseButton.MIDDLE) {
- if (GLib.find_program_in_path("gnome-control-center"))
- execAsync("app2unit -- gnome-control-center wifi").catch(console.error);
- else {
- network.wifi.scan();
- execAsync(
- "app2unit -- foot -T nmtui -- fish -c 'sleep .1; set -e COLORTERM; TERM=xterm-old nmtui connect'"
- ).catch(() => {}); // Ignore errors
- }
- }
- }}
- setup={self => {
- const network = AstalNetwork.get_default();
- const tooltipText = Variable("");
- const update = () => {
- if (network.primary === AstalNetwork.Primary.WIFI) {
- if (network.wifi.internet === AstalNetwork.Internet.CONNECTED)
- tooltipText.set(`${network.wifi.ssid} | Strength: ${network.wifi.strength}/100`);
- else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING)
- tooltipText.set(`Connecting to ${network.wifi.ssid}`);
- else tooltipText.set("Disconnected");
- } else if (network.primary === AstalNetwork.Primary.WIRED) {
- if (network.wired.internet === AstalNetwork.Internet.CONNECTED)
- tooltipText.set(`Speed: ${network.wired.speed}`);
- else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) tooltipText.set("Connecting");
- else tooltipText.set("Disconnected");
- } else {
- tooltipText.set("Unknown");
- }
- };
- self.hook(network, "notify::primary", update);
- self.hook(network.wifi, "notify::internet", update);
- self.hook(network.wifi, "notify::ssid", update);
- self.hook(network.wifi, "notify::strength", update);
- if (network.wired) {
- self.hook(network.wired, "notify::internet", update);
- self.hook(network.wired, "notify::speed", update);
- }
- update();
- setupCustomTooltip(self, bind(tooltipText));
- }}
- >
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- shown={bind(AstalNetwork.get_default(), "primary").as(p =>
- p === AstalNetwork.Primary.WIFI ? "wifi" : "wired"
- )}
- >
- <stack
- name="wifi"
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- setup={self => {
- const network = AstalNetwork.get_default();
- const update = () => {
- if (network.wifi.internet === AstalNetwork.Internet.CONNECTED)
- self.shown = String(Math.ceil(network.wifi.strength / 25));
- else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting";
- else self.shown = "disconnected";
- };
- self.hook(network.wifi, "notify::internet", update);
- self.hook(network.wifi, "notify::strength", update);
- update();
- }}
- >
- <label className="icon" label="wifi_off" name="disconnected" />
- <label className="icon" label="settings_ethernet" name="connecting" />
- <label className="icon" label="signal_wifi_0_bar" name="0" />
- <label className="icon" label="network_wifi_1_bar" name="1" />
- <label className="icon" label="network_wifi_2_bar" name="2" />
- <label className="icon" label="network_wifi_3_bar" name="3" />
- <label className="icon" label="signal_wifi_4_bar" name="4" />
- </stack>
- <stack
- name="wired"
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- setup={self => {
- const network = AstalNetwork.get_default();
- const update = () => {
- if (network.primary !== AstalNetwork.Primary.WIRED) return;
-
- if (network.wired.internet === AstalNetwork.Internet.CONNECTED) self.shown = "connected";
- else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting";
- else self.shown = "disconnected";
- };
- self.hook(network, "notify::primary", update);
- if (network.wired) self.hook(network.wired, "notify::internet", update);
- update();
- }}
- >
- <label className="icon" label="wifi_off" name="disconnected" />
- <label className="icon" label="settings_ethernet" name="connecting" />
- <label className="icon" label="lan" name="connected" />
- </stack>
- </stack>
- </button>
-);
-
-const BluetoothDevice = ({ monitor, device }: { monitor: Monitor; device: AstalBluetooth.Device }) => (
- <button
- visible={bind(device, "connected")}
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY)
- device.disconnect_device((_, res) => device.disconnect_device_finish(res));
- else if (event.button === Astal.MouseButton.MIDDLE)
- execAsync("app2unit -- blueman-manager").catch(console.error);
- }}
- setup={self => setupCustomTooltip(self, bind(device, "alias"))}
- >
- <icon
- icon={bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "caelestia-bluetooth-device-symbolic"
- )}
- />
- </button>
-);
-
-const Bluetooth = ({ monitor }: { monitor: Monitor }) => (
- <box vertical={bind(config.vertical)} className="bluetooth">
- <button
- onClick={(_, event) => {
- if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity");
- else if (event.button === Astal.MouseButton.SECONDARY) AstalBluetooth.get_default().toggle();
- else if (event.button === Astal.MouseButton.MIDDLE)
- execAsync("app2unit -- blueman-manager").catch(console.error);
- }}
- setup={self => {
- const bluetooth = AstalBluetooth.get_default();
- const tooltipText = Variable("");
- const update = () => {
- const devices = bluetooth.get_devices().filter(d => d.connected);
- tooltipText.set(
- devices.length > 0
- ? `Connected devices: ${devices.map(d => d.alias).join(", ")}`
- : "No connected devices"
- );
- };
- const hookDevice = (device: AstalBluetooth.Device) => {
- self.hook(device, "notify::connected", update);
- self.hook(device, "notify::alias", update);
- };
- bluetooth.get_devices().forEach(hookDevice);
- self.hook(bluetooth, "device-added", (_, device) => {
- hookDevice(device);
- update();
- });
- update();
- setupCustomTooltip(self, bind(tooltipText));
- }}
- >
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={120}
- shown={bind(AstalBluetooth.get_default(), "isPowered").as(p => (p ? "enabled" : "disabled"))}
- >
- <label className="icon" label="bluetooth" name="enabled" />
- <label className="icon" label="bluetooth_disabled" name="disabled" />
- </stack>
- </button>
- {bind(AstalBluetooth.get_default(), "devices").as(d =>
- d.map(d => <BluetoothDevice monitor={monitor} device={d} />)
- )}
- </box>
-);
-
-const StatusIcons = ({ monitor, ...props }: ModuleProps) => (
- <box vertical={bind(config.vertical)} className={`module status-icons ${getClassName(props)}`}>
- <Network monitor={monitor} />
- <Bluetooth monitor={monitor} />
- </box>
-);
-
-const PkgUpdates = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "packages")}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`)
- )
- }
- >
- <box vertical={bind(config.vertical)} className={`module pkg-updates ${getClassName(props)}`}>
- <label className="icon" label="download" />
- <label label={bind(Updates.get_default(), "numUpdates").as(String)} />
- </box>
- </button>
-);
-
-const NotifCount = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "alerts")}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(AstalNotifd.get_default(), "notifications").as(
- n => `${n.length} notification${n.length === 1 ? "" : "s"}`
- )
- )
- }
- >
- <box vertical={bind(config.vertical)} className={`module notif-count ${getClassName(props)}`}>
- <label
- className="icon"
- label={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "notifications_off" : "info"))}
- />
- <revealer
- transitionType={bind(config.vertical).as(v =>
- v ? Gtk.RevealerTransitionType.SLIDE_DOWN : Gtk.RevealerTransitionType.SLIDE_RIGHT
- )}
- transitionDuration={120}
- revealChild={bind(AstalNotifd.get_default(), "dontDisturb").as(d => !d)}
- >
- <label label={bind(AstalNotifd.get_default(), "notifications").as(n => String(n.length))} />
- </revealer>
- </box>
- </button>
-);
-
-const Battery = ({ monitor, ...props }: ModuleProps) => {
- const className = Variable.derive(
- [bind(AstalBattery.get_default(), "percentage"), bind(AstalBattery.get_default(), "charging")],
- (p, c) => `module battery ${c ? "charging" : p < 0.2 ? "low" : ""} ${getClassName(props)}`
- );
- const tooltip = Variable.derive(
- [bind(AstalBattery.get_default(), "timeToEmpty"), bind(AstalBattery.get_default(), "timeToFull")],
- (e, f) => (f > 0 ? `${formatSeconds(f)} until full` : `${formatSeconds(e)} remaining`)
- );
-
- return (
- <box
- visible={bind(AstalBattery.get_default(), "isBattery")}
- vertical={bind(config.vertical)}
- className={bind(className)}
- setup={self => setupCustomTooltip(self, bind(tooltip))}
- onDestroy={() => {
- className.drop();
- tooltip.drop();
- }}
- >
- <label className="icon" label={bind(AstalBattery.get_default(), "percentage").as(getBatteryIcon)} />
- <label label={bind(AstalBattery.get_default(), "percentage").as(p => `${Math.round(p * 100)}%`)} />
- </box>
- );
-};
-
-const DateTimeHoriz = (props: ClassNameProps) => (
- <box className={`module date-time ${getClassName(props)}`}>
- <label className="icon" label="calendar_month" />
- <label
- setup={self => {
- const time = bindCurrentTime(bind(config.modules.dateTime.format), undefined, self);
- self.label = time.get();
- self.hook(time, (_, t) => (self.label = t));
- }}
- />
- </box>
-);
-
-const DateTimeVertical = (props: ClassNameProps) => (
- <box vertical className={`module date-time ${getClassName(props)}`}>
- <label className="icon" label="calendar_month" />
- <label label={bindCurrentTime("%H")} />
- <label label={bindCurrentTime("%M")} />
- </box>
-);
-
-const DateTime = ({ monitor, ...props }: ModuleProps) => (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "time")}
- setup={self =>
- setupCustomTooltip(self, bindCurrentTime(bind(config.modules.dateTime.detailedFormat), undefined, self))
- }
- >
- {bind(config.vertical).as(v => (v ? <DateTimeVertical {...props} /> : <DateTimeHoriz {...props} />))}
- </button>
-);
-
-const Power = ({ monitor, ...props }: ModuleProps) => (
- <button
- className={`module power ${getClassName(props)}`}
- label="power_settings_new"
- onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && App.toggle_window("session")}
- />
-);
-
-const Spacer = ({ onScroll }: { onScroll: (self: Widget.EventBox, event: Astal.ScrollEvent) => void }) => (
- <eventbox onScroll={onScroll}>
- <box vertical={bind(config.vertical)}>
- <ScreenCorner place="topleft" />
- <box expand />
- <ScreenCorner place={bind(config.vertical).as(v => (v ? "bottomleft" : "topright"))} />
- </box>
- </eventbox>
-);
-
-const BrightnessSpacer = ({ monitor }: { monitor: Monitor }) => (
- <Spacer onScroll={(_, event) => (event.delta_y > 0 ? (monitor.brightness -= 0.1) : (monitor.brightness += 0.1))} />
-);
-
-const VolumeSpacer = () => (
- <Spacer
- onScroll={(_, event) => {
- const speaker = AstalWp.get_default()?.audio.defaultSpeaker;
- if (!speaker) return console.error("Unable to connect to WirePlumber.");
- speaker.mute = false;
- if (event.delta_y > 0) speaker.volume -= 0.1;
- else speaker.volume += 0.1;
- }}
- />
-);
-
-const Bar = ({ monitor, layout }: { monitor: Monitor; layout: string }) => {
- const className = Variable.derive(
- [bind(config.vertical), bind(config.style)],
- (v, s) => `bar ${v ? "vertical" : " horizontal"} ${s}`
- );
- const modules =
- layout === "centerbox"
- ? Variable.derive(Object.values(config.layout.centerbox))
- : bind(config.layout.flowbox).as(m => [m]);
-
- const Layout = layout === "centerbox" ? Widget.CenterBox : Widget.Box;
- return (
- <Layout
- vertical={bind(config.vertical)}
- className={bind(className)}
- onDestroy={() => {
- className.drop();
- if (modules instanceof Variable) modules.drop();
- }}
- >
- {bind(modules).as(modules =>
- modules.map((m, i) => (
- <box vertical={bind(config.vertical)}>
- {m.map((n, j) => {
- let beforeSpacer = false;
- if (j < m.length - 1) beforeSpacer = isSpacer(m[j + 1]);
- else if (i < modules.length - 1) beforeSpacer = isSpacer(modules[i + 1][0]);
- let afterSpacer = false;
- if (j > 0) afterSpacer = isSpacer(m[j - 1]);
- else if (i > 0) afterSpacer = isSpacer(modules[i - 1].at(-1));
- const M = getModule(n);
- return (
- <M
- monitor={monitor}
- beforeSpacer={beforeSpacer}
- afterSpacer={afterSpacer}
- first={i === 0 && j === 0}
- last={i === modules.length - 1 && j === m.length - 1}
- />
- );
- })}
- </box>
- ))
- )}
- </Layout>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-bar"
- monitor={monitor.id}
- anchor={bind(config.vertical).as(
- v =>
- Astal.WindowAnchor.TOP |
- Astal.WindowAnchor.LEFT |
- (v ? Astal.WindowAnchor.BOTTOM : Astal.WindowAnchor.RIGHT)
- )}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- >
- <overlay
- passThrough
- overlays={[
- <ScreenCorner visible={bind(config.style).as(s => s !== "embedded")} place="topleft" />,
- <ScreenCorner
- visible={bind(config.style).as(s => s !== "embedded")}
- halign={bind(config.vertical).as(v => (v ? undefined : Gtk.Align.END))}
- valign={bind(config.vertical).as(v => (v ? Gtk.Align.END : undefined))}
- place={bind(config.vertical).as(v => (v ? "bottomleft" : "topright"))}
- />,
- ]}
- >
- {bind(config.layout.type).as(l => (
- <Bar monitor={monitor} layout={l} />
- ))}
- </overlay>
- </window>
-);
diff --git a/src/modules/launcher/actions.tsx b/src/modules/launcher/actions.tsx
deleted file mode 100644
index 40d37b5..0000000
--- a/src/modules/launcher/actions.tsx
+++ /dev/null
@@ -1,522 +0,0 @@
-import { Apps } from "@/services/apps";
-import Palette from "@/services/palette";
-import Schemes, { type Colours } from "@/services/schemes";
-import Wallpapers, { type ICategory, type IWallpaper } from "@/services/wallpapers";
-import { basename } from "@/utils/strings";
-import { notify } from "@/utils/system";
-import { setupCustomTooltip, type FlowBox } from "@/utils/widgets";
-import { bind, execAsync, GLib, readFile, register, type Variable } from "astal";
-import { Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import { setConfig } from "config/funcs";
-import fuzzysort from "fuzzysort";
-import AstalHyprland from "gi://AstalHyprland";
-import { close, ContentBox, type LauncherContent, type Mode } from "./util";
-
-interface IAction {
- icon: string;
- name: string;
- description: string;
- action: (...args: string[]) => void;
- available?: () => boolean;
-}
-
-interface ActionMap {
- [k: string]: IAction;
-}
-
-const variantActions = {
- vibrant: {
- icon: "sentiment_very_dissatisfied",
- name: "Vibrant",
- description: "A high chroma palette. The primary palette's chroma is at maximum.",
- },
- tonalspot: {
- icon: "android",
- name: "Tonal Spot",
- description: "Default for Material theme colours. A pastel palette with a low chroma.",
- },
- expressive: {
- icon: "compare_arrows",
- name: "Expressive",
- description:
- "A medium chroma palette. The primary palette's hue is different from the seed colour, for variety.",
- },
- fidelity: {
- icon: "compare",
- name: "Fidelity",
- description: "Matches the seed colour, even if the seed colour is very bright (high chroma).",
- },
- content: {
- icon: "sentiment_calm",
- name: "Content",
- description: "Almost identical to fidelity.",
- },
- fruitsalad: {
- icon: "nutrition",
- name: "Fruit Salad",
- description: "A playful theme - the seed colour's hue does not appear in the theme.",
- },
- rainbow: {
- icon: "looks",
- name: "Rainbow",
- description: "A playful theme - the seed colour's hue does not appear in the theme.",
- },
- neutral: {
- icon: "contrast",
- name: "Neutral",
- description: "Close to grayscale, a hint of chroma.",
- },
- monochrome: {
- icon: "filter_b_and_w",
- name: "Monochrome",
- description: "All colours are grayscale, no chroma.",
- },
-};
-
-const transparencyActions = {
- off: {
- icon: "blur_off",
- name: "Off",
- description: "Completely opaque",
- },
- low: {
- icon: "blur_circular",
- name: "Low",
- description: "Less transparent",
- },
- normal: {
- icon: "blur_linear",
- name: "Normal",
- description: "Somewhat transparent",
- },
- high: {
- icon: "blur_on",
- name: "High",
- description: "Extremely transparent",
- },
-};
-
-const autocomplete = (entry: Widget.Entry, action: string) => {
- entry.set_text(`${config.actionPrefix.get()}${action} `);
- entry.set_position(-1);
-};
-
-const actions = (mode: Variable<Mode>, entry: Widget.Entry): ActionMap => ({
- apps: {
- icon: "apps",
- name: "Apps",
- description: "Search for apps",
- action: () => {
- mode.set("apps");
- entry.set_text("");
- },
- },
- files: {
- icon: "folder",
- name: "Files",
- description: "Search for files",
- action: () => {
- mode.set("files");
- entry.set_text("");
- },
- },
- math: {
- icon: "calculate",
- name: "Math",
- description: "Do math calculations",
- action: () => {
- mode.set("math");
- entry.set_text("");
- },
- },
- light: {
- icon: "light_mode",
- name: "Light",
- description: "Change scheme to light mode",
- action: () => {
- Palette.get_default().switchMode("light");
- close();
- },
- available: () => Palette.get_default().hasMode("light"),
- },
- dark: {
- icon: "dark_mode",
- name: "Dark",
- description: "Change scheme to dark mode",
- action: () => {
- Palette.get_default().switchMode("dark");
- close();
- },
- available: () => Palette.get_default().hasMode("dark"),
- },
- scheme: {
- icon: "palette",
- name: "Scheme",
- description: "Change the current colour scheme",
- action: () => autocomplete(entry, "scheme"),
- },
- variant: {
- icon: "colors",
- name: "Variant",
- description: "Change the current scheme variant",
- action: () => autocomplete(entry, "variant"),
- available: () => Palette.get_default().scheme === "dynamic",
- },
- wallpaper: {
- icon: "image",
- name: "Wallpaper",
- description: "Change the current wallpaper",
- action: () => autocomplete(entry, "wallpaper"),
- },
- transparency: {
- icon: "opacity",
- name: "Transparency",
- description: "Change shell transparency",
- action: () => autocomplete(entry, "transparency"),
- },
- todo: {
- icon: "checklist",
- name: "Todo",
- description: "Create a todo in Todoist",
- action: (...args) => {
- // If no args, autocomplete cmd
- if (args.length === 0) return autocomplete(entry, "todo");
-
- // If tod not configured, notify
- let token = null;
- try {
- token = JSON.parse(readFile(GLib.get_user_config_dir() + "/tod.cfg")).token;
- } catch {} // Ignore
- if (!token) {
- notify({
- summary: "Tod not configured",
- body: "You need to configure tod first. Run any tod command to do this.",
- icon: "dialog-warning-symbolic",
- urgency: "critical",
- });
- } else {
- // Create todo and notify if configured
- execAsync(`tod t q -c ${args.join(" ")}`).catch(console.error);
- if (config.todo.notify.get())
- notify({
- summary: "Todo created",
- body: `Created todo with content: ${args.join(" ")}`,
- icon: "view-list-bullet-symbolic",
- urgency: "low",
- transient: true,
- actions: {
- "Copy content": () => execAsync(`wl-copy -- ${args.join(" ")}`).catch(console.error),
- View: () => {
- const client = AstalHyprland.get_default().clients.find(c => c.class === "Todoist");
- if (client) client.focus();
- else execAsync("app2unit -- todoist").catch(console.error);
- },
- },
- });
- }
-
- close();
- },
- available: () => !!GLib.find_program_in_path("tod"),
- },
- reload: {
- icon: "refresh",
- name: "Reload",
- description: "Reload app list",
- action: () => {
- Apps.reload();
- entry.set_text("");
- },
- },
- lock: {
- icon: "lock",
- name: "Lock",
- description: "Lock the current session",
- action: () => {
- execAsync("loginctl lock-session").catch(console.error);
- close();
- },
- },
- logout: {
- icon: "logout",
- name: "Logout",
- description: "End the current session",
- action: () => {
- execAsync("uwsm stop").catch(console.error);
- close();
- },
- },
- sleep: {
- icon: "bedtime",
- name: "Sleep",
- description: "Suspend then hibernate",
- action: () => {
- execAsync("systemctl suspend-then-hibernate").catch(console.error);
- close();
- },
- },
- reboot: {
- icon: "cached",
- name: "Reboot",
- description: "Restart the machine",
- action: () => {
- execAsync("systemctl reboot").catch(console.error);
- close();
- },
- },
- hibernate: {
- icon: "downloading",
- name: "Hibernate",
- description: "Suspend to RAM",
- action: () => {
- execAsync("systemctl hibernate").catch(console.error);
- close();
- },
- },
- shutdown: {
- icon: "power_settings_new",
- name: "Shutdown",
- description: "Suspend to disk",
- action: () => {
- execAsync("systemctl poweroff").catch(console.error);
- close();
- },
- },
-});
-
-const Action = ({ args, icon, name, description, action }: IAction & { args: string[] }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => action(...args)}
- setup={self => setupCustomTooltip(self, description)}
- >
- <box>
- <label className="icon" label={icon} />
- <box vertical className="has-sublabel">
- <label truncate xalign={0} label={name} />
- <label truncate xalign={0} label={description} className="sublabel" />
- </box>
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const Swatch = ({ colour }: { colour: string }) => <box className="swatch" css={"background-color: " + colour + ";"} />;
-
-const Scheme = ({ scheme, name, colours }: { scheme?: string; name: string; colours?: Colours }) => {
- const palette = colours![Palette.get_default().mode] ?? colours!.light ?? colours!.dark!;
- return (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- execAsync(`caelestia scheme ${scheme ?? ""} ${name}`).catch(console.error);
- close();
- }}
- >
- <box>
- <box valign={Gtk.Align.CENTER}>
- <box className="swatch big left" css={"background-color: " + palette.base + ";"} />
- <box className="swatch big right" css={"background-color: " + palette.primary + ";"} />
- </box>
- <box vertical className="has-sublabel">
- <label truncate xalign={0} label={scheme ? `${scheme} (${name})` : name} />
- <box className="swatches">
- <Swatch colour={palette.rosewater} />
- <Swatch colour={palette.flamingo} />
- <Swatch colour={palette.pink} />
- <Swatch colour={palette.mauve} />
- <Swatch colour={palette.red} />
- <Swatch colour={palette.maroon} />
- <Swatch colour={palette.peach} />
- <Swatch colour={palette.yellow} />
- <Swatch colour={palette.green} />
- <Swatch colour={palette.teal} />
- <Swatch colour={palette.sky} />
- <Swatch colour={palette.sapphire} />
- <Swatch colour={palette.blue} />
- <Swatch colour={palette.lavender} />
- </box>
- </box>
- </box>
- </button>
- </Gtk.FlowBoxChild>
- );
-};
-
-const Variant = ({ name }: { name: keyof typeof variantActions }) => (
- <Action
- {...variantActions[name]}
- args={[]}
- action={() => {
- execAsync(`caelestia variant ${name}`).catch(console.error);
- close();
- }}
- />
-);
-
-const Wallpaper = ({ path, thumbnails }: IWallpaper) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result wallpaper-container"
- cursor="pointer"
- onClicked={() => {
- execAsync(`caelestia wallpaper -f ${path}`).catch(console.error);
- close();
- }}
- setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))}
- >
- <box
- vertical={config.wallpaper.style.get() !== "compact"}
- className={`wallpaper ${config.wallpaper.style.get()}`}
- >
- <box
- className="thumbnail"
- css={bind(config.wallpaper.style).as(
- s => "background-image: url('" + thumbnails[s as keyof typeof thumbnails] + "');"
- )}
- />
- <label truncate label={basename(path)} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const CategoryThumbnail = ({ style, wallpapers }: { style: string; wallpapers: IWallpaper[] }) => (
- <box className="thumbnail">
- {wallpapers.slice(0, 3).map(w => (
- <box hexpand css={"background-image: url('" + w.thumbnails[style as keyof typeof w.thumbnails] + "');"} />
- ))}
- </box>
-);
-
-const Category = ({ path, wallpapers }: ICategory) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result wallpaper-container"
- cursor="pointer"
- onClicked={() => {
- execAsync(`caelestia wallpaper -d ${path}`).catch(console.error);
- close();
- }}
- setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))}
- >
- <box
- vertical={config.wallpaper.style.get() !== "compact"}
- className={`wallpaper ${config.wallpaper.style.get()}`}
- >
- {bind(config.wallpaper.style).as(s =>
- s === "compact" ? (
- <box
- className="thumbnail"
- css={"background-image: url('" + wallpapers[0].thumbnails.compact + "');"}
- />
- ) : (
- <CategoryThumbnail style={s} wallpapers={wallpapers} />
- )
- )}
- <label truncate label={basename(path)} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const Transparency = ({ amount }: { amount: keyof typeof transparencyActions }) => (
- <Action
- {...transparencyActions[amount]}
- args={[]}
- action={() => {
- setConfig("style.transparency", amount).catch(console.error);
- close();
- }}
- />
-);
-
-@register()
-export default class Actions extends Widget.Box implements LauncherContent {
- #map: ActionMap;
- #list: string[];
-
- #content: FlowBox;
-
- constructor(mode: Variable<Mode>, entry: Widget.Entry) {
- super({ name: "actions", className: "actions" });
-
- this.#map = actions(mode, entry);
- this.#list = Object.keys(this.#map);
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#content.foreach(c => c.destroy());
- const args = search.split(" ");
- const action = args[0].slice(1).toLowerCase();
-
- if (action === "scheme") {
- const scheme = args[1] ?? "";
- const schemes = Object.values(Schemes.get_default().map)
- .flatMap(s => (s.colours ? s.name : Object.values(s.flavours!).map(f => `${f.scheme}-${f.name}`)))
- .filter(s => s !== undefined)
- .sort();
- for (const { target } of fuzzysort.go(scheme, schemes, { all: true })) {
- if (Schemes.get_default().map.hasOwnProperty(target))
- this.#content.add(<Scheme {...Schemes.get_default().map[target]} />);
- else {
- const [scheme, flavour] = target.split("-");
- this.#content.add(<Scheme {...Schemes.get_default().map[scheme].flavours![flavour]} />);
- }
- }
- } else if (action === "variant") {
- const list = Object.keys(variantActions);
-
- for (const { target } of fuzzysort.go(args[1], list, { all: true }))
- this.#content.add(<Variant name={target as keyof typeof variantActions} />);
- } else if (action === "wallpaper") {
- if (args[1]?.toLowerCase() === "random") {
- const list = Wallpapers.get_default().categories;
- for (const { obj } of fuzzysort.go(args[2] ?? "", list, { all: true, key: "path" }))
- this.#content.add(<Category {...obj} />);
- } else {
- const list = Wallpapers.get_default().list;
- let limit = undefined;
- if ((args[1] || !config.wallpaper.showAllEmpty.get()) && config.wallpaper.maxResults.get() > 0)
- limit = config.wallpaper.maxResults.get();
-
- for (const { obj } of fuzzysort.go(args[1] ?? "", list, { all: true, key: "path", limit }))
- this.#content.add(<Wallpaper {...obj} />);
- }
- } else if (action === "transparency") {
- const list = Object.keys(transparencyActions);
-
- for (const { target } of fuzzysort.go(args[1], list, { all: true }))
- this.#content.add(<Transparency amount={target as keyof typeof transparencyActions} />);
- } else {
- const list = this.#list.filter(
- a => this.#map[a].available?.() ?? !config.disabledActions.get().includes(a)
- );
- for (const { target } of fuzzysort.go(action, list, { all: true }))
- this.#content.add(<Action {...this.#map[target]} args={args.slice(1)} />);
- }
- }
-
- handleActivate(search: string): void {
- const args = search.split(" ");
- const action = args[0].slice(1).toLowerCase();
-
- if (action === "scheme" && args[1]?.toLowerCase() === "random") {
- execAsync(`caelestia scheme`).catch(console.error);
- close();
- } else this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
diff --git a/src/modules/launcher/index.tsx b/src/modules/launcher/index.tsx
deleted file mode 100644
index b75ecce..0000000
--- a/src/modules/launcher/index.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import PopupWindow from "@/widgets/popupwindow";
-import { bind, register, Variable } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import Actions from "./actions";
-import Modes from "./modes";
-import type { Mode } from "./util";
-
-const getModeIcon = (mode: Mode) => {
- if (mode === "apps") return "apps";
- if (mode === "files") return "folder";
- if (mode === "math") return "calculate";
- return "search";
-};
-
-const getPrettyMode = (mode: Mode) => {
- if (mode === "apps") return "Apps";
- if (mode === "files") return "Files";
- if (mode === "math") return "Math";
- return mode;
-};
-
-const isAction = (text: string, action: string = "") => text.startsWith(config.actionPrefix.get() + action);
-
-const SearchBar = ({ mode, entry }: { mode: Variable<Mode>; entry: Widget.Entry }) => (
- <box className="search-bar">
- <box className="mode">
- <label className="icon" label={bind(mode).as(getModeIcon)} />
- <label label={bind(mode).as(getPrettyMode)} />
- </box>
- {entry}
- </box>
-);
-
-const ModeSwitcher = ({ mode, modes }: { mode: Variable<Mode>; modes: Mode[] }) => (
- <box homogeneous hexpand className="mode-switcher">
- {modes.map(m => (
- <button
- className={bind(mode).as(c => `mode ${c === m ? "selected" : ""}`)}
- cursor="pointer"
- onClicked={() => mode.set(m)}
- >
- <box halign={Gtk.Align.CENTER}>
- <label className="icon" label={getModeIcon(m)} />
- <label label={getPrettyMode(m)} />
- </box>
- </button>
- ))}
- </box>
-);
-
-@register()
-export default class Launcher extends PopupWindow {
- readonly mode: Variable<Mode>;
-
- constructor() {
- const entry = (
- <entry
- hexpand
- className="entry"
- placeholderText={bind(config.actionPrefix).as(p => `Type "${p}" for subcommands`)}
- />
- ) as Widget.Entry;
- const mode = Variable<Mode>("apps");
- const content = Modes();
- const actions = new Actions(mode, entry);
- const className = Variable.derive([mode, config.style], (m, s) => `launcher ${m} ${s}`);
-
- super({
- name: "launcher",
- anchor:
- Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
- keymode: Astal.Keymode.EXCLUSIVE,
- exclusivity: Astal.Exclusivity.IGNORE,
- borderWidth: 0,
- onKeyPressEvent(_, event) {
- const keyval = event.get_keyval()[1];
- // Focus entry on typing
- if (!entry.isFocus && keyval >= 32 && keyval <= 126) {
- entry.text += String.fromCharCode(keyval);
- entry.grab_focus();
- entry.set_position(-1);
-
- // Consume event, if not consumed it will duplicate character in entry
- return true;
- }
- },
- child: (
- <box
- vertical
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- className={bind(className)}
- onDestroy={() => className.drop()}
- >
- <SearchBar mode={mode} entry={entry} />
- <stack
- expand
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={100}
- shown={bind(entry, "text").as(t => (isAction(t) ? "actions" : "content"))}
- >
- <stack
- name="content"
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={200}
- shown={bind(mode)}
- >
- {Object.values(content)}
- </stack>
- {actions}
- </stack>
- <ModeSwitcher mode={mode} modes={Object.keys(content) as Mode[]} />
- </box>
- ),
- });
-
- this.mode = mode;
-
- content[mode.get()].updateContent(entry.get_text());
- this.hook(mode, (_, v: Mode) => {
- entry.set_text("");
- content[v].updateContent(entry.get_text());
- });
- this.hook(entry, "changed", () =>
- (isAction(entry.get_text()) ? actions : content[mode.get()]).updateContent(entry.get_text())
- );
- this.hook(entry, "activate", () => {
- (isAction(entry.get_text()) ? actions : content[mode.get()]).handleActivate(entry.get_text());
- if (mode.get() === "math" && !isAction(entry.get_text())) entry.set_text(""); // Cause math mode doesn't auto clear
- });
-
- // Clear search on hide if not in math mode or creating a todo
- this.connect("hide", () => {
- if ((mode.get() !== "math" || isAction(entry.get_text())) && !isAction(entry.get_text(), "todo"))
- entry.set_text("");
- });
- }
-
- open(mode: Mode) {
- this.mode.set(mode);
- this.show();
- }
-}
diff --git a/src/modules/launcher/modes.tsx b/src/modules/launcher/modes.tsx
deleted file mode 100644
index e278779..0000000
--- a/src/modules/launcher/modes.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-import { Apps as AppsService } from "@/services/apps";
-import MathService, { type HistoryItem } from "@/services/math";
-import { getAppCategoryIcon } from "@/utils/icons";
-import { launch } from "@/utils/system";
-import { type FlowBox, setupCustomTooltip } from "@/utils/widgets";
-import { bind, execAsync, Gio, register, Variable } from "astal";
-import { Astal, Gtk, Widget } from "astal/gtk3";
-import { launcher as config } from "config";
-import type AstalApps from "gi://AstalApps";
-import { close, ContentBox, type LauncherContent, limitLength } from "./util";
-
-const AppResult = ({ app }: { app: AstalApps.Application }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- launch(app);
- close();
- }}
- setup={self => setupCustomTooltip(self, app.description ? `${app.name}: ${app.description}` : app.name)}
- >
- <box>
- {app.iconName && Astal.Icon.lookup_icon(app.iconName) ? (
- <icon className="icon" icon={app.iconName} />
- ) : (
- <label className="icon" label={getAppCategoryIcon(app)} />
- )}
- <label truncate label={app.name} />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const FileResult = ({ path }: { path: string }) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- execAsync([
- "bash",
- "-c",
- `dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:'file://${path}' string:'' || app2unit -O '${path}'`,
- ]).catch(console.error);
- close();
- }}
- >
- <box setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))}>
- <icon
- className="icon"
- gicon={
- Gio.File.new_for_path(path)
- .query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, null)
- .get_icon()!
- }
- />
- <label
- truncate
- label={
- path.replace(HOME, "~").length > config.files.shortenThreshold.get()
- ? path
- .replace(HOME, "~")
- .split("/")
- .map((n, i, arr) => (i === 0 || i === arr.length - 1 ? n : n.slice(0, 1)))
- .join("/")
- : path.replace(HOME, "~")
- }
- />
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-const MathResult = ({ icon, equation, result }: HistoryItem) => (
- <Gtk.FlowBoxChild visible canFocus={false}>
- <button
- className="result"
- cursor="pointer"
- onClicked={() => {
- execAsync(["wl-copy", "--", result]).catch(console.error);
- close();
- }}
- setup={self => setupCustomTooltip(self, `${equation} -> ${result}`)}
- >
- <box>
- <label className="icon" label={icon} />
- <box vertical className="has-sublabel">
- <label truncate xalign={0} label={equation} />
- <label truncate xalign={0} label={result} className="sublabel" />
- </box>
- </box>
- </button>
- </Gtk.FlowBoxChild>
-);
-
-@register()
-class Apps extends Widget.Box implements LauncherContent {
- #content: FlowBox;
-
- constructor() {
- super({ name: "apps", className: "apps" });
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#content.foreach(c => c.destroy());
- for (const app of limitLength(AppsService.fuzzy_query(search), config.apps))
- this.#content.add(<AppResult app={app} />);
- }
-
- handleActivate(): void {
- this.#content.get_child_at_index(0)?.get_child()?.grab_focus();
- this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
-
-@register()
-class Files extends Widget.Box implements LauncherContent {
- #content: FlowBox;
-
- constructor() {
- super({ name: "files", className: "files" });
-
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- execAsync(["fd", ...config.files.fdOpts.get(), search, HOME])
- .then(out => {
- this.#content.foreach(c => c.destroy());
- const paths = out.split("\n").filter(path => path);
- for (const path of limitLength(paths, config.files)) this.#content.add(<FileResult path={path} />);
- })
- .catch(() => {}); // Ignore errors
- }
-
- handleActivate(): void {
- this.#content.get_child_at_index(0)?.get_child()?.grab_focus();
- this.#content.get_child_at_index(0)?.get_child()?.activate();
- }
-}
-
-@register()
-class Math extends Widget.Box implements LauncherContent {
- #showResult: Variable<boolean>;
- #result: Variable<HistoryItem>;
- #content: FlowBox;
-
- constructor() {
- super({ name: "math", className: "math", vertical: true });
-
- this.#showResult = Variable(false);
- this.#result = Variable({ equation: "", result: "", icon: "" });
- this.#content = (<ContentBox />) as FlowBox;
-
- this.add(
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(this.#showResult)}
- >
- <box vertical className="preview">
- <box className="result">
- <label className="icon" label={bind(this.#result).as(r => r.icon)} />
- <box vertical>
- <label xalign={0} label="Result" />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={bind(this.#result).as(r => r.result)}
- />
- </box>
- </box>
- <box visible={bind(config.style).as(s => s === "lines")} className="separator" />
- </box>
- </revealer>
- );
- this.add(
- <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
- {this.#content}
- </scrollable>
- );
- }
-
- updateContent(search: string): void {
- this.#showResult.set(search.length > 0);
- this.#result.set(MathService.get_default().evaluate(search));
-
- this.#content.foreach(c => c.destroy());
- for (const item of limitLength(MathService.get_default().history, config.math))
- this.#content.add(<MathResult {...item} />);
- }
-
- handleActivate(search: string): void {
- if (!search) return;
- MathService.get_default().commit();
- const res = this.#result.get();
- // Copy and close if not assignment, help or error
- if (!["equal", "help", "error"].includes(res.icon)) {
- execAsync(["wl-copy", "--", res.result]).catch(console.error);
- close();
- }
- }
-}
-
-export default () => ({
- apps: new Apps(),
- files: new Files(),
- math: new Math(),
-});
diff --git a/src/modules/launcher/util.tsx b/src/modules/launcher/util.tsx
deleted file mode 100644
index 8288588..0000000
--- a/src/modules/launcher/util.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { FlowBox } from "@/utils/widgets";
-import type { Variable } from "astal";
-import { App, Gtk } from "astal/gtk3";
-
-export type Mode = "apps" | "files" | "math";
-
-export interface LauncherContent {
- updateContent(search: string): void;
- handleActivate(search: string): void;
-}
-
-export const close = () => App.get_window("launcher")?.hide();
-
-export const limitLength = <T,>(arr: T[], cfg: { maxResults: Variable<number> }) =>
- cfg.maxResults.get() > 0 && arr.length > cfg.maxResults.get() ? arr.slice(0, cfg.maxResults.get()) : arr;
-
-export const ContentBox = () => (
- <FlowBox homogeneous valign={Gtk.Align.START} minChildrenPerLine={2} maxChildrenPerLine={2} />
-);
diff --git a/src/modules/mediadisplay/index.tsx b/src/modules/mediadisplay/index.tsx
deleted file mode 100644
index 307087c..0000000
--- a/src/modules/mediadisplay/index.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import { bind, Variable } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-import AstalMpris from "gi://AstalMpris";
-import Visualiser from "./visualiser";
-
-type Selected = Variable<AstalMpris.Player | null>;
-
-const bindIcon = (player: AstalMpris.Player) =>
- bind(player, "identity").as(i => {
- const icon = `caelestia-${i?.toLowerCase().replaceAll(" ", "-")}-symbolic`;
- return Astal.Icon.lookup_icon(icon) ? icon : "caelestia-media-generic-symbolic";
- });
-
-const PlayerButton = ({
- player,
- selected,
- showDropdown,
-}: {
- player: AstalMpris.Player;
- selected: Selected;
- showDropdown: Variable<boolean>;
-}) => (
- <button
- cursor="pointer"
- onClicked={() => {
- showDropdown.set(false);
- selected.set(player);
- }}
- >
- <box className="identity" halign={Gtk.Align.CENTER}>
- <label label={bind(player, "identity").as(i => i ?? "-")} />
- <label label="•" />
- <label label={bind(player, "title").as(t => t ?? "-")} />
- </box>
- </button>
-);
-
-const Selector = ({ player, selected }: { player?: AstalMpris.Player; selected: Selected }) => {
- const showDropdown = Variable(false);
-
- return (
- <box vertical valign={Gtk.Align.START} className="selector">
- <button
- sensitive={bind(Players.get_default(), "list").as(ps => ps.length > 1)}
- cursor="pointer"
- onClicked={() => showDropdown.set(!showDropdown.get())}
- >
- <box className="identity" halign={Gtk.Align.CENTER}>
- <icon icon={player ? bindIcon(player) : "caelestia-media-none-symbolic"} />
- <label label={player ? bind(player, "identity").as(i => i ?? "") : "No media"} />
- </box>
- </button>
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(showDropdown)}
- >
- <box vertical className="list">
- {bind(Players.get_default(), "list").as(ps =>
- ps
- .filter(p => p !== player)
- .map(p => <PlayerButton player={p} selected={selected} showDropdown={showDropdown} />)
- )}
- </box>
- </revealer>
- </box>
- );
-};
-
-const NoMedia = ({ selected }: { selected: Selected }) => (
- <box>
- <box homogeneous halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="cover-art">
- <label xalign={0.36} label="" />
- </box>
- <box>
- <box vertical className="details">
- <label truncate xalign={0} className="title" label="No media" />
- <label truncate xalign={0} className="artist" label="Try play something!" />
- <box halign={Gtk.Align.START} className="controls">
- <button sensitive={false} label="skip_previous" />
- <button sensitive={false} label="play_arrow" />
- <button sensitive={false} label="skip_next" />
- </box>
- </box>
- <box className="center-module">
- <overlay
- expand
- overlay={<label halign={Gtk.Align.CENTER} valign={Gtk.Align.END} className="time" label="-1:-1" />}
- >
- <Visualiser />
- </overlay>
- </box>
- <Selector selected={selected} />
- </box>
- </box>
-);
-
-const Player = ({ player, selected }: { player: AstalMpris.Player; selected: Selected }) => {
- const time = Variable.derive(
- [bind(player, "position"), bind(player, "length")],
- (p, l) => lengthStr(p) + " / " + lengthStr(l)
- );
-
- return (
- <box>
- <box
- homogeneous
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.CENTER}
- 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.36} label="" />))}
- </box>
- <box>
- <box vertical className="details">
- <label truncate xalign={0} className="title" label={bind(player, "title").as(t => t ?? "-")} />
- <label truncate xalign={0} className="artist" label={bind(player, "artist").as(t => t ?? "-")} />
- <box halign={Gtk.Align.START} className="controls">
- <button
- sensitive={bind(player, "canGoPrevious")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="skip_previous"
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.play_pause()}
- label={bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "pause" : "play_arrow"
- )}
- />
- <button
- sensitive={bind(player, "canGoNext")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="skip_next"
- />
- </box>
- </box>
- <box className="center-module">
- <overlay
- expand
- overlay={
- <label
- halign={Gtk.Align.CENTER}
- valign={Gtk.Align.END}
- className="time"
- label={bind(time)}
- onDestroy={() => time.drop()}
- />
- }
- >
- <Visualiser />
- </overlay>
- </box>
- <Selector player={player} selected={selected} />
- </box>
- </box>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => {
- const selected = Variable(Players.get_default().lastPlayer);
- selected.observe(Players.get_default(), "notify::last-player", () => Players.get_default().lastPlayer);
-
- return (
- <window
- application={App}
- name={`mediadisplay${monitor.id}`}
- namespace="caelestia-mediadisplay"
- monitor={monitor.id}
- anchor={Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- visible={false}
- >
- <box className="mediadisplay" onDestroy={() => selected.drop()}>
- {bind(selected).as(p =>
- p ? <Player player={p} selected={selected} /> : <NoMedia selected={selected} />
- )}
- </box>
- </window>
- );
-};
diff --git a/src/modules/mediadisplay/visualiser.tsx b/src/modules/mediadisplay/visualiser.tsx
deleted file mode 100644
index d788e7b..0000000
--- a/src/modules/mediadisplay/visualiser.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Gtk } from "astal/gtk3";
-import cairo from "cairo";
-import AstalCava from "gi://AstalCava";
-import PangoCairo from "gi://PangoCairo";
-
-export default () => (
- <drawingarea
- className="visualiser"
- setup={self => {
- const cava = AstalCava.get_default();
-
- if (cava) {
- cava.set_stereo(true);
- cava.set_noise_reduction(0.77);
- cava.set_input(AstalCava.Input.PIPEWIRE);
-
- self.hook(cava, "notify::values", () => self.queue_draw());
- self.connect("size-allocate", () => {
- const width = self.get_allocated_width();
- const barWidth = self
- .get_style_context()
- .get_property("min-width", Gtk.StateFlags.NORMAL) as number;
- const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right;
- const bars = Math.floor((width - gaps) / (barWidth + gaps));
- if (bars > 0) cava.set_bars(bars % 2 ? bars : bars - 1);
- });
- }
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const { width, height } = self.get_allocation();
-
- if (!cava) {
- // Show error text if cava unavailable
- const fg = self.get_style_context().get_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
- const layout = self.create_pango_layout("Visualiser module requires Cava");
- const [w, h] = layout.get_pixel_size();
- cr.moveTo((width - w) / 2, (height - h) / 2);
- cr.setAntialias(cairo.Antialias.BEST);
- PangoCairo.show_layout(cr, layout);
-
- return;
- }
-
- const bg = self.get_style_context().get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
- const barWidth = self.get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL) as number;
- const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right;
-
- const values = cava.get_values();
- const len = values.length - 1;
- const radius = barWidth / 2;
- const xOff = (width - len * (barWidth + gaps) - gaps) / 2 - radius;
- const center = height / 2;
- const half = len / 2;
-
- const renderPill = (x: number, value: number) => {
- x = x * (barWidth + gaps) + xOff;
- value *= center;
- cr.arc(x, center + value, radius, 0, Math.PI);
- cr.arc(x, center - value, radius, Math.PI, Math.PI * 2);
- cr.fill();
- };
-
- // Render channels facing each other
- for (let i = half - 1; i >= 0; i--) renderPill(half - i, values[i]);
- for (let i = half; i < len; i++) renderPill(i + 1, values[i]);
- });
- }}
- />
-);
diff --git a/src/modules/navbar.tsx b/src/modules/navbar.tsx
deleted file mode 100644
index 35d3900..0000000
--- a/src/modules/navbar.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { capitalize } from "@/utils/strings";
-import type { AstalWidget } from "@/utils/types";
-import { bind, execAsync, Variable } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-import { navbar as config } from "config";
-import AstalHyprland from "gi://AstalHyprland";
-import Pango from "gi://Pango";
-import SideBar, { awaitSidebar, paneNames, switchPane, type PaneName } from "./sidebar";
-
-const layerNames = ["mediadisplay"] as const;
-type LayerName = `${(typeof layerNames)[number]}${number}`;
-
-const specialWsNames = ["sysmon", "communication", "music", "todo"] as const;
-type SpecialWsName = (typeof specialWsNames)[number];
-
-const getPaneIcon = (name: PaneName) => {
- if (name === "dashboard") return "dashboard";
- if (name === "audio") return "tune";
- if (name === "connectivity") return "settings_ethernet";
- if (name === "packages") return "package_2";
- if (name === "alerts") return "notifications";
- return "date_range";
-};
-
-const getLayerIcon = (name: LayerName) => {
- return "graphic_eq";
-};
-
-const getSpecialWsIcon = (name: SpecialWsName) => {
- if (name === "sysmon") return "speed";
- if (name === "communication") return "communication";
- if (name === "music") return "music_note";
- return "checklist";
-};
-
-const hookIsCurrent = (
- self: AstalWidget,
- sidebar: Variable<SideBar | null>,
- name: PaneName,
- callback: (isCurrent: boolean) => void
-) => {
- const unsub = sidebar.subscribe(s => {
- if (!s) return;
- self.hook(s.shown, (_, v) => callback(s.visible && v === name));
- self.hook(s, "notify::visible", () => callback(s.visible && s.shown.get() === name));
- callback(s.visible && s.shown.get() === name);
- unsub();
- });
-};
-
-const PaneButton = ({
- monitor,
- name,
- sidebar,
-}: {
- monitor: Monitor;
- name: PaneName;
- sidebar: Variable<SideBar | null>;
-}) => (
- <button
- cursor="pointer"
- onClicked={() => switchPane(monitor, name)}
- setup={self => hookIsCurrent(self, sidebar, name, c => self.toggleClassName("current", c))}
- >
- <box vertical className="nav-button">
- <label className="icon" label={getPaneIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- setup={self => {
- let isCurrent = false;
- hookIsCurrent(self, sidebar, name, c => {
- isCurrent = c;
- self.set_reveal_child(config.showLabels.get() && c);
- });
- self.hook(config.showLabels, (_, v) => self.set_reveal_child(v && isCurrent));
- }}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
-);
-
-const LayerButton = ({ name }: { name: LayerName }) => (
- <button
- cursor="pointer"
- onClicked={() => App.toggle_window(name)}
- setup={self =>
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === name) self.toggleClassName("current", window.visible);
- })
- }
- >
- <box vertical className="nav-button">
- <label className="icon" label={getLayerIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- setup={self => {
- let visible = false;
- self.hook(config.showLabels, (_, v) => self.toggleClassName(v && visible));
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === name)
- self.toggleClassName("current", config.showLabels.get() && window.visible);
- });
- }}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
-);
-
-const SpecialWsButton = ({ name }: { name: SpecialWsName }) => {
- const revealChild = Variable.derive(
- [config.showLabels, bind(AstalHyprland.get_default(), "focusedClient")],
- (l, c) => l && c?.get_workspace().get_name() === `special:${name}`
- );
-
- return (
- <button
- className={bind(AstalHyprland.get_default(), "focusedClient").as(c =>
- c?.get_workspace().get_name() === `special:${name}` ? "current" : ""
- )}
- cursor="pointer"
- onClicked={() => execAsync(`caelestia toggle ${name}`).catch(console.error)}
- >
- <box vertical className="nav-button">
- <label className="icon" label={getSpecialWsIcon(name)} />
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={150}
- revealChild={bind(revealChild)}
- onDestroy={() => revealChild.drop()}
- >
- <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} />
- </revealer>
- </box>
- </button>
- );
-};
-
-export default ({ monitor }: { monitor: Monitor }) => {
- const sidebar = Variable<SideBar | null>(null);
- awaitSidebar(monitor).then(s => sidebar.set(s));
-
- return (
- <window
- namespace="caelestia-navbar"
- monitor={monitor.id}
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM}
- exclusivity={Astal.Exclusivity.EXCLUSIVE}
- visible={config.persistent.get()}
- setup={self => {
- const hyprland = AstalHyprland.get_default();
- const visible = Variable(config.persistent.get());
-
- visible.poll(100, () => {
- const width = self.visible
- ? Math.max(config.appearWidth.get(), self.get_allocated_width())
- : config.appearWidth.get();
- return hyprland.get_cursor_position().x < width;
- });
- if (config.persistent.get()) visible.stopPoll();
-
- self.hook(config.persistent, (_, v) => {
- if (v) {
- visible.stopPoll();
- visible.set(true);
- } else visible.startPoll();
- });
-
- self.hook(visible, (_, v) => self.set_visible(v));
- self.connect("destroy", () => visible.drop());
- }}
- >
- <eventbox
- onScroll={(_, event) => {
- const shown = sidebar.get()?.shown;
- if (!shown) return;
- const idx = paneNames.indexOf(shown.get());
- if (event.delta_y > 0) shown.set(paneNames[Math.min(paneNames.length - 1, idx + 1)]);
- else shown.set(paneNames[Math.max(0, idx - 1)]);
- }}
- >
- <box vertical className="navbar">
- {paneNames.map(n => (
- <PaneButton monitor={monitor} name={n} sidebar={sidebar} />
- ))}
- {layerNames.map(n => (
- <LayerButton name={`${n}${monitor.id}`} />
- ))}
- <box vexpand />
- {specialWsNames.map(n => (
- <SpecialWsButton name={n} />
- ))}
- </box>
- </eventbox>
- </window>
- );
-};
diff --git a/src/modules/notifpopups.tsx b/src/modules/notifpopups.tsx
deleted file mode 100644
index cb5984d..0000000
--- a/src/modules/notifpopups.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { setupChildClickthrough } from "@/utils/widgets";
-import Notification from "@/widgets/notification";
-import { Astal, Gtk } from "astal/gtk3";
-import { notifpopups as config } from "config";
-import AstalNotifd from "gi://AstalNotifd";
-import type SideBar from "./sidebar";
-import { awaitSidebar } from "./sidebar";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- monitor={monitor.id}
- namespace="caelestia-notifpopups"
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM}
- >
- <box
- vertical
- valign={Gtk.Align.START}
- className="notifpopups"
- setup={self => {
- const notifd = AstalNotifd.get_default();
- const map = new Map<number, Notification>();
-
- self.hook(notifd, "notified", (self, id) => {
- if (notifd.dontDisturb) return;
-
- const notification = notifd.get_notification(id);
-
- const popup = (<Notification popup notification={notification} />) as Notification;
- popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
- map.get(notification.id)?.destroyWithAnims();
- map.set(notification.id, popup);
-
- self.add(
- <eventbox
- onClick={(_, event) => {
- // Activate notif or go to notif center on primary click
- if (event.button === Astal.MouseButton.PRIMARY) {
- if (notification.actions.length === 1)
- notification.invoke(notification.actions[0].id);
- else {
- sidebar?.shown.set("alerts");
- sidebar?.show();
- popup.destroyWithAnims();
- }
- }
- // Dismiss on middle click
- else if (event.button === Astal.MouseButton.MIDDLE) notification.dismiss();
- }}
- // Close on hover lost
- onHoverLost={() => popup.destroyWithAnims()}
- setup={self => self.hook(popup, "destroy", () => self.destroy())}
- >
- {popup}
- </eventbox>
- );
-
- // Limit number of popups
- if (config.maxPopups.get() > 0 && self.children.length > config.maxPopups.get())
- map.values().next().value?.destroyWithAnims();
- });
- self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
-
- let sidebar: SideBar | null = null;
- awaitSidebar(monitor).then(s => (sidebar = s));
-
- // Change input region to child region so can click through empty space
- setupChildClickthrough(self);
- }}
- />
- </window>
-);
diff --git a/src/modules/osds.tsx b/src/modules/osds.tsx
deleted file mode 100644
index 0f38823..0000000
--- a/src/modules/osds.tsx
+++ /dev/null
@@ -1,327 +0,0 @@
-import Monitors, { type Monitor } from "@/services/monitors";
-import { capitalize } from "@/utils/strings";
-import PopupWindow from "@/widgets/popupwindow";
-import { bind, execAsync, register, timeout, Variable, type Time } from "astal";
-import { App, Astal, Gtk, Widget } from "astal/gtk3";
-import cairo from "cairo";
-import { osds as config } from "config";
-import AstalWp from "gi://AstalWp";
-import Cairo from "gi://cairo";
-import Pango from "gi://Pango";
-import PangoCairo from "gi://PangoCairo";
-
-const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL);
-const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number;
-
-const mix = (a: number, b: number, r: number) => a * r + b * (1 - r);
-
-const pangoWeightToStr = (weight: Pango.Weight) => {
- switch (weight) {
- case Pango.Weight.ULTRALIGHT:
- return "UltraLight";
- case Pango.Weight.LIGHT:
- return "Light";
- case Pango.Weight.BOLD:
- return "Bold";
- case Pango.Weight.ULTRABOLD:
- return "UltraBold";
- case Pango.Weight.HEAVY:
- return "Heavy";
- default:
- return "Normal";
- }
-};
-
-const SliderOsd = ({
- fillIcons,
- monitor,
- type,
- windowSetup,
- className = "",
- initValue,
- drawAreaSetup,
-}: {
- fillIcons?: boolean;
- monitor?: Monitor;
- type: "volume" | "brightness";
- windowSetup: (self: Widget.Window, show: () => void) => void;
- className?: string;
- initValue: number;
- drawAreaSetup: (self: Widget.DrawingArea, icon: Variable<string>) => void;
-}) => (
- <PopupWindow
- name={type}
- monitor={monitor?.id}
- keymode={Astal.Keymode.NONE}
- anchor={bind(config[type].position)}
- margin={bind(config[type].margin)}
- setup={self => {
- let time: Time | null = null;
- const hideAfterTimeout = () => {
- time?.cancel();
- time = timeout(config[type].hideDelay.get(), () => self.hide());
- };
- self.connect("show", hideAfterTimeout);
- windowSetup(self, () => {
- self.show();
- hideAfterTimeout();
- });
- }}
- >
- <box className={type}>
- <drawingarea
- className={`inner ${className}`}
- css={"font-size: " + initValue + "px;"}
- setup={self => {
- const halfPi = Math.PI / 2;
- const vertical =
- config[type].position.get() === Astal.WindowAnchor.LEFT ||
- config[type].position.get() === Astal.WindowAnchor.RIGHT;
-
- const icon = Variable("");
- drawAreaSetup(self, icon);
- self.hook(icon, () => self.queue_draw());
-
- // Init size
- const styleContext = self.get_style_context();
- const width = getNumStyle(styleContext, "min-width");
- const height = getNumStyle(styleContext, "min-height");
- if (vertical) self.set_size_request(height, width);
- else self.set_size_request(width, height);
-
- let fontDesc: Pango.FontDescription | null = null;
-
- self.connect("draw", (_, cr: cairo.Context) => {
- const parent = self.get_parent();
- if (!parent) return;
-
- const styleContext = self.get_style_context();
- const pContext = parent.get_style_context();
-
- let width = getNumStyle(styleContext, "min-width");
- let height = getNumStyle(styleContext, "min-height");
-
- const progressValue = getNumStyle(styleContext, "font-size");
- let radius = getNumStyle(pContext, "border-radius");
- // Flatten when near 0, do before swap cause its simpler
- radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
-
- if (vertical) [width, height] = [height, width]; // Swap if vertical
- self.set_size_request(width, height);
-
- const progressPosition = vertical
- ? height * (1 - progressValue) + radius // Top is 0, but we want it to start from the bottom
- : width * progressValue - radius;
-
- const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
-
- // Background
- if (vertical) {
- cr.arc(radius, progressPosition, radius, -Math.PI, -halfPi); // Top left
- cr.arc(width - radius, progressPosition, radius, -halfPi, 0); // Top right
- cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
- } else {
- cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
- cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
- cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
- }
- cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
- cr.fill();
-
- const fg = pContext.get_background_color(Gtk.StateFlags.NORMAL);
- cr.setAntialias(cairo.Antialias.BEST);
-
- // Progress number, at top/right
- let nw = 0;
- let nh = 0;
- if (config[type].showValue.get()) {
- const numLayout = parent.create_pango_layout(String(Math.round(progressValue * 100)));
- [nw, nh] = numLayout.get_pixel_size();
- let diff;
- if (vertical) {
- diff = ((1 - progressValue) * height) / nh;
- cr.moveTo((width - nw) / 2, radius / 2);
- } else {
- diff = ((1 - progressValue) * width) / nw;
- cr.moveTo(width - nw - radius, (height - nh) / 2);
- }
- diff = Math.max(0, Math.min(1, diff));
-
- cr.setSourceRGBA(
- mix(bg.red, fg.red, diff),
- mix(bg.green, fg.green, diff),
- mix(bg.blue, fg.blue, diff),
- mix(bg.alpha, fg.alpha, diff)
- );
-
- PangoCairo.show_layout(cr, numLayout);
- }
-
- // Progress icon, follows progress
- if (fontDesc === null) {
- const weight = pangoWeightToStr(getStyle(pContext, "font-weight") as Pango.Weight);
- const size = getNumStyle(pContext, "font-size") * 1.5;
- fontDesc = Pango.font_description_from_string(
- `Material Symbols Rounded ${weight} ${size}px`
- );
- // Ugh GTK CSS doesn't support font-variations, so you need to manually create the layout and font desc instead of using Gtk.Widget#create_pango_layout
- if (fillIcons) fontDesc.set_variations("FILL=1");
- }
-
- const iconLayout = PangoCairo.create_layout(cr);
- iconLayout.set_font_description(fontDesc);
- iconLayout.set_text(icon.get(), -1);
-
- const [iw, ih] = iconLayout.get_pixel_size();
- let diff;
- if (vertical) {
- diff = (progressValue * height) / ih;
- cr.moveTo(
- (width - iw) / 2,
- Math.max(nh, Math.min(height - ih, progressPosition - ih / 2 + radius))
- );
- } else {
- diff = (progressValue * width) / iw;
- cr.moveTo(
- Math.min(
- width - nw * 1.1 - iw - radius,
- Math.max(0, progressPosition - iw / 2 - radius)
- ),
- (height - ih) / 2
- );
- }
- diff = Math.max(0, Math.min(1, diff));
-
- cr.setSourceRGBA(
- mix(fg.red, bg.red, diff),
- mix(fg.green, bg.green, diff),
- mix(fg.blue, bg.blue, diff),
- mix(fg.alpha, bg.alpha, diff)
- );
-
- PangoCairo.show_layout(cr, iconLayout);
- });
- }}
- />
- </box>
- </PopupWindow>
-);
-
-const Volume = ({ audio }: { audio: AstalWp.Audio }) => (
- <SliderOsd
- fillIcons
- type="volume"
- windowSetup={(self, show) => {
- self.hook(audio.defaultSpeaker, "notify::volume", show);
- self.hook(audio.defaultSpeaker, "notify::mute", show);
- }}
- className={audio.defaultSpeaker.mute ? "mute" : ""}
- initValue={audio.defaultSpeaker.volume}
- drawAreaSetup={(self, icon) => {
- const updateIcon = () => {
- if (/head(phone|set)/i.test(audio.defaultSpeaker.icon)) icon.set("headphones");
- else if (audio.defaultSpeaker.mute) icon.set("no_sound");
- else if (audio.defaultSpeaker.volume === 0) icon.set("volume_mute");
- else if (audio.defaultSpeaker.volume <= 0.5) icon.set("volume_down");
- else icon.set("volume_up");
- };
- updateIcon();
- self.hook(audio.defaultSpeaker, "notify::icon", updateIcon);
- self.hook(audio.defaultSpeaker, "notify::mute", () => {
- updateIcon();
- self.toggleClassName("mute", audio.defaultSpeaker.mute);
- });
- self.hook(audio.defaultSpeaker, "notify::volume", () => {
- updateIcon();
- self.css = `font-size: ${audio.defaultSpeaker.volume}px`;
- });
- }}
- />
-);
-
-const Brightness = ({ monitor }: { monitor: Monitor }) => (
- <SliderOsd
- monitor={monitor}
- type="brightness"
- windowSetup={(self, show) => self.hook(monitor, "notify::brightness", show)}
- initValue={monitor.brightness}
- drawAreaSetup={(self, icon) => {
- const update = () => {
- if (monitor.brightness > 0.66) icon.set("brightness_high");
- else if (monitor.brightness > 0.33) icon.set("brightness_medium");
- else if (monitor.brightness > 0) icon.set("brightness_low");
- else icon.set("brightness_empty");
- self.css = `font-size: ${monitor.brightness}px`;
- };
- self.hook(monitor, "notify::brightness", update);
- update();
- }}
- />
-);
-
-@register()
-class LockOsd extends Widget.Window {
- readonly lockType: "caps" | "num";
-
- #timeout: Time | null = null;
-
- constructor({ type, icon, right }: { type: "caps" | "num"; icon: string; right?: boolean }) {
- super({
- visible: false,
- name: `lock-${type}`,
- application: App,
- namespace: `caelestia-lock-${type}`,
- anchor:
- Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
- exclusivity: Astal.Exclusivity.IGNORE,
- });
-
- this.lockType = type;
- this.#update();
-
- this.add(
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className={`lock ${type}`}>
- <label vexpand className="icon" label={icon} />
- <label vexpand className="text" label={capitalize(type) + "lock"} />
- </box>
- );
-
- // Clickthrough
- this.connect("size-allocate", () => this.input_shape_combine_region(new Cairo.Region()));
-
- // Move over when other indicator opens/closes
- this.hook(App, "window-toggled", (_, window) => {
- if (window !== this && window instanceof LockOsd) {
- const child = this.get_child();
- if (!child) return;
- this[right ? "marginLeft" : "marginRight"] = window.visible
- ? child.get_preferred_width()[1] + config.lock.spacing.get()
- : 0;
- }
- });
- }
-
- #update() {
- execAsync(`fish -c 'cat /sys/class/leds/input*::${this.lockType}lock/brightness'`)
- .then(out => (this.get_child() as Widget.Box | null)?.toggleClassName("enabled", out.includes("1")))
- .catch(console.error);
- }
-
- show() {
- super.show();
- this.#update();
- this.#timeout?.cancel();
- this.#timeout = timeout(config.lock[this.lockType].hideDelay.get(), () => this.hide());
- }
-}
-
-export default () => {
- if (AstalWp.get_default()) <Volume audio={AstalWp.get_default()!.audio} />;
- Monitors.get_default().forEach(monitor => <Brightness monitor={monitor} />);
-
- <LockOsd type="caps" icon="keyboard_capslock" />;
- <LockOsd right type="num" icon="filter_1" />;
-
- return null;
-};
diff --git a/src/modules/screencorners.tsx b/src/modules/screencorners.tsx
deleted file mode 100644
index 4368b87..0000000
--- a/src/modules/screencorners.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import ScreenCorner from "@/widgets/screencorner";
-import { bind } from "astal/binding";
-import { Astal } from "astal/gtk3";
-import { bar } from "config";
-import Cairo from "gi://cairo";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-screencorners"
- monitor={monitor.id}
- anchor={bind(bar.vertical).as(
- v =>
- Astal.WindowAnchor.BOTTOM |
- Astal.WindowAnchor.RIGHT |
- (v ? Astal.WindowAnchor.TOP : Astal.WindowAnchor.LEFT)
- )}
- setup={self =>
- self.connect("size-allocate", () => self.get_window()?.input_shape_combine_region(new Cairo.Region(), 0, 0))
- }
- >
- <box vertical={bind(bar.vertical)}>
- <ScreenCorner place={bind(bar.vertical).as(v => (v ? "topright" : "bottomleft"))} />
- <box expand />
- <ScreenCorner place="bottomright" />
- </box>
- </window>
-);
-
-export const BarScreenCorners = ({ monitor }: { monitor: Monitor }) => (
- <window
- namespace="caelestia-screencorners"
- monitor={monitor.id}
- anchor={bind(bar.vertical).as(
- v =>
- Astal.WindowAnchor.TOP |
- Astal.WindowAnchor.LEFT |
- (v ? Astal.WindowAnchor.BOTTOM : Astal.WindowAnchor.RIGHT)
- )}
- visible={bind(bar.style).as(s => s === "embedded")}
- setup={self =>
- self.connect("size-allocate", () => self.get_window()?.input_shape_combine_region(new Cairo.Region(), 0, 0))
- }
- >
- <box vertical={bind(bar.vertical)}>
- <ScreenCorner place="topleft" />
- <box expand />
- <ScreenCorner place={bind(bar.vertical).as(v => (v ? "bottomleft" : "topright"))} />
- </box>
- </window>
-);
diff --git a/src/modules/session.tsx b/src/modules/session.tsx
deleted file mode 100644
index 40d3b31..0000000
--- a/src/modules/session.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import PopupWindow from "@/widgets/popupwindow";
-import { execAsync } from "astal";
-import { App, Astal, Gtk } from "astal/gtk3";
-
-const Item = ({ icon, label, cmd, isDefault }: { icon: string; label: string; cmd: string; isDefault?: boolean }) => (
- <box vertical className="item">
- <button
- cursor="pointer"
- onClicked={() => execAsync(cmd).catch(console.error)}
- setup={self =>
- isDefault &&
- self.hook(App, "window-toggled", (_, window) => {
- if (window.name === "session" && window.visible) self.grab_focus();
- })
- }
- >
- <label className="icon" label={icon} />
- </button>
- <label className="label" label={label} />
- </box>
-);
-
-export default () => (
- <PopupWindow
- className="session"
- name="session"
- anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT}
- exclusivity={Astal.Exclusivity.IGNORE}
- keymode={Astal.Keymode.EXCLUSIVE}
- layer={Astal.Layer.OVERLAY}
- borderWidth={0} // Don't need border width cause takes up entire screen
- >
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="inner">
- <box>
- <Item icon="logout" label="Logout" cmd="uwsm stop" isDefault />
- <Item icon="cached" label="Reboot" cmd="systemctl reboot" />
- </box>
- <box>
- <Item icon="downloading" label="Hibernate" cmd="systemctl hibernate" />
- <Item icon="power_settings_new" label="Shutdown" cmd="systemctl poweroff" />
- </box>
- </box>
- </PopupWindow>
-);
diff --git a/src/modules/sidebar/alerts.tsx b/src/modules/sidebar/alerts.tsx
deleted file mode 100644
index 9599aff..0000000
--- a/src/modules/sidebar/alerts.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Headlines from "./modules/headlines";
-import Notifications from "./modules/notifications";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="pane alerts" name="alerts">
- <Notifications />
- <box className="separator" />
- <Headlines monitor={monitor} />
- </box>
-);
diff --git a/src/modules/sidebar/audio.tsx b/src/modules/sidebar/audio.tsx
deleted file mode 100644
index 20a6551..0000000
--- a/src/modules/sidebar/audio.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import DeviceSelector from "./modules/deviceselector";
-import Media from "./modules/media";
-import Streams from "./modules/streams";
-
-export default () => (
- <box vertical className="pane audio" name="audio">
- <Media />
- <box className="separator" />
- <Streams />
- <box className="separator" />
- <DeviceSelector />
- </box>
-);
diff --git a/src/modules/sidebar/connectivity.tsx b/src/modules/sidebar/connectivity.tsx
deleted file mode 100644
index 2962b56..0000000
--- a/src/modules/sidebar/connectivity.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import Bluetooth from "./modules/bluetooth";
-import Networks from "./modules/networks";
-
-export default () => (
- <box vertical className="pane connectivity" name="connectivity">
- <Networks />
- <box className="separator" />
- <Bluetooth />
- </box>
-);
diff --git a/src/modules/sidebar/dashboard.tsx b/src/modules/sidebar/dashboard.tsx
deleted file mode 100644
index 1a8626f..0000000
--- a/src/modules/sidebar/dashboard.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import { bindCurrentTime, osIcon } 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";
-import Upcoming from "./modules/upcoming";
-
-const noNull = (s: string | null) => s ?? "-";
-
-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));
-
- return (
- <box className="user">
- <box
- homogeneous
- className="face"
- setup={self => {
- self.css = `background-image: url("${HOME}/.face");`;
- const monitor = monitorFile(HOME + "/.face", () => {
- hasFace.set(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS));
- self.css = `background-image: url("${HOME}/.face");`;
- });
- self.connect("destroy", () => monitor.cancel());
- }}
- >
- {bind(hasFace).as(h => (h ? <box visible={false} /> : <FaceFallback />))}
- </box>
- <box vertical hexpand valign={Gtk.Align.CENTER} className="details">
- <label truncate xalign={0} className="name" label={`${osIcon} ${GLib.get_user_name()}`} />
- <label truncate xalign={0} label={bind(uptime)} onDestroy={() => uptime.drop()} />
- <label truncate xalign={0} label={bindCurrentTime("%A, %e %B")} />
- </box>
- </box>
- );
-};
-
-const Media = ({ player }: { player: AstalMpris.Player | null }) => {
- const position = player
- ? Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l)
- : Variable(0);
-
- return (
- <box className="media" onDestroy={() => position.drop()}>
- <box
- homogeneous
- className="cover-art"
- css={player ? bind(player, "coverArt").as(a => `background-image: url("${a}");`) : ""}
- >
- {player ? (
- bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.31} label="" />))
- ) : (
- <label xalign={0.31} label="" />
- )}
- </box>
- <box vertical className="details">
- <label truncate className="title" label={player ? bind(player, "title").as(noNull) : ""} />
- <label truncate className="artist" label={player ? bind(player, "artist").as(noNull) : "No media"} />
- <box hexpand className="controls">
- <button
- hexpand
- sensitive={player ? bind(player, "canGoPrevious") : false}
- cursor="pointer"
- onClicked={() => player?.next()}
- label="󰒮"
- />
- <button
- hexpand
- sensitive={player ? bind(player, "canControl") : false}
- cursor="pointer"
- onClicked={() => player?.play_pause()}
- label={
- player
- ? bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"
- )
- : "󰐊"
- }
- />
- <button
- hexpand
- sensitive={player ? bind(player, "canGoNext") : false}
- cursor="pointer"
- onClicked={() => player?.next()}
- label="󰒭"
- />
- </box>
- <Slider value={bind(position)} onChange={(_, v) => player?.set_position(v * player.length)} />
- <box className="time">
- <label label={player ? bind(player, "position").as(lengthStr) : "-1:-1"} />
- <box hexpand />
- <label label={player ? bind(player, "length").as(lengthStr) : "-1:-1"} />
- </box>
- </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 compact />
- <box className="separator" />
- <Upcoming />
- </box>
-);
diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx
deleted file mode 100644
index 7570283..0000000
--- a/src/modules/sidebar/index.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import { bind, idle, register, Variable } from "astal";
-import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3";
-import { sidebar as config } from "config";
-import Alerts from "./alerts";
-import Audio from "./audio";
-import Connectivity from "./connectivity";
-import Dashboard from "./dashboard";
-import Packages from "./packages";
-import Time from "./time";
-
-export const paneNames = ["dashboard", "audio", "connectivity", "packages", "alerts", "time"] as const;
-export type PaneName = (typeof paneNames)[number];
-
-export const switchPane = (monitor: Monitor, name: PaneName) => {
- const sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
- if (sidebar) {
- if (sidebar.visible && sidebar.shown.get() === name) sidebar.hide();
- else sidebar.show();
- sidebar.shown.set(name);
- }
-};
-
-export const awaitSidebar = (monitor: Monitor) =>
- new Promise<SideBar>(resolve => {
- let sidebar: SideBar | null = null;
-
- const awaitSidebar = () => {
- sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null;
- if (sidebar) resolve(sidebar);
- else idle(awaitSidebar);
- };
- idle(awaitSidebar);
- });
-
-const getPane = (monitor: Monitor, name: PaneName) => {
- if (name === "dashboard") return <Dashboard />;
- if (name === "audio") return <Audio />;
- if (name === "connectivity") return <Connectivity />;
- if (name === "packages") return <Packages monitor={monitor} />;
- if (name === "alerts") return <Alerts monitor={monitor} />;
- return <Time />;
-};
-
-@register()
-export default class SideBar extends Widget.Window {
- readonly shown: Variable<PaneName>;
-
- constructor({ monitor }: { monitor: Monitor }) {
- super({
- application: App,
- name: `sidebar${monitor.id}`,
- namespace: "caelestia-sidebar",
- monitor: monitor.id,
- anchor: Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
- exclusivity: Astal.Exclusivity.EXCLUSIVE,
- visible: false,
- });
-
- this.shown = Variable(paneNames[0]);
-
- this.add(
- <eventbox
- onScroll={(_, event) => {
- if (event.modifier & Gdk.ModifierType.BUTTON1_MASK) {
- const index = paneNames.indexOf(this.shown.get()) + (event.delta_y < 0 ? -1 : 1);
- if (index < 0 || index >= paneNames.length) return;
- this.shown.set(paneNames[index]);
- }
- }}
- >
- <box vertical className="sidebar">
- <stack
- vexpand
- transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
- transitionDuration={200}
- shown={bind(this.shown)}
- >
- {paneNames.map(n => getPane(monitor, n))}
- </stack>
- </box>
- </eventbox>
- );
-
- if (config.showOnStartup.get()) idle(() => this.show());
- }
-}
diff --git a/src/modules/sidebar/modules/bluetooth.tsx b/src/modules/sidebar/modules/bluetooth.tsx
deleted file mode 100644
index 89d0cb7..0000000
--- a/src/modules/sidebar/modules/bluetooth.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import { bind, Variable } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalBluetooth from "gi://AstalBluetooth";
-
-const sortDevices = (a: AstalBluetooth.Device, b: AstalBluetooth.Device) => {
- if (a.connected || b.connected) return a.connected ? -1 : 1;
- if (a.paired || b.paired) return a.paired ? -1 : 1;
- return 0;
-};
-
-const BluetoothDevice = (device: AstalBluetooth.Device) => (
- <box className={bind(device, "connected").as(c => `device ${c ? "connected" : ""}`)}>
- <icon
- className="icon"
- icon={bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "bluetooth-symbolic"
- )}
- />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(device, "alias")} />
- <label
- truncate
- className="sublabel"
- xalign={0}
- setup={self => {
- const update = () => {
- self.label =
- (device.connected ? "Connected" : "Paired") +
- (device.batteryPercentage >= 0 ? ` (${device.batteryPercentage * 100}%)` : "");
- self.visible = device.connected || device.paired;
- };
- self.hook(device, "notify::connected", update);
- self.hook(device, "notify::paired", update);
- self.hook(device, "notify::battery-percentage", update);
- update();
- }}
- />
- </box>
- <button
- valign={Gtk.Align.CENTER}
- visible={bind(device, "paired")}
- cursor="pointer"
- onClicked={() => AstalBluetooth.get_default().adapter.remove_device(device)}
- label="delete"
- />
- <button
- valign={Gtk.Align.CENTER}
- cursor="pointer"
- onClicked={self => {
- if (device.connected)
- device.disconnect_device((_, res) => {
- self.sensitive = true;
- device.disconnect_device_finish(res);
- });
- else
- device.connect_device((_, res) => {
- self.sensitive = true;
- device.connect_device_finish(res);
- });
- self.sensitive = false;
- }}
- label={bind(device, "connected").as(c => (c ? "bluetooth_disabled" : "bluetooth_searching"))}
- />
- </box>
-);
-
-const List = ({ devNotify }: { devNotify: Variable<boolean> }) => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(devNotify).as(() => AstalBluetooth.get_default().devices.sort(sortDevices).map(BluetoothDevice))}
- </box>
-);
-
-const NoDevices = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="bluetooth_searching" />
- <label label="No Bluetooth devices" />
- </box>
- </box>
-);
-
-export default () => {
- const bluetooth = AstalBluetooth.get_default();
- const devNotify = Variable(false); // Aggregator for device state changes (connected/paired)
-
- const update = () => devNotify.set(!devNotify.get());
- const connectSignals = (device: AstalBluetooth.Device) => {
- device.connect("notify::connected", update);
- device.connect("notify::paired", update);
- };
- bluetooth.get_devices().forEach(connectSignals);
- bluetooth.connect("device-added", (_, device) => connectSignals(device));
- bluetooth.connect("notify::devices", update);
-
- return (
- <box vertical className="bluetooth">
- <box className="header-bar">
- <label
- label={bind(devNotify).as(() => {
- const nConnected = bluetooth.get_devices().filter(d => d.connected).length;
- return `${nConnected} connected device${nConnected === 1 ? "" : "s"}`;
- })}
- />
- <box hexpand />
- <button
- className={bind(bluetooth.adapter, "discovering").as(d => (d ? "enabled" : ""))}
- cursor="pointer"
- onClicked={() => {
- if (bluetooth.adapter.discovering) bluetooth.adapter.start_discovery();
- else bluetooth.adapter.stop_discovery();
- }}
- label="󰀂 Discovery"
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(bluetooth, "devices").as(d => (d.length > 0 ? "list" : "empty"))}
- >
- <NoDevices />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List devNotify={devNotify} />
- </scrollable>
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/calendar.tsx b/src/modules/sidebar/modules/calendar.tsx
deleted file mode 100644
index bb36909..0000000
--- a/src/modules/sidebar/modules/calendar.tsx
+++ /dev/null
@@ -1,252 +0,0 @@
-import Calendar, { type IEvent } from "@/services/calendar";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, GLib, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import ical from "ical.js";
-
-const isLeapYear = (year: number) => year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0);
-
-const getMonthDays = (month: number, year: number) => {
- const leapYear = isLeapYear(year);
- if (month === 2 && leapYear) return leapYear ? 29 : 28;
- if ((month <= 7 && month % 2 === 1) || (month >= 8 && month % 2 === 0)) return 31;
- return 30;
-};
-
-const getNextMonthDays = (month: number, year: number) => {
- if (month === 12) return 31;
- return getMonthDays(month + 1, year);
-};
-
-const getPrevMonthDays = (month: number, year: number) => {
- if (month === 1) return 31;
- return getMonthDays(month - 1, year);
-};
-
-export function getCalendarLayout(date: ical.Time) {
- const weekdayOfMonthFirst = date.startOfMonth().dayOfWeek(ical.Time.MONDAY);
- const daysInMonth = getMonthDays(date.month, date.year);
- const daysInPrevMonth = getPrevMonthDays(date.month, date.year);
-
- const calendar: ical.Time[][] = [];
- let idx = -weekdayOfMonthFirst + 2;
-
- for (let i = 0; i < 6; i++) {
- calendar.push([]);
-
- for (let j = 0; j < 7; j++) {
- let cDay = idx++;
- let cMonth = date.month;
- let cYear = date.year;
-
- if (idx < 0) {
- cDay = daysInPrevMonth + cDay;
- cMonth--;
-
- if (cMonth < 0) {
- cMonth += 12;
- cYear--;
- }
- } else if (idx > daysInMonth) {
- cDay -= daysInMonth;
- cMonth++;
-
- if (cMonth > 12) {
- cMonth -= 12;
- cYear++;
- }
- }
-
- calendar[i].push(ical.Time.fromData({ day: cDay, month: cMonth, year: cYear }));
- }
- }
-
- return calendar;
-}
-
-const dateToMonthYear = (date: ical.Time) => {
- const months = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
- ];
- return `${months[date.month - 1]} ${date.year}`;
-};
-
-const addMonths = (date: ical.Time, num: number) => {
- date = date.clone();
- if (num > 0) for (let i = 0; i < num; i++) date.adjust(getNextMonthDays(date.month, date.year), 0, 0, 0);
- else for (let i = 0; i > num; i--) date.adjust(-getPrevMonthDays(date.month, date.year), 0, 0, 0);
- return date;
-};
-
-const getDayClassName = (day: ical.Time, current: Variable<ical.Time>) => {
- const isToday = day.toJSDate().toDateString() === new Date().toDateString() ? "today" : "";
- const numEvents = Math.min(5, Calendar.get_default().getEventsForDay(day).length);
- return `day ${isToday} ${day.month !== current.get().month ? "dim" : ""} events-${numEvents}`;
-};
-
-const getDayTooltip = (day: ical.Time) => {
- const events = Calendar.get_default().getEventsForDay(day);
- if (!events.length) return "";
- const eventsStr = events
- .map(e => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- return `<b>${e.event.summary.replaceAll("&", "&amp;")}</b> • ${time}`;
- })
- .join("\n");
- return `${events.length} event${events.length === 1 ? "" : "s"}\n${eventsStr}`;
-};
-
-const getEventsHeader = (current: ical.Time) => {
- const events = Calendar.get_default().getEventsForDay(current);
- const isToday = current.toJSDate().toDateString() === new Date().toDateString();
- return (
- (isToday ? "Today • " : "") +
- GLib.DateTime.new_from_unix_local(current.toUnixTime()).format("%B %-d • %A") +
- ` • ${events.length} event${events.length === 1 ? "" : "s"}`
- );
-};
-
-const getEventHeader = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const time = `${start.format("%-I")}${start.get_minute() > 0 ? `:${start.get_minute()}` : ""}${start.format("%P")}`;
- return `${time} <b>${e.event.summary.replaceAll("&", "&amp;")}</b>`;
-};
-
-const getEventTooltip = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.event.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%A, %-d %B • %-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- const locIfExists = e.event.location ? ` ${e.event.location}\n` : "";
- const descIfExists = e.event.description ? `󰒿 ${e.event.description}\n` : "";
- return `<b>${e.event.summary}</b>\n${time}\n${locIfExists}${descIfExists}󰃭 ${e.calendar}`.replaceAll("&", "&amp;");
-};
-
-const Day = ({ day, shown, current }: { day: ical.Time; shown: Variable<string>; current: Variable<ical.Time> }) => (
- <button
- className={bind(Calendar.get_default(), "calendars").as(() => getDayClassName(day, current))}
- cursor="pointer"
- onClicked={() => {
- shown.set("events");
- current.set(day);
- }}
- setup={self =>
- setupCustomTooltip(
- self,
- bind(Calendar.get_default(), "calendars").as(() => getDayTooltip(day)),
- { useMarkup: true }
- )
- }
- >
- <box vertical>
- <label label={day.day.toString()} />
- <box className="indicator" />
- </box>
- </button>
-);
-
-const CalendarView = ({ shown, current }: { shown: Variable<string>; current: Variable<ical.Time> }) => (
- <box vertical className="calendar-view" name="calendar">
- <box className="header">
- <button
- cursor="pointer"
- onClicked={() => current.set(ical.Time.now())}
- label={bind(current).as(dateToMonthYear)}
- />
- <box hexpand />
- <button cursor="pointer" onClicked={() => current.set(addMonths(current.get(), -1))} label="" />
- <button cursor="pointer" onClicked={() => current.set(addMonths(current.get(), 1))} label="" />
- </box>
- <box halign={Gtk.Align.CENTER} className="weekdays">
- {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(d => (
- <label label={d} />
- ))}
- </box>
- <box vertical halign={Gtk.Align.CENTER} className="month">
- {bind(current).as(c =>
- getCalendarLayout(c).map(r => (
- <box className="week">
- {r.map(d => (
- <Day day={d} shown={shown} current={current} />
- ))}
- </box>
- ))
- )}
- </box>
- </box>
-);
-
-const Event = (event: IEvent) => (
- <box className="event" setup={self => setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}>
- <box className={`calendar-indicator calendar-${Calendar.get_default().getCalendarIndex(event.calendar)}`} />
- <box vertical>
- <label truncate useMarkup xalign={0} label={getEventHeader(event)} />
- {event.event.location && <label truncate xalign={0} label={event.event.location} className="sublabel" />}
- {event.event.description && (
- <label truncate useMarkup xalign={0} label={event.event.description} className="sublabel" />
- )}
- </box>
- </box>
-);
-
-const List = ({ current }: { current: Variable<ical.Time> }) => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(current).as(c => Calendar.get_default().getEventsForDay(c).map(Event))}
- </box>
-);
-
-const NoEvents = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="calendar_month" />
- <label label="Day all clear!" />
- </box>
- </box>
-);
-
-const Events = ({ shown, current }: { shown: Variable<string>; current: Variable<ical.Time> }) => (
- <box vertical className="events" name="events">
- <box className="header">
- <button cursor="pointer" onClicked={() => shown.set("calendar")} label="" />
- <label hexpand truncate xalign={0} label={bind(current).as(getEventsHeader)} />
- </box>
- <stack shown={bind(current).as(c => (Calendar.get_default().getEventsForDay(c).length > 0 ? "list" : "empty"))}>
- <NoEvents />
- <scrollable hscroll={Gtk.PolicyType.NEVER} name="list">
- <List current={current} />
- </scrollable>
- </stack>
- </box>
-);
-
-export default () => {
- const shown = Variable<"calendar" | "events">("calendar");
- const current = Variable(ical.Time.now());
-
- return (
- <box vertical className="calendar">
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={150}
- shown={bind(shown)}
- >
- <CalendarView shown={shown} current={current} />
- <Events shown={shown} current={current} />
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/deviceselector.tsx b/src/modules/sidebar/modules/deviceselector.tsx
deleted file mode 100644
index e74e6f5..0000000
--- a/src/modules/sidebar/modules/deviceselector.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-import { bind, execAsync, Variable, type Binding } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalWp from "gi://AstalWp";
-
-const Device = ({
- input,
- defaultDevice,
- showDropdown,
- device,
-}: {
- input?: boolean;
- defaultDevice: Binding<AstalWp.Endpoint>;
- showDropdown: Variable<boolean>;
- device: AstalWp.Endpoint;
-}) => (
- <button
- visible={defaultDevice.get().id !== device.id}
- cursor="pointer"
- onClicked={() => {
- execAsync(`wpctl set-default ${device.id}`).catch(console.error);
- showDropdown.set(false);
- }}
- setup={self => {
- let last: { d: AstalWp.Endpoint; id: number } | null = {
- d: defaultDevice.get(),
- id: defaultDevice
- .get()
- .connect("notify::id", () => self.set_visible(defaultDevice.get().id !== device.id)),
- };
- self.hook(defaultDevice, (_, d) => {
- last?.d.disconnect(last.id);
- self.set_visible(d.id !== device.id);
- last = {
- d,
- id: d.connect("notify::id", () => self.set_visible(d.id !== device.id)),
- };
- });
- self.connect("destroy", () => last?.d.disconnect(last.id));
- }}
- >
- <box className="device">
- {bind(device, "icon").as(i =>
- Astal.Icon.lookup_icon(i) ? (
- <icon className="icon" icon={device.icon} />
- ) : (
- <label className="icon" label={input ? "mic" : "media_output"} />
- )
- )}
- <label truncate label={bind(device, "description")} />
- </box>
- </button>
-);
-
-const DefaultDevice = ({ input, device }: { input?: boolean; device: AstalWp.Endpoint }) => (
- <box className="selected">
- <label className="icon" label={input ? "mic" : "media_output"} />
- <box vertical>
- <label
- truncate
- xalign={0}
- label={bind(device, "description").as(d => (input ? "[In] " : "[Out] ") + (d ?? "Unknown"))}
- />
- <label
- xalign={0}
- className="sublabel"
- label={bind(device, "volume").as(v => `Volume ${Math.round(v * 100)}%`)}
- />
- </box>
- </box>
-);
-
-const Selector = ({ input, audio }: { input?: boolean; audio: AstalWp.Audio }) => {
- const showDropdown = Variable(false);
- const defaultDevice = bind(audio, input ? "defaultMicrophone" : "defaultSpeaker");
-
- return (
- <box vertical className="selector">
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_UP}
- transitionDuration={150}
- revealChild={bind(showDropdown)}
- >
- <box vertical className="list">
- {bind(audio, input ? "microphones" : "speakers").as(ds =>
- ds.map(d => (
- <Device
- input={input}
- defaultDevice={defaultDevice}
- showDropdown={showDropdown}
- device={d}
- />
- ))
- )}
- <box className="separator" />
- </box>
- </revealer>
- <button cursor="pointer" onClick={() => showDropdown.set(!showDropdown.get())}>
- {defaultDevice.as(d => (
- <DefaultDevice input={input} device={d} />
- ))}
- </button>
- </box>
- );
-};
-
-const NoWp = () => (
- <box homogeneous>
- <box vertical valign={Gtk.Align.CENTER}>
- <label label="Device selector unavailable" />
- <label className="no-wp-prompt" label="WirePlumber is required for this module" />
- </box>
- </box>
-);
-
-export default () => {
- const audio = AstalWp.get_default()?.get_audio();
-
- if (!audio) return <NoWp />;
-
- return (
- <box vertical className="device-selector">
- <Selector input audio={audio} />
- <Selector audio={audio} />
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/headlines.tsx b/src/modules/sidebar/modules/headlines.tsx
deleted file mode 100644
index 40d468b..0000000
--- a/src/modules/sidebar/modules/headlines.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import News, { type IArticle } from "@/services/news";
-import Palette, { type IPalette } from "@/services/palette";
-import { capitalize } from "@/utils/strings";
-import { bind, execAsync, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import { sidebar } from "config";
-import { setConfig } from "config/funcs";
-
-const fixGoogleNews = (colours: IPalette, title: string, desc: string) => {
- // Add separator, bold and split at domain (domain is at the end of each headline)
- const domain = title.split(" - ").at(-1);
- if (domain) desc = desc.replaceAll(domain, `— <span foreground="${colours.subtext0}">${domain}</span>\n\n`);
- // Split headlines
- desc = desc.replace(/(( |\.)[^A-Z][a-z]+)([A-Z])/g, "$1\n\n$3");
- desc = desc.replace(/( [A-Z]+)([A-Z](?![s])[a-z])/g, "$1\n\n$2");
- // Add separator and bold domains
- desc = desc.replace(/ ([a-zA-Z.]+)\n\n/g, ` — <span foreground="${colours.subtext0}">$1</span>\n\n`);
- desc = desc.replace(/ ([a-zA-Z.]+)$/, ` — <span foreground="${colours.subtext0}">$1</span>`); // Last domain
- return desc.trim();
-};
-
-const fixNews = (colours: IPalette, title: string, desc: string, source: string) => {
- // Add spaces between sentences
- desc = desc.replace(/\.([A-Z])/g, ". $1");
- // Google News needs some other fixes
- if (source === "Google News") desc = fixGoogleNews(colours, title, desc);
- return desc.replaceAll("&", "&amp;");
-};
-
-const getCategoryIcon = (category: string) => {
- if (category === "business") return "monitoring";
- if (category === "crime") return "speed_camera";
- if (category === "domestic") return "home";
- if (category === "education") return "school";
- if (category === "entertainment") return "tv";
- if (category === "environment") return "eco";
- if (category === "food") return "restaurant";
- if (category === "health") return "health_and_safety";
- if (category === "lifestyle") return "digital_wellbeing";
- if (category === "politics") return "account_balance";
- if (category === "science") return "science";
- if (category === "sports") return "sports_basketball";
- if (category === "technology") return "account_tree";
- if (category === "top") return "breaking_news";
- if (category === "tourism") return "travel";
- if (category === "world") return "public";
- return "newsmode";
-};
-
-const Article = ({ title, description, creator, pubDate, source_name, link }: IArticle) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="article">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box hexpand className="header">
- <box vertical>
- <label truncate xalign={0} label={title} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={source_name + (creator ? ` (${creator.join(", ")})` : "")}
- />
- </box>
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <button onClicked={() => execAsync(`app2unit -O -- ${link}`)}>
- <box vertical className="article-body">
- <label wrap className="title" xalign={0} label={title} />
- <label wrap xalign={0} label={`Published on ${new Date(pubDate).toLocaleString()}`} />
- <label
- wrap
- xalign={0}
- className="sublabel"
- label={`By ${
- creator?.join(", ") ??
- (source_name === "Google News" ? title.split(" - ").at(-1) : source_name)
- }`}
- />
- {description && (
- <label
- wrap
- useMarkup
- xalign={0}
- label={bind(Palette.get_default(), "colours").as(c =>
- fixNews(c, title, description, source_name)
- )}
- />
- )}
- </box>
- </button>
- </revealer>
- </box>
- );
-};
-
-const Category = ({ title, articles }: { title: string; articles: IArticle[] }) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="category">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box className="header">
- <label className="icon" label={getCategoryIcon(title)} />
- <label label={`${capitalize(title)} (${articles.length})`} />
- <box hexpand />
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <box vertical className="body">
- {articles
- .sort((a, b) => a.source_priority - b.source_priority)
- .map(a => (
- <Article {...a} />
- ))}
- </box>
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(News.get_default(), "categories").as(c =>
- Object.entries(c).map(([k, v]) => <Category title={k} articles={v} />)
- )}
- </box>
-);
-
-const NoNews = ({ disabled }: { disabled?: boolean }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="full_coverage" />
- <label label={disabled ? "Headlines disabled" : "No news headlines!"} />
- </box>
- </box>
-);
-
-const HeadlinesDisabled = () => (
- <>
- <box vertical className="headlines">
- <box className="header-bar">
- <label label="Top news headlines" />
- <box hexpand />
- <button
- cursor="pointer"
- onClicked={() => setConfig("sidebar.modules.headlines.enabled", true)}
- label="󰞉 Enable"
- />
- </box>
- <NoNews disabled />
- </box>
- </>
-);
-
-const Headlines = ({ monitor }: { monitor: Monitor }) => (
- <>
- <box className="header-bar">
- <label label="Top news headlines" />
- <box hexpand />
- <button
- className={bind(News.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(News.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => News.get_default().getNews()}
- label={bind(News.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(News.get_default(), "articles").as(a => (a.length > 0 ? "list" : "empty"))}
- >
- <NoNews />
- <scrollable
- css={bind(News.get_default(), "articles").as(a =>
- a.length > 0 ? `min-height: ${Math.round(monitor.height * 0.4)}px;` : ""
- )}
- hscroll={Gtk.PolicyType.NEVER}
- name="list"
- >
- <List />
- </scrollable>
- </stack>
- </>
-);
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="headlines">
- {bind(sidebar.modules.headlines.enabled).as(e => (e ? <Headlines monitor={monitor} /> : <HeadlinesDisabled />))}
- </box>
-);
diff --git a/src/modules/sidebar/modules/hwresources.tsx b/src/modules/sidebar/modules/hwresources.tsx
deleted file mode 100644
index 768d8bd..0000000
--- a/src/modules/sidebar/modules/hwresources.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-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/media.tsx b/src/modules/sidebar/modules/media.tsx
deleted file mode 100644
index 169a98d..0000000
--- a/src/modules/sidebar/modules/media.tsx
+++ /dev/null
@@ -1,168 +0,0 @@
-import Players from "@/services/players";
-import { lengthStr } from "@/utils/strings";
-import Slider from "@/widgets/slider";
-import { bind, timeout, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalMpris from "gi://AstalMpris";
-
-const noNull = (s: string | null) => s ?? "-";
-
-const NoMedia = () => (
- <box vertical className="player" name="none">
- <box homogeneous halign={Gtk.Align.CENTER} className="cover-art">
- <label xalign={0.4} label="" />
- </box>
- <box vertical className="progress">
- <Slider value={bind(Variable(0))} />
- <box className="time">
- <label label="-1:-1" />
- <box hexpand />
- <label label="-1:-1" />
- </box>
- </box>
- <box vertical className="details">
- <label truncate className="title" label="No media" />
- <label truncate className="artist" label="Try play some music!" />
- <label truncate className="album" label="" />
- </box>
- <box vertical className="controls">
- <box halign={Gtk.Align.CENTER} className="playback">
- <button sensitive={false} cursor="pointer" label="󰒮" />
- <button sensitive={false} cursor="pointer" label="󰐊" />
- <button sensitive={false} cursor="pointer" label="󰒭" />
- </box>
- <box className="options">
- <button sensitive={false} cursor="pointer" label="󰊓" />
- <button sensitive={false} cursor="pointer" label="󰒞" />
- <box hexpand />
- <button className="needs-adjustment" sensitive={false} cursor="pointer" label="󰑗" />
- <button className="needs-adjustment" sensitive={false} cursor="pointer" label="󰀽" />
- </box>
- </box>
- </box>
-);
-
-const Player = ({ player }: { player: AstalMpris.Player }) => {
- const position = Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l);
-
- return (
- <box vertical className="player" name={player.busName} onDestroy={() => position.drop()}>
- <box
- homogeneous
- halign={Gtk.Align.CENTER}
- 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.4} label="" />))}
- </box>
- <box vertical className="progress">
- <Slider value={bind(position)} onChange={(_, v) => player.set_position(v * player.length)} />
- <box className="time">
- <label label={bind(player, "position").as(lengthStr)} />
- <box hexpand />
- <label label={bind(player, "length").as(lengthStr)} />
- </box>
- </box>
- <box vertical className="details">
- <label truncate className="title" label={bind(player, "title").as(noNull)} />
- <label truncate className="artist" label={bind(player, "artist").as(noNull)} />
- <label truncate className="album" label={bind(player, "album").as(noNull)} />
- </box>
- <box vertical className="controls">
- <box halign={Gtk.Align.CENTER} className="playback">
- <button
- sensitive={bind(player, "canGoPrevious")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="󰒮"
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.play_pause()}
- label={bind(player, "playbackStatus").as(s =>
- s === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"
- )}
- />
- <button
- sensitive={bind(player, "canGoNext")}
- cursor="pointer"
- onClicked={() => player.next()}
- label="󰒭"
- />
- </box>
- <box className="options">
- <button
- sensitive={bind(player, "canSetFullscreen")}
- cursor="pointer"
- onClicked={() => player.toggle_fullscreen()}
- label={bind(player, "fullscreen").as(f => (f ? "󰊔" : "󰊓"))}
- />
- <button
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.shuffle()}
- label={bind(player, "shuffleStatus").as(s => (s === AstalMpris.Shuffle.ON ? "󰒝" : "󰒞"))}
- />
- <box hexpand />
- <button
- className="needs-adjustment"
- sensitive={bind(player, "canControl")}
- cursor="pointer"
- onClicked={() => player.loop()}
- label={bind(player, "loopStatus").as(l =>
- l === AstalMpris.Loop.TRACK ? "󰑘" : l === AstalMpris.Loop.PLAYLIST ? "󰑖" : "󰑗"
- )}
- />
- <button
- className="needs-adjustment"
- sensitive={bind(player, "canRaise")}
- cursor="pointer"
- onClicked={() => player.raise()}
- label="󰀽"
- />
- </box>
- </box>
- </box>
- );
-};
-
-const Indicator = ({ active, player }: { active: Variable<string>; player: AstalMpris.Player }) => (
- <button
- className={bind(active).as(a => (a === player.busName ? "active" : ""))}
- cursor="pointer"
- onClicked={() => active.set(player.busName)}
- />
-);
-
-export default () => {
- const players = Players.get_default();
- const active = Variable(players.lastPlayer?.busName ?? "none");
-
- active.observe(players, "notify::list", () => {
- timeout(10, () => active.set(players.lastPlayer?.busName ?? "none"));
- return "none";
- });
-
- return (
- <box vertical className="players" onDestroy={() => active.drop()}>
- <stack
- transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT}
- transitionDuration={150}
- shown={bind(active)}
- >
- <NoMedia />
- {bind(players, "list").as(ps => ps.map(p => <Player player={p} />))}
- </stack>
- <revealer
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={120}
- revealChild={bind(players, "list").as(l => l.length > 1)}
- >
- <box halign={Gtk.Align.CENTER} className="indicators">
- {bind(players, "list").as(ps => ps.map(p => <Indicator active={active} player={p} />))}
- </box>
- </revealer>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/networks.tsx b/src/modules/sidebar/modules/networks.tsx
deleted file mode 100644
index f98a62c..0000000
--- a/src/modules/sidebar/modules/networks.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { bind, execAsync, Variable, type Binding } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalNetwork from "gi://AstalNetwork";
-
-const sortAPs = (saved: string[], a: AstalNetwork.AccessPoint, b: AstalNetwork.AccessPoint) => {
- const { wifi } = AstalNetwork.get_default();
- if (a === wifi.activeAccessPoint || b === wifi.activeAccessPoint) return a === wifi.activeAccessPoint ? -1 : 1;
- if (saved.includes(a.ssid) || saved.includes(b.ssid)) return saved.includes(a.ssid) ? -1 : 1;
- return b.strength - a.strength;
-};
-
-const Network = (accessPoint: AstalNetwork.AccessPoint) => (
- <box
- className={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(
- a => `network ${a === accessPoint ? "connected" : ""}`
- )}
- >
- <icon className="icon" icon={bind(accessPoint, "iconName")} />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(accessPoint, "ssid").as(s => s ?? "Unknown")} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={bind(accessPoint, "strength").as(s => `${accessPoint.frequency > 5000 ? 5 : 2.4}GHz • ${s}/100`)}
- />
- </box>
- <box hexpand />
- <button
- valign={Gtk.Align.CENTER}
- visible={false}
- cursor="pointer"
- onClicked={() => execAsync(`nmcli c delete id '${accessPoint.ssid}'`).catch(console.error)}
- label="delete_forever"
- setup={self => {
- let destroyed = false;
- execAsync(`fish -c "nmcli -t -f name,type c show | sed -nE 's/(.*)\\:.*wireless/\\1/p'"`)
- .then(out => !destroyed && (self.visible = out.split("\n").includes(accessPoint.ssid)))
- .catch(console.error);
- self.connect("destroy", () => (destroyed = true));
- }}
- />
- <button
- valign={Gtk.Align.CENTER}
- cursor="pointer"
- onClicked={self => {
- let destroyed = false;
- const id = self.connect("destroy", () => (destroyed = true));
- const cmd =
- AstalNetwork.get_default().wifi.activeAccessPoint === accessPoint ? "c down id" : "d wifi connect";
- execAsync(`nmcli ${cmd} '${accessPoint.ssid}'`)
- .then(() => {
- if (!destroyed) {
- self.sensitive = true;
- self.disconnect(id);
- }
- })
- .catch(console.error);
- self.sensitive = false;
- }}
- label={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(a =>
- a === accessPoint ? "wifi_off" : "wifi"
- )}
- />
- </box>
-);
-
-const List = () => {
- const { wifi } = AstalNetwork.get_default();
- const children = Variable<JSX.Element[]>([]);
-
- const update = async () => {
- const out = await execAsync(`fish -c "nmcli -t -f name,type c show | sed -nE 's/(.*)\\:.*wireless/\\1/p'"`);
- const saved = out.split("\n");
- const aps = wifi.accessPoints
- .filter(a => a.ssid)
- .sort((a, b) => sortAPs(saved, a, b))
- .map(Network);
- children.set(aps);
- };
-
- wifi.connect("notify::active-access-point", () => update().catch(console.error));
- wifi.connect("notify::access-points", () => update().catch(console.error));
- update().catch(console.error);
-
- return (
- <box vertical valign={Gtk.Align.START} className="list" onDestroy={() => children.drop()}>
- {bind(children)}
- </box>
- );
-};
-
-const NoNetworks = ({ label }: { label: Binding<string> | string }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="wifi_off" />
- <label label={label} />
- </box>
- </box>
-);
-
-export default () => {
- const network = AstalNetwork.get_default();
- const label = Variable("");
-
- const update = () => {
- if (network.primary === AstalNetwork.Primary.WIFI) label.set(network.wifi.ssid ?? "Disconnected");
- else if (network.primary === AstalNetwork.Primary.WIRED) label.set(`Ethernet (${network.wired.speed})`);
- else label.set("No Wifi");
- };
- network.connect("notify::primary", update);
- network.get_wifi()?.connect("notify::ssid", update);
- network.get_wired()?.connect("notify::speed", update);
- update();
-
- return (
- <box vertical className="networks">
- <box className="header-bar">
- <label label={bind(label)} />
- <box hexpand />
- <button
- sensitive={network.get_wifi() ? bind(network.wifi, "scanning").as(e => !e) : false}
- className={network.get_wifi() ? bind(network.wifi, "scanning").as(s => (s ? "enabled" : "")) : ""}
- cursor="pointer"
- onClicked={() => network.get_wifi()?.scan()}
- label={
- network.get_wifi()
- ? bind(network.wifi, "scanning").as(s => (s ? "󰀂 Scanning" : "󰀂 Scan"))
- : "󰀂 Scan"
- }
- />
- </box>
- {network.get_wifi() ? (
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(network.wifi, "accessPoints").as(a => (a.length > 0 ? "list" : "empty"))}
- >
- <NoNetworks
- label={bind(network.wifi, "enabled").as(p => (p ? "No available networks" : "Wifi is off"))}
- />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- ) : (
- <NoNetworks label="Wifi not available" />
- )}
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/news.tsx b/src/modules/sidebar/modules/news.tsx
deleted file mode 100644
index c799757..0000000
--- a/src/modules/sidebar/modules/news.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import Palette from "@/services/palette";
-import Updates from "@/services/updates";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-
-const countNews = (news: string) => news.match(/^([0-9]{4}-[0-9]{2}-[0-9]{2} .+)$/gm)?.length ?? 0;
-
-const News = ({ header, body }: { header: string; body: string }) => {
- const expanded = Variable(false);
-
- body = body
- .slice(0, -5) // Remove last unopened \x1b[0m
- .replaceAll("\x1b[0m", "</span>"); // Replace reset code with end span
-
- return (
- <box vertical className="article">
- <button
- className="wrapper"
- cursor="pointer"
- onClicked={() => expanded.set(!expanded.get())}
- setup={self => setupCustomTooltip(self, header)}
- >
- <box hexpand className="header">
- <label className="icon" label="newspaper" />
- <box vertical>
- <label xalign={0} label={header.split(" ")[0]} />
- <label
- truncate
- xalign={0}
- className="sublabel"
- label={header.replace(/[0-9]{4}-[0-9]{2}-[0-9]{2} /, "")}
- />
- </box>
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <label
- wrap
- useMarkup
- xalign={0}
- className="body"
- label={bind(Palette.get_default(), "teal").as(
- c => body.replaceAll("\x1b[36m", `<span foreground="${c}">`) // Replace colour codes with html spans
- )}
- />
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(Updates.get_default(), "news").as(n => {
- const children = [];
- const news = n.split(/^([0-9]{4}-[0-9]{2}-[0-9]{2} .+)$/gm);
- for (let i = 1; i < news.length - 1; i += 2)
- children.push(<News header={news[i].trim()} body={news[i + 1].trim()} />);
- return children;
- })}
- </box>
-);
-
-const NoNews = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="breaking_news" />
- <label label="No Arch news!" />
- </box>
- </box>
-);
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="news">
- <box className="header-bar">
- <label
- label={bind(Updates.get_default(), "news")
- .as(countNews)
- .as(n => `${n} news article${n === 1 ? "" : "s"}`)}
- />
- <box hexpand />
- <button
- className={bind(Updates.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Updates.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Updates.get_default().getUpdates()}
- label={bind(Updates.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Updates.get_default(), "news").as(n => (n ? "list" : "empty"))}
- >
- <NoNews />
- <scrollable
- css={bind(Updates.get_default(), "news").as(n =>
- n ? `min-height: ${Math.round(monitor.height * 0.4)}px;` : ""
- )}
- hscroll={Gtk.PolicyType.NEVER}
- name="list"
- >
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/notifications.tsx b/src/modules/sidebar/modules/notifications.tsx
deleted file mode 100644
index e9347ec..0000000
--- a/src/modules/sidebar/modules/notifications.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import Notification from "@/widgets/notification";
-import { bind } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-import AstalNotifd from "gi://AstalNotifd";
-
-const List = ({ compact }: { compact?: boolean }) => (
- <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={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());
- }}
- />
-);
-
-const NoNotifs = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="mark_email_unread" />
- <label label="All caught up!" />
- </box>
- </box>
-);
-
-export default ({ compact }: { compact?: boolean }) => (
- <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()
- .get_notifications()
- .forEach(n => n.dismiss())
- }
- label="󰎟 Clear"
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(AstalNotifd.get_default(), "notifications").as(n => (n.length > 0 ? "list" : "empty"))}
- >
- <NoNotifs />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List compact={compact} />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/streams.tsx b/src/modules/sidebar/modules/streams.tsx
deleted file mode 100644
index 18a9a58..0000000
--- a/src/modules/sidebar/modules/streams.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { bind, execAsync, Variable } from "astal";
-import { Gtk } from "astal/gtk3";
-import AstalWp from "gi://AstalWp";
-
-interface IStream {
- stream: AstalWp.Endpoint;
- playing: boolean;
-}
-
-const header = (audio: AstalWp.Audio, key: "streams" | "speakers" | "recorders") =>
- `${audio[key].length} ${audio[key].length === 1 ? key.slice(0, -1) : key}`;
-
-const sortStreams = (a: IStream, b: IStream) => {
- if (a.playing || b.playing) return a.playing ? -1 : 1;
- return 0;
-};
-
-const Stream = ({ stream, playing }: IStream) => (
- <box className={`stream ${playing ? "playing" : ""}`}>
- <icon className="icon" icon={bind(stream, "icon")} />
- <box vertical hexpand>
- <label truncate xalign={0} label={bind(stream, "name")} />
- <label truncate xalign={0} className="sublabel" label={bind(stream, "description")} />
- </box>
- <button valign={Gtk.Align.CENTER} cursor="pointer" onClicked={() => (stream.volume -= 0.05)} label="-" />
- <slider
- showFillLevel
- restrictToFillLevel={false}
- fillLevel={2 / 3}
- value={bind(stream, "volume").as(v => v * (2 / 3))}
- setup={self => self.connect("value-changed", () => stream.set_volume(self.value * 1.5))}
- />
- <button valign={Gtk.Align.CENTER} cursor="pointer" onClicked={() => (stream.volume += 0.05)} label="+" />
- </box>
-);
-
-const List = ({ audio }: { audio: AstalWp.Audio }) => {
- const streams = Variable<IStream[]>([]);
-
- const update = async () => {
- const paStreams = JSON.parse(await execAsync("pactl -f json list sink-inputs"));
- streams.set(
- audio.streams.map(s => ({
- stream: s,
- playing: paStreams.find((p: any) => p.properties["object.serial"] == s.serial)?.corked === false,
- }))
- );
- };
-
- streams.watch("pactl -f json subscribe", out => {
- if (JSON.parse(out).on === "sink-input") update().catch(console.error);
- return streams.get();
- });
- audio.connect("notify::streams", () => update().catch(console.error));
-
- return (
- <box vertical valign={Gtk.Align.START} className="list" onDestroy={() => streams.drop()}>
- {bind(streams).as(ps => ps.sort(sortStreams).map(s => <Stream stream={s.stream} playing={s.playing} />))}
- </box>
- );
-};
-
-const NoSources = ({ icon, label }: { icon: string; label: string }) => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label={icon} />
- <label label={label} />
- </box>
- </box>
-);
-
-const NoWp = () => (
- <box vexpand homogeneous>
- <box vertical valign={Gtk.Align.CENTER}>
- <NoSources icon="no_sound" label="Streams module unavailable" />
- <label className="no-wp-prompt" label="WirePlumber is required for this module" />
- </box>
- </box>
-);
-
-export default () => {
- const audio = AstalWp.get_default()?.get_audio();
-
- if (!audio) return <NoWp />;
-
- const label = Variable(`${header(audio, "streams")} • ${header(audio, "recorders")}`);
-
- label.observe(
- ["streams", "recorders"].map(k => [audio, `notify::${k}`]),
- () => `${header(audio, "streams")} • ${header(audio, "recorders")}`
- );
-
- return (
- <box vertical className="streams" onDestroy={() => label.drop()}>
- <box halign={Gtk.Align.CENTER} className="header-bar">
- <label label={bind(label)} />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(audio, "streams").as(s => (s.length > 0 ? "list" : "empty"))}
- >
- <NoSources icon="stream" label="No audio sources" />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List audio={audio} />
- </scrollable>
- </stack>
- </box>
- );
-};
diff --git a/src/modules/sidebar/modules/upcoming.tsx b/src/modules/sidebar/modules/upcoming.tsx
deleted file mode 100644
index a64e051..0000000
--- a/src/modules/sidebar/modules/upcoming.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import Calendar, { type IEvent } from "@/services/calendar";
-import { setupCustomTooltip } from "@/utils/widgets";
-import { bind, GLib } from "astal";
-import { Gtk } from "astal/gtk3";
-
-const getDateHeader = (events: IEvent[]) => {
- const date = events[0].startDate;
- const isToday = date.toJSDate().toDateString() === new Date().toDateString();
- return (
- (isToday ? "Today • " : "") +
- GLib.DateTime.new_from_unix_local(date.toUnixTime()).format("%B %-d • %A") +
- ` • ${events.length} event${events.length === 1 ? "" : "s"}`
- );
-};
-
-const getEventHeader = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const time = `${start.format("%-I")}${start.get_minute() > 0 ? `:${start.get_minute()}` : ""}${start.format("%P")}`;
- return `${time} <b>${e.event.summary.replaceAll("&", "&amp;")}</b>`;
-};
-
-const getEventTooltip = (e: IEvent) => {
- const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime());
- const end = GLib.DateTime.new_from_unix_local(e.event.endDate.toUnixTime());
- const sameAmPm = start.format("%P") === end.format("%P");
- const time = `${start.format(`%A, %-d %B • %-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`;
- const locIfExists = e.event.location ? ` ${e.event.location}\n` : "";
- const descIfExists = e.event.description ? `󰒿 ${e.event.description}\n` : "";
- return `<b>${e.event.summary}</b>\n${time}\n${locIfExists}${descIfExists}󰃭 ${e.calendar}`.replaceAll("&", "&amp;");
-};
-
-const Event = (event: IEvent) => (
- <box className="event" setup={self => setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}>
- <box className={`calendar-indicator calendar-${Calendar.get_default().getCalendarIndex(event.calendar)}`} />
- <box vertical>
- <label truncate useMarkup xalign={0} label={getEventHeader(event)} />
- {event.event.location && <label truncate xalign={0} label={event.event.location} className="sublabel" />}
- {event.event.description && (
- <label truncate useMarkup xalign={0} label={event.event.description} className="sublabel" />
- )}
- </box>
- </box>
-);
-
-const Day = ({ events }: { events: IEvent[] }) => (
- <box vertical className="day">
- <label className="date" xalign={0} label={getDateHeader(events)} />
- <box vertical className="events">
- {events.map(Event)}
- </box>
- </box>
-);
-
-const List = () => (
- <box vertical valign={Gtk.Align.START}>
- {bind(Calendar.get_default(), "upcoming").as(u =>
- Object.values(u)
- .sort((a, b) => a[0].startDate.compare(b[0].startDate))
- .map(e => <Day events={e} />)
- )}
- </box>
-);
-
-const NoEvents = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="calendar_month" />
- <label label="No upcoming events" />
- </box>
- </box>
-);
-
-export default () => (
- <box vertical className="upcoming">
- <box className="header-bar">
- <label
- label={bind(Calendar.get_default(), "numUpcoming").as(n => `${n} upcoming event${n === 1 ? "" : "s"}`)}
- />
- <box hexpand />
- <button
- className={bind(Calendar.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Calendar.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Calendar.get_default().updateCalendars().catch(console.error)}
- label={bind(Calendar.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Calendar.get_default(), "numUpcoming").as(n => (n > 0 ? "list" : "empty"))}
- >
- <NoEvents />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/modules/updates.tsx b/src/modules/sidebar/modules/updates.tsx
deleted file mode 100644
index e58d848..0000000
--- a/src/modules/sidebar/modules/updates.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import Palette from "@/services/palette";
-import Updates, { Repo as IRepo, Update as IUpdate } from "@/services/updates";
-import { MenuItem, setupCustomTooltip } from "@/utils/widgets";
-import { bind, execAsync, GLib, Variable } from "astal";
-import { Astal, Gtk } from "astal/gtk3";
-
-const constructItem = (label: string, exec: string, quiet = true) =>
- new MenuItem({ label, onActivate: () => execAsync(exec).catch(e => !quiet && console.error(e)) });
-
-const Update = (update: IUpdate) => {
- const menu = new Gtk.Menu();
- menu.append(constructItem("Open info in browser", `app2unit -O '${update.url}'`, false));
- menu.append(constructItem("Open info in terminal", `app2unit -- foot -H -- pacman -Qi ${update.name}`));
- menu.append(new Gtk.SeparatorMenuItem({ visible: true }));
- menu.append(constructItem("Reinstall", `app2unit -- foot -H -- yay -S ${update.name}`));
- menu.append(constructItem("Remove with dependencies", `app2unit -- foot -H -- yay -Rns ${update.name}`));
-
- return (
- <button
- onClick={(_, event) => event.button === Astal.MouseButton.SECONDARY && menu.popup_at_pointer(null)}
- onDestroy={() => menu.destroy()}
- >
- <label
- truncate
- useMarkup
- xalign={0}
- label={bind(Palette.get_default(), "colours").as(
- c =>
- `${update.name} <span foreground="${c.teal}">(${update.version.old} -> ${
- update.version.new
- })</span>\n <span foreground="${c.subtext0}">${GLib.markup_escape_text(
- update.description,
- update.description.length
- )}</span>`
- )}
- setup={self => setupCustomTooltip(self, `${update.name} • ${update.description}`)}
- />
- </button>
- );
-};
-
-const Repo = ({ repo }: { repo: IRepo }) => {
- const expanded = Variable(false);
-
- return (
- <box vertical className="repo">
- <button className="wrapper" cursor="pointer" onClicked={() => expanded.set(!expanded.get())}>
- <box className="header">
- <label className="icon" label={repo.icon} />
- <label label={`${repo.name} (${repo.updates.length})`} />
- <box hexpand />
- <label className="icon" label={bind(expanded).as(e => (e ? "expand_less" : "expand_more"))} />
- </box>
- </button>
- <revealer
- revealChild={bind(expanded)}
- transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
- transitionDuration={200}
- >
- <box vertical className="body">
- {repo.updates.map(Update)}
- </box>
- </revealer>
- </box>
- );
-};
-
-const List = () => (
- <box vertical valign={Gtk.Align.START} className="list">
- {bind(Updates.get_default(), "updateData").as(d => d.repos.map(r => <Repo repo={r} />))}
- </box>
-);
-
-const NoUpdates = () => (
- <box homogeneous name="empty">
- <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
- <label className="icon" label="deployed_code_history" />
- <label label="All packages up to date!" />
- </box>
- </box>
-);
-
-export default () => (
- <box vertical className="updates">
- <box className="header-bar">
- <label
- label={bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`)}
- />
- <box hexpand />
- <button
- className={bind(Updates.get_default(), "loading").as(l => (l ? "enabled" : ""))}
- sensitive={bind(Updates.get_default(), "loading").as(l => !l)}
- cursor="pointer"
- onClicked={() => Updates.get_default().getUpdates()}
- label={bind(Updates.get_default(), "loading").as(l => (l ? "󰑓 Loading" : "󰑓 Reload"))}
- />
- </box>
- <stack
- transitionType={Gtk.StackTransitionType.CROSSFADE}
- transitionDuration={200}
- shown={bind(Updates.get_default(), "numUpdates").as(n => (n > 0 ? "list" : "empty"))}
- >
- <NoUpdates />
- <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
- <List />
- </scrollable>
- </stack>
- </box>
-);
diff --git a/src/modules/sidebar/packages.tsx b/src/modules/sidebar/packages.tsx
deleted file mode 100644
index 02b0702..0000000
--- a/src/modules/sidebar/packages.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Monitor } from "@/services/monitors";
-import News from "./modules/news";
-import Updates from "./modules/updates";
-
-export default ({ monitor }: { monitor: Monitor }) => (
- <box vertical className="pane packages" name="packages">
- <Updates />
- <box className="separator" />
- <News monitor={monitor} />
- </box>
-);
diff --git a/src/modules/sidebar/time.tsx b/src/modules/sidebar/time.tsx
deleted file mode 100644
index 1f5ef99..0000000
--- a/src/modules/sidebar/time.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { bindCurrentTime } from "@/utils/system";
-import { Gtk } from "astal/gtk3";
-import Calendar from "./modules/calendar";
-import Upcoming from "./modules/upcoming";
-
-const TimeDate = () => (
- <box vertical className="time-date">
- <box halign={Gtk.Align.CENTER}>
- <label label={bindCurrentTime("%I:%M:%S")} />
- <label className="ampm" label={bindCurrentTime("%p", c => (c.get_hour() < 12 ? "AM" : "PM"))} />
- </box>
- <label className="date" label={bindCurrentTime("%A, %d %B")} />
- </box>
-);
-
-export default () => (
- <box vertical className="pane time" name="time">
- <TimeDate />
- <box className="separator" />
- <Upcoming />
- <box className="separator" />
- <Calendar />
- </box>
-);