noctalia-shell/Services/NetworkService.qml
2025-08-15 22:19:18 -04:00

470 lines
13 KiB
QML

pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
Singleton {
id: root
property var networks: ({})
property string connectingSsid: ""
property string connectStatus: ""
property string connectStatusSsid: ""
property string connectError: ""
property string detectedInterface: ""
property string lastConnectedNetwork: ""
property bool isLoading: false
Component.onCompleted: {
console.log("[Network] Service started")
// Only refresh networks if WiFi is enabled
if (Settings.data.network.wifiEnabled) {
refreshNetworks()
}
}
function signalIcon(signal) {
if (signal >= 80)
return "network_wifi"
if (signal >= 60)
return "network_wifi_3_bar"
if (signal >= 40)
return "network_wifi_2_bar"
if (signal >= 20)
return "network_wifi_1_bar"
return "signal_wifi_0_bar"
}
function isSecured(security) {
return security && security.trim() !== "" && security.trim() !== "--"
}
function refreshNetworks() {
isLoading = true
existingNetwork.running = true
}
function setWifiEnabled(enabled) {
if (enabled) {
// Enable WiFi radio
isLoading = true
enableWifiProcess.running = true
} else {
// Store the currently connected network before disabling
for (const ssid in networks) {
if (networks[ssid].connected) {
lastConnectedNetwork = ssid
break
}
}
// Disable WiFi radio
disableWifiProcess.running = true
}
}
function connectNetwork(ssid, security) {
pendingConnect = {
"ssid": ssid,
"security": security,
"password": ""
}
doConnect()
}
function submitPassword(ssid, password) {
pendingConnect = {
"ssid": ssid,
"security": networks[ssid].security,
"password": password
}
doConnect()
}
function disconnectNetwork(ssid) {
disconnectProfileProcess.connectionName = ssid
disconnectProfileProcess.running = true
}
property var pendingConnect: null
function doConnect() {
const params = pendingConnect
if (!params)
return
connectingSsid = params.ssid
connectStatus = ""
connectStatusSsid = params.ssid
const targetNetwork = networks[params.ssid]
if (targetNetwork && targetNetwork.existing) {
upConnectionProcess.profileName = params.ssid
upConnectionProcess.running = true
pendingConnect = null
return
}
if (params.security && params.security !== "--") {
getInterfaceProcess.running = true
return
}
connectProcess.security = params.security
connectProcess.ssid = params.ssid
connectProcess.password = params.password
connectProcess.running = true
pendingConnect = null
}
property int refreshInterval: 25000
// Only refresh when we have an active connection and WiFi is enabled
property bool hasActiveConnection: {
for (const net in networks) {
if (networks[net].connected) {
return true
}
}
return false
}
property Timer refreshTimer: Timer {
interval: root.refreshInterval
// Only run timer when we're connected to a network and WiFi is enabled
running: root.hasActiveConnection && Settings.data.network.wifiEnabled
repeat: true
onTriggered: root.refreshNetworks()
}
// Force a refresh when menu is opened
function onMenuOpened() {
if (Settings.data.network.wifiEnabled) {
refreshNetworks()
}
}
function onMenuClosed() {// No need to do anything special on close
}
// Process to enable WiFi radio
property Process enableWifiProcess: Process {
id: enableWifiProcess
running: false
command: ["nmcli", "radio", "wifi", "on"]
onRunningChanged: {
if (!running) {
// Wait a moment for the radio to be enabled, then refresh networks
enableWifiDelayTimer.start()
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim() !== "") {
console.warn("Error enabling WiFi:", text)
}
}
}
}
// Timer to delay network refresh after enabling WiFi
property Timer enableWifiDelayTimer: Timer {
id: enableWifiDelayTimer
interval: 2000 // Wait 2 seconds for radio to be ready
repeat: false
onTriggered: {
// Force refresh networks multiple times to ensure UI updates
root.refreshNetworks()
// Try to auto-reconnect to the last connected network if it exists
if (lastConnectedNetwork) {
autoReconnectTimer.start()
}
// Set up additional refresh to ensure UI is populated
postEnableRefreshTimer.start()
}
}
// Additional timer to ensure networks are populated after enabling
property Timer postEnableRefreshTimer: Timer {
id: postEnableRefreshTimer
interval: 1000
repeat: false
onTriggered: {
root.refreshNetworks()
}
}
// Timer to attempt auto-reconnection to the last connected network
property Timer autoReconnectTimer: Timer {
id: autoReconnectTimer
interval: 3000 // Wait 3 seconds after scan for networks to be available
repeat: false
onTriggered: {
if (lastConnectedNetwork && networks[lastConnectedNetwork]) {
const network = networks[lastConnectedNetwork]
if (network.existing && !network.connected) {
upConnectionProcess.profileName = lastConnectedNetwork
upConnectionProcess.running = true
}
}
}
}
// Process to disable WiFi radio
property Process disableWifiProcess: Process {
id: disableWifiProcess
running: false
command: ["nmcli", "radio", "wifi", "off"]
onRunningChanged: {
if (!running) {
// Clear networks when WiFi is disabled
root.networks = ({})
root.connectingSsid = ""
root.connectStatus = ""
root.connectStatusSsid = ""
root.connectError = ""
root.isLoading = false
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim() !== "") {
console.warn("Error disabling WiFi:", text)
}
}
}
}
property Process disconnectProfileProcess: Process {
id: disconnectProfileProcess
property string connectionName: ""
running: false
command: ["nmcli", "connection", "down", connectionName]
onRunningChanged: {
if (!running) {
root.refreshNetworks()
}
}
}
property Process existingNetwork: Process {
id: existingNetwork
running: false
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n")
const networksMap = {}
for (var i = 0; i < lines.length; ++i) {
const line = lines[i].trim()
if (!line)
continue
const parts = line.split(":")
if (parts.length < 2) {
console.warn("Malformed nmcli output line:", line)
continue
}
const ssid = parts[0]
const type = parts[1]
if (ssid) {
networksMap[ssid] = {
"ssid": ssid,
"type": type
}
}
}
scanProcess.existingNetwork = networksMap
scanProcess.running = true
}
}
}
property Process scanProcess: Process {
id: scanProcess
running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
property var existingNetwork
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n")
const networksMap = {}
for (var i = 0; i < lines.length; ++i) {
const line = lines[i].trim()
if (!line)
continue
const parts = line.split(":")
if (parts.length < 4) {
console.warn("Malformed nmcli output line:", line)
continue
}
const ssid = parts[0]
const security = parts[1]
const signal = parseInt(parts[2])
const inUse = parts[3] === "*"
if (ssid) {
if (!networksMap[ssid]) {
networksMap[ssid] = {
"ssid": ssid,
"security": security,
"signal": signal,
"connected": inUse,
"existing": ssid in scanProcess.existingNetwork
}
} else {
const existingNet = networksMap[ssid]
if (inUse) {
existingNet.connected = true
}
if (signal > existingNet.signal) {
existingNet.signal = signal
existingNet.security = security
}
}
}
}
root.networks = networksMap
root.isLoading = false
scanProcess.existingNetwork = {}
}
}
}
property Process connectProcess: Process {
id: connectProcess
property string ssid: ""
property string password: ""
property string security: ""
running: false
command: {
if (password) {
return ["nmcli", "device", "wifi", "connect", `'${ssid}'`, "password", password]
} else {
return ["nmcli", "device", "wifi", "connect", `'${ssid}'`]
}
}
stdout: StdioCollector {
onStreamFinished: {
root.connectingSsid = ""
root.connectStatus = "success"
root.connectStatusSsid = connectProcess.ssid
root.connectError = ""
root.lastConnectedNetwork = connectProcess.ssid
root.refreshNetworks()
}
}
stderr: StdioCollector {
onStreamFinished: {
root.connectingSsid = ""
root.connectStatus = "error"
root.connectStatusSsid = connectProcess.ssid
root.connectError = text
}
}
}
property Process getInterfaceProcess: Process {
id: getInterfaceProcess
running: false
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector {
onStreamFinished: {
var lines = text.split("\n")
for (var i = 0; i < lines.length; ++i) {
var parts = lines[i].split(":")
if (parts[1] === "wifi" && parts[2] !== "unavailable") {
root.detectedInterface = parts[0]
break
}
}
if (root.detectedInterface) {
var params = root.pendingConnect
addConnectionProcess.ifname = root.detectedInterface
addConnectionProcess.ssid = params.ssid
addConnectionProcess.password = params.password
addConnectionProcess.profileName = params.ssid
addConnectionProcess.security = params.security
addConnectionProcess.running = true
} else {
root.connectStatus = "error"
root.connectStatusSsid = root.pendingConnect.ssid
root.connectError = "No Wi-Fi interface found."
root.connectingSsid = ""
root.pendingConnect = null
}
}
}
}
property Process addConnectionProcess: Process {
id: addConnectionProcess
property string ifname: ""
property string ssid: ""
property string password: ""
property string profileName: ""
property string security: ""
running: false
command: {
var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid]
if (security && security !== "--") {
cmd.push("wifi-sec.key-mgmt")
cmd.push("wpa-psk")
cmd.push("wifi-sec.psk")
cmd.push(password)
}
return cmd
}
stdout: StdioCollector {
onStreamFinished: {
upConnectionProcess.profileName = addConnectionProcess.profileName
upConnectionProcess.running = true
}
}
stderr: StdioCollector {
onStreamFinished: {
upConnectionProcess.profileName = addConnectionProcess.profileName
upConnectionProcess.running = true
}
}
}
property Process upConnectionProcess: Process {
id: upConnectionProcess
property string profileName: ""
running: false
command: ["nmcli", "connection", "up", "id", profileName]
stdout: StdioCollector {
onStreamFinished: {
root.connectingSsid = ""
root.connectStatus = "success"
root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : profileName
root.connectError = ""
root.lastConnectedNetwork = profileName
root.pendingConnect = null
root.refreshNetworks()
}
}
stderr: StdioCollector {
onStreamFinished: {
root.connectingSsid = ""
root.connectStatus = "error"
root.connectStatusSsid = root.pendingConnect ? root.pendingConnect.ssid : profileName
root.connectError = text
root.pendingConnect = null
}
}
}
}