A Process's exited and streamFinished signals aren't guaranteed to fire in order, and several services decided an outcome on whichever fired first: KdeConnect could report a successful file transfer as failed if exited landed before the real stdout payload; Clipboard could present a failed history query as an empty-but-healthy one; Brightness could strand the last queued write of a drag; SoundFeedback and SystemLocale could drop or misapply a rapid second toggle/click because re-arming an already-running Process is a no-op. All five now wait for both signals and let the authoritative one decide, matching the pattern HomeAssistantConfig.qml already used correctly. Health's "copy report" never enabled stdin, so it copied nothing while claiming success. Capture announced every recording as saved regardless of the recorder's actual exit code. Connectivity never restarted Bluetooth discovery when the adapter was enabled from an already-open page. CalendarAgenda left the UI in "loading" forever if its helper died at startup, and the helper itself could crash unguarded instead of reporting unavailable. Geocoding silently dropped a query typed while the previous one was still in flight. Notifs leaked tracked-but-undisplayed notifications under Do Not Disturb, and dismissAll() skipped them. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
157 lines
5.7 KiB
QML
157 lines
5.7 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;
|
|
}
|
|
|
|
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(); }
|
|
}
|
|
}
|