import { App, Astal, Gtk, Gdk } from "astal/gtk4" import { bind, exec, GLib, Variable } from "astal" import AstalTray from "gi://AstalTray?version=0.1"; import AstalWp from "gi://AstalWp?version=0.1"; type NiriWorkspace = { id: number, idx: number, name: string | null, output: string, is_active: boolean, is_focused: boolean, active_window_id: number | null, }; function getWorkspaces(): NiriWorkspace[] { // NOTE: this works only in non-systemd environment on NixOS // TODO: try to use Niri socket if it is documented return JSON.parse(exec("niri msg -j workspaces")); } function getWorkspacesByOutput(output: string): NiriWorkspace[] { return getWorkspaces().filter(workspace => workspace.output == output).sort((a, b) => a.idx - b.idx); } function focusWorkspace(idx: number) { // NOTE: this works only in non-systemd environment on NixOS // TODO: try to use Niri socket if it is documented exec(`niri msg action focus-workspace ${idx}`); } type WorkspaceButtonArguments = { idx: number, isActive: boolean, isFocused: boolean, }; function WorkspaceButton(args: WorkspaceButtonArguments) { const classes: string[] = []; args.isActive && classes.push("active"); args.isFocused && classes.push("focused"); return <button cssClasses={classes} onClicked={() => focusWorkspace(args.idx)} /> } type WorkspacesArguments = { connector: string, }; function Workspaces(args: WorkspacesArguments) { // NOTE: it is pretty inefficient and not so much responsive // TODO: it would be better to use Niri socket in the future const workspaces: Variable<NiriWorkspace[]> = Variable(getWorkspacesByOutput(args.connector)) .poll(1000, () => getWorkspacesByOutput(args.connector)); // BUG: on workspace change there's no CSS transition // I guess it is due to rerender here return <box cssClasses={["Workspaces"]}> {workspaces(v => v.map(workspace => <WorkspaceButton idx={workspace.idx} isActive={workspace.is_active} isFocused={workspace.is_focused} />))} </box> } function AudioVolume() { const wireplumber = AstalWp.get_default()!; const speaker = wireplumber.audio.get_default_speaker()!; return <box cssClasses={["AudioVolume"]}> <image iconName={bind(speaker, "volumeIcon")} /> {/* {bind(speaker, "volume")} */} <slider hexpand onScroll={(_self, dx, dy) => speaker.volume += (dx + dy) * -0.05} // BUG: this doesn't work due to value being updated immediately with dragging // so that new value is never reached (slider "freezes") // onChangeValue={({ value }) => new_volume = value} // onKeyReleased={() => speaker.volume = new_volume} value={bind(speaker, "volume")} /> </box>; } function Tray() { // BUG: personally I have one fantom icon being along other tray icons // For now I don't have any ideas why this is happening // TODO: rewrite this using more elements, as this is really restricted design const tray = AstalTray.get_default(); return <box cssClasses={["Tray"]}> {bind(tray, "items").as(items => items.map(item => <menubutton setup={self => self.insert_action_group("dbusmenu", item.actionGroup)} tooltipText={bind(item, "tooltipMarkup")} > <image gicon={bind(item, "gicon")} /> {Gtk.PopoverMenu.new_from_model(item.menuModel)} </menubutton> ) )} </box> } type TimeArguments = { format: string, }; function Time(args: TimeArguments) { const time = Variable<GLib.DateTime>(GLib.DateTime.new_now_local()).poll(1000, () => GLib.DateTime.new_now_local()) return <box> {time(v => v.format(args.format))} </box> } export default function Bar(gdkmonitor: Gdk.Monitor) { const { TOP, LEFT, RIGHT } = Astal.WindowAnchor return <window visible name={"Bar"} cssClasses={["Bar"]} gdkmonitor={gdkmonitor} exclusivity={Astal.Exclusivity.EXCLUSIVE} anchor={TOP | LEFT | RIGHT} application={App}> <centerbox cssClasses={["bar-container"]}> <box halign={Gtk.Align.START}> <Workspaces connector={gdkmonitor.get_connector()!} /> </box> <box halign={Gtk.Align.CENTER}> </box> <box halign={Gtk.Align.END}> <AudioVolume /> <Tray /> <Time format="%I:%M:%S %p %Z" /> </box> </centerbox> </window> }