372 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			372 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
import { Astal, Gdk, Gtk } from "ags/gtk4";
 | 
						|
import app from "ags/gtk4/app";
 | 
						|
import { createBinding, createState, For, With, Accessor } from "ags";
 | 
						|
import { createPoll } from "ags/time";
 | 
						|
import Tray from "gi://AstalTray";
 | 
						|
import { execAsync } from "ags/process";
 | 
						|
import Hyprland from "gi://AstalHyprland";
 | 
						|
import { getIconName } from "./utils";
 | 
						|
import Wp from "gi://AstalWp";
 | 
						|
import Battery from "gi://AstalBattery";
 | 
						|
import GLib from "gi://GLib";
 | 
						|
 | 
						|
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();
 | 
						|
    let items = createBinding(tray, "items");
 | 
						|
 | 
						|
    const init = (btn: Gtk.MenuButton, item: Tray.TrayItem) => {
 | 
						|
        btn.menuModel = item.menuModel;
 | 
						|
        btn.insert_action_group("dbusmenu", item.actionGroup);
 | 
						|
        item.connect("notify::action-group", () => {
 | 
						|
            btn.insert_action_group("dbusmenu", item.actionGroup);
 | 
						|
        });
 | 
						|
    };
 | 
						|
 | 
						|
    return (
 | 
						|
        <box>
 | 
						|
            <For each={items}>
 | 
						|
                {(item: Tray.TrayItem) => {
 | 
						|
                    if (item.iconThemePath) app.add_icons(item.iconThemePath);
 | 
						|
                    return (
 | 
						|
                        <menubutton
 | 
						|
                            $={(self) => init(self, item)}
 | 
						|
                            class="systray"
 | 
						|
                            tooltipMarkup={createBinding(item, "tooltipMarkup")}
 | 
						|
                            menuModel={createBinding(item, "menuModel")}
 | 
						|
                        >
 | 
						|
                            <image
 | 
						|
                                gicon={createBinding(item, "gicon")}
 | 
						|
                                class="systray-item"
 | 
						|
                            />
 | 
						|
                        </menubutton>
 | 
						|
                    );
 | 
						|
                }}
 | 
						|
            </For>
 | 
						|
        </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 = createPoll<string>(
 | 
						|
        "",
 | 
						|
        60000,
 | 
						|
        () => GLib.DateTime.new_now_local().format(format)!,
 | 
						|
    );
 | 
						|
    return (
 | 
						|
        <button
 | 
						|
            class="item"
 | 
						|
            label={time}
 | 
						|
            onClicked={() => execAsync(["gnome-calendar"])}
 | 
						|
        />
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
function Time({ format = "%H:%M:%S" }): JSX.Element {
 | 
						|
    const time = createPoll<string>(
 | 
						|
        "",
 | 
						|
        1000,
 | 
						|
        () => GLib.DateTime.new_now_local().format(format)!,
 | 
						|
    );
 | 
						|
    return <label class="item blue" label={time} />;
 | 
						|
}
 | 
						|
 | 
						|
function Temp(): JSX.Element {
 | 
						|
    let [label, _setlabel] = createState<string>("N/A");
 | 
						|
    if (sensorsAvailable) {
 | 
						|
        label = createPoll<string>("", 3000, "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 class="item blue" label={label} />;
 | 
						|
}
 | 
						|
 | 
						|
function Memory(): JSX.Element {
 | 
						|
    const memory = createPoll<string>("", 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 class="item blue" 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 = createPoll<string>("", 1000, command, (out) => out);
 | 
						|
    return <label class="item" label={speed} />;
 | 
						|
}
 | 
						|
 | 
						|
function CPU(): JSX.Element {
 | 
						|
    const usage = createPoll<string>("", 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 class="item">
 | 
						|
            <image iconName="speedometer" css="margin-right: 0.7em;" />
 | 
						|
            <label label={usage} />
 | 
						|
        </box>
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
function Right() {
 | 
						|
    return (
 | 
						|
        <box class="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 />;
 | 
						|
    let batteryPercentage = createBinding(battery, "percentage");
 | 
						|
    return (
 | 
						|
        <button
 | 
						|
            class="battery-item"
 | 
						|
            onClicked={() => execAsync(["gnome-power-statistics"])}
 | 
						|
        >
 | 
						|
            <box>
 | 
						|
                <With value={batteryPercentage}>
 | 
						|
                    {(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 (
 | 
						|
                            <image
 | 
						|
                                iconName={
 | 
						|
                                    battery.charging
 | 
						|
                                        ? `battery-level-${icon}-${charging_name}-symbolic`
 | 
						|
                                        : `battery-level-${icon}-symbolic`
 | 
						|
                                }
 | 
						|
                            />
 | 
						|
                        );
 | 
						|
                    }}
 | 
						|
                </With>
 | 
						|
            </box>
 | 
						|
        </button>
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
function Icons() {
 | 
						|
    return (
 | 
						|
        <box class="item icon-group">
 | 
						|
            <SysTray />
 | 
						|
            <BatteryIcon />
 | 
						|
        </box>
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
function Volume(): JSX.Element {
 | 
						|
    if (!wirePlumber) return <box />;
 | 
						|
 | 
						|
    const audio = wirePlumber.audio;
 | 
						|
    const icon = createBinding(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 = createBinding(audio.default_speaker, "mute").as((mute) => {
 | 
						|
        return mute ? "margin-left:0;" : "margin-left: 0.7em;";
 | 
						|
    });
 | 
						|
    let volume = createBinding(audio.default_speaker, "volume");
 | 
						|
    let mute = createBinding(audio.default_speaker, "mute");
 | 
						|
    return (
 | 
						|
        <button
 | 
						|
            class="item blue"
 | 
						|
            onClicked={() =>
 | 
						|
                (audio.default_speaker.mute = !audio.default_speaker.mute)
 | 
						|
            }
 | 
						|
        >
 | 
						|
            <box>
 | 
						|
                <image iconName={icon} />
 | 
						|
                <With value={volume}>
 | 
						|
                    {(vol) => (
 | 
						|
                        <box>
 | 
						|
                            <With value={mute}>
 | 
						|
                                {(muted) => {
 | 
						|
                                    return (
 | 
						|
                                        <label
 | 
						|
                                            label={muted ? "" : `${Math.floor(vol * 100)}%`}
 | 
						|
                                            css={css}
 | 
						|
                                        />
 | 
						|
                                    );
 | 
						|
                                }}
 | 
						|
                            </With>
 | 
						|
                        </box>
 | 
						|
                    )}
 | 
						|
                </With>
 | 
						|
            </box>
 | 
						|
        </button>
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
function Workspaces(): JSX.Element {
 | 
						|
    const hyprland = Hyprland.get_default();
 | 
						|
    let workspaces = createBinding(hyprland, "workspaces");
 | 
						|
    return (
 | 
						|
        <box class="workspaces">
 | 
						|
            <With value={workspaces}>
 | 
						|
                {(wss: Array<Hyprland.Workspace>) => (
 | 
						|
                    <box>
 | 
						|
                        <With value={createBinding(hyprland, "focusedMonitor")}>
 | 
						|
                            {(fm: Hyprland.Monitor) => {
 | 
						|
                                let filtered_wss = new Accessor(() =>
 | 
						|
                                    wss
 | 
						|
                                        .sort((a, b) => a.id - b.id)
 | 
						|
                                        .filter(
 | 
						|
                                            (ws) =>
 | 
						|
                                                ws &&
 | 
						|
                                                ws.get_monitor() &&
 | 
						|
                                                ws.get_monitor().get_id() === fm.get_id(),
 | 
						|
                                        ),
 | 
						|
                                );
 | 
						|
                                return (
 | 
						|
                                    <box>
 | 
						|
                                        <For each={filtered_wss}>
 | 
						|
                                            {(ws: Hyprland.Workspace, _index) => (
 | 
						|
                                                <button
 | 
						|
                                                    class={createBinding(hyprland, "focusedWorkspace").as(
 | 
						|
                                                        (fw) => (ws === fw ? "focused" : ""),
 | 
						|
                                                    )}
 | 
						|
                                                    onClicked={() => ws.focus()}
 | 
						|
                                                >
 | 
						|
                                                    {`${ws.id}`.slice(-1)}
 | 
						|
                                                </button>
 | 
						|
                                            )}
 | 
						|
                                        </For>
 | 
						|
                                    </box>
 | 
						|
                                );
 | 
						|
                            }}
 | 
						|
                        </With>
 | 
						|
                    </box>
 | 
						|
                )}
 | 
						|
            </With>
 | 
						|
        </box>
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
function shorten(title: string) {
 | 
						|
    return title.length > 40 ? title.slice(0, 20) + "..." : title;
 | 
						|
}
 | 
						|
 | 
						|
function Clients(): JSX.Element {
 | 
						|
    const hyprland = Hyprland.get_default();
 | 
						|
    let clients = createBinding(hyprland, "clients");
 | 
						|
    return (
 | 
						|
        <box>
 | 
						|
            <With value={createBinding(hyprland, "focusedWorkspace")}>
 | 
						|
                {(fw: Hyprland.Workspace) => (
 | 
						|
                    <box class="clients">
 | 
						|
                        <With value={clients}>
 | 
						|
                            {(cls: Array<Hyprland.Client>) => {
 | 
						|
                                let filtered_clients = new Accessor(() =>
 | 
						|
                                    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(),
 | 
						|
                                        ),
 | 
						|
                                );
 | 
						|
 | 
						|
                                return (
 | 
						|
                                    <box>
 | 
						|
                                        <For each={filtered_clients}>
 | 
						|
                                            {(cl: Hyprland.Client, _index) => (
 | 
						|
                                                <box
 | 
						|
                                                    class={createBinding(hyprland, "focusedClient").as(
 | 
						|
                                                        (a) =>
 | 
						|
                                                            a && a.address === cl.address
 | 
						|
                                                                ? "focused"
 | 
						|
                                                                : "unfocused",
 | 
						|
                                                    )}
 | 
						|
                                                >
 | 
						|
                                                    <image iconName={getIconName(cl)} class="app-icon" />
 | 
						|
                                                    <label
 | 
						|
                                                        label={createBinding(cl, "title").as((title) =>
 | 
						|
                                                            shorten(title),
 | 
						|
                                                        )}
 | 
						|
                                                    />
 | 
						|
                                                </box>
 | 
						|
                                            )}
 | 
						|
                                        </For>
 | 
						|
                                    </box>
 | 
						|
                                );
 | 
						|
                            }}
 | 
						|
                        </With>
 | 
						|
                    </box>
 | 
						|
                )}
 | 
						|
            </With>
 | 
						|
        </box>
 | 
						|
    );
 | 
						|
}
 | 
						|
 | 
						|
export default function Bar(gdkmonitor: Gdk.Monitor, scaleFactor: number = 1) {
 | 
						|
    console.log("Creating Bar on monitor:", gdkmonitor);
 | 
						|
    return (
 | 
						|
        <window
 | 
						|
            visible
 | 
						|
            gdkmonitor={gdkmonitor}
 | 
						|
            css={"font-size: " + scaleFactor + "em;"}
 | 
						|
            exclusivity={Astal.Exclusivity.EXCLUSIVE}
 | 
						|
            anchor={
 | 
						|
                Astal.WindowAnchor.TOP |
 | 
						|
                Astal.WindowAnchor.LEFT |
 | 
						|
                Astal.WindowAnchor.RIGHT
 | 
						|
            }
 | 
						|
            application={app}
 | 
						|
            class="Bar"
 | 
						|
            name="top-bar"
 | 
						|
        >
 | 
						|
            <centerbox class="window-box">
 | 
						|
                <Left $type="start" />
 | 
						|
                <Center $type="center" />
 | 
						|
                <Right $type="end" />
 | 
						|
            </centerbox>
 | 
						|
        </window>
 | 
						|
    );
 | 
						|
}
 |