160 lines
5.1 KiB
TypeScript
160 lines
5.1 KiB
TypeScript
import Gtk from "gi://Gtk?version=3.0"
|
|
import AstalNetwork from "gi://AstalNetwork"
|
|
import { type Binding, type Variable, Variable, bind } from "astal"
|
|
import { ArrowToggleButton, Menu } from "./ToggleButton"
|
|
|
|
const CONNECTING = [40, 50, 60, 70, 80, 90]
|
|
|
|
const wifi = AstalNetwork.get_default().wifi
|
|
|
|
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)
|
|
}
|
|
|
|
export const wifiIcon: Binding<string> | null = wifi
|
|
? bind(wifi, "icon-name")
|
|
: null
|
|
|
|
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 WifiSection({ open }: { open: Variable<boolean> }) {
|
|
if (!wifi) return null
|
|
|
|
const expanded = 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"}`)
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
|
|
open.subscribe((v) => {
|
|
if (v) {
|
|
wifi.scan()
|
|
} else {
|
|
expanded.set(false)
|
|
pendingAP.set(null)
|
|
failed.set("")
|
|
passwordEntry.text = ""
|
|
}
|
|
})
|
|
|
|
const toggle = (
|
|
<ArrowToggleButton
|
|
open={expanded}
|
|
icon={bind(wifi, "icon-name")}
|
|
label={bind(wifi, "ssid").as((ssid) => ssid || "Not Connected")}
|
|
active={bind(wifi, "enabled")}
|
|
onMainClick={() => {
|
|
wifi.enabled = !wifi.enabled
|
|
if (wifi.enabled) wifi.scan()
|
|
}}
|
|
onArrowClick={() => expanded.set(!expanded.get())}
|
|
/>
|
|
)
|
|
|
|
const menu = (
|
|
<Menu open={expanded} icon={bind(wifi, "icon-name")} title="Wi-Fi">
|
|
<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)}
|
|
/>
|
|
<box vertical visible={bind(wifi, "enabled")}>
|
|
{bind(wifi, "access-points").as((aps) =>
|
|
strongest(aps).map((ap) =>
|
|
WifiRow(ap, activeSsid, connect)
|
|
),
|
|
)}
|
|
</box>
|
|
<box className="menu-separator" />
|
|
<button className="wifi-rescan" onClicked={() => wifi.scan()}>
|
|
<box spacing={6}>
|
|
<icon icon="view-refresh-symbolic" />
|
|
<label label="Rescan" />
|
|
</box>
|
|
</button>
|
|
</Menu>
|
|
)
|
|
|
|
return { toggle, menu }
|
|
} |