import type SideBar from "@/modules/sidebar"; 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"; 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 switchPane = (monitor: Monitor, name: string) => { 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); } }; 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) => ( ); const ActiveWindow = ({ monitor, ...props }: ModuleProps) => ( { 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()); } }} > ); const MediaPlaying = ({ monitor, ...props }: ModuleProps) => { const players = Players.get_default(); const getLabel = (fallback = "") => players.lastPlayer ? `${players.lastPlayer.title} - ${players.lastPlayer.artist}` : fallback; return ( ); }; const Workspace = ({ idx }: { idx: number }) => { let wsId = hyprland.focusedWorkspace ? Math.floor((hyprland.focusedWorkspace.id - 1) / config.modules.workspaces.shown.get()) * config.modules.workspaces.shown.get() + idx : idx; return ( ); }; 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 ( { 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); }} > className.drop()}> {bind(config.modules.workspaces.shown).as( n => Array.from({ length: n }).map((_, idx) => ) // Start from 1 )} ); }; const TrayItem = (item: AstalTray.TrayItem) => ( 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"))} > ); const Tray = ({ monitor, ...props }: ModuleProps) => ( i.length > 0)} vertical={bind(config.vertical)} className={`module tray ${getClassName(props)}`} > {bind(AstalTray.get_default(), "items").as(i => i.map(TrayItem))} ); const Network = ({ monitor }: { monitor: Monitor }) => ( ); const BluetoothDevice = ({ monitor, device }: { monitor: Monitor; device: AstalBluetooth.Device }) => ( ); const Bluetooth = ({ monitor }: { monitor: Monitor }) => ( {bind(AstalBluetooth.get_default(), "devices").as(d => d.map(d => ) )} ); const StatusIcons = ({ monitor, ...props }: ModuleProps) => ( ); const PkgUpdates = ({ monitor, ...props }: ModuleProps) => ( ); const NotifCount = ({ monitor, ...props }: ModuleProps) => ( ); 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 ( setupCustomTooltip(self, bind(tooltip))} onDestroy={() => { className.drop(); tooltip.drop(); }} > ); }; const DateTimeHoriz = (props: ClassNameProps) => ( ); const DateTimeVertical = (props: ClassNameProps) => ( ); const DateTime = ({ monitor, ...props }: ModuleProps) => ( ); const Power = ({ monitor, ...props }: ModuleProps) => (