diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-04-26 22:36:23 +1000 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-04-26 22:36:23 +1000 |
| commit | 3c579d0e275cdaf6f2c9589abade94bde7905c82 (patch) | |
| tree | 4b825dc642cb6eb9a060e54bf8d69288fbee4904 /src/modules/sidebar | |
| parent | schemes: fix (diff) | |
| download | caelestia-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.tsx | 11 | ||||
| -rw-r--r-- | src/modules/sidebar/audio.tsx | 13 | ||||
| -rw-r--r-- | src/modules/sidebar/connectivity.tsx | 10 | ||||
| -rw-r--r-- | src/modules/sidebar/dashboard.tsx | 132 | ||||
| -rw-r--r-- | src/modules/sidebar/index.tsx | 87 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/bluetooth.tsx | 127 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/calendar.tsx | 252 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/deviceselector.tsx | 126 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/headlines.tsx | 204 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/hwresources.tsx | 67 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/media.tsx | 168 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/networks.tsx | 151 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/news.tsx | 113 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/notifications.tsx | 90 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/streams.tsx | 110 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/upcoming.tsx | 99 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/updates.tsx | 109 | ||||
| -rw-r--r-- | src/modules/sidebar/packages.tsx | 11 | ||||
| -rw-r--r-- | src/modules/sidebar/time.tsx | 24 |
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("&", "&")}</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("&", "&")}</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("&", "&"); -}; - -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("&", "&"); -}; - -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("&", "&")}</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("&", "&"); -}; - -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> -); |