241 lines
10 KiB
QML
241 lines
10 KiB
QML
pragma Singleton
|
|
|
|
// Network and Bluetooth state for the settings page.
|
|
//
|
|
// The hard parts -- scanning, joining, pairing -- already work in the quick
|
|
// settings panel through Quickshell.Networking and Quickshell.Bluetooth, which
|
|
// speak to NetworkManager and BlueZ over DBus. Nothing here shells out to nmcli
|
|
// or bluetoothctl, and nothing should: the founding requirement for this
|
|
// desktop was never having to drop to a terminal to join a network.
|
|
//
|
|
// This exists so the page does not have to reach into those modules for the
|
|
// same derived values the panel already computes, and so scanning is driven by
|
|
// whether the page is actually on screen. Scanning while nobody is looking is
|
|
// radio time and battery spent on a list that is not being read.
|
|
|
|
import Quickshell
|
|
import Quickshell.Networking
|
|
import Quickshell.Bluetooth
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
// Set by the page while it is visible; drives both scanners.
|
|
property bool active: false
|
|
|
|
readonly property var wifiDevice: {
|
|
for (const device of Networking.devices.values) {
|
|
if (device.type === DeviceType.Wifi)
|
|
return device;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
readonly property var wiredDevice: {
|
|
let fallback = null;
|
|
for (const device of Networking.devices.values) {
|
|
if (device.type !== DeviceType.Wired)
|
|
continue;
|
|
if (device.connected)
|
|
return device;
|
|
if (!fallback)
|
|
fallback = device;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
// Whether the wired connection is on, as a person means it: carrying
|
|
// traffic, and set to come back by itself.
|
|
readonly property bool wiredOn: root.wiredDevice !== null
|
|
&& root.wiredDevice.connected
|
|
|
|
// Deliberately NOT gated on hasLink. That property reads false while the
|
|
// device is merely disconnected -- NetworkManager still reports the carrier
|
|
// as on -- so using it to decide whether the switch works built a trap
|
|
// door: turning Ethernet off made the switch disable itself, claim "no
|
|
// cable", and leave no way to turn it back on. A wired device that exists
|
|
// can always be asked to come up; if there is genuinely no cable, the
|
|
// attempt fails and says so, which is the honest failure.
|
|
readonly property bool wiredAvailable: root.wiredDevice !== null
|
|
|
|
// Turning wired networking off means both halves. Disconnecting alone lasts
|
|
// about a second: NetworkManager sees a managed device with autoconnect set
|
|
// and immediately brings it back, so the switch would flip itself on again
|
|
// and read as broken.
|
|
function setWired(enabled: bool): void {
|
|
const device = root.wiredDevice;
|
|
if (!device)
|
|
return;
|
|
device.autoconnect = enabled;
|
|
if (enabled) {
|
|
// With autoconnect restored NetworkManager will usually bring it up
|
|
// on its own; asking directly makes it immediate rather than
|
|
// whenever the daemon next looks.
|
|
if (device.network)
|
|
device.network.connect();
|
|
} else {
|
|
device.disconnect();
|
|
}
|
|
}
|
|
|
|
readonly property var adapter: Bluetooth.defaultAdapter
|
|
|
|
readonly property bool wifiEnabled: Networking.wifiEnabled
|
|
readonly property bool wifiAvailable: Networking.wifiHardwareEnabled
|
|
|
|
// The radio switches, as functions rather than as writes a page makes for
|
|
// itself. Both are one-line assignments to the native modules, which is
|
|
// exactly the point: a page that reached past this service to set
|
|
// Networking.wifiEnabled directly would be one page's worth of the rule
|
|
// this file exists to keep -- and the next such write, needing a retry or a
|
|
// guard, would have nowhere to live but the page.
|
|
//
|
|
// Native both ways. Nothing here shells out to nmcli or rfkill; airplane
|
|
// mode, which genuinely needs rfkill, lives in NetworkTools.qml instead.
|
|
function setWifiEnabled(enabled: bool): void {
|
|
if (Networking.wifiEnabled !== enabled)
|
|
Networking.wifiEnabled = enabled;
|
|
}
|
|
|
|
function setBluetoothEnabled(enabled: bool): void {
|
|
const device = root.adapter;
|
|
if (!device || device.enabled === enabled)
|
|
return;
|
|
device.enabled = enabled;
|
|
}
|
|
|
|
readonly property bool bluetoothEnabled: root.adapter?.enabled ?? false
|
|
readonly property bool bluetoothAvailable: !!root.adapter
|
|
|
|
// Current network, then saved, then by signal -- the order GNOME uses,
|
|
// which is the order you actually look for things in.
|
|
readonly property var networks: {
|
|
if (!root.wifiDevice || !root.wifiDevice.networks)
|
|
return [];
|
|
const list = root.wifiDevice.networks.values.slice();
|
|
list.sort((a, b) => {
|
|
if (a.connected !== b.connected)
|
|
return a.connected ? -1 : 1;
|
|
if (a.known !== b.known)
|
|
return a.known ? -1 : 1;
|
|
return b.signalStrength - a.signalStrength;
|
|
});
|
|
return list;
|
|
}
|
|
|
|
readonly property var savedNetworks: root.networks.filter(network => network.known)
|
|
|
|
readonly property var bluetoothDevices: {
|
|
if (!Bluetooth.devices)
|
|
return [];
|
|
const list = Bluetooth.devices.values.slice();
|
|
list.sort((a, b) => {
|
|
if (a.connected !== b.connected)
|
|
return a.connected ? -1 : 1;
|
|
if (a.paired !== b.paired)
|
|
return a.paired ? -1 : 1;
|
|
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
});
|
|
return list;
|
|
}
|
|
|
|
readonly property var activeNetwork: root.networks.find(network => network.connected) ?? null
|
|
|
|
function isSecured(network: var): bool {
|
|
return network.security !== WifiSecurityType.Open
|
|
&& network.security !== WifiSecurityType.Owe
|
|
&& network.security !== WifiSecurityType.Unknown;
|
|
}
|
|
|
|
// Member names come from the installed plugin's own qmltypes --
|
|
// Quickshell/Networking/quickshell-network.qmltypes, whose WifiSecurityType
|
|
// is exactly: Wpa3SuiteB192, Sae, Wpa2Eap, Wpa2Psk, WpaEap, WpaPsk,
|
|
// StaticWep, DynamicWep, Leap, Owe, Open, Unknown -- rather than the
|
|
// GNOME-style names this switch used to test (Wep, Wpa, Wpa2, Wpa3,
|
|
// Enterprise). None of those five exist, and a `case` against an undefined
|
|
// member simply never matches, so every secured network read "Secured" and
|
|
// "Enterprise" was unreachable. The enterprise join form keys off exactly
|
|
// that string, so the whole 802.1X flow was dead.
|
|
//
|
|
// The open cases are named here rather than delegated to isSecured(),
|
|
// which treats Unknown as unsecured. Calling a network whose security
|
|
// nobody could read "Open" is a claim this cannot back up; Unknown falls
|
|
// through to "Secured" instead. isSecured() keeps its own meaning -- "does
|
|
// joining this need a passphrase" -- and is unchanged.
|
|
//
|
|
// Owe is Enhanced Open: encrypted, but joined without a passphrase, so to
|
|
// someone picking a network it reads as open.
|
|
function securityLabel(network: var): string {
|
|
switch (network.security) {
|
|
case WifiSecurityType.Open: return "Open";
|
|
case WifiSecurityType.Owe: return "Open";
|
|
case WifiSecurityType.StaticWep: return "WEP";
|
|
case WifiSecurityType.WpaPsk: return "WPA";
|
|
case WifiSecurityType.Wpa2Psk: return "WPA2";
|
|
case WifiSecurityType.Sae: return "WPA3";
|
|
case WifiSecurityType.WpaEap: return "Enterprise";
|
|
case WifiSecurityType.Wpa2Eap: return "Enterprise";
|
|
case WifiSecurityType.Wpa3SuiteB192: return "Enterprise";
|
|
case WifiSecurityType.DynamicWep: return "Enterprise";
|
|
case WifiSecurityType.Leap: return "Enterprise";
|
|
}
|
|
return "Secured";
|
|
}
|
|
|
|
// Four bars is what people read signal as, so bucket rather than showing a
|
|
// percentage that changes every scan and means nothing to anyone.
|
|
//
|
|
// signalStrength is 0.0-1.0, NOT a percentage. Treating it as 0-100 puts
|
|
// every network including the connected one in the bottom bucket, which is
|
|
// exactly as useless as showing nothing. Thresholds match the icon buckets
|
|
// in modules/quicksettings/WifiList.qml so the two never disagree.
|
|
function signalLabel(strength: real): string {
|
|
if (strength >= 0.8) return "Excellent";
|
|
if (strength >= 0.55) return "Good";
|
|
if (strength >= 0.3) return "Fair";
|
|
if (strength > 0.05) return "Weak";
|
|
return "No signal";
|
|
}
|
|
|
|
// NoSecrets, not "Authentication" -- the qmltypes members are NoSecrets,
|
|
// Unknown, WifiAuthTimeout, WifiClientDisconnected, WifiClientFailed,
|
|
// WifiNetworkLost. A case against an undefined member never matches, so a
|
|
// wrong password used to fall through to the generic text.
|
|
function connectionFailureText(reason: var): string {
|
|
switch (reason) {
|
|
case ConnectionFailReason.WifiAuthTimeout:
|
|
case ConnectionFailReason.NoSecrets:
|
|
return "Wrong password";
|
|
case ConnectionFailReason.WifiNetworkLost:
|
|
return "Network out of range";
|
|
}
|
|
return "Could not connect";
|
|
}
|
|
|
|
// Scanning follows visibility. NetworkManager keeps scanning as long as it
|
|
// is asked to, and Bluetooth discovery is worse -- it holds the radio.
|
|
function syncScanners(): void {
|
|
if (root.wifiDevice)
|
|
root.wifiDevice.scannerEnabled = root.active && root.wifiEnabled;
|
|
|
|
if (root.adapter && root.adapter.enabled) {
|
|
const shouldDiscover = root.active;
|
|
if (root.adapter.discovering !== shouldDiscover)
|
|
root.adapter.discovering = shouldDiscover;
|
|
}
|
|
}
|
|
|
|
onActiveChanged: root.syncScanners()
|
|
onWifiDeviceChanged: root.syncScanners()
|
|
onWifiEnabledChanged: root.syncScanners()
|
|
onAdapterChanged: root.syncScanners()
|
|
|
|
// adapter.enabled has no property on root to bind onXChanged to, so it
|
|
// needs its own Connections -- the Bluetooth equivalent of onWifiEnabledChanged.
|
|
Connections {
|
|
target: root.adapter
|
|
function onEnabledChanged(): void { root.syncScanners(); }
|
|
}
|
|
}
|