summaryrefslogtreecommitdiff
path: root/src/modules/sidebar
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/sidebar
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/sidebar')
-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
19 files changed, 0 insertions, 1904 deletions
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>
-);