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 | |
| 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')
31 files changed, 0 insertions, 4473 deletions
diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx deleted file mode 100644 index c131029..0000000 --- a/src/modules/bar.tsx +++ /dev/null @@ -1,703 +0,0 @@ -import type { Monitor } from "@/services/monitors"; -import Players from "@/services/players"; -import Updates from "@/services/updates"; -import { getAppCategoryIcon } from "@/utils/icons"; -import { bindCurrentTime, osIcon } from "@/utils/system"; -import type { AstalWidget } from "@/utils/types"; -import { setupCustomTooltip } from "@/utils/widgets"; -import ScreenCorner from "@/widgets/screencorner"; -import { execAsync, GLib, Variable } from "astal"; -import { bind, kebabify } from "astal/binding"; -import { App, Astal, Gtk, Widget } from "astal/gtk3"; -import { bar as config } from "config"; -import AstalBattery from "gi://AstalBattery"; -import AstalBluetooth from "gi://AstalBluetooth"; -import AstalHyprland from "gi://AstalHyprland"; -import AstalNetwork from "gi://AstalNetwork"; -import AstalNotifd from "gi://AstalNotifd"; -import AstalTray from "gi://AstalTray"; -import AstalWp from "gi://AstalWp"; -import { switchPane } from "./sidebar"; - -interface ClassNameProps { - beforeSpacer: boolean; - afterSpacer: boolean; - first: boolean; - last: boolean; -} - -interface ModuleProps extends ClassNameProps { - monitor: Monitor; -} - -const hyprland = AstalHyprland.get_default(); - -const getBatteryIcon = (perc: number) => { - if (perc < 0.1) return ""; - if (perc < 0.2) return ""; - if (perc < 0.3) return ""; - if (perc < 0.4) return ""; - if (perc < 0.5) return ""; - if (perc < 0.6) return ""; - if (perc < 0.7) return ""; - if (perc < 0.8) return ""; - if (perc < 0.9) return ""; - return ""; -}; - -const formatSeconds = (sec: number) => { - if (sec >= 3600) { - const hours = Math.floor(sec / 3600); - let str = `${hours} hour${hours === 1 ? "" : "s"}`; - const mins = Math.floor((sec % 3600) / 60); - if (mins > 0) str += ` ${mins} minute${mins === 1 ? "" : "s"}`; - return str; - } else if (sec >= 60) { - const mins = Math.floor(sec / 60); - return `${mins} minute${mins === 1 ? "" : "s"}`; - } else return `${sec} second${sec === 1 ? "" : "s"}`; -}; - -const hookFocusedClientProp = ( - self: AstalWidget, - prop: keyof AstalHyprland.Client, - callback: (c: AstalHyprland.Client | null) => void -) => { - let id: number | null = null; - let lastClient: AstalHyprland.Client | null = null; - self.hook(hyprland, "notify::focused-client", () => { - if (id) lastClient?.disconnect(id); - lastClient = hyprland.focusedClient; // Can be null - id = lastClient?.connect(`notify::${kebabify(prop)}`, () => callback(lastClient)); - callback(lastClient); - }); - self.connect("destroy", () => id && lastClient?.disconnect(id)); - callback(lastClient); -}; - -const getClassName = ({ beforeSpacer, afterSpacer, first, last }: ClassNameProps) => - `${beforeSpacer ? "before-spacer" : ""} ${afterSpacer ? "after-spacer" : ""}` + - ` ${first ? "first" : ""} ${last ? "last" : ""}`; - -const getModule = (module: string) => { - module = module.toLowerCase(); - if (module === "osicon") return OSIcon; - if (module === "activewindow") return ActiveWindow; - if (module === "mediaplaying") return MediaPlaying; - if (module === "workspaces") return Workspaces; - if (module === "tray") return Tray; - if (module === "statusicons") return StatusIcons; - if (module === "pkgupdates") return PkgUpdates; - if (module === "notifcount") return NotifCount; - if (module === "battery") return Battery; - if (module === "datetime") return DateTime; - if (module === "power") return Power; - if (module === "brightnessspacer") return BrightnessSpacer; - if (module === "volumespacer") return VolumeSpacer; - return () => null; -}; - -const isSpacer = (module?: string) => module?.toLowerCase().endsWith("spacer") ?? false; - -const OSIcon = ({ monitor, ...props }: ModuleProps) => ( - <button - className={`module os-icon ${getClassName(props)}`} - onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "dashboard")} - > - {osIcon} - </button> -); - -const ActiveWindow = ({ monitor, ...props }: ModuleProps) => ( - <box - vertical={bind(config.vertical)} - className={`module active-window ${getClassName(props)}`} - setup={self => { - const title = Variable(""); - const updateTooltip = (c: AstalHyprland.Client | null) => - title.set(c?.class && c?.title ? `${c.class}: ${c.title}` : ""); - hookFocusedClientProp(self, "class", updateTooltip); - hookFocusedClientProp(self, "title", updateTooltip); - updateTooltip(hyprland.focusedClient); - - const window = setupCustomTooltip(self, bind(title)); - if (window) { - self.hook(title, (_, v) => !v && window.hide()); - self.hook(window, "map", () => !title.get() && window.hide()); - } - }} - > - <label - className="icon" - setup={self => - hookFocusedClientProp(self, "class", c => { - self.label = c?.class ? getAppCategoryIcon(c.class) : "desktop_windows"; - }) - } - /> - <label - truncate - angle={bind(config.vertical).as(v => (v ? 270 : 0))} - setup={self => { - const update = () => - (self.label = hyprland.focusedClient?.title ? hyprland.focusedClient.title : "Desktop"); - hookFocusedClientProp(self, "title", update); - self.hook(config.vertical, update); - }} - /> - </box> -); - -const MediaPlaying = ({ monitor, ...props }: ModuleProps) => { - const players = Players.get_default(); - const getLabel = (fallback = "") => - players.lastPlayer ? `${players.lastPlayer.title} - ${players.lastPlayer.artist}` : fallback; - return ( - <button - onClick={(_, event) => { - if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "audio"); - else if (event.button === Astal.MouseButton.SECONDARY) players.lastPlayer?.play_pause(); - else if (event.button === Astal.MouseButton.MIDDLE) players.lastPlayer?.raise(); - }} - setup={self => { - const label = Variable(getLabel()); - players.hookLastPlayer(self, ["notify::title", "notify::artist"], () => label.set(getLabel())); - setupCustomTooltip(self, bind(label)); - }} - > - <box vertical={bind(config.vertical)} className={`module media-playing ${getClassName(props)}`}> - <icon - setup={self => - players.hookLastPlayer(self, "notify::identity", () => { - const icon = `caelestia-${players.lastPlayer?.identity - .toLowerCase() - .replaceAll(" ", "-")}-symbolic`; - self.icon = players.lastPlayer - ? Astal.Icon.lookup_icon(icon) - ? icon - : "caelestia-media-generic-symbolic" - : "caelestia-media-none-symbolic"; - }) - } - /> - <label - truncate - angle={bind(config.vertical).as(v => (v ? 270 : 0))} - setup={self => { - const update = () => (self.label = getLabel("No media")); - players.hookLastPlayer(self, ["notify::title", "notify::artist"], update); - self.hook(config.vertical, update); - }} - /> - </box> - </button> - ); -}; - -const Workspace = ({ idx }: { idx: number }) => { - const wsId = Variable.derive([bind(hyprland, "focusedWorkspace"), config.modules.workspaces.shown], (f, s) => - f ? Math.floor((f.id - 1) / s) * s + idx : idx - ); - - const label = ( - <label - css={bind(config.modules.workspaces.xalign).as(a => `margin-left: ${a}px; margin-right: ${-a}px;`)} - label={bind(config.modules.workspaces.labels).as(l => l[idx - 1] ?? String(idx))} - /> - ); - - return ( - <button - halign={Gtk.Align.CENTER} - valign={Gtk.Align.CENTER} - onClicked={() => hyprland.dispatch("workspace", String(wsId.get()))} - setup={self => { - const updateOccupied = () => { - const occupied = hyprland.clients.some(c => c.workspace?.id === wsId.get()); - self.toggleClassName("occupied", occupied); - }; - const updateFocused = () => { - self.toggleClassName("focused", hyprland.focusedWorkspace?.id === wsId.get()); - updateOccupied(); - }; - - self.hook(hyprland, "client-added", updateOccupied); - self.hook(hyprland, "client-moved", updateOccupied); - self.hook(hyprland, "client-removed", updateOccupied); - self.hook(hyprland, "notify::focused-workspace", updateFocused); - updateFocused(); - }} - onDestroy={() => wsId.drop()} - > - <box - visible={bind(config.modules.workspaces.showLabels)} - vertical={bind(config.vertical)} - setup={self => { - const update = () => { - if (config.modules.workspaces.showWindows.get()) { - const clients = hyprland.clients.filter(c => c.workspace?.id === wsId.get()); - self.children = [ - label, - ...clients.map(c => ( - <label className="icon" label={bind(c, "class").as(getAppCategoryIcon)} /> - )), - ]; - } else self.children = [label]; - }; - self.hook(wsId, update); - self.hook(hyprland, "client-added", update); - self.hook(hyprland, "client-moved", update); - self.hook(hyprland, "client-removed", update); - update(); - }} - /> - </button> - ); -}; - -const Workspaces = ({ monitor, ...props }: ModuleProps) => { - const className = Variable.derive( - [config.modules.workspaces.shown, config.modules.workspaces.showLabels], - (s, l) => `module workspaces ${s % 2 === 0 ? "even" : "odd"} ${l ? "labels-shown" : ""} ${getClassName(props)}` - ); - - return ( - <eventbox - onScroll={(_, event) => { - const activeWs = hyprland.focusedClient?.workspace.name; - if (activeWs?.startsWith("special:")) hyprland.dispatch("togglespecialworkspace", activeWs.slice(8)); - else if (event.delta_y > 0 || hyprland.focusedWorkspace?.id > 1) - hyprland.dispatch("workspace", (event.delta_y < 0 ? "-" : "+") + 1); - }} - > - <box vertical={bind(config.vertical)} className={bind(className)} onDestroy={() => className.drop()}> - {bind(config.modules.workspaces.shown).as( - n => Array.from({ length: n }).map((_, idx) => <Workspace idx={idx + 1} />) // Start from 1 - )} - </box> - </eventbox> - ); -}; - -const TrayItem = (item: AstalTray.TrayItem) => ( - <menubutton - onButtonPressEvent={(_, event) => event.get_button()[1] === Astal.MouseButton.SECONDARY && item.activate(0, 0)} - usePopover={false} - direction={bind(config.vertical).as(v => (v ? Gtk.ArrowType.RIGHT : Gtk.ArrowType.DOWN))} - menuModel={bind(item, "menuModel")} - actionGroup={bind(item, "actionGroup").as(a => ["dbusmenu", a])} - setup={self => setupCustomTooltip(self, bind(item, "tooltipMarkup"))} - > - <icon halign={Gtk.Align.CENTER} gicon={bind(item, "gicon")} /> - </menubutton> -); - -const Tray = ({ monitor, ...props }: ModuleProps) => ( - <box - visible={bind(AstalTray.get_default(), "items").as(i => i.length > 0)} - vertical={bind(config.vertical)} - className={`module tray ${getClassName(props)}`} - > - {bind(AstalTray.get_default(), "items").as(i => i.map(TrayItem))} - </box> -); - -const Network = ({ monitor }: { monitor: Monitor }) => ( - <button - onClick={(_, event) => { - const network = AstalNetwork.get_default(); - if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity"); - else if (event.button === Astal.MouseButton.SECONDARY) network.wifi.enabled = !network.wifi.enabled; - else if (event.button === Astal.MouseButton.MIDDLE) { - if (GLib.find_program_in_path("gnome-control-center")) - execAsync("app2unit -- gnome-control-center wifi").catch(console.error); - else { - network.wifi.scan(); - execAsync( - "app2unit -- foot -T nmtui -- fish -c 'sleep .1; set -e COLORTERM; TERM=xterm-old nmtui connect'" - ).catch(() => {}); // Ignore errors - } - } - }} - setup={self => { - const network = AstalNetwork.get_default(); - const tooltipText = Variable(""); - const update = () => { - if (network.primary === AstalNetwork.Primary.WIFI) { - if (network.wifi.internet === AstalNetwork.Internet.CONNECTED) - tooltipText.set(`${network.wifi.ssid} | Strength: ${network.wifi.strength}/100`); - else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING) - tooltipText.set(`Connecting to ${network.wifi.ssid}`); - else tooltipText.set("Disconnected"); - } else if (network.primary === AstalNetwork.Primary.WIRED) { - if (network.wired.internet === AstalNetwork.Internet.CONNECTED) - tooltipText.set(`Speed: ${network.wired.speed}`); - else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) tooltipText.set("Connecting"); - else tooltipText.set("Disconnected"); - } else { - tooltipText.set("Unknown"); - } - }; - self.hook(network, "notify::primary", update); - self.hook(network.wifi, "notify::internet", update); - self.hook(network.wifi, "notify::ssid", update); - self.hook(network.wifi, "notify::strength", update); - if (network.wired) { - self.hook(network.wired, "notify::internet", update); - self.hook(network.wired, "notify::speed", update); - } - update(); - setupCustomTooltip(self, bind(tooltipText)); - }} - > - <stack - transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN} - transitionDuration={120} - shown={bind(AstalNetwork.get_default(), "primary").as(p => - p === AstalNetwork.Primary.WIFI ? "wifi" : "wired" - )} - > - <stack - name="wifi" - transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN} - transitionDuration={120} - setup={self => { - const network = AstalNetwork.get_default(); - const update = () => { - if (network.wifi.internet === AstalNetwork.Internet.CONNECTED) - self.shown = String(Math.ceil(network.wifi.strength / 25)); - else if (network.wifi.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting"; - else self.shown = "disconnected"; - }; - self.hook(network.wifi, "notify::internet", update); - self.hook(network.wifi, "notify::strength", update); - update(); - }} - > - <label className="icon" label="wifi_off" name="disconnected" /> - <label className="icon" label="settings_ethernet" name="connecting" /> - <label className="icon" label="signal_wifi_0_bar" name="0" /> - <label className="icon" label="network_wifi_1_bar" name="1" /> - <label className="icon" label="network_wifi_2_bar" name="2" /> - <label className="icon" label="network_wifi_3_bar" name="3" /> - <label className="icon" label="signal_wifi_4_bar" name="4" /> - </stack> - <stack - name="wired" - transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN} - transitionDuration={120} - setup={self => { - const network = AstalNetwork.get_default(); - const update = () => { - if (network.primary !== AstalNetwork.Primary.WIRED) return; - - if (network.wired.internet === AstalNetwork.Internet.CONNECTED) self.shown = "connected"; - else if (network.wired.internet === AstalNetwork.Internet.CONNECTING) self.shown = "connecting"; - else self.shown = "disconnected"; - }; - self.hook(network, "notify::primary", update); - if (network.wired) self.hook(network.wired, "notify::internet", update); - update(); - }} - > - <label className="icon" label="wifi_off" name="disconnected" /> - <label className="icon" label="settings_ethernet" name="connecting" /> - <label className="icon" label="lan" name="connected" /> - </stack> - </stack> - </button> -); - -const BluetoothDevice = ({ monitor, device }: { monitor: Monitor; device: AstalBluetooth.Device }) => ( - <button - visible={bind(device, "connected")} - onClick={(_, event) => { - if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity"); - else if (event.button === Astal.MouseButton.SECONDARY) - device.disconnect_device((_, res) => device.disconnect_device_finish(res)); - else if (event.button === Astal.MouseButton.MIDDLE) - execAsync("app2unit -- blueman-manager").catch(console.error); - }} - setup={self => setupCustomTooltip(self, bind(device, "alias"))} - > - <icon - icon={bind(device, "icon").as(i => - Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "caelestia-bluetooth-device-symbolic" - )} - /> - </button> -); - -const Bluetooth = ({ monitor }: { monitor: Monitor }) => ( - <box vertical={bind(config.vertical)} className="bluetooth"> - <button - onClick={(_, event) => { - if (event.button === Astal.MouseButton.PRIMARY) switchPane(monitor, "connectivity"); - else if (event.button === Astal.MouseButton.SECONDARY) AstalBluetooth.get_default().toggle(); - else if (event.button === Astal.MouseButton.MIDDLE) - execAsync("app2unit -- blueman-manager").catch(console.error); - }} - setup={self => { - const bluetooth = AstalBluetooth.get_default(); - const tooltipText = Variable(""); - const update = () => { - const devices = bluetooth.get_devices().filter(d => d.connected); - tooltipText.set( - devices.length > 0 - ? `Connected devices: ${devices.map(d => d.alias).join(", ")}` - : "No connected devices" - ); - }; - const hookDevice = (device: AstalBluetooth.Device) => { - self.hook(device, "notify::connected", update); - self.hook(device, "notify::alias", update); - }; - bluetooth.get_devices().forEach(hookDevice); - self.hook(bluetooth, "device-added", (_, device) => { - hookDevice(device); - update(); - }); - update(); - setupCustomTooltip(self, bind(tooltipText)); - }} - > - <stack - transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN} - transitionDuration={120} - shown={bind(AstalBluetooth.get_default(), "isPowered").as(p => (p ? "enabled" : "disabled"))} - > - <label className="icon" label="bluetooth" name="enabled" /> - <label className="icon" label="bluetooth_disabled" name="disabled" /> - </stack> - </button> - {bind(AstalBluetooth.get_default(), "devices").as(d => - d.map(d => <BluetoothDevice monitor={monitor} device={d} />) - )} - </box> -); - -const StatusIcons = ({ monitor, ...props }: ModuleProps) => ( - <box vertical={bind(config.vertical)} className={`module status-icons ${getClassName(props)}`}> - <Network monitor={monitor} /> - <Bluetooth monitor={monitor} /> - </box> -); - -const PkgUpdates = ({ monitor, ...props }: ModuleProps) => ( - <button - onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "packages")} - setup={self => - setupCustomTooltip( - self, - bind(Updates.get_default(), "numUpdates").as(n => `${n} update${n === 1 ? "" : "s"} available`) - ) - } - > - <box vertical={bind(config.vertical)} className={`module pkg-updates ${getClassName(props)}`}> - <label className="icon" label="download" /> - <label label={bind(Updates.get_default(), "numUpdates").as(String)} /> - </box> - </button> -); - -const NotifCount = ({ monitor, ...props }: ModuleProps) => ( - <button - onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "alerts")} - setup={self => - setupCustomTooltip( - self, - bind(AstalNotifd.get_default(), "notifications").as( - n => `${n.length} notification${n.length === 1 ? "" : "s"}` - ) - ) - } - > - <box vertical={bind(config.vertical)} className={`module notif-count ${getClassName(props)}`}> - <label - className="icon" - label={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "notifications_off" : "info"))} - /> - <revealer - transitionType={bind(config.vertical).as(v => - v ? Gtk.RevealerTransitionType.SLIDE_DOWN : Gtk.RevealerTransitionType.SLIDE_RIGHT - )} - transitionDuration={120} - revealChild={bind(AstalNotifd.get_default(), "dontDisturb").as(d => !d)} - > - <label label={bind(AstalNotifd.get_default(), "notifications").as(n => String(n.length))} /> - </revealer> - </box> - </button> -); - -const Battery = ({ monitor, ...props }: ModuleProps) => { - const className = Variable.derive( - [bind(AstalBattery.get_default(), "percentage"), bind(AstalBattery.get_default(), "charging")], - (p, c) => `module battery ${c ? "charging" : p < 0.2 ? "low" : ""} ${getClassName(props)}` - ); - const tooltip = Variable.derive( - [bind(AstalBattery.get_default(), "timeToEmpty"), bind(AstalBattery.get_default(), "timeToFull")], - (e, f) => (f > 0 ? `${formatSeconds(f)} until full` : `${formatSeconds(e)} remaining`) - ); - - return ( - <box - visible={bind(AstalBattery.get_default(), "isBattery")} - vertical={bind(config.vertical)} - className={bind(className)} - setup={self => setupCustomTooltip(self, bind(tooltip))} - onDestroy={() => { - className.drop(); - tooltip.drop(); - }} - > - <label className="icon" label={bind(AstalBattery.get_default(), "percentage").as(getBatteryIcon)} /> - <label label={bind(AstalBattery.get_default(), "percentage").as(p => `${Math.round(p * 100)}%`)} /> - </box> - ); -}; - -const DateTimeHoriz = (props: ClassNameProps) => ( - <box className={`module date-time ${getClassName(props)}`}> - <label className="icon" label="calendar_month" /> - <label - setup={self => { - const time = bindCurrentTime(bind(config.modules.dateTime.format), undefined, self); - self.label = time.get(); - self.hook(time, (_, t) => (self.label = t)); - }} - /> - </box> -); - -const DateTimeVertical = (props: ClassNameProps) => ( - <box vertical className={`module date-time ${getClassName(props)}`}> - <label className="icon" label="calendar_month" /> - <label label={bindCurrentTime("%H")} /> - <label label={bindCurrentTime("%M")} /> - </box> -); - -const DateTime = ({ monitor, ...props }: ModuleProps) => ( - <button - onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && switchPane(monitor, "time")} - setup={self => - setupCustomTooltip(self, bindCurrentTime(bind(config.modules.dateTime.detailedFormat), undefined, self)) - } - > - {bind(config.vertical).as(v => (v ? <DateTimeVertical {...props} /> : <DateTimeHoriz {...props} />))} - </button> -); - -const Power = ({ monitor, ...props }: ModuleProps) => ( - <button - className={`module power ${getClassName(props)}`} - label="power_settings_new" - onClick={(_, event) => event.button === Astal.MouseButton.PRIMARY && App.toggle_window("session")} - /> -); - -const Spacer = ({ onScroll }: { onScroll: (self: Widget.EventBox, event: Astal.ScrollEvent) => void }) => ( - <eventbox onScroll={onScroll}> - <box vertical={bind(config.vertical)}> - <ScreenCorner place="topleft" /> - <box expand /> - <ScreenCorner place={bind(config.vertical).as(v => (v ? "bottomleft" : "topright"))} /> - </box> - </eventbox> -); - -const BrightnessSpacer = ({ monitor }: { monitor: Monitor }) => ( - <Spacer onScroll={(_, event) => (event.delta_y > 0 ? (monitor.brightness -= 0.1) : (monitor.brightness += 0.1))} /> -); - -const VolumeSpacer = () => ( - <Spacer - onScroll={(_, event) => { - const speaker = AstalWp.get_default()?.audio.defaultSpeaker; - if (!speaker) return console.error("Unable to connect to WirePlumber."); - speaker.mute = false; - if (event.delta_y > 0) speaker.volume -= 0.1; - else speaker.volume += 0.1; - }} - /> -); - -const Bar = ({ monitor, layout }: { monitor: Monitor; layout: string }) => { - const className = Variable.derive( - [bind(config.vertical), bind(config.style)], - (v, s) => `bar ${v ? "vertical" : " horizontal"} ${s}` - ); - const modules = - layout === "centerbox" - ? Variable.derive(Object.values(config.layout.centerbox)) - : bind(config.layout.flowbox).as(m => [m]); - - const Layout = layout === "centerbox" ? Widget.CenterBox : Widget.Box; - return ( - <Layout - vertical={bind(config.vertical)} - className={bind(className)} - onDestroy={() => { - className.drop(); - if (modules instanceof Variable) modules.drop(); - }} - > - {bind(modules).as(modules => - modules.map((m, i) => ( - <box vertical={bind(config.vertical)}> - {m.map((n, j) => { - let beforeSpacer = false; - if (j < m.length - 1) beforeSpacer = isSpacer(m[j + 1]); - else if (i < modules.length - 1) beforeSpacer = isSpacer(modules[i + 1][0]); - let afterSpacer = false; - if (j > 0) afterSpacer = isSpacer(m[j - 1]); - else if (i > 0) afterSpacer = isSpacer(modules[i - 1].at(-1)); - const M = getModule(n); - return ( - <M - monitor={monitor} - beforeSpacer={beforeSpacer} - afterSpacer={afterSpacer} - first={i === 0 && j === 0} - last={i === modules.length - 1 && j === m.length - 1} - /> - ); - })} - </box> - )) - )} - </Layout> - ); -}; - -export default ({ monitor }: { monitor: Monitor }) => ( - <window - namespace="caelestia-bar" - monitor={monitor.id} - anchor={bind(config.vertical).as( - v => - Astal.WindowAnchor.TOP | - Astal.WindowAnchor.LEFT | - (v ? Astal.WindowAnchor.BOTTOM : Astal.WindowAnchor.RIGHT) - )} - exclusivity={Astal.Exclusivity.EXCLUSIVE} - > - <overlay - passThrough - overlays={[ - <ScreenCorner visible={bind(config.style).as(s => s !== "embedded")} place="topleft" />, - <ScreenCorner - visible={bind(config.style).as(s => s !== "embedded")} - halign={bind(config.vertical).as(v => (v ? undefined : Gtk.Align.END))} - valign={bind(config.vertical).as(v => (v ? Gtk.Align.END : undefined))} - place={bind(config.vertical).as(v => (v ? "bottomleft" : "topright"))} - />, - ]} - > - {bind(config.layout.type).as(l => ( - <Bar monitor={monitor} layout={l} /> - ))} - </overlay> - </window> -); diff --git a/src/modules/launcher/actions.tsx b/src/modules/launcher/actions.tsx deleted file mode 100644 index 40d37b5..0000000 --- a/src/modules/launcher/actions.tsx +++ /dev/null @@ -1,522 +0,0 @@ -import { Apps } from "@/services/apps"; -import Palette from "@/services/palette"; -import Schemes, { type Colours } from "@/services/schemes"; -import Wallpapers, { type ICategory, type IWallpaper } from "@/services/wallpapers"; -import { basename } from "@/utils/strings"; -import { notify } from "@/utils/system"; -import { setupCustomTooltip, type FlowBox } from "@/utils/widgets"; -import { bind, execAsync, GLib, readFile, register, type Variable } from "astal"; -import { Gtk, Widget } from "astal/gtk3"; -import { launcher as config } from "config"; -import { setConfig } from "config/funcs"; -import fuzzysort from "fuzzysort"; -import AstalHyprland from "gi://AstalHyprland"; -import { close, ContentBox, type LauncherContent, type Mode } from "./util"; - -interface IAction { - icon: string; - name: string; - description: string; - action: (...args: string[]) => void; - available?: () => boolean; -} - -interface ActionMap { - [k: string]: IAction; -} - -const variantActions = { - vibrant: { - icon: "sentiment_very_dissatisfied", - name: "Vibrant", - description: "A high chroma palette. The primary palette's chroma is at maximum.", - }, - tonalspot: { - icon: "android", - name: "Tonal Spot", - description: "Default for Material theme colours. A pastel palette with a low chroma.", - }, - expressive: { - icon: "compare_arrows", - name: "Expressive", - description: - "A medium chroma palette. The primary palette's hue is different from the seed colour, for variety.", - }, - fidelity: { - icon: "compare", - name: "Fidelity", - description: "Matches the seed colour, even if the seed colour is very bright (high chroma).", - }, - content: { - icon: "sentiment_calm", - name: "Content", - description: "Almost identical to fidelity.", - }, - fruitsalad: { - icon: "nutrition", - name: "Fruit Salad", - description: "A playful theme - the seed colour's hue does not appear in the theme.", - }, - rainbow: { - icon: "looks", - name: "Rainbow", - description: "A playful theme - the seed colour's hue does not appear in the theme.", - }, - neutral: { - icon: "contrast", - name: "Neutral", - description: "Close to grayscale, a hint of chroma.", - }, - monochrome: { - icon: "filter_b_and_w", - name: "Monochrome", - description: "All colours are grayscale, no chroma.", - }, -}; - -const transparencyActions = { - off: { - icon: "blur_off", - name: "Off", - description: "Completely opaque", - }, - low: { - icon: "blur_circular", - name: "Low", - description: "Less transparent", - }, - normal: { - icon: "blur_linear", - name: "Normal", - description: "Somewhat transparent", - }, - high: { - icon: "blur_on", - name: "High", - description: "Extremely transparent", - }, -}; - -const autocomplete = (entry: Widget.Entry, action: string) => { - entry.set_text(`${config.actionPrefix.get()}${action} `); - entry.set_position(-1); -}; - -const actions = (mode: Variable<Mode>, entry: Widget.Entry): ActionMap => ({ - apps: { - icon: "apps", - name: "Apps", - description: "Search for apps", - action: () => { - mode.set("apps"); - entry.set_text(""); - }, - }, - files: { - icon: "folder", - name: "Files", - description: "Search for files", - action: () => { - mode.set("files"); - entry.set_text(""); - }, - }, - math: { - icon: "calculate", - name: "Math", - description: "Do math calculations", - action: () => { - mode.set("math"); - entry.set_text(""); - }, - }, - light: { - icon: "light_mode", - name: "Light", - description: "Change scheme to light mode", - action: () => { - Palette.get_default().switchMode("light"); - close(); - }, - available: () => Palette.get_default().hasMode("light"), - }, - dark: { - icon: "dark_mode", - name: "Dark", - description: "Change scheme to dark mode", - action: () => { - Palette.get_default().switchMode("dark"); - close(); - }, - available: () => Palette.get_default().hasMode("dark"), - }, - scheme: { - icon: "palette", - name: "Scheme", - description: "Change the current colour scheme", - action: () => autocomplete(entry, "scheme"), - }, - variant: { - icon: "colors", - name: "Variant", - description: "Change the current scheme variant", - action: () => autocomplete(entry, "variant"), - available: () => Palette.get_default().scheme === "dynamic", - }, - wallpaper: { - icon: "image", - name: "Wallpaper", - description: "Change the current wallpaper", - action: () => autocomplete(entry, "wallpaper"), - }, - transparency: { - icon: "opacity", - name: "Transparency", - description: "Change shell transparency", - action: () => autocomplete(entry, "transparency"), - }, - todo: { - icon: "checklist", - name: "Todo", - description: "Create a todo in Todoist", - action: (...args) => { - // If no args, autocomplete cmd - if (args.length === 0) return autocomplete(entry, "todo"); - - // If tod not configured, notify - let token = null; - try { - token = JSON.parse(readFile(GLib.get_user_config_dir() + "/tod.cfg")).token; - } catch {} // Ignore - if (!token) { - notify({ - summary: "Tod not configured", - body: "You need to configure tod first. Run any tod command to do this.", - icon: "dialog-warning-symbolic", - urgency: "critical", - }); - } else { - // Create todo and notify if configured - execAsync(`tod t q -c ${args.join(" ")}`).catch(console.error); - if (config.todo.notify.get()) - notify({ - summary: "Todo created", - body: `Created todo with content: ${args.join(" ")}`, - icon: "view-list-bullet-symbolic", - urgency: "low", - transient: true, - actions: { - "Copy content": () => execAsync(`wl-copy -- ${args.join(" ")}`).catch(console.error), - View: () => { - const client = AstalHyprland.get_default().clients.find(c => c.class === "Todoist"); - if (client) client.focus(); - else execAsync("app2unit -- todoist").catch(console.error); - }, - }, - }); - } - - close(); - }, - available: () => !!GLib.find_program_in_path("tod"), - }, - reload: { - icon: "refresh", - name: "Reload", - description: "Reload app list", - action: () => { - Apps.reload(); - entry.set_text(""); - }, - }, - lock: { - icon: "lock", - name: "Lock", - description: "Lock the current session", - action: () => { - execAsync("loginctl lock-session").catch(console.error); - close(); - }, - }, - logout: { - icon: "logout", - name: "Logout", - description: "End the current session", - action: () => { - execAsync("uwsm stop").catch(console.error); - close(); - }, - }, - sleep: { - icon: "bedtime", - name: "Sleep", - description: "Suspend then hibernate", - action: () => { - execAsync("systemctl suspend-then-hibernate").catch(console.error); - close(); - }, - }, - reboot: { - icon: "cached", - name: "Reboot", - description: "Restart the machine", - action: () => { - execAsync("systemctl reboot").catch(console.error); - close(); - }, - }, - hibernate: { - icon: "downloading", - name: "Hibernate", - description: "Suspend to RAM", - action: () => { - execAsync("systemctl hibernate").catch(console.error); - close(); - }, - }, - shutdown: { - icon: "power_settings_new", - name: "Shutdown", - description: "Suspend to disk", - action: () => { - execAsync("systemctl poweroff").catch(console.error); - close(); - }, - }, -}); - -const Action = ({ args, icon, name, description, action }: IAction & { args: string[] }) => ( - <Gtk.FlowBoxChild visible canFocus={false}> - <button - className="result" - cursor="pointer" - onClicked={() => action(...args)} - setup={self => setupCustomTooltip(self, description)} - > - <box> - <label className="icon" label={icon} /> - <box vertical className="has-sublabel"> - <label truncate xalign={0} label={name} /> - <label truncate xalign={0} label={description} className="sublabel" /> - </box> - </box> - </button> - </Gtk.FlowBoxChild> -); - -const Swatch = ({ colour }: { colour: string }) => <box className="swatch" css={"background-color: " + colour + ";"} />; - -const Scheme = ({ scheme, name, colours }: { scheme?: string; name: string; colours?: Colours }) => { - const palette = colours![Palette.get_default().mode] ?? colours!.light ?? colours!.dark!; - return ( - <Gtk.FlowBoxChild visible canFocus={false}> - <button - className="result" - cursor="pointer" - onClicked={() => { - execAsync(`caelestia scheme ${scheme ?? ""} ${name}`).catch(console.error); - close(); - }} - > - <box> - <box valign={Gtk.Align.CENTER}> - <box className="swatch big left" css={"background-color: " + palette.base + ";"} /> - <box className="swatch big right" css={"background-color: " + palette.primary + ";"} /> - </box> - <box vertical className="has-sublabel"> - <label truncate xalign={0} label={scheme ? `${scheme} (${name})` : name} /> - <box className="swatches"> - <Swatch colour={palette.rosewater} /> - <Swatch colour={palette.flamingo} /> - <Swatch colour={palette.pink} /> - <Swatch colour={palette.mauve} /> - <Swatch colour={palette.red} /> - <Swatch colour={palette.maroon} /> - <Swatch colour={palette.peach} /> - <Swatch colour={palette.yellow} /> - <Swatch colour={palette.green} /> - <Swatch colour={palette.teal} /> - <Swatch colour={palette.sky} /> - <Swatch colour={palette.sapphire} /> - <Swatch colour={palette.blue} /> - <Swatch colour={palette.lavender} /> - </box> - </box> - </box> - </button> - </Gtk.FlowBoxChild> - ); -}; - -const Variant = ({ name }: { name: keyof typeof variantActions }) => ( - <Action - {...variantActions[name]} - args={[]} - action={() => { - execAsync(`caelestia variant ${name}`).catch(console.error); - close(); - }} - /> -); - -const Wallpaper = ({ path, thumbnails }: IWallpaper) => ( - <Gtk.FlowBoxChild visible canFocus={false}> - <button - className="result wallpaper-container" - cursor="pointer" - onClicked={() => { - execAsync(`caelestia wallpaper -f ${path}`).catch(console.error); - close(); - }} - setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))} - > - <box - vertical={config.wallpaper.style.get() !== "compact"} - className={`wallpaper ${config.wallpaper.style.get()}`} - > - <box - className="thumbnail" - css={bind(config.wallpaper.style).as( - s => "background-image: url('" + thumbnails[s as keyof typeof thumbnails] + "');" - )} - /> - <label truncate label={basename(path)} /> - </box> - </button> - </Gtk.FlowBoxChild> -); - -const CategoryThumbnail = ({ style, wallpapers }: { style: string; wallpapers: IWallpaper[] }) => ( - <box className="thumbnail"> - {wallpapers.slice(0, 3).map(w => ( - <box hexpand css={"background-image: url('" + w.thumbnails[style as keyof typeof w.thumbnails] + "');"} /> - ))} - </box> -); - -const Category = ({ path, wallpapers }: ICategory) => ( - <Gtk.FlowBoxChild visible canFocus={false}> - <button - className="result wallpaper-container" - cursor="pointer" - onClicked={() => { - execAsync(`caelestia wallpaper -d ${path}`).catch(console.error); - close(); - }} - setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))} - > - <box - vertical={config.wallpaper.style.get() !== "compact"} - className={`wallpaper ${config.wallpaper.style.get()}`} - > - {bind(config.wallpaper.style).as(s => - s === "compact" ? ( - <box - className="thumbnail" - css={"background-image: url('" + wallpapers[0].thumbnails.compact + "');"} - /> - ) : ( - <CategoryThumbnail style={s} wallpapers={wallpapers} /> - ) - )} - <label truncate label={basename(path)} /> - </box> - </button> - </Gtk.FlowBoxChild> -); - -const Transparency = ({ amount }: { amount: keyof typeof transparencyActions }) => ( - <Action - {...transparencyActions[amount]} - args={[]} - action={() => { - setConfig("style.transparency", amount).catch(console.error); - close(); - }} - /> -); - -@register() -export default class Actions extends Widget.Box implements LauncherContent { - #map: ActionMap; - #list: string[]; - - #content: FlowBox; - - constructor(mode: Variable<Mode>, entry: Widget.Entry) { - super({ name: "actions", className: "actions" }); - - this.#map = actions(mode, entry); - this.#list = Object.keys(this.#map); - - this.#content = (<ContentBox />) as FlowBox; - - this.add( - <scrollable expand hscroll={Gtk.PolicyType.NEVER}> - {this.#content} - </scrollable> - ); - } - - updateContent(search: string): void { - this.#content.foreach(c => c.destroy()); - const args = search.split(" "); - const action = args[0].slice(1).toLowerCase(); - - if (action === "scheme") { - const scheme = args[1] ?? ""; - const schemes = Object.values(Schemes.get_default().map) - .flatMap(s => (s.colours ? s.name : Object.values(s.flavours!).map(f => `${f.scheme}-${f.name}`))) - .filter(s => s !== undefined) - .sort(); - for (const { target } of fuzzysort.go(scheme, schemes, { all: true })) { - if (Schemes.get_default().map.hasOwnProperty(target)) - this.#content.add(<Scheme {...Schemes.get_default().map[target]} />); - else { - const [scheme, flavour] = target.split("-"); - this.#content.add(<Scheme {...Schemes.get_default().map[scheme].flavours![flavour]} />); - } - } - } else if (action === "variant") { - const list = Object.keys(variantActions); - - for (const { target } of fuzzysort.go(args[1], list, { all: true })) - this.#content.add(<Variant name={target as keyof typeof variantActions} />); - } else if (action === "wallpaper") { - if (args[1]?.toLowerCase() === "random") { - const list = Wallpapers.get_default().categories; - for (const { obj } of fuzzysort.go(args[2] ?? "", list, { all: true, key: "path" })) - this.#content.add(<Category {...obj} />); - } else { - const list = Wallpapers.get_default().list; - let limit = undefined; - if ((args[1] || !config.wallpaper.showAllEmpty.get()) && config.wallpaper.maxResults.get() > 0) - limit = config.wallpaper.maxResults.get(); - - for (const { obj } of fuzzysort.go(args[1] ?? "", list, { all: true, key: "path", limit })) - this.#content.add(<Wallpaper {...obj} />); - } - } else if (action === "transparency") { - const list = Object.keys(transparencyActions); - - for (const { target } of fuzzysort.go(args[1], list, { all: true })) - this.#content.add(<Transparency amount={target as keyof typeof transparencyActions} />); - } else { - const list = this.#list.filter( - a => this.#map[a].available?.() ?? !config.disabledActions.get().includes(a) - ); - for (const { target } of fuzzysort.go(action, list, { all: true })) - this.#content.add(<Action {...this.#map[target]} args={args.slice(1)} />); - } - } - - handleActivate(search: string): void { - const args = search.split(" "); - const action = args[0].slice(1).toLowerCase(); - - if (action === "scheme" && args[1]?.toLowerCase() === "random") { - execAsync(`caelestia scheme`).catch(console.error); - close(); - } else this.#content.get_child_at_index(0)?.get_child()?.activate(); - } -} diff --git a/src/modules/launcher/index.tsx b/src/modules/launcher/index.tsx deleted file mode 100644 index b75ecce..0000000 --- a/src/modules/launcher/index.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import PopupWindow from "@/widgets/popupwindow"; -import { bind, register, Variable } from "astal"; -import { Astal, Gtk, Widget } from "astal/gtk3"; -import { launcher as config } from "config"; -import Actions from "./actions"; -import Modes from "./modes"; -import type { Mode } from "./util"; - -const getModeIcon = (mode: Mode) => { - if (mode === "apps") return "apps"; - if (mode === "files") return "folder"; - if (mode === "math") return "calculate"; - return "search"; -}; - -const getPrettyMode = (mode: Mode) => { - if (mode === "apps") return "Apps"; - if (mode === "files") return "Files"; - if (mode === "math") return "Math"; - return mode; -}; - -const isAction = (text: string, action: string = "") => text.startsWith(config.actionPrefix.get() + action); - -const SearchBar = ({ mode, entry }: { mode: Variable<Mode>; entry: Widget.Entry }) => ( - <box className="search-bar"> - <box className="mode"> - <label className="icon" label={bind(mode).as(getModeIcon)} /> - <label label={bind(mode).as(getPrettyMode)} /> - </box> - {entry} - </box> -); - -const ModeSwitcher = ({ mode, modes }: { mode: Variable<Mode>; modes: Mode[] }) => ( - <box homogeneous hexpand className="mode-switcher"> - {modes.map(m => ( - <button - className={bind(mode).as(c => `mode ${c === m ? "selected" : ""}`)} - cursor="pointer" - onClicked={() => mode.set(m)} - > - <box halign={Gtk.Align.CENTER}> - <label className="icon" label={getModeIcon(m)} /> - <label label={getPrettyMode(m)} /> - </box> - </button> - ))} - </box> -); - -@register() -export default class Launcher extends PopupWindow { - readonly mode: Variable<Mode>; - - constructor() { - const entry = ( - <entry - hexpand - className="entry" - placeholderText={bind(config.actionPrefix).as(p => `Type "${p}" for subcommands`)} - /> - ) as Widget.Entry; - const mode = Variable<Mode>("apps"); - const content = Modes(); - const actions = new Actions(mode, entry); - const className = Variable.derive([mode, config.style], (m, s) => `launcher ${m} ${s}`); - - super({ - name: "launcher", - anchor: - Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT, - keymode: Astal.Keymode.EXCLUSIVE, - exclusivity: Astal.Exclusivity.IGNORE, - borderWidth: 0, - onKeyPressEvent(_, event) { - const keyval = event.get_keyval()[1]; - // Focus entry on typing - if (!entry.isFocus && keyval >= 32 && keyval <= 126) { - entry.text += String.fromCharCode(keyval); - entry.grab_focus(); - entry.set_position(-1); - - // Consume event, if not consumed it will duplicate character in entry - return true; - } - }, - child: ( - <box - vertical - halign={Gtk.Align.CENTER} - valign={Gtk.Align.CENTER} - className={bind(className)} - onDestroy={() => className.drop()} - > - <SearchBar mode={mode} entry={entry} /> - <stack - expand - transitionType={Gtk.StackTransitionType.CROSSFADE} - transitionDuration={100} - shown={bind(entry, "text").as(t => (isAction(t) ? "actions" : "content"))} - > - <stack - name="content" - transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT} - transitionDuration={200} - shown={bind(mode)} - > - {Object.values(content)} - </stack> - {actions} - </stack> - <ModeSwitcher mode={mode} modes={Object.keys(content) as Mode[]} /> - </box> - ), - }); - - this.mode = mode; - - content[mode.get()].updateContent(entry.get_text()); - this.hook(mode, (_, v: Mode) => { - entry.set_text(""); - content[v].updateContent(entry.get_text()); - }); - this.hook(entry, "changed", () => - (isAction(entry.get_text()) ? actions : content[mode.get()]).updateContent(entry.get_text()) - ); - this.hook(entry, "activate", () => { - (isAction(entry.get_text()) ? actions : content[mode.get()]).handleActivate(entry.get_text()); - if (mode.get() === "math" && !isAction(entry.get_text())) entry.set_text(""); // Cause math mode doesn't auto clear - }); - - // Clear search on hide if not in math mode or creating a todo - this.connect("hide", () => { - if ((mode.get() !== "math" || isAction(entry.get_text())) && !isAction(entry.get_text(), "todo")) - entry.set_text(""); - }); - } - - open(mode: Mode) { - this.mode.set(mode); - this.show(); - } -} diff --git a/src/modules/launcher/modes.tsx b/src/modules/launcher/modes.tsx deleted file mode 100644 index e278779..0000000 --- a/src/modules/launcher/modes.tsx +++ /dev/null @@ -1,225 +0,0 @@ -import { Apps as AppsService } from "@/services/apps"; -import MathService, { type HistoryItem } from "@/services/math"; -import { getAppCategoryIcon } from "@/utils/icons"; -import { launch } from "@/utils/system"; -import { type FlowBox, setupCustomTooltip } from "@/utils/widgets"; -import { bind, execAsync, Gio, register, Variable } from "astal"; -import { Astal, Gtk, Widget } from "astal/gtk3"; -import { launcher as config } from "config"; -import type AstalApps from "gi://AstalApps"; -import { close, ContentBox, type LauncherContent, limitLength } from "./util"; - -const AppResult = ({ app }: { app: AstalApps.Application }) => ( - <Gtk.FlowBoxChild visible canFocus={false}> - <button - className="result" - cursor="pointer" - onClicked={() => { - launch(app); - close(); - }} - setup={self => setupCustomTooltip(self, app.description ? `${app.name}: ${app.description}` : app.name)} - > - <box> - {app.iconName && Astal.Icon.lookup_icon(app.iconName) ? ( - <icon className="icon" icon={app.iconName} /> - ) : ( - <label className="icon" label={getAppCategoryIcon(app)} /> - )} - <label truncate label={app.name} /> - </box> - </button> - </Gtk.FlowBoxChild> -); - -const FileResult = ({ path }: { path: string }) => ( - <Gtk.FlowBoxChild visible canFocus={false}> - <button - className="result" - cursor="pointer" - onClicked={() => { - execAsync([ - "bash", - "-c", - `dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:'file://${path}' string:'' || app2unit -O '${path}'`, - ]).catch(console.error); - close(); - }} - > - <box setup={self => setupCustomTooltip(self, path.replace(HOME, "~"))}> - <icon - className="icon" - gicon={ - Gio.File.new_for_path(path) - .query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, null) - .get_icon()! - } - /> - <label - truncate - label={ - path.replace(HOME, "~").length > config.files.shortenThreshold.get() - ? path - .replace(HOME, "~") - .split("/") - .map((n, i, arr) => (i === 0 || i === arr.length - 1 ? n : n.slice(0, 1))) - .join("/") - : path.replace(HOME, "~") - } - /> - </box> - </button> - </Gtk.FlowBoxChild> -); - -const MathResult = ({ icon, equation, result }: HistoryItem) => ( - <Gtk.FlowBoxChild visible canFocus={false}> - <button - className="result" - cursor="pointer" - onClicked={() => { - execAsync(["wl-copy", "--", result]).catch(console.error); - close(); - }} - setup={self => setupCustomTooltip(self, `${equation} -> ${result}`)} - > - <box> - <label className="icon" label={icon} /> - <box vertical className="has-sublabel"> - <label truncate xalign={0} label={equation} /> - <label truncate xalign={0} label={result} className="sublabel" /> - </box> - </box> - </button> - </Gtk.FlowBoxChild> -); - -@register() -class Apps extends Widget.Box implements LauncherContent { - #content: FlowBox; - - constructor() { - super({ name: "apps", className: "apps" }); - - this.#content = (<ContentBox />) as FlowBox; - - this.add( - <scrollable expand hscroll={Gtk.PolicyType.NEVER}> - {this.#content} - </scrollable> - ); - } - - updateContent(search: string): void { - this.#content.foreach(c => c.destroy()); - for (const app of limitLength(AppsService.fuzzy_query(search), config.apps)) - this.#content.add(<AppResult app={app} />); - } - - handleActivate(): void { - this.#content.get_child_at_index(0)?.get_child()?.grab_focus(); - this.#content.get_child_at_index(0)?.get_child()?.activate(); - } -} - -@register() -class Files extends Widget.Box implements LauncherContent { - #content: FlowBox; - - constructor() { - super({ name: "files", className: "files" }); - - this.#content = (<ContentBox />) as FlowBox; - - this.add( - <scrollable expand hscroll={Gtk.PolicyType.NEVER}> - {this.#content} - </scrollable> - ); - } - - updateContent(search: string): void { - execAsync(["fd", ...config.files.fdOpts.get(), search, HOME]) - .then(out => { - this.#content.foreach(c => c.destroy()); - const paths = out.split("\n").filter(path => path); - for (const path of limitLength(paths, config.files)) this.#content.add(<FileResult path={path} />); - }) - .catch(() => {}); // Ignore errors - } - - handleActivate(): void { - this.#content.get_child_at_index(0)?.get_child()?.grab_focus(); - this.#content.get_child_at_index(0)?.get_child()?.activate(); - } -} - -@register() -class Math extends Widget.Box implements LauncherContent { - #showResult: Variable<boolean>; - #result: Variable<HistoryItem>; - #content: FlowBox; - - constructor() { - super({ name: "math", className: "math", vertical: true }); - - this.#showResult = Variable(false); - this.#result = Variable({ equation: "", result: "", icon: "" }); - this.#content = (<ContentBox />) as FlowBox; - - this.add( - <revealer - transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} - transitionDuration={150} - revealChild={bind(this.#showResult)} - > - <box vertical className="preview"> - <box className="result"> - <label className="icon" label={bind(this.#result).as(r => r.icon)} /> - <box vertical> - <label xalign={0} label="Result" /> - <label - truncate - xalign={0} - className="sublabel" - label={bind(this.#result).as(r => r.result)} - /> - </box> - </box> - <box visible={bind(config.style).as(s => s === "lines")} className="separator" /> - </box> - </revealer> - ); - this.add( - <scrollable expand hscroll={Gtk.PolicyType.NEVER}> - {this.#content} - </scrollable> - ); - } - - updateContent(search: string): void { - this.#showResult.set(search.length > 0); - this.#result.set(MathService.get_default().evaluate(search)); - - this.#content.foreach(c => c.destroy()); - for (const item of limitLength(MathService.get_default().history, config.math)) - this.#content.add(<MathResult {...item} />); - } - - handleActivate(search: string): void { - if (!search) return; - MathService.get_default().commit(); - const res = this.#result.get(); - // Copy and close if not assignment, help or error - if (!["equal", "help", "error"].includes(res.icon)) { - execAsync(["wl-copy", "--", res.result]).catch(console.error); - close(); - } - } -} - -export default () => ({ - apps: new Apps(), - files: new Files(), - math: new Math(), -}); diff --git a/src/modules/launcher/util.tsx b/src/modules/launcher/util.tsx deleted file mode 100644 index 8288588..0000000 --- a/src/modules/launcher/util.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { FlowBox } from "@/utils/widgets"; -import type { Variable } from "astal"; -import { App, Gtk } from "astal/gtk3"; - -export type Mode = "apps" | "files" | "math"; - -export interface LauncherContent { - updateContent(search: string): void; - handleActivate(search: string): void; -} - -export const close = () => App.get_window("launcher")?.hide(); - -export const limitLength = <T,>(arr: T[], cfg: { maxResults: Variable<number> }) => - cfg.maxResults.get() > 0 && arr.length > cfg.maxResults.get() ? arr.slice(0, cfg.maxResults.get()) : arr; - -export const ContentBox = () => ( - <FlowBox homogeneous valign={Gtk.Align.START} minChildrenPerLine={2} maxChildrenPerLine={2} /> -); diff --git a/src/modules/mediadisplay/index.tsx b/src/modules/mediadisplay/index.tsx deleted file mode 100644 index 307087c..0000000 --- a/src/modules/mediadisplay/index.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import type { Monitor } from "@/services/monitors"; -import Players from "@/services/players"; -import { lengthStr } from "@/utils/strings"; -import { bind, Variable } from "astal"; -import { App, Astal, Gtk } from "astal/gtk3"; -import AstalMpris from "gi://AstalMpris"; -import Visualiser from "./visualiser"; - -type Selected = Variable<AstalMpris.Player | null>; - -const bindIcon = (player: AstalMpris.Player) => - bind(player, "identity").as(i => { - const icon = `caelestia-${i?.toLowerCase().replaceAll(" ", "-")}-symbolic`; - return Astal.Icon.lookup_icon(icon) ? icon : "caelestia-media-generic-symbolic"; - }); - -const PlayerButton = ({ - player, - selected, - showDropdown, -}: { - player: AstalMpris.Player; - selected: Selected; - showDropdown: Variable<boolean>; -}) => ( - <button - cursor="pointer" - onClicked={() => { - showDropdown.set(false); - selected.set(player); - }} - > - <box className="identity" halign={Gtk.Align.CENTER}> - <label label={bind(player, "identity").as(i => i ?? "-")} /> - <label label="•" /> - <label label={bind(player, "title").as(t => t ?? "-")} /> - </box> - </button> -); - -const Selector = ({ player, selected }: { player?: AstalMpris.Player; selected: Selected }) => { - const showDropdown = Variable(false); - - return ( - <box vertical valign={Gtk.Align.START} className="selector"> - <button - sensitive={bind(Players.get_default(), "list").as(ps => ps.length > 1)} - cursor="pointer" - onClicked={() => showDropdown.set(!showDropdown.get())} - > - <box className="identity" halign={Gtk.Align.CENTER}> - <icon icon={player ? bindIcon(player) : "caelestia-media-none-symbolic"} /> - <label label={player ? bind(player, "identity").as(i => i ?? "") : "No media"} /> - </box> - </button> - <revealer - transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} - transitionDuration={150} - revealChild={bind(showDropdown)} - > - <box vertical className="list"> - {bind(Players.get_default(), "list").as(ps => - ps - .filter(p => p !== player) - .map(p => <PlayerButton player={p} selected={selected} showDropdown={showDropdown} />) - )} - </box> - </revealer> - </box> - ); -}; - -const NoMedia = ({ selected }: { selected: Selected }) => ( - <box> - <box homogeneous halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="cover-art"> - <label xalign={0.36} label="" /> - </box> - <box> - <box vertical className="details"> - <label truncate xalign={0} className="title" label="No media" /> - <label truncate xalign={0} className="artist" label="Try play something!" /> - <box halign={Gtk.Align.START} className="controls"> - <button sensitive={false} label="skip_previous" /> - <button sensitive={false} label="play_arrow" /> - <button sensitive={false} label="skip_next" /> - </box> - </box> - <box className="center-module"> - <overlay - expand - overlay={<label halign={Gtk.Align.CENTER} valign={Gtk.Align.END} className="time" label="-1:-1" />} - > - <Visualiser /> - </overlay> - </box> - <Selector selected={selected} /> - </box> - </box> -); - -const Player = ({ player, selected }: { player: AstalMpris.Player; selected: Selected }) => { - const time = Variable.derive( - [bind(player, "position"), bind(player, "length")], - (p, l) => lengthStr(p) + " / " + lengthStr(l) - ); - - return ( - <box> - <box - homogeneous - halign={Gtk.Align.CENTER} - valign={Gtk.Align.CENTER} - className="cover-art" - css={bind(player, "coverArt").as(a => `background-image: url("${a}");`)} - > - {bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.36} label="" />))} - </box> - <box> - <box vertical className="details"> - <label truncate xalign={0} className="title" label={bind(player, "title").as(t => t ?? "-")} /> - <label truncate xalign={0} className="artist" label={bind(player, "artist").as(t => t ?? "-")} /> - <box halign={Gtk.Align.START} className="controls"> - <button - sensitive={bind(player, "canGoPrevious")} - cursor="pointer" - onClicked={() => player.next()} - label="skip_previous" - /> - <button - sensitive={bind(player, "canControl")} - cursor="pointer" - onClicked={() => player.play_pause()} - label={bind(player, "playbackStatus").as(s => - s === AstalMpris.PlaybackStatus.PLAYING ? "pause" : "play_arrow" - )} - /> - <button - sensitive={bind(player, "canGoNext")} - cursor="pointer" - onClicked={() => player.next()} - label="skip_next" - /> - </box> - </box> - <box className="center-module"> - <overlay - expand - overlay={ - <label - halign={Gtk.Align.CENTER} - valign={Gtk.Align.END} - className="time" - label={bind(time)} - onDestroy={() => time.drop()} - /> - } - > - <Visualiser /> - </overlay> - </box> - <Selector player={player} selected={selected} /> - </box> - </box> - ); -}; - -export default ({ monitor }: { monitor: Monitor }) => { - const selected = Variable(Players.get_default().lastPlayer); - selected.observe(Players.get_default(), "notify::last-player", () => Players.get_default().lastPlayer); - - return ( - <window - application={App} - name={`mediadisplay${monitor.id}`} - namespace="caelestia-mediadisplay" - monitor={monitor.id} - anchor={Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM} - exclusivity={Astal.Exclusivity.EXCLUSIVE} - visible={false} - > - <box className="mediadisplay" onDestroy={() => selected.drop()}> - {bind(selected).as(p => - p ? <Player player={p} selected={selected} /> : <NoMedia selected={selected} /> - )} - </box> - </window> - ); -}; diff --git a/src/modules/mediadisplay/visualiser.tsx b/src/modules/mediadisplay/visualiser.tsx deleted file mode 100644 index d788e7b..0000000 --- a/src/modules/mediadisplay/visualiser.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Gtk } from "astal/gtk3"; -import cairo from "cairo"; -import AstalCava from "gi://AstalCava"; -import PangoCairo from "gi://PangoCairo"; - -export default () => ( - <drawingarea - className="visualiser" - setup={self => { - const cava = AstalCava.get_default(); - - if (cava) { - cava.set_stereo(true); - cava.set_noise_reduction(0.77); - cava.set_input(AstalCava.Input.PIPEWIRE); - - self.hook(cava, "notify::values", () => self.queue_draw()); - self.connect("size-allocate", () => { - const width = self.get_allocated_width(); - const barWidth = self - .get_style_context() - .get_property("min-width", Gtk.StateFlags.NORMAL) as number; - const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right; - const bars = Math.floor((width - gaps) / (barWidth + gaps)); - if (bars > 0) cava.set_bars(bars % 2 ? bars : bars - 1); - }); - } - - self.connect("draw", (_, cr: cairo.Context) => { - const { width, height } = self.get_allocation(); - - if (!cava) { - // Show error text if cava unavailable - const fg = self.get_style_context().get_color(Gtk.StateFlags.NORMAL); - cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - const layout = self.create_pango_layout("Visualiser module requires Cava"); - const [w, h] = layout.get_pixel_size(); - cr.moveTo((width - w) / 2, (height - h) / 2); - cr.setAntialias(cairo.Antialias.BEST); - PangoCairo.show_layout(cr, layout); - - return; - } - - const bg = self.get_style_context().get_background_color(Gtk.StateFlags.NORMAL); - cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha); - const barWidth = self.get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL) as number; - const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right; - - const values = cava.get_values(); - const len = values.length - 1; - const radius = barWidth / 2; - const xOff = (width - len * (barWidth + gaps) - gaps) / 2 - radius; - const center = height / 2; - const half = len / 2; - - const renderPill = (x: number, value: number) => { - x = x * (barWidth + gaps) + xOff; - value *= center; - cr.arc(x, center + value, radius, 0, Math.PI); - cr.arc(x, center - value, radius, Math.PI, Math.PI * 2); - cr.fill(); - }; - - // Render channels facing each other - for (let i = half - 1; i >= 0; i--) renderPill(half - i, values[i]); - for (let i = half; i < len; i++) renderPill(i + 1, values[i]); - }); - }} - /> -); diff --git a/src/modules/navbar.tsx b/src/modules/navbar.tsx deleted file mode 100644 index 35d3900..0000000 --- a/src/modules/navbar.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import type { Monitor } from "@/services/monitors"; -import { capitalize } from "@/utils/strings"; -import type { AstalWidget } from "@/utils/types"; -import { bind, execAsync, Variable } from "astal"; -import { App, Astal, Gtk } from "astal/gtk3"; -import { navbar as config } from "config"; -import AstalHyprland from "gi://AstalHyprland"; -import Pango from "gi://Pango"; -import SideBar, { awaitSidebar, paneNames, switchPane, type PaneName } from "./sidebar"; - -const layerNames = ["mediadisplay"] as const; -type LayerName = `${(typeof layerNames)[number]}${number}`; - -const specialWsNames = ["sysmon", "communication", "music", "todo"] as const; -type SpecialWsName = (typeof specialWsNames)[number]; - -const getPaneIcon = (name: PaneName) => { - if (name === "dashboard") return "dashboard"; - if (name === "audio") return "tune"; - if (name === "connectivity") return "settings_ethernet"; - if (name === "packages") return "package_2"; - if (name === "alerts") return "notifications"; - return "date_range"; -}; - -const getLayerIcon = (name: LayerName) => { - return "graphic_eq"; -}; - -const getSpecialWsIcon = (name: SpecialWsName) => { - if (name === "sysmon") return "speed"; - if (name === "communication") return "communication"; - if (name === "music") return "music_note"; - return "checklist"; -}; - -const hookIsCurrent = ( - self: AstalWidget, - sidebar: Variable<SideBar | null>, - name: PaneName, - callback: (isCurrent: boolean) => void -) => { - const unsub = sidebar.subscribe(s => { - if (!s) return; - self.hook(s.shown, (_, v) => callback(s.visible && v === name)); - self.hook(s, "notify::visible", () => callback(s.visible && s.shown.get() === name)); - callback(s.visible && s.shown.get() === name); - unsub(); - }); -}; - -const PaneButton = ({ - monitor, - name, - sidebar, -}: { - monitor: Monitor; - name: PaneName; - sidebar: Variable<SideBar | null>; -}) => ( - <button - cursor="pointer" - onClicked={() => switchPane(monitor, name)} - setup={self => hookIsCurrent(self, sidebar, name, c => self.toggleClassName("current", c))} - > - <box vertical className="nav-button"> - <label className="icon" label={getPaneIcon(name)} /> - <revealer - transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} - transitionDuration={150} - setup={self => { - let isCurrent = false; - hookIsCurrent(self, sidebar, name, c => { - isCurrent = c; - self.set_reveal_child(config.showLabels.get() && c); - }); - self.hook(config.showLabels, (_, v) => self.set_reveal_child(v && isCurrent)); - }} - > - <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} /> - </revealer> - </box> - </button> -); - -const LayerButton = ({ name }: { name: LayerName }) => ( - <button - cursor="pointer" - onClicked={() => App.toggle_window(name)} - setup={self => - self.hook(App, "window-toggled", (_, window) => { - if (window.name === name) self.toggleClassName("current", window.visible); - }) - } - > - <box vertical className="nav-button"> - <label className="icon" label={getLayerIcon(name)} /> - <revealer - transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} - transitionDuration={150} - setup={self => { - let visible = false; - self.hook(config.showLabels, (_, v) => self.toggleClassName(v && visible)); - self.hook(App, "window-toggled", (_, window) => { - if (window.name === name) - self.toggleClassName("current", config.showLabels.get() && window.visible); - }); - }} - > - <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} /> - </revealer> - </box> - </button> -); - -const SpecialWsButton = ({ name }: { name: SpecialWsName }) => { - const revealChild = Variable.derive( - [config.showLabels, bind(AstalHyprland.get_default(), "focusedClient")], - (l, c) => l && c?.get_workspace().get_name() === `special:${name}` - ); - - return ( - <button - className={bind(AstalHyprland.get_default(), "focusedClient").as(c => - c?.get_workspace().get_name() === `special:${name}` ? "current" : "" - )} - cursor="pointer" - onClicked={() => execAsync(`caelestia toggle ${name}`).catch(console.error)} - > - <box vertical className="nav-button"> - <label className="icon" label={getSpecialWsIcon(name)} /> - <revealer - transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} - transitionDuration={150} - revealChild={bind(revealChild)} - onDestroy={() => revealChild.drop()} - > - <label truncate wrapMode={Pango.WrapMode.WORD_CHAR} className="label" label={capitalize(name)} /> - </revealer> - </box> - </button> - ); -}; - -export default ({ monitor }: { monitor: Monitor }) => { - const sidebar = Variable<SideBar | null>(null); - awaitSidebar(monitor).then(s => sidebar.set(s)); - - return ( - <window - namespace="caelestia-navbar" - monitor={monitor.id} - anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM} - exclusivity={Astal.Exclusivity.EXCLUSIVE} - visible={config.persistent.get()} - setup={self => { - const hyprland = AstalHyprland.get_default(); - const visible = Variable(config.persistent.get()); - - visible.poll(100, () => { - const width = self.visible - ? Math.max(config.appearWidth.get(), self.get_allocated_width()) - : config.appearWidth.get(); - return hyprland.get_cursor_position().x < width; - }); - if (config.persistent.get()) visible.stopPoll(); - - self.hook(config.persistent, (_, v) => { - if (v) { - visible.stopPoll(); - visible.set(true); - } else visible.startPoll(); - }); - - self.hook(visible, (_, v) => self.set_visible(v)); - self.connect("destroy", () => visible.drop()); - }} - > - <eventbox - onScroll={(_, event) => { - const shown = sidebar.get()?.shown; - if (!shown) return; - const idx = paneNames.indexOf(shown.get()); - if (event.delta_y > 0) shown.set(paneNames[Math.min(paneNames.length - 1, idx + 1)]); - else shown.set(paneNames[Math.max(0, idx - 1)]); - }} - > - <box vertical className="navbar"> - {paneNames.map(n => ( - <PaneButton monitor={monitor} name={n} sidebar={sidebar} /> - ))} - {layerNames.map(n => ( - <LayerButton name={`${n}${monitor.id}`} /> - ))} - <box vexpand /> - {specialWsNames.map(n => ( - <SpecialWsButton name={n} /> - ))} - </box> - </eventbox> - </window> - ); -}; diff --git a/src/modules/notifpopups.tsx b/src/modules/notifpopups.tsx deleted file mode 100644 index cb5984d..0000000 --- a/src/modules/notifpopups.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import type { Monitor } from "@/services/monitors"; -import { setupChildClickthrough } from "@/utils/widgets"; -import Notification from "@/widgets/notification"; -import { Astal, Gtk } from "astal/gtk3"; -import { notifpopups as config } from "config"; -import AstalNotifd from "gi://AstalNotifd"; -import type SideBar from "./sidebar"; -import { awaitSidebar } from "./sidebar"; - -export default ({ monitor }: { monitor: Monitor }) => ( - <window - monitor={monitor.id} - namespace="caelestia-notifpopups" - anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT | Astal.WindowAnchor.BOTTOM} - > - <box - vertical - valign={Gtk.Align.START} - className="notifpopups" - setup={self => { - const notifd = AstalNotifd.get_default(); - const map = new Map<number, Notification>(); - - self.hook(notifd, "notified", (self, id) => { - if (notifd.dontDisturb) return; - - const notification = notifd.get_notification(id); - - const popup = (<Notification popup notification={notification} />) as Notification; - popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id)); - map.get(notification.id)?.destroyWithAnims(); - map.set(notification.id, popup); - - self.add( - <eventbox - onClick={(_, event) => { - // Activate notif or go to notif center on primary click - if (event.button === Astal.MouseButton.PRIMARY) { - if (notification.actions.length === 1) - notification.invoke(notification.actions[0].id); - else { - sidebar?.shown.set("alerts"); - sidebar?.show(); - popup.destroyWithAnims(); - } - } - // Dismiss on middle click - else if (event.button === Astal.MouseButton.MIDDLE) notification.dismiss(); - }} - // Close on hover lost - onHoverLost={() => popup.destroyWithAnims()} - setup={self => self.hook(popup, "destroy", () => self.destroy())} - > - {popup} - </eventbox> - ); - - // Limit number of popups - if (config.maxPopups.get() > 0 && self.children.length > config.maxPopups.get()) - map.values().next().value?.destroyWithAnims(); - }); - self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims()); - - let sidebar: SideBar | null = null; - awaitSidebar(monitor).then(s => (sidebar = s)); - - // Change input region to child region so can click through empty space - setupChildClickthrough(self); - }} - /> - </window> -); diff --git a/src/modules/osds.tsx b/src/modules/osds.tsx deleted file mode 100644 index 0f38823..0000000 --- a/src/modules/osds.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import Monitors, { type Monitor } from "@/services/monitors"; -import { capitalize } from "@/utils/strings"; -import PopupWindow from "@/widgets/popupwindow"; -import { bind, execAsync, register, timeout, Variable, type Time } from "astal"; -import { App, Astal, Gtk, Widget } from "astal/gtk3"; -import cairo from "cairo"; -import { osds as config } from "config"; -import AstalWp from "gi://AstalWp"; -import Cairo from "gi://cairo"; -import Pango from "gi://Pango"; -import PangoCairo from "gi://PangoCairo"; - -const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL); -const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number; - -const mix = (a: number, b: number, r: number) => a * r + b * (1 - r); - -const pangoWeightToStr = (weight: Pango.Weight) => { - switch (weight) { - case Pango.Weight.ULTRALIGHT: - return "UltraLight"; - case Pango.Weight.LIGHT: - return "Light"; - case Pango.Weight.BOLD: - return "Bold"; - case Pango.Weight.ULTRABOLD: - return "UltraBold"; - case Pango.Weight.HEAVY: - return "Heavy"; - default: - return "Normal"; - } -}; - -const SliderOsd = ({ - fillIcons, - monitor, - type, - windowSetup, - className = "", - initValue, - drawAreaSetup, -}: { - fillIcons?: boolean; - monitor?: Monitor; - type: "volume" | "brightness"; - windowSetup: (self: Widget.Window, show: () => void) => void; - className?: string; - initValue: number; - drawAreaSetup: (self: Widget.DrawingArea, icon: Variable<string>) => void; -}) => ( - <PopupWindow - name={type} - monitor={monitor?.id} - keymode={Astal.Keymode.NONE} - anchor={bind(config[type].position)} - margin={bind(config[type].margin)} - setup={self => { - let time: Time | null = null; - const hideAfterTimeout = () => { - time?.cancel(); - time = timeout(config[type].hideDelay.get(), () => self.hide()); - }; - self.connect("show", hideAfterTimeout); - windowSetup(self, () => { - self.show(); - hideAfterTimeout(); - }); - }} - > - <box className={type}> - <drawingarea - className={`inner ${className}`} - css={"font-size: " + initValue + "px;"} - setup={self => { - const halfPi = Math.PI / 2; - const vertical = - config[type].position.get() === Astal.WindowAnchor.LEFT || - config[type].position.get() === Astal.WindowAnchor.RIGHT; - - const icon = Variable(""); - drawAreaSetup(self, icon); - self.hook(icon, () => self.queue_draw()); - - // Init size - const styleContext = self.get_style_context(); - const width = getNumStyle(styleContext, "min-width"); - const height = getNumStyle(styleContext, "min-height"); - if (vertical) self.set_size_request(height, width); - else self.set_size_request(width, height); - - let fontDesc: Pango.FontDescription | null = null; - - self.connect("draw", (_, cr: cairo.Context) => { - const parent = self.get_parent(); - if (!parent) return; - - const styleContext = self.get_style_context(); - const pContext = parent.get_style_context(); - - let width = getNumStyle(styleContext, "min-width"); - let height = getNumStyle(styleContext, "min-height"); - - const progressValue = getNumStyle(styleContext, "font-size"); - let radius = getNumStyle(pContext, "border-radius"); - // Flatten when near 0, do before swap cause its simpler - radius = Math.min(radius, Math.min(width * progressValue, height) / 2); - - if (vertical) [width, height] = [height, width]; // Swap if vertical - self.set_size_request(width, height); - - const progressPosition = vertical - ? height * (1 - progressValue) + radius // Top is 0, but we want it to start from the bottom - : width * progressValue - radius; - - const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL); - cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha); - - // Background - if (vertical) { - cr.arc(radius, progressPosition, radius, -Math.PI, -halfPi); // Top left - cr.arc(width - radius, progressPosition, radius, -halfPi, 0); // Top right - cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right - } else { - cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left - cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right - cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right - } - cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left - cr.fill(); - - const fg = pContext.get_background_color(Gtk.StateFlags.NORMAL); - cr.setAntialias(cairo.Antialias.BEST); - - // Progress number, at top/right - let nw = 0; - let nh = 0; - if (config[type].showValue.get()) { - const numLayout = parent.create_pango_layout(String(Math.round(progressValue * 100))); - [nw, nh] = numLayout.get_pixel_size(); - let diff; - if (vertical) { - diff = ((1 - progressValue) * height) / nh; - cr.moveTo((width - nw) / 2, radius / 2); - } else { - diff = ((1 - progressValue) * width) / nw; - cr.moveTo(width - nw - radius, (height - nh) / 2); - } - diff = Math.max(0, Math.min(1, diff)); - - cr.setSourceRGBA( - mix(bg.red, fg.red, diff), - mix(bg.green, fg.green, diff), - mix(bg.blue, fg.blue, diff), - mix(bg.alpha, fg.alpha, diff) - ); - - PangoCairo.show_layout(cr, numLayout); - } - - // Progress icon, follows progress - if (fontDesc === null) { - const weight = pangoWeightToStr(getStyle(pContext, "font-weight") as Pango.Weight); - const size = getNumStyle(pContext, "font-size") * 1.5; - fontDesc = Pango.font_description_from_string( - `Material Symbols Rounded ${weight} ${size}px` - ); - // Ugh GTK CSS doesn't support font-variations, so you need to manually create the layout and font desc instead of using Gtk.Widget#create_pango_layout - if (fillIcons) fontDesc.set_variations("FILL=1"); - } - - const iconLayout = PangoCairo.create_layout(cr); - iconLayout.set_font_description(fontDesc); - iconLayout.set_text(icon.get(), -1); - - const [iw, ih] = iconLayout.get_pixel_size(); - let diff; - if (vertical) { - diff = (progressValue * height) / ih; - cr.moveTo( - (width - iw) / 2, - Math.max(nh, Math.min(height - ih, progressPosition - ih / 2 + radius)) - ); - } else { - diff = (progressValue * width) / iw; - cr.moveTo( - Math.min( - width - nw * 1.1 - iw - radius, - Math.max(0, progressPosition - iw / 2 - radius) - ), - (height - ih) / 2 - ); - } - diff = Math.max(0, Math.min(1, diff)); - - cr.setSourceRGBA( - mix(fg.red, bg.red, diff), - mix(fg.green, bg.green, diff), - mix(fg.blue, bg.blue, diff), - mix(fg.alpha, bg.alpha, diff) - ); - - PangoCairo.show_layout(cr, iconLayout); - }); - }} - /> - </box> - </PopupWindow> -); - -const Volume = ({ audio }: { audio: AstalWp.Audio }) => ( - <SliderOsd - fillIcons - type="volume" - windowSetup={(self, show) => { - self.hook(audio.defaultSpeaker, "notify::volume", show); - self.hook(audio.defaultSpeaker, "notify::mute", show); - }} - className={audio.defaultSpeaker.mute ? "mute" : ""} - initValue={audio.defaultSpeaker.volume} - drawAreaSetup={(self, icon) => { - const updateIcon = () => { - if (/head(phone|set)/i.test(audio.defaultSpeaker.icon)) icon.set("headphones"); - else if (audio.defaultSpeaker.mute) icon.set("no_sound"); - else if (audio.defaultSpeaker.volume === 0) icon.set("volume_mute"); - else if (audio.defaultSpeaker.volume <= 0.5) icon.set("volume_down"); - else icon.set("volume_up"); - }; - updateIcon(); - self.hook(audio.defaultSpeaker, "notify::icon", updateIcon); - self.hook(audio.defaultSpeaker, "notify::mute", () => { - updateIcon(); - self.toggleClassName("mute", audio.defaultSpeaker.mute); - }); - self.hook(audio.defaultSpeaker, "notify::volume", () => { - updateIcon(); - self.css = `font-size: ${audio.defaultSpeaker.volume}px`; - }); - }} - /> -); - -const Brightness = ({ monitor }: { monitor: Monitor }) => ( - <SliderOsd - monitor={monitor} - type="brightness" - windowSetup={(self, show) => self.hook(monitor, "notify::brightness", show)} - initValue={monitor.brightness} - drawAreaSetup={(self, icon) => { - const update = () => { - if (monitor.brightness > 0.66) icon.set("brightness_high"); - else if (monitor.brightness > 0.33) icon.set("brightness_medium"); - else if (monitor.brightness > 0) icon.set("brightness_low"); - else icon.set("brightness_empty"); - self.css = `font-size: ${monitor.brightness}px`; - }; - self.hook(monitor, "notify::brightness", update); - update(); - }} - /> -); - -@register() -class LockOsd extends Widget.Window { - readonly lockType: "caps" | "num"; - - #timeout: Time | null = null; - - constructor({ type, icon, right }: { type: "caps" | "num"; icon: string; right?: boolean }) { - super({ - visible: false, - name: `lock-${type}`, - application: App, - namespace: `caelestia-lock-${type}`, - anchor: - Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT, - exclusivity: Astal.Exclusivity.IGNORE, - }); - - this.lockType = type; - this.#update(); - - this.add( - <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className={`lock ${type}`}> - <label vexpand className="icon" label={icon} /> - <label vexpand className="text" label={capitalize(type) + "lock"} /> - </box> - ); - - // Clickthrough - this.connect("size-allocate", () => this.input_shape_combine_region(new Cairo.Region())); - - // Move over when other indicator opens/closes - this.hook(App, "window-toggled", (_, window) => { - if (window !== this && window instanceof LockOsd) { - const child = this.get_child(); - if (!child) return; - this[right ? "marginLeft" : "marginRight"] = window.visible - ? child.get_preferred_width()[1] + config.lock.spacing.get() - : 0; - } - }); - } - - #update() { - execAsync(`fish -c 'cat /sys/class/leds/input*::${this.lockType}lock/brightness'`) - .then(out => (this.get_child() as Widget.Box | null)?.toggleClassName("enabled", out.includes("1"))) - .catch(console.error); - } - - show() { - super.show(); - this.#update(); - this.#timeout?.cancel(); - this.#timeout = timeout(config.lock[this.lockType].hideDelay.get(), () => this.hide()); - } -} - -export default () => { - if (AstalWp.get_default()) <Volume audio={AstalWp.get_default()!.audio} />; - Monitors.get_default().forEach(monitor => <Brightness monitor={monitor} />); - - <LockOsd type="caps" icon="keyboard_capslock" />; - <LockOsd right type="num" icon="filter_1" />; - - return null; -}; diff --git a/src/modules/screencorners.tsx b/src/modules/screencorners.tsx deleted file mode 100644 index 4368b87..0000000 --- a/src/modules/screencorners.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { Monitor } from "@/services/monitors"; -import ScreenCorner from "@/widgets/screencorner"; -import { bind } from "astal/binding"; -import { Astal } from "astal/gtk3"; -import { bar } from "config"; -import Cairo from "gi://cairo"; - -export default ({ monitor }: { monitor: Monitor }) => ( - <window - namespace="caelestia-screencorners" - monitor={monitor.id} - anchor={bind(bar.vertical).as( - v => - Astal.WindowAnchor.BOTTOM | - Astal.WindowAnchor.RIGHT | - (v ? Astal.WindowAnchor.TOP : Astal.WindowAnchor.LEFT) - )} - setup={self => - self.connect("size-allocate", () => self.get_window()?.input_shape_combine_region(new Cairo.Region(), 0, 0)) - } - > - <box vertical={bind(bar.vertical)}> - <ScreenCorner place={bind(bar.vertical).as(v => (v ? "topright" : "bottomleft"))} /> - <box expand /> - <ScreenCorner place="bottomright" /> - </box> - </window> -); - -export const BarScreenCorners = ({ monitor }: { monitor: Monitor }) => ( - <window - namespace="caelestia-screencorners" - monitor={monitor.id} - anchor={bind(bar.vertical).as( - v => - Astal.WindowAnchor.TOP | - Astal.WindowAnchor.LEFT | - (v ? Astal.WindowAnchor.BOTTOM : Astal.WindowAnchor.RIGHT) - )} - visible={bind(bar.style).as(s => s === "embedded")} - setup={self => - self.connect("size-allocate", () => self.get_window()?.input_shape_combine_region(new Cairo.Region(), 0, 0)) - } - > - <box vertical={bind(bar.vertical)}> - <ScreenCorner place="topleft" /> - <box expand /> - <ScreenCorner place={bind(bar.vertical).as(v => (v ? "bottomleft" : "topright"))} /> - </box> - </window> -); diff --git a/src/modules/session.tsx b/src/modules/session.tsx deleted file mode 100644 index 40d3b31..0000000 --- a/src/modules/session.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import PopupWindow from "@/widgets/popupwindow"; -import { execAsync } from "astal"; -import { App, Astal, Gtk } from "astal/gtk3"; - -const Item = ({ icon, label, cmd, isDefault }: { icon: string; label: string; cmd: string; isDefault?: boolean }) => ( - <box vertical className="item"> - <button - cursor="pointer" - onClicked={() => execAsync(cmd).catch(console.error)} - setup={self => - isDefault && - self.hook(App, "window-toggled", (_, window) => { - if (window.name === "session" && window.visible) self.grab_focus(); - }) - } - > - <label className="icon" label={icon} /> - </button> - <label className="label" label={label} /> - </box> -); - -export default () => ( - <PopupWindow - className="session" - name="session" - anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT} - exclusivity={Astal.Exclusivity.IGNORE} - keymode={Astal.Keymode.EXCLUSIVE} - layer={Astal.Layer.OVERLAY} - borderWidth={0} // Don't need border width cause takes up entire screen - > - <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="inner"> - <box> - <Item icon="logout" label="Logout" cmd="uwsm stop" isDefault /> - <Item icon="cached" label="Reboot" cmd="systemctl reboot" /> - </box> - <box> - <Item icon="downloading" label="Hibernate" cmd="systemctl hibernate" /> - <Item icon="power_settings_new" label="Shutdown" cmd="systemctl poweroff" /> - </box> - </box> - </PopupWindow> -); diff --git a/src/modules/sidebar/alerts.tsx b/src/modules/sidebar/alerts.tsx deleted file mode 100644 index 9599aff..0000000 --- a/src/modules/sidebar/alerts.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import type { Monitor } from "@/services/monitors"; -import Headlines from "./modules/headlines"; -import Notifications from "./modules/notifications"; - -export default ({ monitor }: { monitor: Monitor }) => ( - <box vertical className="pane alerts" name="alerts"> - <Notifications /> - <box className="separator" /> - <Headlines monitor={monitor} /> - </box> -); diff --git a/src/modules/sidebar/audio.tsx b/src/modules/sidebar/audio.tsx deleted file mode 100644 index 20a6551..0000000 --- a/src/modules/sidebar/audio.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import DeviceSelector from "./modules/deviceselector"; -import Media from "./modules/media"; -import Streams from "./modules/streams"; - -export default () => ( - <box vertical className="pane audio" name="audio"> - <Media /> - <box className="separator" /> - <Streams /> - <box className="separator" /> - <DeviceSelector /> - </box> -); diff --git a/src/modules/sidebar/connectivity.tsx b/src/modules/sidebar/connectivity.tsx deleted file mode 100644 index 2962b56..0000000 --- a/src/modules/sidebar/connectivity.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Bluetooth from "./modules/bluetooth"; -import Networks from "./modules/networks"; - -export default () => ( - <box vertical className="pane connectivity" name="connectivity"> - <Networks /> - <box className="separator" /> - <Bluetooth /> - </box> -); diff --git a/src/modules/sidebar/dashboard.tsx b/src/modules/sidebar/dashboard.tsx deleted file mode 100644 index 1a8626f..0000000 --- a/src/modules/sidebar/dashboard.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import Players from "@/services/players"; -import { lengthStr } from "@/utils/strings"; -import { bindCurrentTime, osIcon } from "@/utils/system"; -import Slider from "@/widgets/slider"; -import { bind, GLib, monitorFile, Variable } from "astal"; -import { Gtk } from "astal/gtk3"; -import AstalMpris from "gi://AstalMpris"; -import Notifications from "./modules/notifications"; -import Upcoming from "./modules/upcoming"; - -const noNull = (s: string | null) => s ?? "-"; - -const FaceFallback = () => ( - <label - setup={self => { - const name = GLib.get_real_name(); - if (name !== "Unknown") - self.label = name - .split(" ") - .map(s => s[0].toUpperCase()) - .join(""); - else { - self.label = ""; - self.xalign = 0.44; - } - }} - /> -); - -const User = () => { - const uptime = Variable("").poll(5000, "uptime -p"); - const hasFace = Variable(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS)); - - return ( - <box className="user"> - <box - homogeneous - className="face" - setup={self => { - self.css = `background-image: url("${HOME}/.face");`; - const monitor = monitorFile(HOME + "/.face", () => { - hasFace.set(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS)); - self.css = `background-image: url("${HOME}/.face");`; - }); - self.connect("destroy", () => monitor.cancel()); - }} - > - {bind(hasFace).as(h => (h ? <box visible={false} /> : <FaceFallback />))} - </box> - <box vertical hexpand valign={Gtk.Align.CENTER} className="details"> - <label truncate xalign={0} className="name" label={`${osIcon} ${GLib.get_user_name()}`} /> - <label truncate xalign={0} label={bind(uptime)} onDestroy={() => uptime.drop()} /> - <label truncate xalign={0} label={bindCurrentTime("%A, %e %B")} /> - </box> - </box> - ); -}; - -const Media = ({ player }: { player: AstalMpris.Player | null }) => { - const position = player - ? Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l) - : Variable(0); - - return ( - <box className="media" onDestroy={() => position.drop()}> - <box - homogeneous - className="cover-art" - css={player ? bind(player, "coverArt").as(a => `background-image: url("${a}");`) : ""} - > - {player ? ( - bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.31} label="" />)) - ) : ( - <label xalign={0.31} label="" /> - )} - </box> - <box vertical className="details"> - <label truncate className="title" label={player ? bind(player, "title").as(noNull) : ""} /> - <label truncate className="artist" label={player ? bind(player, "artist").as(noNull) : "No media"} /> - <box hexpand className="controls"> - <button - hexpand - sensitive={player ? bind(player, "canGoPrevious") : false} - cursor="pointer" - onClicked={() => player?.next()} - label="" - /> - <button - hexpand - sensitive={player ? bind(player, "canControl") : false} - cursor="pointer" - onClicked={() => player?.play_pause()} - label={ - player - ? bind(player, "playbackStatus").as(s => - s === AstalMpris.PlaybackStatus.PLAYING ? "" : "" - ) - : "" - } - /> - <button - hexpand - sensitive={player ? bind(player, "canGoNext") : false} - cursor="pointer" - onClicked={() => player?.next()} - label="" - /> - </box> - <Slider value={bind(position)} onChange={(_, v) => player?.set_position(v * player.length)} /> - <box className="time"> - <label label={player ? bind(player, "position").as(lengthStr) : "-1:-1"} /> - <box hexpand /> - <label label={player ? bind(player, "length").as(lengthStr) : "-1:-1"} /> - </box> - </box> - </box> - ); -}; - -export default () => ( - <box vertical className="pane dashboard" name="dashboard"> - <User /> - <box className="separator" /> - {bind(Players.get_default(), "lastPlayer").as(p => ( - <Media player={p} /> - ))} - <box className="separator" /> - <Notifications compact /> - <box className="separator" /> - <Upcoming /> - </box> -); diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx deleted file mode 100644 index 7570283..0000000 --- a/src/modules/sidebar/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { Monitor } from "@/services/monitors"; -import { bind, idle, register, Variable } from "astal"; -import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3"; -import { sidebar as config } from "config"; -import Alerts from "./alerts"; -import Audio from "./audio"; -import Connectivity from "./connectivity"; -import Dashboard from "./dashboard"; -import Packages from "./packages"; -import Time from "./time"; - -export const paneNames = ["dashboard", "audio", "connectivity", "packages", "alerts", "time"] as const; -export type PaneName = (typeof paneNames)[number]; - -export const switchPane = (monitor: Monitor, name: PaneName) => { - const sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null; - if (sidebar) { - if (sidebar.visible && sidebar.shown.get() === name) sidebar.hide(); - else sidebar.show(); - sidebar.shown.set(name); - } -}; - -export const awaitSidebar = (monitor: Monitor) => - new Promise<SideBar>(resolve => { - let sidebar: SideBar | null = null; - - const awaitSidebar = () => { - sidebar = App.get_window(`sidebar${monitor.id}`) as SideBar | null; - if (sidebar) resolve(sidebar); - else idle(awaitSidebar); - }; - idle(awaitSidebar); - }); - -const getPane = (monitor: Monitor, name: PaneName) => { - if (name === "dashboard") return <Dashboard />; - if (name === "audio") return <Audio />; - if (name === "connectivity") return <Connectivity />; - if (name === "packages") return <Packages monitor={monitor} />; - if (name === "alerts") return <Alerts monitor={monitor} />; - return <Time />; -}; - -@register() -export default class SideBar extends Widget.Window { - readonly shown: Variable<PaneName>; - - constructor({ monitor }: { monitor: Monitor }) { - super({ - application: App, - name: `sidebar${monitor.id}`, - namespace: "caelestia-sidebar", - monitor: monitor.id, - anchor: Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM, - exclusivity: Astal.Exclusivity.EXCLUSIVE, - visible: false, - }); - - this.shown = Variable(paneNames[0]); - - this.add( - <eventbox - onScroll={(_, event) => { - if (event.modifier & Gdk.ModifierType.BUTTON1_MASK) { - const index = paneNames.indexOf(this.shown.get()) + (event.delta_y < 0 ? -1 : 1); - if (index < 0 || index >= paneNames.length) return; - this.shown.set(paneNames[index]); - } - }} - > - <box vertical className="sidebar"> - <stack - vexpand - transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN} - transitionDuration={200} - shown={bind(this.shown)} - > - {paneNames.map(n => getPane(monitor, n))} - </stack> - </box> - </eventbox> - ); - - if (config.showOnStartup.get()) idle(() => this.show()); - } -} diff --git a/src/modules/sidebar/modules/bluetooth.tsx b/src/modules/sidebar/modules/bluetooth.tsx deleted file mode 100644 index 89d0cb7..0000000 --- a/src/modules/sidebar/modules/bluetooth.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { bind, Variable } from "astal"; -import { Astal, Gtk } from "astal/gtk3"; -import AstalBluetooth from "gi://AstalBluetooth"; - -const sortDevices = (a: AstalBluetooth.Device, b: AstalBluetooth.Device) => { - if (a.connected || b.connected) return a.connected ? -1 : 1; - if (a.paired || b.paired) return a.paired ? -1 : 1; - return 0; -}; - -const BluetoothDevice = (device: AstalBluetooth.Device) => ( - <box className={bind(device, "connected").as(c => `device ${c ? "connected" : ""}`)}> - <icon - className="icon" - icon={bind(device, "icon").as(i => - Astal.Icon.lookup_icon(`${i}-symbolic`) ? `${i}-symbolic` : "bluetooth-symbolic" - )} - /> - <box vertical hexpand> - <label truncate xalign={0} label={bind(device, "alias")} /> - <label - truncate - className="sublabel" - xalign={0} - setup={self => { - const update = () => { - self.label = - (device.connected ? "Connected" : "Paired") + - (device.batteryPercentage >= 0 ? ` (${device.batteryPercentage * 100}%)` : ""); - self.visible = device.connected || device.paired; - }; - self.hook(device, "notify::connected", update); - self.hook(device, "notify::paired", update); - self.hook(device, "notify::battery-percentage", update); - update(); - }} - /> - </box> - <button - valign={Gtk.Align.CENTER} - visible={bind(device, "paired")} - cursor="pointer" - onClicked={() => AstalBluetooth.get_default().adapter.remove_device(device)} - label="delete" - /> - <button - valign={Gtk.Align.CENTER} - cursor="pointer" - onClicked={self => { - if (device.connected) - device.disconnect_device((_, res) => { - self.sensitive = true; - device.disconnect_device_finish(res); - }); - else - device.connect_device((_, res) => { - self.sensitive = true; - device.connect_device_finish(res); - }); - self.sensitive = false; - }} - label={bind(device, "connected").as(c => (c ? "bluetooth_disabled" : "bluetooth_searching"))} - /> - </box> -); - -const List = ({ devNotify }: { devNotify: Variable<boolean> }) => ( - <box vertical valign={Gtk.Align.START} className="list"> - {bind(devNotify).as(() => AstalBluetooth.get_default().devices.sort(sortDevices).map(BluetoothDevice))} - </box> -); - -const NoDevices = () => ( - <box homogeneous name="empty"> - <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty"> - <label className="icon" label="bluetooth_searching" /> - <label label="No Bluetooth devices" /> - </box> - </box> -); - -export default () => { - const bluetooth = AstalBluetooth.get_default(); - const devNotify = Variable(false); // Aggregator for device state changes (connected/paired) - - const update = () => devNotify.set(!devNotify.get()); - const connectSignals = (device: AstalBluetooth.Device) => { - device.connect("notify::connected", update); - device.connect("notify::paired", update); - }; - bluetooth.get_devices().forEach(connectSignals); - bluetooth.connect("device-added", (_, device) => connectSignals(device)); - bluetooth.connect("notify::devices", update); - - return ( - <box vertical className="bluetooth"> - <box className="header-bar"> - <label - label={bind(devNotify).as(() => { - const nConnected = bluetooth.get_devices().filter(d => d.connected).length; - return `${nConnected} connected device${nConnected === 1 ? "" : "s"}`; - })} - /> - <box hexpand /> - <button - className={bind(bluetooth.adapter, "discovering").as(d => (d ? "enabled" : ""))} - cursor="pointer" - onClicked={() => { - if (bluetooth.adapter.discovering) bluetooth.adapter.start_discovery(); - else bluetooth.adapter.stop_discovery(); - }} - label=" Discovery" - /> - </box> - <stack - transitionType={Gtk.StackTransitionType.CROSSFADE} - transitionDuration={200} - shown={bind(bluetooth, "devices").as(d => (d.length > 0 ? "list" : "empty"))} - > - <NoDevices /> - <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list"> - <List devNotify={devNotify} /> - </scrollable> - </stack> - </box> - ); -}; diff --git a/src/modules/sidebar/modules/calendar.tsx b/src/modules/sidebar/modules/calendar.tsx deleted file mode 100644 index bb36909..0000000 --- a/src/modules/sidebar/modules/calendar.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import Calendar, { type IEvent } from "@/services/calendar"; -import { setupCustomTooltip } from "@/utils/widgets"; -import { bind, GLib, Variable } from "astal"; -import { Gtk } from "astal/gtk3"; -import ical from "ical.js"; - -const isLeapYear = (year: number) => year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0); - -const getMonthDays = (month: number, year: number) => { - const leapYear = isLeapYear(year); - if (month === 2 && leapYear) return leapYear ? 29 : 28; - if ((month <= 7 && month % 2 === 1) || (month >= 8 && month % 2 === 0)) return 31; - return 30; -}; - -const getNextMonthDays = (month: number, year: number) => { - if (month === 12) return 31; - return getMonthDays(month + 1, year); -}; - -const getPrevMonthDays = (month: number, year: number) => { - if (month === 1) return 31; - return getMonthDays(month - 1, year); -}; - -export function getCalendarLayout(date: ical.Time) { - const weekdayOfMonthFirst = date.startOfMonth().dayOfWeek(ical.Time.MONDAY); - const daysInMonth = getMonthDays(date.month, date.year); - const daysInPrevMonth = getPrevMonthDays(date.month, date.year); - - const calendar: ical.Time[][] = []; - let idx = -weekdayOfMonthFirst + 2; - - for (let i = 0; i < 6; i++) { - calendar.push([]); - - for (let j = 0; j < 7; j++) { - let cDay = idx++; - let cMonth = date.month; - let cYear = date.year; - - if (idx < 0) { - cDay = daysInPrevMonth + cDay; - cMonth--; - - if (cMonth < 0) { - cMonth += 12; - cYear--; - } - } else if (idx > daysInMonth) { - cDay -= daysInMonth; - cMonth++; - - if (cMonth > 12) { - cMonth -= 12; - cYear++; - } - } - - calendar[i].push(ical.Time.fromData({ day: cDay, month: cMonth, year: cYear })); - } - } - - return calendar; -} - -const dateToMonthYear = (date: ical.Time) => { - const months = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]; - return `${months[date.month - 1]} ${date.year}`; -}; - -const addMonths = (date: ical.Time, num: number) => { - date = date.clone(); - if (num > 0) for (let i = 0; i < num; i++) date.adjust(getNextMonthDays(date.month, date.year), 0, 0, 0); - else for (let i = 0; i > num; i--) date.adjust(-getPrevMonthDays(date.month, date.year), 0, 0, 0); - return date; -}; - -const getDayClassName = (day: ical.Time, current: Variable<ical.Time>) => { - const isToday = day.toJSDate().toDateString() === new Date().toDateString() ? "today" : ""; - const numEvents = Math.min(5, Calendar.get_default().getEventsForDay(day).length); - return `day ${isToday} ${day.month !== current.get().month ? "dim" : ""} events-${numEvents}`; -}; - -const getDayTooltip = (day: ical.Time) => { - const events = Calendar.get_default().getEventsForDay(day); - if (!events.length) return ""; - const eventsStr = events - .map(e => { - const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime()); - const end = GLib.DateTime.new_from_unix_local(e.endDate.toUnixTime()); - const sameAmPm = start.format("%P") === end.format("%P"); - const time = `${start.format(`%-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`; - return `<b>${e.event.summary.replaceAll("&", "&")}</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> -); |