summaryrefslogtreecommitdiff
path: root/src/modules/sidebar
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-03-26 20:22:51 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-03-26 20:22:51 +1100
commit5a1ea91e35487dc877a4422c31c2e225b4ec2aef (patch)
tree5f95a8e3a9717569164d7059217d3fd3e1a2dffe /src/modules/sidebar
parentsidebar: fix negative content width warnings (diff)
downloadcaelestia-shell-5a1ea91e35487dc877a4422c31c2e225b4ec2aef.tar.gz
caelestia-shell-5a1ea91e35487dc877a4422c31c2e225b4ec2aef.tar.bz2
caelestia-shell-5a1ea91e35487dc877a4422c31c2e225b4ec2aef.zip
sidebar: networks module
Diffstat (limited to 'src/modules/sidebar')
-rw-r--r--src/modules/sidebar/connectivity.tsx3
-rw-r--r--src/modules/sidebar/modules/networks.tsx139
2 files changed, 142 insertions, 0 deletions
diff --git a/src/modules/sidebar/connectivity.tsx b/src/modules/sidebar/connectivity.tsx
index 437e513..2962b56 100644
--- a/src/modules/sidebar/connectivity.tsx
+++ b/src/modules/sidebar/connectivity.tsx
@@ -1,7 +1,10 @@
import Bluetooth from "./modules/bluetooth";
+import Networks from "./modules/networks";
export default () => (
<box vertical className="pane connectivity" name="connectivity">
+ <Networks />
+ <box className="separator" />
<Bluetooth />
</box>
);
diff --git a/src/modules/sidebar/modules/networks.tsx b/src/modules/sidebar/modules/networks.tsx
new file mode 100644
index 0000000..34ec8fd
--- /dev/null
+++ b/src/modules/sidebar/modules/networks.tsx
@@ -0,0 +1,139 @@
+import { bind, execAsync, Variable, type Binding } from "astal";
+import { Gtk } from "astal/gtk3";
+import AstalNetwork from "gi://AstalNetwork";
+
+const sortAPs = (saved: string[], a: AstalNetwork.AccessPoint, b: AstalNetwork.AccessPoint) => {
+ const { wifi } = AstalNetwork.get_default();
+ if (a === wifi.activeAccessPoint || b === wifi.activeAccessPoint) return a === wifi.activeAccessPoint ? -1 : 1;
+ if (saved.includes(a.ssid) || saved.includes(b.ssid)) return saved.includes(a.ssid) ? -1 : 1;
+ return b.strength - a.strength;
+};
+
+const Network = (accessPoint: AstalNetwork.AccessPoint) => (
+ <box
+ className={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(
+ a => `network ${a === accessPoint ? "connected" : ""}`
+ )}
+ >
+ <icon className="icon" icon={bind(accessPoint, "iconName")} />
+ <box vertical hexpand>
+ <label truncate xalign={0} label={bind(accessPoint, "ssid").as(s => s ?? "Unknown")} />
+ <label
+ truncate
+ xalign={0}
+ className="sublabel"
+ label={bind(accessPoint, "strength").as(s => `${accessPoint.frequency > 5000 ? 5 : 2.4}GHz • ${s}/100`)}
+ />
+ </box>
+ <box hexpand />
+ <button
+ valign={Gtk.Align.CENTER}
+ visible={false}
+ cursor="pointer"
+ onClicked={() => execAsync(`nmcli c delete id '${accessPoint.ssid}'`).catch(() => {})}
+ label="delete_forever"
+ setup={self => {
+ let destroyed = false;
+ execAsync(`fish -c "nmcli -t -f name,type c show | sed -nE 's/(.*)\\:.*wireless/\\1/p'"`)
+ .then(out => !destroyed && (self.visible = out.split("\n").includes(accessPoint.ssid)))
+ .catch(console.error);
+ self.connect("destroy", () => (destroyed = true));
+ }}
+ />
+ <button
+ valign={Gtk.Align.CENTER}
+ cursor="pointer"
+ onClicked={self => {
+ const cmd =
+ AstalNetwork.get_default().wifi.activeAccessPoint === accessPoint ? "c down id" : "d wifi connect";
+ execAsync(`nmcli ${cmd} '${accessPoint.ssid}'`)
+ .then(() => (self.sensitive = true))
+ .catch(console.error);
+ self.sensitive = false;
+ }}
+ label={bind(AstalNetwork.get_default().wifi, "activeAccessPoint").as(a =>
+ a === accessPoint ? "wifi_off" : "wifi"
+ )}
+ />
+ </box>
+);
+
+const List = () => {
+ const { wifi } = AstalNetwork.get_default();
+ const children = Variable<JSX.Element[]>([]);
+
+ const update = async () => {
+ const out = await execAsync(`fish -c "nmcli -t -f name,type c show | sed -nE 's/(.*)\\:.*wireless/\\1/p'"`);
+ const saved = out.split("\n");
+ const aps = wifi.accessPoints
+ .filter(a => a.ssid)
+ .sort((a, b) => sortAPs(saved, a, b))
+ .map(Network);
+ children.set(aps);
+ };
+
+ wifi.connect("notify::active-access-point", () => update().catch(console.error));
+ wifi.connect("notify::access-points", () => update().catch(console.error));
+ update().catch(console.error);
+
+ return (
+ <box vertical valign={Gtk.Align.START} className="list" onDestroy={() => children.drop()}>
+ {bind(children)}
+ </box>
+ );
+};
+
+const NoNetworks = ({ label }: { label: Binding<string> | string }) => (
+ <box homogeneous name="empty">
+ <box vertical halign={Gtk.Align.CENTER} valign={Gtk.Align.CENTER} className="empty">
+ <label className="icon" label="wifi_off" />
+ <label label={label} />
+ </box>
+ </box>
+);
+
+export default () => {
+ const network = AstalNetwork.get_default();
+ const label = Variable("");
+
+ const update = () => {
+ if (network.primary === AstalNetwork.Primary.WIFI) label.set(network.wifi.ssid ?? "Disconnected");
+ else if (network.primary === AstalNetwork.Primary.WIRED) label.set(`Ethernet (${network.wired.speed})`);
+ else label.set("No Wifi");
+ };
+ network.connect("notify::primary", update);
+ network.get_wifi()?.connect("notify::ssid", update);
+ network.get_wired()?.connect("notify::speed", update);
+ update();
+
+ return (
+ <box vertical className="networks">
+ <box className="header-bar">
+ <label label={bind(label)} />
+ <box hexpand />
+ <button
+ className={bind(network.wifi, "scanning").as(s => (s ? "enabled" : ""))}
+ cursor="pointer"
+ onClicked={() => network.wifi.scan()}
+ label="󰀂 Scan"
+ />
+ </box>
+ {network.get_wifi() ? (
+ <stack
+ transitionType={Gtk.StackTransitionType.CROSSFADE}
+ transitionDuration={200}
+ shown={bind(network.wifi, "accessPoints").as(a => (a.length > 0 ? "list" : "empty"))}
+ >
+ <NoNetworks
+ label={bind(network.wifi, "enabled").as(p => (p ? "No available networks" : "Wifi is off"))}
+ />
+ <scrollable expand hscroll={Gtk.PolicyType.NEVER} name="list">
+ <List />
+ </scrollable>
+ </stack>
+ ) : (
+ <NoNetworks label="Wifi not available" />
+ )}
+ </box>
+ );
+};