nixos/modules/home/ags/app/widget/Wifi.tsx

162 lines
5.2 KiB
TypeScript

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 }
}