wifi menu

This commit is contained in:
end-4
2025-08-27 17:52:23 +07:00
parent bb49747fd9
commit 20d9561143
15 changed files with 768 additions and 202 deletions
+170 -3
View File
@@ -1,12 +1,15 @@
pragma Singleton
pragma ComponentBehavior: Bound
// Took many bits from https://github.com/caelestia-dots/shell (GPLv3)
import Quickshell
import Quickshell.Io
import QtQuick
import "./network"
/**
* Simple polled network state service.
* Network service with nmcli.
*/
Singleton {
id: root
@@ -15,6 +18,12 @@ Singleton {
property bool ethernet: false
property bool wifiEnabled: false
property bool wifiScanning: false
property bool wifiConnecting: connectProc.running
property WifiAccessPoint wifiConnectTarget
readonly property list<WifiAccessPoint> wifiNetworks: []
readonly property WifiAccessPoint active: wifiNetworks.find(n => n.active) ?? null
property string networkName: ""
property int networkStrength
property string materialSymbol: ethernet ? "lan" :
@@ -27,15 +36,99 @@ Singleton {
) : "signal_wifi_off"
// Control
function toggleWifi(): void {
const cmd = wifiEnabled ? "off" : "on";
function enableWifi(enabled = true): void {
const cmd = enabled ? "on" : "off";
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]);
}
function toggleWifi(): void {
enableWifi(!wifiEnabled);
}
function rescanWifi(): void {
wifiScanning = true;
rescanProcess.running = true;
}
function connectToWifiNetwork(accessPoint: WifiAccessPoint): void {
accessPoint.askingPassword = false;
root.wifiConnectTarget = accessPoint;
// We use this instead of `nmcli connection up SSID` because this also creates a connection profile
connectProc.exec(["nmcli", "dev", "wifi", "connect", accessPoint.ssid])
}
function disconnectWifiNetwork(): void {
if (active) disconnectProc.exec(["nmcli", "connection", "down", active.ssid]);
}
function changePassword(network: WifiAccessPoint, password: string, username = ""): void {
// TODO: enterprise wifi with username
network.askingPassword = false;
changePasswordProc.exec({
"environment": {
"PASSWORD": password
},
"command": ["bash", "-c", `nmcli connection modify ${network.ssid} wifi-sec.psk "$PASSWORD"`]
})
}
Process {
id: enableWifiProc
}
Process {
id: connectProc
environment: ({
LANG: "C",
LC_ALL: "C"
})
stdout: SplitParser {
onRead: line => {
// print(line)
getNetworks.running = true
}
}
stderr: SplitParser {
onRead: line => {
// print("err:", line)
if (line.includes("Secrets were required")) {
root.wifiConnectTarget.askingPassword = true
}
}
}
onExited: (exitCode, exitStatus) => {
root.wifiConnectTarget.askingPassword = (exitCode !== 0)
root.wifiConnectTarget = null
}
}
Process {
id: disconnectProc
stdout: SplitParser {
onRead: getNetworks.running = true
}
}
Process {
id: changePasswordProc
onExited: { // Re-attempt connection after changing password
connectProc.running = false
connectProc.running = true
}
}
Process {
id: rescanProcess
command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"]
stdout: SplitParser {
onRead: {
wifiScanning = false;
getNetworks.running = true;
}
}
}
// Status update
function update() {
updateConnectionType.startCheck();
@@ -118,4 +211,78 @@ Singleton {
}
}
}
Process {
id: getNetworks
running: true
command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"]
environment: ({
LANG: "C",
LC_ALL: "C"
})
stdout: StdioCollector {
onStreamFinished: {
const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED";
const rep = new RegExp("\\\\:", "g");
const rep2 = new RegExp(PLACEHOLDER, "g");
const allNetworks = text.trim().split("\n").map(n => {
const net = n.replace(rep, PLACEHOLDER).split(":");
return {
active: net[0] === "yes",
strength: parseInt(net[1]),
frequency: parseInt(net[2]),
ssid: net[3],
bssid: net[4]?.replace(rep2, ":") ?? "",
security: net[5] || ""
};
}).filter(n => n.ssid && n.ssid.length > 0);
// Group networks by SSID and prioritize connected ones
const networkMap = new Map();
for (const network of allNetworks) {
const existing = networkMap.get(network.ssid);
if (!existing) {
networkMap.set(network.ssid, network);
} else {
// Prioritize active/connected networks
if (network.active && !existing.active) {
networkMap.set(network.ssid, network);
} else if (!network.active && !existing.active) {
// If both are inactive, keep the one with better signal
if (network.strength > existing.strength) {
networkMap.set(network.ssid, network);
}
}
// If existing is active and new is not, keep existing
}
}
const wifiNetworks = Array.from(networkMap.values());
const rNetworks = root.wifiNetworks;
const destroyed = rNetworks.filter(rn => !wifiNetworks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid));
for (const network of destroyed)
rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy());
for (const network of wifiNetworks) {
const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid);
if (match) {
match.lastIpcObject = network;
} else {
rNetworks.push(apComp.createObject(root, {
lastIpcObject: network
}));
}
}
}
}
}
Component {
id: apComp
WifiAccessPoint {}
}
}
@@ -0,0 +1,14 @@
import QtQuick
QtObject {
required property var lastIpcObject
readonly property string ssid: lastIpcObject.ssid
readonly property string bssid: lastIpcObject.bssid
readonly property int strength: lastIpcObject.strength
readonly property int frequency: lastIpcObject.frequency
readonly property bool active: lastIpcObject.active
readonly property string security: lastIpcObject.security
readonly property bool isSecure: security.length > 0
property bool askingPassword: false
}