162 lines
5.2 KiB
TypeScript
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 }
|
|
} |