sidebar: networks module

This commit is contained in:
2 * r + 2 * t 2025-03-26 20:22:51 +11:00
parent 5ac2cc2f27
commit 5a1ea91e35
3 changed files with 203 additions and 0 deletions

View file

@ -284,6 +284,67 @@
}
}
.networks {
.list {
@include lib.spacing(10, true);
}
.network {
@include lib.rounded(20);
background-color: color.change(scheme.$surface1, $alpha: 0.4);
padding: lib.s(10) lib.s(15);
@include lib.spacing(5);
&.connected {
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.4);
& > button {
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 50%), $alpha: 0.5);
&:hover,
&:focus {
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 30%), $alpha: 0.5);
}
&:active {
background-color: color.change(color.mix(scheme.$surface1, scheme.$primary, 20%), $alpha: 0.5);
}
}
}
.icon {
font-size: lib.s(28);
margin-right: lib.s(12);
}
.sublabel {
font-size: lib.s(14);
color: scheme.$subtext0;
}
& > button {
@include lib.rounded(1000);
@include lib.element-decel;
@include font.icon;
font-size: lib.s(18);
min-width: lib.s(30);
min-height: lib.s(30);
background-color: color.change(scheme.$surface1, $alpha: 0.5);
&:hover,
&:focus {
background-color: color.change(scheme.$surface2, $alpha: 0.5);
}
&:active {
background-color: color.change(scheme.$overlay0, $alpha: 0.5);
}
}
}
}
.bluetooth {
.list {

View file

@ -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>
);

View file

@ -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>
);
};