From 69a1977e5a3c623dad47a520b0f68bdcecdbef72 Mon Sep 17 00:00:00 2001 From: Joshua Yuen Date: Sat, 12 Sep 2026 02:25:18 -0400 Subject: [PATCH] Add AGS WiFi widget --- modules/home/ags/app/style.scss | 76 ++++++++++++- modules/home/ags/app/widget/Bar.tsx | 14 ++- modules/home/ags/app/widget/Wifi.tsx | 162 +++++++++++++++++++++++++++ modules/home/ags/default.nix | 2 +- 4 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 modules/home/ags/app/widget/Wifi.tsx diff --git a/modules/home/ags/app/style.scss b/modules/home/ags/app/style.scss index 68a554c..8de1907 100644 --- a/modules/home/ags/app/style.scss +++ b/modules/home/ags/app/style.scss @@ -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); +} diff --git a/modules/home/ags/app/widget/Bar.tsx b/modules/home/ags/app/widget/Bar.tsx index 2142d6d..1ca74d8 100644 --- a/modules/home/ags/app/widget/Bar.tsx +++ b/modules/home/ags/app/widget/Bar.tsx @@ -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 = [] + if (wifi.button) right.push(wifi.button) + right.push(volume.button) + + const popups: Array = [] + 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} ), - volume.popup, + ...popups, ] } diff --git a/modules/home/ags/app/widget/Wifi.tsx b/modules/home/ags/app/widget/Wifi.tsx new file mode 100644 index 0000000..19ce34f --- /dev/null +++ b/modules/home/ags/app/widget/Wifi.tsx @@ -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() + 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, + connect: (ap: AstalNetwork.AccessPoint) => void, +) { + return ( + + ) +} + +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(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 = ( + 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 = ( + + ) + + const popup = ( + + + + + ) + + open.subscribe((v) => { + if (v) { + wifi.scan() + } else { + pendingAP.set(null) + failed.set("") + passwordEntry.text = "" + } + }) + + return { button, popup } +} \ No newline at end of file diff --git a/modules/home/ags/default.nix b/modules/home/ags/default.nix index 5998d72..2dff0a8 100644 --- a/modules/home/ags/default.nix +++ b/modules/home/ags/default.nix @@ -44,7 +44,7 @@ let network battery wireplumber - ]; + ] ++ [ pkgs.networkmanager ]; ags-bar = pkgs.ags.bundle { pname = "ags-bar";