Handle Wi-Fi and Bluetooth in Settings
Network & Devices was 92 lines and two buttons that opened GNOME. It now scans, joins, and pairs directly through Quickshell.Networking and Quickshell.Bluetooth -- NetworkManager and BlueZ over DBus, no shelling out to nmcli or bluetoothctl. That was the founding requirement for this desktop: never having to drop to a terminal to join a network. Scanning follows the page being visible. Wi-Fi scanning and especially Bluetooth discovery hold the radio, and running either for a list nobody is looking at spends airtime on nothing. Joining a secured network gets a real password field, not the clipboard popover's search box with different placeholder text: a Wi-Fi key typed into a field that echoes it is readable by anyone behind you, and a search glyph in front of a password prompt is simply wrong. Two bugs found by looking at the rendered page, both silent: The device lookups used enum names that do not exist -- NetworkDeviceType.Wifi rather than DeviceType.Wifi -- so both returned null and the page reported "No Wi-Fi adapter" on a machine whose Wi-Fi was connected. Nothing was logged; QML resolves an unknown enum member to undefined and compares happily. signalStrength is 0.0-1.0, not a percentage, so thresholds written for 0-100 put every network including the connected one in the bottom bucket. The labels now use the same buckets as the icons in quicksettings/WifiList.qml so the two cannot disagree. The contract compares what the service resolves against what nmcli reports, rather than only checking that nothing crashed. Also makes the Home Assistant bridge hermetic: resolve_config read the user's private env file even when a caller supplied an explicit environment, so adding a real PANAMA_HOME_ASSISTANT_ENTITIES to that file silently overrode a fixture asserting the legacy fallback. An explicit environment is now the whole environment; production still reads the file. Its live contract skips when no token is configured -- an absent credential is not a defect, and a suite expected to be red stops being read -- while a configured-but-broken bridge still fails. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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: {
|
||||
for (const device of Networking.devices.values) {
|
||||
if (device.type === DeviceType.Wired)
|
||||
return device;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user