Add AGS WiFi widget

This commit is contained in:
Joshua Yuen 2026-09-12 02:25:18 -04:00
parent 32251c2dfa
commit d0c2d822a5
Signed by: josh
GPG Key ID: 530DA0331EFE99F5
4 changed files with 249 additions and 5 deletions

View File

@ -43,12 +43,15 @@
}
.ws:hover,
.volume:hover {
.volume:hover,
.wifi:hover {
background: transparentize($base05, 0.88);
}
.volume icon,
.mute icon {
.mute icon,
.wifi icon,
.wifi-header icon {
font-size: 16px;
}
@ -84,3 +87,72 @@
}
}
}
.wifi-header {
label {
font-weight: bold;
margin: 0 2px;
}
.wifi-rescan {
padding: 0 2px;
margin-right: 6px;
}
switch {
background: $base03;
border-radius: 999px;
min-width: 34px;
min-height: 16px;
margin: 1px 0;
slider {
background: $base05;
border-radius: 999px;
min-width: 12px;
min-height: 12px;
margin: 2px;
}
}
switch:checked {
background: $base0D;
slider {
background: $base00;
}
}
}
.wifi-status,
.wifi-empty {
font-size: 0.85em;
opacity: 0.7;
margin: 2px 0;
}
.wifi-password {
margin: 4px 0;
padding: 4px 8px;
border-radius: 6px;
background: $base02;
color: $base05;
}
.wifi-row {
padding: 4px 6px;
border-radius: 6px;
margin: 1px 0;
icon {
font-size: 16px;
}
.wifi-row-ssid {
margin: 0 6px;
}
}
.wifi-row:hover {
background: transparentize($base05, 0.88);
}

View File

@ -2,10 +2,20 @@ import { App, Astal, Gdk, Gtk } from "astal/gtk3"
import Workspaces from "./Workspaces"
import Clock from "./Clock"
import { VolumeWidget } from "./Volume"
import { WifiWidget } from "./Wifi"
export default function Bar(gdkmonitor: Gdk.Monitor) {
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
const volume = VolumeWidget(gdkmonitor)
const wifi = WifiWidget(gdkmonitor)
const right: Array<JSX.Element> = []
if (wifi.button) right.push(wifi.button)
right.push(volume.button)
const popups: Array<JSX.Element> = []
if (wifi.popup) popups.push(wifi.popup)
popups.push(volume.popup)
return [
(
@ -37,11 +47,11 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
halign={Gtk.Align.END}
valign={Gtk.Align.CENTER}
>
{volume.button}
{right}
</box>
</centerbox>
</window>
),
volume.popup,
...popups,
]
}

View File

@ -0,0 +1,162 @@
import Gtk from "gi://Gtk?version=3.0"
import AstalNetwork from "gi://AstalNetwork"
import { type Binding, Variable, bind } from "astal"
import Dropdown from "./Dropdown"
const CONNECTING = [40, 50, 60, 70, 80, 90]
function strongest(aps: AstalNetwork.AccessPoint[]) {
const bySsid = new Map<string, AstalNetwork.AccessPoint>()
for (const ap of aps) {
const ssid = ap.ssid ?? ""
const best = bySsid.get(ssid)
if (!best || ap.strength > best.strength) bySsid.set(ssid, ap)
}
return [...bySsid.values()].sort((a, b) => b.strength - a.strength)
}
function WifiRow(
ap: AstalNetwork.AccessPoint,
activeSsid: Binding<string>,
connect: (ap: AstalNetwork.AccessPoint) => void,
) {
return (
<button className="wifi-row" onClicked={() => connect(ap)}>
<box>
<icon icon={bind(ap, "icon-name")} />
<label
className="wifi-row-ssid"
label={ap.ssid ?? "(hidden network)"}
halign={Gtk.Align.START}
hexpand
/>
<icon
visible={ap.requires_password}
icon="network-wireless-encrypted-symbolic"
/>
<icon
visible={activeSsid.as((ssid) => ssid === ap.ssid)}
icon="object-select-symbolic"
/>
</box>
</button>
)
}
export function WifiWidget(gdkmonitor: Gdk.Monitor) {
const wifi = AstalNetwork.get_default().wifi
if (!wifi) return { button: null, popup: null }
const open = Variable(false)
const pendingAP = Variable<AstalNetwork.AccessPoint | null>(null)
const failed = Variable("")
const status = Variable.derive(
[bind(wifi, "scanning"), bind(wifi, "state"), bind(wifi, "ssid"), failed],
(scanning, state, ssid, fail) => {
if (scanning) return "scanning…"
if (CONNECTING.includes(state)) return `connecting to ${ssid ?? "network"}…`
return fail
},
)
const activeSsid = bind(wifi, "active-access-point").as((ap) => ap?.ssid ?? "")
function connect(ap: AstalNetwork.AccessPoint) {
if (ap.requires_password) {
failed.set("")
pendingAP.set(ap)
} else {
ap.activate()
.then(() => failed.set(""))
.catch(() => failed.set(`failed to connect to ${ap.ssid ?? "network"}`))
}
}
const passwordEntry = (
<entry
className="wifi-password"
hexpand
visible={pendingAP().as((ap) => ap !== null)}
placeholderText={pendingAP().as(
(ap) => (ap ? `password for ${ap.ssid ?? "network"}` : ""),
)}
onActivate={(self) => {
const ap = pendingAP.get()
if (!ap || !self.text) return
ap.activate(self.text)
.then(() => {
pendingAP.set(null)
self.text = ""
})
.catch(() =>
failed.set(`failed to connect to ${ap.ssid ?? "network"}`)
)
}}
/>
)
const button = (
<button
className="wifi"
onClicked={() => open.set(!open.get())}
>
<icon icon={bind(wifi, "icon-name")} />
</button>
)
const popup = (
<Dropdown gdkmonitor={gdkmonitor} target={button} open={open} width={260}>
<box className="wifi-header">
<label label="Wi-Fi" hexpand halign={Gtk.Align.START} />
<button className="wifi-rescan" onClicked={() => wifi.scan()}>
<icon icon="view-refresh-symbolic" />
</button>
<switch
active={bind(wifi, "enabled")}
onStateSet={(_, state) => {
wifi.enabled = state
return true
}}
/>
</box>
<label
className="wifi-status"
label={status()}
visible={status().as((text) => text !== "")}
/>
{passwordEntry}
<label
className="wifi-empty"
label="wi-fi is off"
visible={bind(wifi, "enabled").as((enabled) => !enabled)}
/>
<scrollable
hscroll={Gtk.PolicyType.NEVER}
vscroll={Gtk.PolicyType.AUTOMATIC}
maxContentHeight={360}
propagateNaturalHeight
visible={bind(wifi, "enabled")}
>
<box vertical>
{bind(wifi, "access-points").as((aps) =>
strongest(aps).map((ap) => WifiRow(ap, activeSsid, connect))
)}
</box>
</scrollable>
</Dropdown>
)
open.subscribe((v) => {
if (v) {
wifi.scan()
} else {
pendingAP.set(null)
failed.set("")
passwordEntry.text = ""
}
})
return { button, popup }
}

View File

@ -44,7 +44,7 @@ let
network
battery
wireplumber
];
] ++ [ pkgs.networkmanager ];
ags-bar = pkgs.ags.bundle {
pname = "ags-bar";