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) => (
);
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 }) => {
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 = (
`margin-left: ${a}px; margin-right: ${-a}px;`)}
label={bind(config.modules.workspaces.labels).as(l => l[idx - 1] ?? String(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();
}}
>
`${Math.round(p * 100)}%`)} />
);
};
const DateTimeHoriz = (props: ClassNameProps) => (
{
const time = bindCurrentTime(bind(config.modules.dateTime.format), undefined, self);
self.label = time.get();
self.hook(time, (_, t) => (self.label = t));
}}
/>
);
const DateTimeVertical = (props: ClassNameProps) => (
);
const DateTime = ({ monitor, ...props }: ModuleProps) => (
);
const Power = ({ monitor, ...props }: ModuleProps) => (