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; } readonly property var adapter: Bluetooth.defaultAdapter readonly property bool wifiEnabled: Networking.wifiEnabled readonly property bool wifiAvailable: Networking.wifiHardwareEnabled // 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; } function securityLabel(network: var): string { if (!root.isSecured(network)) return "Open"; switch (network.security) { case WifiSecurityType.Wep: return "WEP"; case WifiSecurityType.Wpa: return "WPA"; case WifiSecurityType.Wpa2: return "WPA2"; case WifiSecurityType.Wpa3: return "WPA3"; case WifiSecurityType.Enterprise: 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"; } function connectionFailureText(reason: var): string { switch (reason) { case ConnectionFailReason.WifiAuthTimeout: case ConnectionFailReason.Authentication: 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(); } } }