import { App, Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { GLib, Variable, bind } from "astal"; import Tray from "gi://AstalTray"; import { execAsync } from "astal/process" import Hyprland from "gi://AstalHyprland"; import { getIconName } from "./utils"; import Wp from "gi://AstalWp" import Battery from "gi://AstalBattery" const battery = Battery.get_default() const sensorsAvailable = await execAsync(['sensors']).then(() => true).catch(() => false); const wirePlumber = Wp.get_default(); function SysTray(): JSX.Element { const tray = Tray.get_default(); return ( <box> {bind(tray, "items").as((items) => items.map((item) => { if (item.iconThemePath) App.add_icons(item.iconThemePath); const menu = item.create_menu(); return ( <button className="systray" tooltipMarkup={bind(item, "tooltipMarkup")} onDestroy={() => menu?.destroy()} onClickRelease={(self) => { menu?.popup_at_widget( self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null, ); }}> <icon gIcon={bind(item, "gicon")} className="systray-item" /> </button> ); }), )} </box> ); } function Left() : JSX.Element { return ( <box hexpand halign={Gtk.Align.START}> <Clients /> </box> ); } function Center() : JSX.Element { return ( <box> <Workspaces /> </box> ); } function Date({ format = "%Y-%m-%d" }): JSX.Element { const time = Variable<string>("").poll(60000, () => GLib.DateTime.new_now_local().format(format)!) return <button className="item" onDestroy={() => time.drop()} label={time()} onClicked={() => execAsync(['gnome-calendar'])} /> } function Time({ format = "%H:%M:%S" }): JSX.Element { const time = Variable<string>("").poll(1000, () => GLib.DateTime.new_now_local().format(format)!) return <label className="item blue" onDestroy={() => time.drop()} label={time()} /> } function Temp(): JSX.Element { let label = Variable<string>("N/A"); if (sensorsAvailable) { label = Variable<string>("").poll(5000, 'sensors', out => { const match = out.split('\n').find(line => line.includes('Tctl') || line.includes('Package'))?.match(/[0-9.]*°C/); return match ? match[0] : "N/A"; }) } return <label className="item blue" onDestroy={() => label.drop()} label={label()} /> } function Memory(): JSX.Element { const memory = Variable<string>("").poll(2000, "free", out => { const line = out.split('\n').find(line => line.includes('Mem:')); if (!line) return "N/A"; const split = line.split(/\s+/).map(Number); return (split[2] / 1000000).toFixed(2) + "GB"; }); return <label className="item blue" onDestroy={() => memory.drop()} label={memory()} /> } function ClockSpeed(): JSX.Element { const command = 'bash -c "cat /proc/cpuinfo | grep \\"MHz\\" | awk \'{print \\$4}\' | sort -n | tail -1 | awk \'{printf \\"%.2fGHz\\", \\$1/1000}\'"'; const speed = Variable<string>("").poll(5000, command) return <label className="item" onDestroy={() => speed.drop()} label={speed()} /> } function CPU(): JSX.Element { const usage = Variable<string>("").poll(2000, "top -b -n 1", out => { const line = out.split("\n").find(line => line.includes('Cpu(s)')); if (!line) return "N/A"; return line.split(/\s+/)[1].replace(',', '.').toString() + "%"; }); return <box className="item"> <icon icon="speedometer" css="margin-right: 0.7em;" /> <label onDestroy={() => usage.drop()} label={usage()} /> </box> } function Right() : JSX.Element { return ( <box className="right" hexpand halign={Gtk.Align.END} spacing={6}> <Icons /> <Volume /> <CPU /> <Memory /> <ClockSpeed /> <Temp /> <Date /> <Time /> </box> ); } function BatteryIcon(): JSX.Element { if (battery.get_state() == 0) return <box />; return <button className="battery-item" onClicked={() => execAsync(['gnome-power-statistics'])}> <box> { bind(battery, "percentage").as((percentage) => { const thresholds = [...Array(11).keys()].map( i => i * 10); const icon = thresholds.find(threshold => threshold >= percentage * 100) const charging_name = battery.percentage >= 0.99 ? "charged" : "charging" return <icon icon={battery.charging? `battery-level-${icon}-${charging_name}-symbolic` : `battery-level-${icon}-symbolic`} /> }) } </box> </button> } function Icons() { return ( <box className="item icon-group"> <SysTray /> <BatteryIcon /> </box> ) } function Volume(): JSX.Element { if (!wirePlumber) return <box />; const audio = wirePlumber.audio; const icon = bind(audio.default_speaker, "volume").as((volume) => { const vol = volume * 100 const icon = [ [101, 'overamplified'], [67, 'high'], [34, 'medium'], [1, 'low'], [0, 'muted'], ].find(([threshold]) => Number(threshold) <= vol)?.[1] return `audio-volume-${icon}-symbolic` }); const css = bind(audio.default_speaker, "mute").as((mute) => { return mute ? "margin-left:0;": "margin-left: 0.7em;" }); return ( <button className="item blue" onClicked={() => audio.default_speaker.mute = !audio.default_speaker.mute}> <box> <icon icon={icon} /> { bind(audio.default_speaker, "volume").as((volume) => <box> { bind(audio.default_speaker, "mute").as((mute) => <box> { <label label={mute? "": `${Math.floor(volume * 100)}%`} css={css} /> } </box>) } </box>) } </box> </button> ); } function Workspaces() : JSX.Element { const hyprland = Hyprland.get_default(); return ( <box className="workspaces"> {bind(hyprland, "workspaces").as((wss) => <box> {bind(hyprland, "focusedMonitor").as((fm) => wss.sort((a, b) => a.id - b.id) .filter(ws => ws && ws.get_monitor() && ws.get_monitor().get_id() === fm.get_id()) .map((ws) => ( <button className={bind(hyprland, "focusedWorkspace").as((fw) => ws === fw ? "focused" : "",)} onClicked={() => ws.focus()} > {`${ws.id}`.slice(-1)} </button> )))} </box> )} </box> ); } function shorten(title: string) { return title.length > 40 ? title.slice(0, 20) + "..." : title } function Clients() : JSX.Element { const hyprland = Hyprland.get_default(); return ( <box> { bind(hyprland, "focusedWorkspace").as(fw => ( <box className="clients"> { bind(hyprland, "clients").as(cls => cls .sort((a, b) => a.pid - b.pid) .filter(cl => !cl.title.includes("rofi")) .filter(cl => fw && cl.get_workspace().get_id() === fw.get_id()) .map(cl => ( <box className={bind(hyprland, "focusedClient").as(a => a && a.address === cl.address ? "focused" : "unfocused")} > <icon icon={getIconName(cl)} className="app-icon" /> <label label={bind(cl, 'title').as(title => shorten(title))} /> </box> ) ) ) } </box> ) ) } </box> ); } export default function Bar(gdkmonitor: Gdk.Monitor, scaleFactor: number = 1): Widget.Window { return new Widget.Window({ gdkmonitor, css: "font-size: " + scaleFactor + "em;", exclusivity: Astal.Exclusivity.EXCLUSIVE, anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT, application: App, className: "Bar", name: "top-bar", setup: self => self.connect("destroy", () => { print("Detroying bar"); App.remove_window(self); }), child: <centerbox className="window-box"> <Left /> <Center /> <Right /> </centerbox> }) }