feat!: migrate to ags v3, redesign styles

This commit is contained in:
2025-12-05 20:32:18 -08:00
parent 42b4f99c37
commit 11c257e4b3
14 changed files with 182 additions and 153 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
node_modules/ node_modules/
@girs/ @girs/
result

View File

@@ -77,7 +77,6 @@
mkdir -p $out/bin mkdir -p $out/bin
mkdir -p $out/share mkdir -p $out/share
cp -r * $out/share cp -r * $out/share
jj
ags bundle app.ts $out/bin/${pname} -d "SRC='$out/share'" ags bundle app.ts $out/bin/${pname} -d "SRC='$out/share'"
''; '';
}; };

View File

@@ -1,7 +1,7 @@
@use "sass:color"; @use "sass:color";
$bg: #212223; $bg: #1a1b26;
$fg: #f1f1f1; $fg: #a9b1d6;
$accent: #378DF7; $accent: #378DF7;
$accent-white: #ffffff; $accent-white: #ffffff;
$radius: 7px; $radius: 7px;
@@ -12,7 +12,8 @@ window.Bar {
border: none; border: none;
box-shadow: none; box-shadow: none;
font-family: Dejavu Sans Mono; font-family: Dejavu Sans Mono;
background-color: color.adjust($bg, $alpha: -0.2); // background-color: color.adjust($bg, $alpha: -0.2);
background-color: $bg;
color: $fg; color: $fg;
font-size: 1.1em; font-size: 1.1em;
@@ -21,12 +22,58 @@ window.Bar {
} }
.status-box { .status-box {
background-color: $bg-color-6; // background-color: $bg-color-6;
padding: 0 4px; // padding: 0 4px;
margin: 0 2px; margin: 0 4px;
font-size: 16px; font-size: 16px;
} }
.disk {
color: #e0af68;
border-bottom: 4px solid #e0af68;
}
.mem {
color: #bb9af7;
border-bottom: 4px solid #bb9af7;
}
.cpu {
color: #9ece6a;
border-bottom: 4px solid #9ece6a;
}
.net {
color: #7aa2f7;
border-bottom: 4px solid #7aa2f7;
}
.audio {
color: #2ac3de;
border-bottom: 4px solid #2ac3de;
}
.date-popover {
all: unset;
color: #73daca;
border-bottom: 4px solid #73daca;
&>* {
all: unset;
border: none;
box-shadow: none;
background: transparent;
}
}
.separator {
background-color: #444b6a;
min-width: 2px;
min-height: 1px;
margin: 8px 0;
}
.inactive { .inactive {
background-color: $inactive-bg-color; background-color: $inactive-bg-color;
} }

View File

@@ -1,15 +1,14 @@
import { Astal, Gtk, Gdk } from "ags/gtk4"; import { Astal, Gdk, Gtk } from "ags/gtk4";
// import Workspaces from "./workspaces";
// import Audio from "./audio";
// import NetworkModule from "./network";
// import Cpu from "./cpu-widget";
// import Ram from "./ram";
// import Disk from "./disk";
// import Battery from "./battery";
import Time from "./time"; import Time from "./time";
import Title from "./title"; import Title from "./title";
import app from "ags/gtk4/app"; import app from "ags/gtk4/app";
import Workspaces from "./workspaces"; import Workspaces from "./workspaces";
import Battery from "./battery";
import Disk from "./disk";
import NetworkModule from "./network";
import Ram from "./ram";
import Cpu from "./cpu-widget";
import Audio from "./audio";
export default function Bar(gdkmonitor: Gdk.Monitor) { export default function Bar(gdkmonitor: Gdk.Monitor) {
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor; const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
@@ -25,11 +24,26 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
application={app} application={app}
> >
<centerbox> <centerbox>
<Workspaces $type="start" monitor={gdkmonitor} /> <box $type="start">
<image iconName="nixos" pixel_size={24} class="nix-icon" />
<Workspaces monitor={gdkmonitor} />
</box>
<box $type="center" class="client-title"> <box $type="center" class="client-title">
<Title /> <Title />
</box> </box>
<box $type="end"> <box $type="end">
<Gtk.Separator class="separator" />
<Audio />
<Gtk.Separator class="separator" />
<NetworkModule />
<Gtk.Separator class="separator" />
<Cpu />
<Gtk.Separator class="separator" />
<Ram />
<Gtk.Separator class="separator" />
<Disk />
<Gtk.Separator class="separator" />
<Battery />
<Time /> <Time />
</box> </box>
</centerbox> </centerbox>

View File

@@ -1,18 +1,21 @@
import { bind, Variable } from "astal"
import AstalWp from "gi://AstalWp?version=0.1" import AstalWp from "gi://AstalWp?version=0.1"
import { createBinding, createMemo } from "gnim"
export default function Audio() { export default function Audio() {
const speaker = AstalWp.get_default()?.default_speaker! const speaker = AstalWp.get_default()?.default_speaker!
// const derived = Variable.derive([bind(speaker, "volume"), bind(speaker, "mute")], (volume: number, muted: boolean) => { const display = createMemo(() => {
// if (muted) { const volume = createBinding(speaker, "volume")
// return { label: ` (muted)`, muted } const muted = createBinding(speaker, "mute")
// }
// return { label: `${Math.floor(volume * 100)}% ` }
// })
// return <box className={derived((v) => ["status-box", v.muted && "inactive"].filter(Boolean).join(" "))}> if (muted()) {
// <label return "(muted)"
// label={derived((v) => v.label)} /> }
// </box> return `${Math.floor(volume() * 100)}% `
})
return (
<box class="status-box audio">
<label label={display} />
</box>
)
} }

View File

@@ -1,21 +1,24 @@
import { bind, Variable } from "astal" import { Gtk } from "ags/gtk4"
import AstalBattery from "gi://AstalBattery?version=0.1" import AstalBattery from "gi://AstalBattery?version=0.1"
import { createBinding, createComputed } from "gnim"
export default function Battery() { export default function Battery() {
const battery = AstalBattery.get_default() const battery = AstalBattery.get_default()
const battery_info = Variable.derive([bind(battery, "percentage"), bind(battery, "charging")], (percentage, charging) => { const percentage = createBinding(battery, "percentage")
const full_percentage = Math.floor(percentage * 100) const full_percentage = Math.floor(percentage.peek() * 100)
if (charging) { const battery_info = createComputed(() => (`${full_percentage === 100 ? "FULL" : "CHR"}: ${full_percentage}%`))
return { label: `${full_percentage == 100 ? "FULL" : "CHR"}: ${full_percentage}%` }
}
return { label: `${full_percentage == 100 ? "FULL" : "BAT"}: ${full_percentage}%` }
})
if (!battery.is_battery) { if (!battery.is_battery) {
return <></> return <></>
} }
return <box className="status-box">
<label label={battery_info((i) => i.label)} /> return (
<>
<box class="status-box">
<label label={battery_info} />
</box> </box>
<Gtk.Separator />
</>
)
} }

View File

@@ -1,19 +1,18 @@
import { bind, GLib, Variable } from "astal"; import { createPoll } from "ags/time";
import { calc_cpu_usage, get_cpu_snapshot } from "./cpu"; import { calc_cpu_usage, get_cpu_snapshot } from "./cpu";
let s1 = get_cpu_snapshot(); let s1 = get_cpu_snapshot()
let cpu_usage_percent = Variable(0);
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
const s2 = get_cpu_snapshot();
cpu_usage_percent.set(calc_cpu_usage(s1, s2));
s1 = s2;
return true;
});
export default function Cpu() { export default function Cpu() {
return <box className="status-box"> const usage = createPoll(0, 1000, () => {
<label label={bind(cpu_usage_percent).as((u) => `${u}% `)} /> const s2 = get_cpu_snapshot()
let calc = calc_cpu_usage(s1, s2)
s1 = s2
return calc
})
return (
<box class="status-box cpu">
<label label={usage.as(u => `CPU: ${u}% `)} />
</box> </box>
)
} }

View File

@@ -1,58 +1,56 @@
import { GLib, Variable } from "astal"; import GTop from "gi://GTop"
import GTop from "gi://GTop";
import Wp05 from "gi://Wp";
type Snapshot = { type Snapshot = {
total: number; total: number
user: number; user: number
sys: number; sys: number
idle: number; idle: number
}; }
export function get_cpu_snapshot() { export function get_cpu_snapshot() {
const cpu = new GTop.glibtop_cpu(); const cpu = new GTop.glibtop_cpu()
GTop.glibtop_get_cpu(cpu); GTop.glibtop_get_cpu(cpu)
return { return {
total: cpu.total, total: cpu.total,
user: cpu.user + cpu.nice, user: cpu.user + cpu.nice,
sys: cpu.sys, sys: cpu.sys,
idle: cpu.idle, idle: cpu.idle,
}; }
} }
export function calc_cpu_usage(a: Snapshot, b: Snapshot) { export function calc_cpu_usage(a: Snapshot, b: Snapshot) {
const total_diff = b.total - a.total; const total_diff = b.total - a.total
const active_diff = b.user + b.sys - (a.user + a.sys); const active_diff = b.user + b.sys - (a.user + a.sys)
return Math.round(total_diff > 0 ? (100 * active_diff) / total_diff : 0); return Math.round(total_diff > 0 ? (100 * active_diff) / total_diff : 0)
} }
export function get_ram_info() { export function get_ram_info() {
const mem = new GTop.glibtop_mem(); const mem = new GTop.glibtop_mem()
GTop.glibtop_get_mem(mem); GTop.glibtop_get_mem(mem)
return { return {
total: mem.total, total: mem.total,
used: mem.total - mem.free - mem.cached - mem.buffer, used: mem.total - mem.free - mem.cached - mem.buffer,
free: mem.free, free: mem.free,
}; }
} }
function format_bytes(bytes: number) { function format_bytes(bytes: number) {
let units = ["B", "KiB", "MiB", "GiB", "TiB"]; let units = ["B", "KiB", "MiB", "GiB", "TiB"]
let i = 0; let i = 0
let num: number = bytes; let num: number = bytes
while (num >= 1024 && i < units.length - 1) { while (num >= 1024 && i < units.length - 1) {
num /= 1024; num /= 1024
i++; i++
} }
return `${num.toFixed(2)}${units[i]}`; return `${num.toFixed(2)}${units[i]}`
} }
export function get_disk_space() { export function get_disk_space() {
const usage = new GTop.glibtop_fsusage(); const usage = new GTop.glibtop_fsusage()
GTop.glibtop_get_fsusage(usage, "/"); GTop.glibtop_get_fsusage(usage, "/")
const free_bytes = usage.bavail * usage.block_size; const free_bytes = usage.bavail * usage.block_size
return format_bytes(free_bytes); return format_bytes(free_bytes)
} }

View File

@@ -1,10 +1,13 @@
import { bind, Variable } from "astal"
import { get_disk_space } from "./cpu" import { get_disk_space } from "./cpu"
import { createPoll } from "ags/time"
let disk_space = Variable(get_disk_space()).poll(5000, () => get_disk_space())
export default function Disk() { export default function Disk() {
return <box className="status-box"> const disk = createPoll(get_disk_space(), 5000, () => get_disk_space())
<label label={bind(disk_space).as((s) => `${s}`)} />
return (
<box class="status-box disk">
<label label={disk.as(d => `Disk: ${d}`)} />
</box> </box>
)
} }

View File

@@ -1,7 +1,7 @@
import { bind, Variable } from "astal";
import AstalNetwork from "gi://AstalNetwork?version=0.1"; import AstalNetwork from "gi://AstalNetwork?version=0.1";
import { createBinding, createMemo } from "gnim";
const disconnected_label = { label: `Disconnected ⚠` }; const disconnected_label = `Disconnected ⚠`
function create_label( function create_label(
primary: AstalNetwork.Primary, primary: AstalNetwork.Primary,
@@ -9,39 +9,28 @@ function create_label(
wifi?: AstalNetwork.Wifi, wifi?: AstalNetwork.Wifi,
) { ) {
if (primary === AstalNetwork.Primary.WIRED) { if (primary === AstalNetwork.Primary.WIRED) {
return { label: `🖧 Wired ${wired.device.interface}` }; return `🖧 Wired ${wired.device.interface}`
} }
if (!wifi) { if (!wifi) {
return disconnected_label; return disconnected_label;
} }
if (wifi.active_access_point !== null) { if (wifi.active_access_point !== null) {
return { label: `${wifi.ssid} (${wifi.strength}%) ` }; return `${wifi.ssid} (${wifi.strength}%) `
} }
return disconnected_label; return disconnected_label;
} }
export default function NetworkModule() { export default function NetworkModule() {
const network = AstalNetwork.get_default(); const network = AstalNetwork.get_default();
const wifi = network.wifi; const primary = createBinding(network, "primary")
const wired = network.wired; const wifi = createBinding(network, "wifi")
const wired = createBinding(network, "wired")
let derived; const display = createMemo(() => create_label(primary(), wired(), wifi()))
if (!wifi) {
derived = Variable.derive([bind(network, "primary")], (primary) => {
return create_label(primary, wired);
});
} else {
derived = Variable.derive(
[bind(network, "primary"), bind(wifi, "ssid"), bind(wifi, "strength")],
(primary) => {
return create_label(primary, wired, wifi);
},
);
}
return ( return (
<box className="status-box"> <box class="status-box net">
<label label={derived((v) => v.label)} /> <label label={display} />
</box> </box>
); )
} }

View File

@@ -1,16 +1,13 @@
import { bind, GLib, Variable } from "astal"
import { get_ram_info } from "./cpu" import { get_ram_info } from "./cpu"
import { createPoll } from "ags/time"
let info = Variable(get_ram_info())
GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
info.set(get_ram_info())
return true
})
export default function Ram() { export default function Ram() {
return <box className="status-box"> const info = createPoll(get_ram_info(), 1000, () => get_ram_info())
<label label={bind(info).as((i) => `${Math.round((i.used / i.total) * 100)}% `)} />
return (
<box class="status-box mem">
<label label={info.as(i => `Mem: ${Math.round((i.used / i.total) * 100)}% `)} />
</box> </box>
)
} }

View File

@@ -1,12 +1,6 @@
// let current_time = Variable(GLib.DateTime.new_now_local().format("%H:%M")) import { Gtk } from "ags/gtk4"
// GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
// const now = GLib.DateTime.new_now_local()
// current_time.set(now.format("%b %e (%a) %H:%M"))
// return true
import { createPoll } from "ags/time" import { createPoll } from "ags/time"
// })
export default function Time() { export default function Time() {
const time = createPoll("", 500, () => { const time = createPoll("", 500, () => {
return new Date().toLocaleString(undefined, { return new Date().toLocaleString(undefined, {
@@ -14,11 +8,13 @@ export default function Time() {
}) })
}) })
return <box class="status-box"> return (
<menubutton class="date-popover status-box">
<popover>
<Gtk.Calendar />
</popover>
<label label={time} /> <label label={time} />
</box> </menubutton>
// return <box class="status-box"> )
// <label label={bind(current_time)} />
// </box>
} }

View File

@@ -1,6 +1,5 @@
import { createPoll } from "ags/time"; import { createPoll } from "ags/time";
import AstalHyprland from "gi://AstalHyprland?version=0.1"; import AstalHyprland from "gi://AstalHyprland?version=0.1";
import { createState } from "gnim";
const hyprland = AstalHyprland.get_default() const hyprland = AstalHyprland.get_default()
@@ -8,15 +7,10 @@ function get_title() {
return hyprland.focusedClient?.title ?? "" return hyprland.focusedClient?.title ?? ""
} }
// const title = Variable(get_title()).poll(200, () => get_title())
export default function Title() { export default function Title() {
const title = createPoll("", 200, () => get_title()) const title = createPoll("", 200, () => get_title())
return ( return (
<label label={title} /> <label label={title} />
) )
// return (
// <label label={title(t => t)} />
// )
} }

View File

@@ -4,32 +4,17 @@ import { createBinding, For } from "gnim"
export default function Workspaces({ monitor }: { monitor: Gdk.Monitor }) { export default function Workspaces({ monitor }: { monitor: Gdk.Monitor }) {
const hypr = AstalHyprland.get_default() const hypr = AstalHyprland.get_default()
const workspaces = createBinding(hypr, "workspaces").as(wss => wss.sort((a, b) => a.id - b.id)) const workspaces = createBinding(hypr, "workspaces").as(wss => wss.filter((ws) => ws.monitor.model === monitor.model).sort((a, b) => a.id - b.id))
return ( return (
<box class="workspaces"> <box class="workspaces">
<For each={workspaces}> <For each={workspaces}>
{(ws) => ( {(ws) => (
<button class={createBinding(hypr, "focusedWorkspace").as((fw) => ws === fw ? "focused" : "")}> <button class={createBinding(hypr, "focusedWorkspace").as((fw) => ws === fw ? "focused" : "")} onClicked={() => ws.focus()}>
<label label={ws.id.toString()} /> <label label={ws.id.toString()} />
</button> </button>
)} )}
</For> </For>
</box> </box>
) )
// return <box className="workspaces">
// {bind(hypr, "workspaces").as(wss => wss.filter(ws => !(ws.id >= -99 && ws.id <= -2))
// .sort((a, b) => a.id - b.id)
// .map(ws => {
// if (ws.monitor.model !== monitor.model) return <></>
// return (
// <button
// className={bind(hypr, "focusedWorkspace").as(fw =>
// ws === fw ? "focused" : "")}
// onClicked={() => ws.focus()}>
// {ws.id}
// </button>
// )
// }))}
// </box>
} }