From 617e686238d3c7155112196043f0883ccf6a7012 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 18:59:48 -0500 Subject: service: Nmcli.qml --- services/Nmcli.qml | 1246 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1246 insertions(+) create mode 100644 services/Nmcli.qml (limited to 'services/Nmcli.qml') diff --git a/services/Nmcli.qml b/services/Nmcli.qml new file mode 100644 index 0000000..4e45b41 --- /dev/null +++ b/services/Nmcli.qml @@ -0,0 +1,1246 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property var deviceStatus: null + property var wirelessInterfaces: [] + property var ethernetInterfaces: [] + property bool isConnected: false + property string activeInterface: "" + property string activeConnection: "" + property bool wifiEnabled: true + readonly property list networks: [] + readonly property AccessPoint active: networks.find(n => n.active) ?? null + property list savedConnections: [] + property list savedConnectionSsids: [] + + property var wifiConnectionQueue: [] + property int currentSsidQueryIndex: 0 + property var pendingConnection: null + signal connectionFailed(string ssid) + property var wirelessDeviceDetails: null + property var ethernetDeviceDetails: null + property list ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + + property list activeProcesses: [] + property var debugLogger: null + + function setDebugLogger(logger: var): void { + root.debugLogger = logger; + } + + function log(message: string): void { + if (root.debugLogger) { + root.debugLogger(message); + } else { + console.log("[Nmcli]", message); + } + } + + function appendLog(message: string): void { + log(message); + } + + function executeCommand(args: list, callback: var): void { + const proc = commandProc.createObject(root); + proc.command = ["nmcli", ...args]; + proc.callback = callback; + + activeProcesses.push(proc); + + proc.processFinished.connect(() => { + const index = activeProcesses.indexOf(proc); + if (index >= 0) { + activeProcesses.splice(index, 1); + } + }); + + Qt.callLater(() => { + proc.exec(proc.command); + }); + } + + function getDeviceStatus(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + if (callback) callback(result.output); + }); + } + + function getWirelessInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && parts[1] === "wifi") { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + root.wirelessInterfaces = interfaces; + if (callback) callback(interfaces); + }); + } + + function getEthernetInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const devices = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && parts[1] === "ethernet") { + const device = parts[0] || ""; + const type = parts[1] || ""; + const state = parts[2] || ""; + const connection = parts[3] || ""; + + const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); + + interfaces.push({ + device: device, + type: type, + state: state, + connection: connection + }); + + devices.push({ + interface: device, + type: type, + state: state, + connection: connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }); + } + } + root.ethernetInterfaces = interfaces; + root.ethernetDevices = devices; + if (callback) callback(interfaces); + }); + } + + function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { + if (connectionName && connectionName.length > 0) { + executeCommand(["connection", "up", connectionName], (result) => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + if (interfaceName && interfaceName.length > 0) { + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + } + }, 500); + } + if (callback) callback(result); + }); + } else if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "connect", interfaceName], (result) => { + if (result.success) { + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + Qt.callLater(() => { + getEthernetDeviceDetails(interfaceName, () => {}); + }, 1000); + }, 500); + } + if (callback) callback(result); + }); + } else { + if (callback) callback({ success: false, output: "", error: "No connection name or interface specified", exitCode: -1 }); + } + } + + function disconnectEthernet(connectionName: string, callback: var): void { + if (!connectionName || connectionName.length === 0) { + if (callback) callback({ success: false, output: "", error: "No connection name specified", exitCode: -1 }); + return; + } + + executeCommand(["connection", "down", connectionName], (result) => { + if (result.success) { + root.ethernetDeviceDetails = null; + Qt.callLater(() => { + getEthernetInterfaces(() => {}); + }, 500); + } + if (callback) callback(result); + }); + } + + function getAllInterfaces(callback: var): void { + executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + const interfaces = []; + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2 && (parts[1] === "wifi" || parts[1] === "ethernet")) { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + if (callback) callback(interfaces); + }); + } + + function isInterfaceConnected(interfaceName: string, callback: var): void { + executeCommand(["device", "status"], (result) => { + const lines = result.output.trim().split("\n"); + for (const line of lines) { + const parts = line.split(/\s+/); + if (parts.length >= 3 && parts[0] === interfaceName) { + const connected = parts[2] === "connected" || parts[2].startsWith("connected"); + if (callback) callback(connected); + return; + } + } + if (callback) callback(false); + }); + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + if (isSecure) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + connectWireless(ssid, "", bssid, (result) => { + if (result.success) { + if (callback) callback({ success: true, usedSavedPassword: true, output: result.output, error: "", exitCode: 0 }); + } else if (result.needsPassword) { + if (callback) callback({ success: false, needsPassword: true, output: result.output, error: result.error, exitCode: result.exitCode }); + } else { + if (callback) callback(result); + } + }); + } else { + connectWireless(ssid, "", bssid, callback); + } + } + + function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + const retries = retryCount !== undefined ? retryCount : 0; + const maxRetries = 2; + + if (callback) { + root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback, retryCount: retries }; + connectionCheckTimer.start(); + immediateCheckTimer.checkCount = 0; + immediateCheckTimer.start(); + } + + if (password && password.length > 0 && hasBssid) { + const bssidUpper = bssid.toUpperCase(); + createConnectionWithPassword(ssid, bssidUpper, password, callback); + return; + } + + let cmd = ["device", "wifi", "connect", ssid]; + if (password && password.length > 0) { + cmd.push("password", password); + } + executeCommand(cmd, (result) => { + if (result.needsPassword && callback) { + if (callback) callback(result); + return; + } + + if (!result.success && root.pendingConnection && retries < maxRetries) { + log("Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + Qt.callLater(() => { + connectWireless(ssid, password, bssid, callback, retries + 1); + }, 1000); + } else if (!result.success && root.pendingConnection) { + } else if (result.success && callback) { + } else if (!result.success && !root.pendingConnection) { + if (callback) callback(result); + } + }); + } + + function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { + checkAndDeleteConnection(ssid, () => { + const cmd = ["connection", "add", + "type", "wifi", + "con-name", ssid, + "ifname", "*", + "ssid", ssid, + "802-11-wireless.bssid", bssidUpper, + "802-11-wireless-security.key-mgmt", "wpa-psk", + "802-11-wireless-security.psk", password]; + + executeCommand(cmd, (result) => { + if (result.success) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + const hasDuplicateWarning = result.error && ( + result.error.includes("another connection with the name") || + result.error.includes("Reference the connection by its uuid") + ); + + if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { + log("Connection profile creation failed, trying fallback..."); + let fallbackCmd = ["device", "wifi", "connect", ssid, "password", password]; + executeCommand(fallbackCmd, (fallbackResult) => { + if (callback) callback(fallbackResult); + }); + } + } + }); + }); + } + + function checkAndDeleteConnection(ssid: string, callback: var): void { + executeCommand(["connection", "show", ssid], (result) => { + if (result.success) { + executeCommand(["connection", "delete", ssid], (deleteResult) => { + Qt.callLater(() => { + if (callback) callback(); + }, 300); + }); + } else { + if (callback) callback(); + } + }); + } + + function activateConnection(connectionName: string, callback: var): void { + executeCommand(["connection", "up", connectionName], (result) => { + if (callback) callback(result); + }); + } + + function loadSavedConnections(callback: var): void { + executeCommand(["-t", "-f", "NAME,TYPE", "connection", "show"], (result) => { + if (!result.success) { + root.savedConnections = []; + root.savedConnectionSsids = []; + if (callback) callback([]); + return; + } + + parseConnectionList(result.output, callback); + }); + } + + function parseConnectionList(output: string, callback: var): void { + const lines = output.trim().split("\n").filter(line => line.length > 0); + const wifiConnections = []; + const connections = []; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const name = parts[0]; + const type = parts[1]; + connections.push(name); + + if (type === "802-11-wireless") { + wifiConnections.push(name); + } + } + } + + root.savedConnections = connections; + + if (wifiConnections.length > 0) { + root.wifiConnectionQueue = wifiConnections; + root.currentSsidQueryIndex = 0; + root.savedConnectionSsids = []; + queryNextSsid(callback); + } else { + root.savedConnectionSsids = []; + root.wifiConnectionQueue = []; + if (callback) callback(root.savedConnectionSsids); + } + } + + function queryNextSsid(callback: var): void { + if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { + const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; + root.currentSsidQueryIndex++; + + executeCommand(["-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName], (result) => { + if (result.success) { + processSsidOutput(result.output); + } + queryNextSsid(callback); + }); + } else { + root.wifiConnectionQueue = []; + root.currentSsidQueryIndex = 0; + if (callback) callback(root.savedConnectionSsids); + } + } + + function processSsidOutput(output: string): void { + const lines = output.trim().split("\n"); + for (const line of lines) { + if (line.startsWith("802-11-wireless.ssid:")) { + const ssid = line.substring("802-11-wireless.ssid:".length).trim(); + if (ssid && ssid.length > 0) { + const ssidLower = ssid.toLowerCase(); + const exists = root.savedConnectionSsids.some(s => s && s.toLowerCase() === ssidLower); + if (!exists) { + const newList = root.savedConnectionSsids.slice(); + newList.push(ssid); + root.savedConnectionSsids = newList; + } + } + } + } + } + + function hasSavedProfile(ssid: string): bool { + if (!ssid || ssid.length === 0) { + return false; + } + const ssidLower = ssid.toLowerCase().trim(); + + if (root.active && root.active.ssid) { + const activeSsidLower = root.active.ssid.toLowerCase().trim(); + if (activeSsidLower === ssidLower) { + return true; + } + } + + const hasSsid = root.savedConnectionSsids.some(savedSsid => + savedSsid && savedSsid.toLowerCase().trim() === ssidLower + ); + + if (hasSsid) { + return true; + } + + const hasConnectionName = root.savedConnections.some(connName => + connName && connName.toLowerCase().trim() === ssidLower + ); + + return hasConnectionName; + } + + function forgetNetwork(ssid: string, callback: var): void { + if (!ssid || ssid.length === 0) { + if (callback) callback({ success: false, output: "", error: "No SSID specified", exitCode: -1 }); + return; + } + + const connectionName = root.savedConnections.find(conn => + conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() + ) || ssid; + + executeCommand(["connection", "delete", connectionName], (result) => { + if (result.success) { + Qt.callLater(() => { + loadSavedConnections(() => {}); + }, 500); + } + if (callback) callback(result); + }); + } + + function disconnect(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "disconnect", interfaceName], (result) => { + if (callback) callback(result.success ? result.output : ""); + }); + } else { + executeCommand(["device", "disconnect", "wifi"], (result) => { + if (callback) callback(result.success ? result.output : ""); + }); + } + } + + function getDeviceDetails(interfaceName: string, callback: var): void { + executeCommand(["device", "show", interfaceName], (result) => { + if (callback) callback(result.output); + }); + } + + function refreshStatus(callback: var): void { + getDeviceStatus((output) => { + const lines = output.trim().split("\n"); + let connected = false; + let activeIf = ""; + let activeConn = ""; + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 4) { + const state = parts[2] || ""; + if (state === "connected" || state.startsWith("connected")) { + connected = true; + activeIf = parts[0] || ""; + activeConn = parts[3] || ""; + break; + } + } + } + + root.isConnected = connected; + root.activeInterface = activeIf; + root.activeConnection = activeConn; + + if (callback) callback({ connected, interface: activeIf, connection: activeConn }); + }); + } + + function bringInterfaceUp(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "connect", interfaceName], (result) => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + } + } + + function bringInterfaceDown(interfaceName: string, callback: var): void { + if (interfaceName && interfaceName.length > 0) { + executeCommand(["device", "disconnect", interfaceName], (result) => { + if (callback) { + callback(result); + } + }); + } else { + if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + } + } + + function scanWirelessNetworks(interfaceName: string, callback: var): void { + let cmd = ["device", "wifi", "rescan"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push("ifname", interfaceName); + } + executeCommand(cmd, (result) => { + if (callback) { + callback(result); + } + }); + } + + function enableWifi(enabled: bool, callback: var): void { + const cmd = enabled ? "on" : "off"; + executeCommand(["radio", "wifi", cmd], (result) => { + if (result.success) { + getWifiStatus((status) => { + root.wifiEnabled = status; + if (callback) callback(result); + }); + } else { + if (callback) callback(result); + } + }); + } + + function toggleWifi(callback: var): void { + const newState = !root.wifiEnabled; + enableWifi(newState, callback); + } + + function getWifiStatus(callback: var): void { + executeCommand(["radio", "wifi"], (result) => { + if (result.success) { + const enabled = result.output.trim() === "enabled"; + root.wifiEnabled = enabled; + if (callback) callback(enabled); + } else { + if (callback) callback(root.wifiEnabled); + } + }); + } + + function getNetworks(callback: var): void { + executeCommand(["-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"], (result) => { + if (!result.success) { + if (callback) callback([]); + return; + } + + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = result.output.trim().split("\n") + .filter(line => line && line.length > 0) + .map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }) + .filter(n => n.ssid && n.ssid.length > 0); + + const networkMap = new Map(); + for (const network of allNetworks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + } + } + + const networks = Array.from(networkMap.values()); + const rNetworks = root.networks; + + const destroyed = rNetworks.filter(rn => !networks.find(n => + n.frequency === rn.frequency && + n.ssid === rn.ssid && + n.bssid === rn.bssid + )); + for (const network of destroyed) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); + } + } + + for (const network of networks) { + const match = rNetworks.find(n => + n.frequency === network.frequency && + n.ssid === network.ssid && + n.bssid === network.bssid + ); + if (match) { + match.lastIpcObject = network; + } else { + rNetworks.push(apComp.createObject(root, { + lastIpcObject: network + })); + } + } + + if (callback) callback(root.networks); + checkPendingConnection(); + }); + } + + function getWirelessSSIDs(interfaceName: string, callback: var): void { + let cmd = ["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push("ifname", interfaceName); + } + executeCommand(cmd, (result) => { + if (!result.success) { + if (callback) callback([]); + return; + } + + const ssids = []; + const lines = result.output.trim().split("\n"); + const seenSSIDs = new Set(); + + for (const line of lines) { + if (!line || line.length === 0) continue; + + const parts = line.split(":"); + if (parts.length >= 1) { + const ssid = parts[0].trim(); + if (ssid && ssid.length > 0 && !seenSSIDs.has(ssid)) { + seenSSIDs.add(ssid); + const signalStr = parts.length >= 2 ? parts[1].trim() : ""; + const signal = signalStr ? parseInt(signalStr, 10) : 0; + const security = parts.length >= 3 ? parts[2].trim() : ""; + ssids.push({ + ssid: ssid, + signal: signalStr, + signalValue: isNaN(signal) ? 0 : signal, + security: security + }); + } + } + } + + ssids.sort((a, b) => { + return b.signalValue - a.signalValue; + }); + + if (callback) callback(ssids); + }); + } + + component CommandProcess: Process { + id: proc + property var callback: null + property list command: [] + property bool callbackCalled: false + property int exitCode: 0 + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: StdioCollector { + id: stdoutCollector + onStreamFinished: { + } + } + + stderr: StdioCollector { + id: stderrCollector + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand && root.pendingConnection && root.pendingConnection.callback) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + } + } + } + } + } + + onExited: { + proc.exitCode = exitCode; + Qt.callLater(() => { + if (proc.callbackCalled) { + proc.processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = proc.exitCode === 0; + + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + const needsPassword = isConnectionCommand && error && error.length > 0 && + (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + proc.processFinished(); + return; + } else if (!success && isConnectionCommand && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + proc.callbackCalled = true; + proc.callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + proc.processFinished(); + } else { + proc.processFinished(); + } + }); + } + } + + Component { + id: commandProc + CommandProcess {} + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + AccessPoint {} + } + + Timer { + id: connectionCheckTimer + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + interval: 500 + repeat: true + triggeredOnStart: false + property int checkCount: 0 + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + const isConnectionCommand = proc.command && proc.command.length > 0 && + (proc.command.includes("wifi") || proc.command.includes("connection")); + + if (isConnectionCommand) { + const needsPassword = (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + + function checkPendingConnection(): void { + if (root.pendingConnection) { + Qt.callLater(() => { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + } + root.pendingConnection = null; + } else { + if (!immediateCheckTimer.running) { + immediateCheckTimer.start(); + } + } + }); + } + } + + function cidrToSubnetMask(cidr: string): string { + const cidrNum = parseInt(cidr, 10); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octet1 = (mask >>> 24) & 0xff; + const octet2 = (mask >>> 16) & 0xff; + const octet3 = (mask >>> 8) & 0xff; + const octet4 = mask & 0xff; + + return `${octet1}.${octet2}.${octet3}.${octet4}`; + } + + function getWirelessDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], (result) => { + if (!result.success || !result.output) { + root.wirelessDeviceDetails = null; + if (callback) callback(null); + return; + } + + const details = parseDeviceDetails(result.output, false); + root.wirelessDeviceDetails = details; + if (callback) callback(details); + }); + } + + function getEthernetDeviceDetails(interfaceName: string, callback: var): void { + if (!interfaceName || interfaceName.length === 0) { + const activeInterface = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeInterface && activeInterface.device) { + interfaceName = activeInterface.device; + } else { + if (callback) callback(null); + return; + } + } + + executeCommand(["device", "show", interfaceName], (result) => { + if (!result.success || !result.output) { + root.ethernetDeviceDetails = null; + if (callback) callback(null); + return; + } + + const details = parseDeviceDetails(result.output, true); + root.ethernetDeviceDetails = details; + if (callback) callback(details); + }); + } + + function parseDeviceDetails(output: string, isEthernet: bool): var { + const details = { + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }; + + if (!output || output.length === 0) { + return details; + } + + const lines = output.trim().split("\n"); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const parts = line.split(":"); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join(":").trim(); + + if (key.startsWith("IP4.ADDRESS")) { + const ipParts = value.split("/"); + details.ipAddress = ipParts[0] || ""; + if (ipParts[1]) { + details.subnet = cidrToSubnetMask(ipParts[1]); + } else { + details.subnet = ""; + } + } else if (key === "IP4.GATEWAY") { + if (value !== "--") { + details.gateway = value; + } + } else if (key.startsWith("IP4.DNS")) { + if (value !== "--" && value.length > 0) { + details.dns.push(value); + } + } else if (isEthernet && key === "WIRED-PROPERTIES.MAC") { + details.macAddress = value; + } else if (isEthernet && key === "WIRED-PROPERTIES.SPEED") { + details.speed = value; + } else if (!isEthernet && key === "GENERAL.HWADDR") { + details.macAddress = value; + } + } + } + + return details; + } + + Process { + id: monitorProc + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: SplitParser { + onRead: { + log("Connection state change detected, refreshing..."); + root.refreshOnConnectionChange(); + } + } + + onExited: { + log("Monitor process exited, restarting..."); + Qt.callLater(() => { + monitorProc.running = true; + }, 2000); + } + } + + function refreshOnConnectionChange(): void { + getNetworks((networks) => { + const newActive = root.active; + + if (newActive && newActive.active) { + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 500); + } else { + root.wirelessDeviceDetails = null; + root.ethernetDeviceDetails = null; + } + + getWirelessInterfaces(() => {}); + getEthernetInterfaces(() => { + if (root.activeEthernet && root.activeEthernet.connected) { + Qt.callLater(() => { + getEthernetDeviceDetails(root.activeEthernet.interface, () => {}); + }, 500); + } + }); + }); + } + + Component.onCompleted: { + getWifiStatus(() => {}); + getNetworks(() => {}); + loadSavedConnections(() => {}); + getEthernetInterfaces(() => {}); + + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return iface.state === "connected" || iface.state.startsWith("connected"); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 2000); + } +} + -- cgit v1.2.3-freya From 36a91213b14f0dfd000761aa0e7be76db0609101 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Thu, 13 Nov 2025 19:05:23 -0500 Subject: network: migrated to nmcli.qml --- modules/bar/popouts/Network.qml | 33 +++++----- plan.md | 137 ---------------------------------------- services/Nmcli.qml | 33 ++++++++++ 3 files changed, 49 insertions(+), 154 deletions(-) delete mode 100644 plan.md (limited to 'services/Nmcli.qml') diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index f040b6a..cb012bf 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -20,27 +20,27 @@ ColumnLayout { StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small - text: qsTr("Wifi %1").arg(Network.wifiEnabled ? "enabled" : "disabled") + text: qsTr("Wifi %1").arg(Nmcli.wifiEnabled ? "enabled" : "disabled") font.weight: 500 } Toggle { label: qsTr("Enabled") - checked: Network.wifiEnabled - toggle.onToggled: Network.enableWifi(checked) + checked: Nmcli.wifiEnabled + toggle.onToggled: Nmcli.enableWifi(checked) } StyledText { Layout.topMargin: Appearance.spacing.small Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 networks available").arg(Network.networks.length) + text: qsTr("%1 networks available").arg(Nmcli.networks.length) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } Repeater { model: ScriptModel { - values: [...Network.networks].sort((a, b) => { + values: [...Nmcli.networks].sort((a, b) => { if (a.active !== b.active) return b.active - a.active; return b.strength - a.strength; @@ -50,7 +50,7 @@ ColumnLayout { RowLayout { id: networkItem - required property Network.AccessPoint modelData + required property Nmcli.AccessPoint modelData readonly property bool isConnecting: root.connectingToSsid === modelData.ssid readonly property bool loading: networkItem.isConnecting @@ -111,14 +111,14 @@ ColumnLayout { StateLayer { color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: networkItem.loading || !Network.wifiEnabled + disabled: networkItem.loading || !Nmcli.wifiEnabled function onClicked(): void { if (networkItem.modelData.active) { - Network.disconnectFromNetwork(); + Nmcli.disconnectFromNetwork(); } else { root.connectingToSsid = networkItem.modelData.ssid; - Network.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); + Nmcli.connectToNetwork(networkItem.modelData.ssid, "", networkItem.modelData.bssid, null); } } } @@ -151,10 +151,10 @@ ColumnLayout { StateLayer { color: Colours.palette.m3onPrimaryContainer - disabled: Network.scanning || !Network.wifiEnabled + disabled: Nmcli.scanning || !Nmcli.wifiEnabled function onClicked(): void { - Network.rescanWifi(); + Nmcli.rescanWifi(); } } @@ -163,7 +163,7 @@ ColumnLayout { anchors.centerIn: parent spacing: Appearance.spacing.small - opacity: Network.scanning ? 0 : 1 + opacity: Nmcli.scanning ? 0 : 1 MaterialIcon { id: scanIcon @@ -188,22 +188,21 @@ ColumnLayout { strokeWidth: Appearance.padding.small / 2 bgColour: "transparent" implicitHeight: parent.implicitHeight - Appearance.padding.smaller * 2 - running: Network.scanning + running: Nmcli.scanning } } - // Reset connecting state when network changes Connections { - target: Network + target: Nmcli function onActiveChanged(): void { - if (Network.active && root.connectingToSsid === Network.active.ssid) { + if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; } } function onScanningChanged(): void { - if (!Network.scanning) + if (!Nmcli.scanning) scanIcon.rotation = 0; } } diff --git a/plan.md b/plan.md deleted file mode 100644 index 4762ef0..0000000 --- a/plan.md +++ /dev/null @@ -1,137 +0,0 @@ -# Nmcli.qml Feature Completion Plan - -This document outlines the missing features needed in `Nmcli.qml` to replace `Network.qml` or rewrite the wireless panel in the control center. - -## Current Status - -`Nmcli.qml` currently has: -- ✅ Device status queries -- ✅ Wireless/Ethernet interface listing -- ✅ Interface connection status checking -- ✅ Basic wireless connection (SSID + password) -- ✅ Disconnect functionality -- ✅ Device details (basic) -- ✅ Interface up/down -- ✅ WiFi scanning -- ✅ SSID listing with signal/security (sorted) - -## Missing Features - -### 1. WiFi Radio Control -- [x] `enableWifi(enabled: bool)` - Turn WiFi radio on/off -- [x] `toggleWifi()` - Toggle WiFi radio state -- [x] `wifiEnabled` property - Current WiFi radio state -- [x] Monitor WiFi radio state changes - -**Implementation Notes:** -- Use `nmcli radio wifi on/off` -- Monitor state with `nmcli radio wifi` -- Update `wifiEnabled` property on state changes - -### 2. Network List Management -- [x] `networks` property - List of AccessPoint objects -- [x] `active` property - Currently active network -- [x] Real-time network list updates -- [x] Network grouping by SSID with signal prioritization -- [x] AccessPoint component/object with properties: - - `ssid`, `bssid`, `strength`, `frequency`, `active`, `security`, `isSecure` - -**Implementation Notes:** -- Use `nmcli -g ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY d w` -- Parse and group networks by SSID -- Prioritize active/connected networks -- Update network list on connection changes - -### 3. Connection Management - BSSID Support -- [x] BSSID support in `connectWireless()` function -- [x] Connection profile creation with BSSID (`createConnectionWithPassword`) -- [x] Handle BSSID in connection commands - -**Implementation Notes:** -- Use `nmcli connection add` with `802-11-wireless.bssid` for BSSID-based connections -- Fallback to SSID-only connection if BSSID not available -- Handle existing connection profiles when BSSID is provided - -### 4. Saved Connection Profile Management -- [x] `savedConnections` property - List of saved connection names -- [x] `savedConnectionSsids` property - List of saved SSIDs -- [x] `hasSavedProfile(ssid: string)` function - Check if profile exists -- [x] `forgetNetwork(ssid: string)` function - Delete connection profile -- [x] Load saved connections on startup -- [x] Update saved connections list after connection changes - -**Implementation Notes:** -- Use `nmcli -t -f NAME,TYPE connection show` to list connections -- Query SSIDs for WiFi connections: `nmcli -t -f 802-11-wireless.ssid connection show ` -- Use `nmcli connection delete ` to forget networks -- Case-insensitive SSID matching - -### 5. Pending Connection Tracking -- [x] `pendingConnection` property - Track connection in progress -- [x] Connection state tracking with timers -- [x] Connection success/failure detection -- [x] Automatic retry or callback on failure - -**Implementation Notes:** -- Track pending connection with SSID/BSSID -- Use timers to check connection status -- Monitor network list updates to detect successful connection -- Handle connection failures and trigger callbacks - -### 6. Connection Failure Handling -- [x] `connectionFailed(ssid: string)` signal -- [x] Password requirement detection from error messages -- [x] Connection retry logic -- [x] Error message parsing and reporting - -**Implementation Notes:** -- Parse stderr output for password requirements -- Detect specific error patterns (e.g., "Secrets were required") -- Emit signals for UI to handle password dialogs -- Provide meaningful error messages - -### 7. Password Callback Handling -- [x] `connectToNetworkWithPasswordCheck()` function -- [x] Try connection without password first (use saved password) -- [x] Callback on password requirement -- [x] Handle both secure and open networks - -**Implementation Notes:** -- Attempt connection without password for secure networks -- If connection fails with password error, trigger callback -- For open networks, connect directly -- Support callback pattern for password dialogs - -### 8. Device Details Parsing -- [x] Full parsing of `device show` output -- [x] `wirelessDeviceDetails` property with: - - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress` -- [x] `ethernetDeviceDetails` property with: - - `ipAddress`, `gateway`, `dns[]`, `subnet`, `macAddress`, `speed` -- [x] `cidrToSubnetMask()` helper function -- [x] Update device details on connection changes - -**Implementation Notes:** -- Parse `nmcli device show ` output -- Extract IP4.ADDRESS, IP4.GATEWAY, IP4.DNS, etc. -- Convert CIDR notation to subnet mask -- Handle both wireless and ethernet device details - -### 9. Connection Status Monitoring -- [x] Automatic network list refresh on connection changes -- [x] Monitor connection state changes -- [x] Update active network property -- [x] Refresh device details on connection - -**Implementation Notes:** -- Use Process stdout SplitParser to monitor changes -- Trigger network list refresh on connection events -- Update `active` property when connection changes -- Refresh device details when connected - -### 10. Ethernet Device Management -- [x] `ethernetDevices` property - List of ethernet devices -- [x] `activeEthernet` property - Currently active ethernet device -- [x] `connectEthernet(connectionName, interfaceName)` function -- [x] `disconnectEthernet(connectionName)` function -- [x] Ethernet device details parsing diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 4e45b41..5fb0c6c 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -14,6 +14,7 @@ Singleton { property string activeInterface: "" property string activeConnection: "" property bool wifiEnabled: true + readonly property bool scanning: rescanProc.running readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property list savedConnections: [] @@ -235,6 +236,10 @@ Singleton { } } + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + connectWireless(ssid, password, bssid, callback); + } + function connectWireless(ssid: string, password: string, bssid: string, callback: var, retryCount: int): void { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; const retries = retryCount !== undefined ? retryCount : 0; @@ -473,6 +478,22 @@ Singleton { } } + function disconnectFromNetwork(): void { + if (active && active.ssid) { + executeCommand(["connection", "down", active.ssid], (result) => { + if (result.success) { + getNetworks(() => {}); + } + }); + } else { + executeCommand(["device", "disconnect", "wifi"], (result) => { + if (result.success) { + getNetworks(() => {}); + } + }); + } + } + function getDeviceDetails(interfaceName: string, callback: var): void { executeCommand(["device", "show", interfaceName], (result) => { if (callback) callback(result.output); @@ -543,6 +564,10 @@ Singleton { }); } + function rescanWifi(): void { + rescanProc.running = true; + } + function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; executeCommand(["radio", "wifi", cmd], (result) => { @@ -1152,6 +1177,14 @@ Singleton { return details; } + Process { + id: rescanProc + command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] + onExited: { + getNetworks(() => {}); + } + } + Process { id: monitorProc running: true -- cgit v1.2.3-freya From 3f58823762ba6a894bb96fae4e9f06480714e460 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Fri, 14 Nov 2025 11:12:07 -0500 Subject: nmcli: refactor to be readable/extensible --- services/Nmcli.qml | 511 ++++++++++++++++++++++++++++------------------------- 1 file changed, 267 insertions(+), 244 deletions(-) (limited to 'services/Nmcli.qml') diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 5fb0c6c..a9f9e8e 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -32,6 +32,29 @@ Singleton { property list activeProcesses: [] property var debugLogger: null + // Constants + readonly property string deviceTypeWifi: "wifi" + readonly property string deviceTypeEthernet: "ethernet" + readonly property string connectionTypeWireless: "802-11-wireless" + readonly property string nmcliCommandDevice: "device" + readonly property string nmcliCommandConnection: "connection" + readonly property string nmcliCommandWifi: "wifi" + readonly property string nmcliCommandRadio: "radio" + readonly property string deviceStatusFields: "DEVICE,TYPE,STATE,CONNECTION" + readonly property string connectionListFields: "NAME,TYPE" + readonly property string wirelessSsidField: "802-11-wireless.ssid" + readonly property string networkListFields: "SSID,SIGNAL,SECURITY" + readonly property string networkDetailFields: "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY" + readonly property string securityKeyMgmt: "802-11-wireless-security.key-mgmt" + readonly property string securityPsk: "802-11-wireless-security.psk" + readonly property string keyMgmtWpaPsk: "wpa-psk" + readonly property string connectionParamType: "type" + readonly property string connectionParamConName: "con-name" + readonly property string connectionParamIfname: "ifname" + readonly property string connectionParamSsid: "ssid" + readonly property string connectionParamPassword: "password" + readonly property string connectionParamBssid: "802-11-wireless.bssid" + function setDebugLogger(logger: var): void { root.debugLogger = logger; } @@ -48,6 +71,128 @@ Singleton { log(message); } + function detectPasswordRequired(error: string): bool { + if (!error || error.length === 0) { + return false; + } + + return (error.includes("Secrets were required") || + error.includes("Secrets were required, but not provided") || + error.includes("No secrets provided") || + error.includes("802-11-wireless-security.psk") || + error.includes("password for") || + (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || + (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && + !error.includes("Connection activated") && + !error.includes("successfully"); + } + + function parseNetworkOutput(output: string): list { + if (!output || output.length === 0) { + return []; + } + + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; + const rep = new RegExp("\\\\:", "g"); + const rep2 = new RegExp(PLACEHOLDER, "g"); + + const allNetworks = output.trim().split("\n") + .filter(line => line && line.length > 0) + .map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }) + .filter(n => n.ssid && n.ssid.length > 0); + + return allNetworks; + } + + function deduplicateNetworks(networks: list): list { + if (!networks || networks.length === 0) { + return []; + } + + const networkMap = new Map(); + for (const network of networks) { + const existing = networkMap.get(network.ssid); + if (!existing) { + networkMap.set(network.ssid, network); + } else { + if (network.active && !existing.active) { + networkMap.set(network.ssid, network); + } else if (!network.active && !existing.active) { + if (network.strength > existing.strength) { + networkMap.set(network.ssid, network); + } + } + } + } + + return Array.from(networkMap.values()); + } + + function isConnectionCommand(command: list): bool { + if (!command || command.length === 0) { + return false; + } + + return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection); + } + + function parseDeviceStatusOutput(output: string, filterType: string): list { + if (!output || output.length === 0) { + return []; + } + + const interfaces = []; + const lines = output.trim().split("\n"); + + for (const line of lines) { + const parts = line.split(":"); + if (parts.length >= 2) { + const deviceType = parts[1]; + let shouldInclude = false; + + if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) { + shouldInclude = true; + } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) { + shouldInclude = true; + } else if (filterType === "both" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) { + shouldInclude = true; + } + + if (shouldInclude) { + interfaces.push({ + device: parts[0] || "", + type: parts[1] || "", + state: parts[2] || "", + connection: parts[3] || "" + }); + } + } + } + + return interfaces; + } + + function isConnectedState(state: string): bool { + if (!state || state.length === 0) { + return false; + } + + return state === "100 (connected)" || + state === "connected" || + state.startsWith("connected"); + } + function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); proc.command = ["nmcli", ...args]; @@ -68,68 +213,42 @@ Singleton { } function getDeviceStatus(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { if (callback) callback(result.output); }); } function getWirelessInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && parts[1] === "wifi") { - interfaces.push({ - device: parts[0] || "", - type: parts[1] || "", - state: parts[2] || "", - connection: parts[3] || "" - }); - } - } + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi); root.wirelessInterfaces = interfaces; if (callback) callback(interfaces); }); } function getEthernetInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet); const devices = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && parts[1] === "ethernet") { - const device = parts[0] || ""; - const type = parts[1] || ""; - const state = parts[2] || ""; - const connection = parts[3] || ""; - - const connected = state === "100 (connected)" || state === "connected" || state.startsWith("connected"); - - interfaces.push({ - device: device, - type: type, - state: state, - connection: connection - }); - - devices.push({ - interface: device, - type: type, - state: state, - connection: connection, - connected: connected, - ipAddress: "", - gateway: "", - dns: [], - subnet: "", - macAddress: "", - speed: "" - }); - } + + for (const iface of interfaces) { + const connected = isConnectedState(iface.state); + + devices.push({ + interface: iface.device, + type: iface.type, + state: iface.state, + connection: iface.connection, + connected: connected, + ipAddress: "", + gateway: "", + dns: [], + subnet: "", + macAddress: "", + speed: "" + }); } + root.ethernetInterfaces = interfaces; root.ethernetDevices = devices; if (callback) callback(interfaces); @@ -138,7 +257,7 @@ Singleton { function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { if (connectionName && connectionName.length > 0) { - executeCommand(["connection", "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -152,7 +271,7 @@ Singleton { if (callback) callback(result); }); } else if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -174,7 +293,7 @@ Singleton { return; } - executeCommand(["connection", "down", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "down", connectionName], (result) => { if (result.success) { root.ethernetDeviceDetails = null; Qt.callLater(() => { @@ -186,31 +305,19 @@ Singleton { } function getAllInterfaces(callback: var): void { - executeCommand(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device", "status"], (result) => { - const interfaces = []; - const lines = result.output.trim().split("\n"); - for (const line of lines) { - const parts = line.split(":"); - if (parts.length >= 2 && (parts[1] === "wifi" || parts[1] === "ethernet")) { - interfaces.push({ - device: parts[0] || "", - type: parts[1] || "", - state: parts[2] || "", - connection: parts[3] || "" - }); - } - } + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + const interfaces = parseDeviceStatusOutput(result.output, "both"); if (callback) callback(interfaces); }); } function isInterfaceConnected(interfaceName: string, callback: var): void { - executeCommand(["device", "status"], (result) => { + executeCommand([root.nmcliCommandDevice, "status"], (result) => { const lines = result.output.trim().split("\n"); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length >= 3 && parts[0] === interfaceName) { - const connected = parts[2] === "connected" || parts[2].startsWith("connected"); + const connected = isConnectedState(parts[2]); if (callback) callback(connected); return; } @@ -258,9 +365,9 @@ Singleton { return; } - let cmd = ["device", "wifi", "connect", ssid]; + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; if (password && password.length > 0) { - cmd.push("password", password); + cmd.push(root.connectionParamPassword, password); } executeCommand(cmd, (result) => { if (result.needsPassword && callback) { @@ -283,14 +390,14 @@ Singleton { function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { checkAndDeleteConnection(ssid, () => { - const cmd = ["connection", "add", - "type", "wifi", - "con-name", ssid, - "ifname", "*", - "ssid", ssid, - "802-11-wireless.bssid", bssidUpper, - "802-11-wireless-security.key-mgmt", "wpa-psk", - "802-11-wireless-security.psk", password]; + const cmd = [root.nmcliCommandConnection, "add", + root.connectionParamType, root.deviceTypeWifi, + root.connectionParamConName, ssid, + root.connectionParamIfname, "*", + root.connectionParamSsid, ssid, + root.connectionParamBssid, bssidUpper, + root.securityKeyMgmt, root.keyMgmtWpaPsk, + root.securityPsk, password]; executeCommand(cmd, (result) => { if (result.success) { @@ -307,7 +414,7 @@ Singleton { activateConnection(ssid, callback); } else { log("Connection profile creation failed, trying fallback..."); - let fallbackCmd = ["device", "wifi", "connect", ssid, "password", password]; + let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; executeCommand(fallbackCmd, (fallbackResult) => { if (callback) callback(fallbackResult); }); @@ -318,9 +425,9 @@ Singleton { } function checkAndDeleteConnection(ssid: string, callback: var): void { - executeCommand(["connection", "show", ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "show", ssid], (result) => { if (result.success) { - executeCommand(["connection", "delete", ssid], (deleteResult) => { + executeCommand([root.nmcliCommandConnection, "delete", ssid], (deleteResult) => { Qt.callLater(() => { if (callback) callback(); }, 300); @@ -332,13 +439,13 @@ Singleton { } function activateConnection(connectionName: string, callback: var): void { - executeCommand(["connection", "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { if (callback) callback(result); }); } function loadSavedConnections(callback: var): void { - executeCommand(["-t", "-f", "NAME,TYPE", "connection", "show"], (result) => { + executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], (result) => { if (!result.success) { root.savedConnections = []; root.savedConnectionSsids = []; @@ -362,7 +469,7 @@ Singleton { const type = parts[1]; connections.push(name); - if (type === "802-11-wireless") { + if (type === root.connectionTypeWireless) { wifiConnections.push(name); } } @@ -387,7 +494,7 @@ Singleton { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; root.currentSsidQueryIndex++; - executeCommand(["-t", "-f", "802-11-wireless.ssid", "connection", "show", connectionName], (result) => { + executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], (result) => { if (result.success) { processSsidOutput(result.output); } @@ -456,7 +563,7 @@ Singleton { conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() ) || ssid; - executeCommand(["connection", "delete", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "delete", connectionName], (result) => { if (result.success) { Qt.callLater(() => { loadSavedConnections(() => {}); @@ -468,11 +575,11 @@ Singleton { function disconnect(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { if (callback) callback(result.success ? result.output : ""); }); } else { - executeCommand(["device", "disconnect", "wifi"], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { if (callback) callback(result.success ? result.output : ""); }); } @@ -480,13 +587,13 @@ Singleton { function disconnectFromNetwork(): void { if (active && active.ssid) { - executeCommand(["connection", "down", active.ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "down", active.ssid], (result) => { if (result.success) { getNetworks(() => {}); } }); } else { - executeCommand(["device", "disconnect", "wifi"], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { if (result.success) { getNetworks(() => {}); } @@ -495,7 +602,7 @@ Singleton { } function getDeviceDetails(interfaceName: string, callback: var): void { - executeCommand(["device", "show", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "show", interfaceName], (result) => { if (callback) callback(result.output); }); } @@ -511,7 +618,7 @@ Singleton { const parts = line.split(":"); if (parts.length >= 4) { const state = parts[2] || ""; - if (state === "connected" || state.startsWith("connected")) { + if (isConnectedState(state)) { connected = true; activeIf = parts[0] || ""; activeConn = parts[3] || ""; @@ -530,7 +637,7 @@ Singleton { function bringInterfaceUp(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { if (callback) { callback(result); } @@ -542,7 +649,7 @@ Singleton { function bringInterfaceDown(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand(["device", "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { if (callback) { callback(result); } @@ -553,9 +660,9 @@ Singleton { } function scanWirelessNetworks(interfaceName: string, callback: var): void { - let cmd = ["device", "wifi", "rescan"]; + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "rescan"]; if (interfaceName && interfaceName.length > 0) { - cmd.push("ifname", interfaceName); + cmd.push(root.connectionParamIfname, interfaceName); } executeCommand(cmd, (result) => { if (callback) { @@ -570,7 +677,7 @@ Singleton { function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; - executeCommand(["radio", "wifi", cmd], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], (result) => { if (result.success) { getWifiStatus((status) => { root.wifiEnabled = status; @@ -588,7 +695,7 @@ Singleton { } function getWifiStatus(callback: var): void { - executeCommand(["radio", "wifi"], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], (result) => { if (result.success) { const enabled = result.output.trim() === "enabled"; root.wifiEnabled = enabled; @@ -600,48 +707,14 @@ Singleton { } function getNetworks(callback: var): void { - executeCommand(["-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"], (result) => { + executeCommand(["-g", root.networkDetailFields, "d", "w"], (result) => { if (!result.success) { if (callback) callback([]); return; } - const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; - const rep = new RegExp("\\\\:", "g"); - const rep2 = new RegExp(PLACEHOLDER, "g"); - - const allNetworks = result.output.trim().split("\n") - .filter(line => line && line.length > 0) - .map(n => { - const net = n.replace(rep, PLACEHOLDER).split(":"); - return { - active: net[0] === "yes", - strength: parseInt(net[1] || "0", 10) || 0, - frequency: parseInt(net[2] || "0", 10) || 0, - ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), - bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), - security: (net[5] ?? "").trim() - }; - }) - .filter(n => n.ssid && n.ssid.length > 0); - - const networkMap = new Map(); - for (const network of allNetworks) { - const existing = networkMap.get(network.ssid); - if (!existing) { - networkMap.set(network.ssid, network); - } else { - if (network.active && !existing.active) { - networkMap.set(network.ssid, network); - } else if (!network.active && !existing.active) { - if (network.strength > existing.strength) { - networkMap.set(network.ssid, network); - } - } - } - } - - const networks = Array.from(networkMap.values()); + const allNetworks = parseNetworkOutput(result.output); + const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; const destroyed = rNetworks.filter(rn => !networks.find(n => @@ -678,9 +751,9 @@ Singleton { } function getWirelessSSIDs(interfaceName: string, callback: var): void { - let cmd = ["-t", "-f", "SSID,SIGNAL,SECURITY", "device", "wifi", "list"]; + let cmd = ["-t", "-f", root.networkListFields, root.nmcliCommandDevice, root.nmcliCommandWifi, "list"]; if (interfaceName && interfaceName.length > 0) { - cmd.push("ifname", interfaceName); + cmd.push(root.connectionParamIfname, interfaceName); } executeCommand(cmd, (result) => { if (!result.success) { @@ -721,6 +794,43 @@ Singleton { }); } + function handlePasswordRequired(proc: var, error: string, output: string, exitCode: int): bool { + if (!proc || !error || error.length === 0) { + return false; + } + + if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { + return false; + } + + const needsPassword = detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: output || "", + error: error, + exitCode: exitCode, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return true; + } + + return false; + } + component CommandProcess: Process { id: proc property var callback: null @@ -745,43 +855,8 @@ Singleton { onStreamFinished: { const error = text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand && root.pendingConnection && root.pendingConnection.callback) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - } - } + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + handlePasswordRequired(proc, error, output, -1); } } } @@ -798,44 +873,16 @@ Singleton { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; const success = proc.exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.command); - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - const needsPassword = isConnectionCommand && error && error.length > 0 && - (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); - - if (needsPassword && root.pendingConnection && root.pendingConnection.callback) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: output, - error: error, - exitCode: proc.exitCode, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } + if (handlePasswordRequired(proc, error, output, proc.exitCode)) { proc.processFinished(); return; - } else if (!success && isConnectionCommand && root.pendingConnection) { + } + + const needsPassword = cmdIsConnection && detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { const failedSsid = root.pendingConnection.ssid; root.connectionFailed(failedSsid); } @@ -891,20 +938,8 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + if (isConnectionCommand(proc.command)) { + const needsPassword = detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection) { const pending = root.pendingConnection; @@ -983,20 +1018,8 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - const isConnectionCommand = proc.command && proc.command.length > 0 && - (proc.command.includes("wifi") || proc.command.includes("connection")); - - if (isConnectionCommand) { - const needsPassword = (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + if (isConnectionCommand(proc.command)) { + const needsPassword = detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { connectionCheckTimer.stop(); @@ -1076,7 +1099,7 @@ Singleton { function getWirelessDeviceDetails(interfaceName: string, callback: var): void { if (!interfaceName || interfaceName.length === 0) { const activeInterface = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; @@ -1102,7 +1125,7 @@ Singleton { function getEthernetDeviceDetails(interfaceName: string, callback: var): void { if (!interfaceName || interfaceName.length === 0) { const activeInterface = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; @@ -1179,7 +1202,7 @@ Singleton { Process { id: rescanProc - command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"] + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] onExited: { getNetworks(() => {}); } @@ -1217,7 +1240,7 @@ Singleton { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { getWirelessDeviceDetails(activeWireless.device, () => {}); @@ -1226,7 +1249,7 @@ Singleton { if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeEthernet && activeEthernet.device) { getEthernetDeviceDetails(activeEthernet.device, () => {}); @@ -1258,7 +1281,7 @@ Singleton { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { getWirelessDeviceDetails(activeWireless.device, () => {}); @@ -1267,7 +1290,7 @@ Singleton { if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { - return iface.state === "connected" || iface.state.startsWith("connected"); + return isConnectedState(iface.state); }); if (activeEthernet && activeEthernet.device) { getEthernetDeviceDetails(activeEthernet.device, () => {}); -- cgit v1.2.3-freya From 9825ad4d3102130ec40bb9324c3e37e1622c9c57 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:53:30 +1100 Subject: nmcli: fix errors + disable most logs --- services/Nmcli.qml | 624 ++++++++++++++++++++++++++++------------------------- 1 file changed, 333 insertions(+), 291 deletions(-) (limited to 'services/Nmcli.qml') diff --git a/services/Nmcli.qml b/services/Nmcli.qml index a9f9e8e..24a93da 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1,4 +1,5 @@ pragma Singleton +pragma ComponentBehavior: Bound import Quickshell import Quickshell.Io @@ -19,7 +20,7 @@ Singleton { readonly property AccessPoint active: networks.find(n => n.active) ?? null property list savedConnections: [] property list savedConnectionSsids: [] - + property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 property var pendingConnection: null @@ -28,9 +29,8 @@ Singleton { property var ethernetDeviceDetails: null property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null - + property list activeProcesses: [] - property var debugLogger: null // Constants readonly property string deviceTypeWifi: "wifi" @@ -55,63 +55,35 @@ Singleton { readonly property string connectionParamPassword: "password" readonly property string connectionParamBssid: "802-11-wireless.bssid" - function setDebugLogger(logger: var): void { - root.debugLogger = logger; - } - - function log(message: string): void { - if (root.debugLogger) { - root.debugLogger(message); - } else { - console.log("[Nmcli]", message); - } - } - - function appendLog(message: string): void { - log(message); - } - function detectPasswordRequired(error: string): bool { if (!error || error.length === 0) { return false; } - - return (error.includes("Secrets were required") || - error.includes("Secrets were required, but not provided") || - error.includes("No secrets provided") || - error.includes("802-11-wireless-security.psk") || - error.includes("password for") || - (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || - (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && - !error.includes("Connection activated") && - !error.includes("successfully"); + + return (error.includes("Secrets were required") || error.includes("Secrets were required, but not provided") || error.includes("No secrets provided") || error.includes("802-11-wireless-security.psk") || error.includes("password for") || (error.includes("password") && !error.includes("Connection activated") && !error.includes("successfully")) || (error.includes("Secrets") && !error.includes("Connection activated") && !error.includes("successfully")) || (error.includes("802.11") && !error.includes("Connection activated") && !error.includes("successfully"))) && !error.includes("Connection activated") && !error.includes("successfully"); } function parseNetworkOutput(output: string): list { if (!output || output.length === 0) { return []; } - + const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED"; const rep = new RegExp("\\\\:", "g"); const rep2 = new RegExp(PLACEHOLDER, "g"); - - const allNetworks = output.trim().split("\n") - .filter(line => line && line.length > 0) - .map(n => { - const net = n.replace(rep, PLACEHOLDER).split(":"); - return { - active: net[0] === "yes", - strength: parseInt(net[1] || "0", 10) || 0, - frequency: parseInt(net[2] || "0", 10) || 0, - ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), - bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), - security: (net[5] ?? "").trim() - }; - }) - .filter(n => n.ssid && n.ssid.length > 0); - + + const allNetworks = output.trim().split("\n").filter(line => line && line.length > 0).map(n => { + const net = n.replace(rep, PLACEHOLDER).split(":"); + return { + active: net[0] === "yes", + strength: parseInt(net[1] || "0", 10) || 0, + frequency: parseInt(net[2] || "0", 10) || 0, + ssid: (net[3]?.replace(rep2, ":") ?? "").trim(), + bssid: (net[4]?.replace(rep2, ":") ?? "").trim(), + security: (net[5] ?? "").trim() + }; + }).filter(n => n.ssid && n.ssid.length > 0); + return allNetworks; } @@ -119,7 +91,7 @@ Singleton { if (!networks || networks.length === 0) { return []; } - + const networkMap = new Map(); for (const network of networks) { const existing = networkMap.get(network.ssid); @@ -135,7 +107,7 @@ Singleton { } } } - + return Array.from(networkMap.values()); } @@ -143,7 +115,7 @@ Singleton { if (!command || command.length === 0) { return false; } - + return command.includes(root.nmcliCommandWifi) || command.includes(root.nmcliCommandConnection); } @@ -151,16 +123,16 @@ Singleton { if (!output || output.length === 0) { return []; } - + const interfaces = []; const lines = output.trim().split("\n"); - + for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { const deviceType = parts[1]; let shouldInclude = false; - + if (filterType === root.deviceTypeWifi && deviceType === root.deviceTypeWifi) { shouldInclude = true; } else if (filterType === root.deviceTypeEthernet && deviceType === root.deviceTypeEthernet) { @@ -168,7 +140,7 @@ Singleton { } else if (filterType === "both" && (deviceType === root.deviceTypeWifi || deviceType === root.deviceTypeEthernet)) { shouldInclude = true; } - + if (shouldInclude) { interfaces.push({ device: parts[0] || "", @@ -179,7 +151,7 @@ Singleton { } } } - + return interfaces; } @@ -187,53 +159,53 @@ Singleton { if (!state || state.length === 0) { return false; } - - return state === "100 (connected)" || - state === "connected" || - state.startsWith("connected"); + + return state === "100 (connected)" || state === "connected" || state.startsWith("connected"); } function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); proc.command = ["nmcli", ...args]; proc.callback = callback; - + activeProcesses.push(proc); - + proc.processFinished.connect(() => { const index = activeProcesses.indexOf(proc); if (index >= 0) { activeProcesses.splice(index, 1); } }); - + Qt.callLater(() => { proc.exec(proc.command); }); } function getDeviceStatus(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { - if (callback) callback(result.output); + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { + if (callback) + callback(result.output); }); } function getWirelessInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeWifi); root.wirelessInterfaces = interfaces; - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function getEthernetInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, root.deviceTypeEthernet); const devices = []; - + for (const iface of interfaces) { const connected = isConnectedState(iface.state); - + devices.push({ interface: iface.device, type: iface.type, @@ -248,16 +220,17 @@ Singleton { speed: "" }); } - + root.ethernetInterfaces = interfaces; root.ethernetDevices = devices; - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function connectEthernet(connectionName: string, interfaceName: string, callback: var): void { if (connectionName && connectionName.length > 0) { - executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -268,10 +241,11 @@ Singleton { } }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } else if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { if (result.success) { Qt.callLater(() => { getEthernetInterfaces(() => {}); @@ -280,62 +254,94 @@ Singleton { }, 1000); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } else { - if (callback) callback({ success: false, output: "", error: "No connection name or interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No connection name or interface specified", + exitCode: -1 + }); } } function disconnectEthernet(connectionName: string, callback: var): void { if (!connectionName || connectionName.length === 0) { - if (callback) callback({ success: false, output: "", error: "No connection name specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No connection name specified", + exitCode: -1 + }); return; } - - executeCommand([root.nmcliCommandConnection, "down", connectionName], (result) => { + + executeCommand([root.nmcliCommandConnection, "down", connectionName], result => { if (result.success) { root.ethernetDeviceDetails = null; Qt.callLater(() => { getEthernetInterfaces(() => {}); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } function getAllInterfaces(callback: var): void { - executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], (result) => { + executeCommand(["-t", "-f", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { const interfaces = parseDeviceStatusOutput(result.output, "both"); - if (callback) callback(interfaces); + if (callback) + callback(interfaces); }); } function isInterfaceConnected(interfaceName: string, callback: var): void { - executeCommand([root.nmcliCommandDevice, "status"], (result) => { + executeCommand([root.nmcliCommandDevice, "status"], result => { const lines = result.output.trim().split("\n"); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length >= 3 && parts[0] === interfaceName) { const connected = isConnectedState(parts[2]); - if (callback) callback(connected); + if (callback) + callback(connected); return; } } - if (callback) callback(false); + if (callback) + callback(false); }); } function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { if (isSecure) { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; - connectWireless(ssid, "", bssid, (result) => { + connectWireless(ssid, "", bssid, result => { if (result.success) { - if (callback) callback({ success: true, usedSavedPassword: true, output: result.output, error: "", exitCode: 0 }); + if (callback) + callback({ + success: true, + usedSavedPassword: true, + output: result.output, + error: "", + exitCode: 0 + }); } else if (result.needsPassword) { - if (callback) callback({ success: false, needsPassword: true, output: result.output, error: result.error, exitCode: result.exitCode }); + if (callback) + callback({ + success: false, + needsPassword: true, + output: result.output, + error: result.error, + exitCode: result.exitCode + }); } else { - if (callback) callback(result); + if (callback) + callback(result); } }); } else { @@ -351,72 +357,68 @@ Singleton { const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; const retries = retryCount !== undefined ? retryCount : 0; const maxRetries = 2; - + if (callback) { - root.pendingConnection = { ssid: ssid, bssid: hasBssid ? bssid : "", callback: callback, retryCount: retries }; + root.pendingConnection = { + ssid: ssid, + bssid: hasBssid ? bssid : "", + callback: callback, + retryCount: retries + }; connectionCheckTimer.start(); immediateCheckTimer.checkCount = 0; immediateCheckTimer.start(); } - + if (password && password.length > 0 && hasBssid) { const bssidUpper = bssid.toUpperCase(); createConnectionWithPassword(ssid, bssidUpper, password, callback); return; } - + let cmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; if (password && password.length > 0) { cmd.push(root.connectionParamPassword, password); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (result.needsPassword && callback) { - if (callback) callback(result); + if (callback) + callback(result); return; } - + if (!result.success && root.pendingConnection && retries < maxRetries) { - log("Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); Qt.callLater(() => { connectWireless(ssid, password, bssid, callback, retries + 1); }, 1000); - } else if (!result.success && root.pendingConnection) { - } else if (result.success && callback) { - } else if (!result.success && !root.pendingConnection) { - if (callback) callback(result); + } else if (!result.success && root.pendingConnection) {} else if (result.success && callback) {} else if (!result.success && !root.pendingConnection) { + if (callback) + callback(result); } }); } function createConnectionWithPassword(ssid: string, bssidUpper: string, password: string, callback: var): void { checkAndDeleteConnection(ssid, () => { - const cmd = [root.nmcliCommandConnection, "add", - root.connectionParamType, root.deviceTypeWifi, - root.connectionParamConName, ssid, - root.connectionParamIfname, "*", - root.connectionParamSsid, ssid, - root.connectionParamBssid, bssidUpper, - root.securityKeyMgmt, root.keyMgmtWpaPsk, - root.securityPsk, password]; - - executeCommand(cmd, (result) => { + const cmd = [root.nmcliCommandConnection, "add", root.connectionParamType, root.deviceTypeWifi, root.connectionParamConName, ssid, root.connectionParamIfname, "*", root.connectionParamSsid, ssid, root.connectionParamBssid, bssidUpper, root.securityKeyMgmt, root.keyMgmtWpaPsk, root.securityPsk, password]; + + executeCommand(cmd, result => { if (result.success) { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { - const hasDuplicateWarning = result.error && ( - result.error.includes("another connection with the name") || - result.error.includes("Reference the connection by its uuid") - ); - + const hasDuplicateWarning = result.error && (result.error.includes("another connection with the name") || result.error.includes("Reference the connection by its uuid")); + if (hasDuplicateWarning || (result.exitCode > 0 && result.exitCode < 10)) { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { - log("Connection profile creation failed, trying fallback..."); + console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; - executeCommand(fallbackCmd, (fallbackResult) => { - if (callback) callback(fallbackResult); + executeCommand(fallbackCmd, fallbackResult => { + if (callback) + callback(fallbackResult); }); } } @@ -425,34 +427,38 @@ Singleton { } function checkAndDeleteConnection(ssid: string, callback: var): void { - executeCommand([root.nmcliCommandConnection, "show", ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "show", ssid], result => { if (result.success) { - executeCommand([root.nmcliCommandConnection, "delete", ssid], (deleteResult) => { + executeCommand([root.nmcliCommandConnection, "delete", ssid], deleteResult => { Qt.callLater(() => { - if (callback) callback(); + if (callback) + callback(); }, 300); }); } else { - if (callback) callback(); + if (callback) + callback(); } }); } function activateConnection(connectionName: string, callback: var): void { - executeCommand([root.nmcliCommandConnection, "up", connectionName], (result) => { - if (callback) callback(result); + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { + if (callback) + callback(result); }); } function loadSavedConnections(callback: var): void { - executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], (result) => { + executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "show"], result => { if (!result.success) { root.savedConnections = []; root.savedConnectionSsids = []; - if (callback) callback([]); + if (callback) + callback([]); return; } - + parseConnectionList(result.output, callback); }); } @@ -461,22 +467,22 @@ Singleton { const lines = output.trim().split("\n").filter(line => line.length > 0); const wifiConnections = []; const connections = []; - + for (const line of lines) { const parts = line.split(":"); if (parts.length >= 2) { const name = parts[0]; const type = parts[1]; connections.push(name); - + if (type === root.connectionTypeWireless) { wifiConnections.push(name); } } } - + root.savedConnections = connections; - + if (wifiConnections.length > 0) { root.wifiConnectionQueue = wifiConnections; root.currentSsidQueryIndex = 0; @@ -485,7 +491,8 @@ Singleton { } else { root.savedConnectionSsids = []; root.wifiConnectionQueue = []; - if (callback) callback(root.savedConnectionSsids); + if (callback) + callback(root.savedConnectionSsids); } } @@ -493,8 +500,8 @@ Singleton { if (root.currentSsidQueryIndex < root.wifiConnectionQueue.length) { const connectionName = root.wifiConnectionQueue[root.currentSsidQueryIndex]; root.currentSsidQueryIndex++; - - executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], (result) => { + + executeCommand(["-t", "-f", root.wirelessSsidField, root.nmcliCommandConnection, "show", connectionName], result => { if (result.success) { processSsidOutput(result.output); } @@ -503,7 +510,8 @@ Singleton { } else { root.wifiConnectionQueue = []; root.currentSsidQueryIndex = 0; - if (callback) callback(root.savedConnectionSsids); + if (callback) + callback(root.savedConnectionSsids); } } @@ -530,70 +538,73 @@ Singleton { return false; } const ssidLower = ssid.toLowerCase().trim(); - + if (root.active && root.active.ssid) { const activeSsidLower = root.active.ssid.toLowerCase().trim(); if (activeSsidLower === ssidLower) { return true; } } - - const hasSsid = root.savedConnectionSsids.some(savedSsid => - savedSsid && savedSsid.toLowerCase().trim() === ssidLower - ); - + + const hasSsid = root.savedConnectionSsids.some(savedSsid => savedSsid && savedSsid.toLowerCase().trim() === ssidLower); + if (hasSsid) { return true; } - - const hasConnectionName = root.savedConnections.some(connName => - connName && connName.toLowerCase().trim() === ssidLower - ); - + + const hasConnectionName = root.savedConnections.some(connName => connName && connName.toLowerCase().trim() === ssidLower); + return hasConnectionName; } function forgetNetwork(ssid: string, callback: var): void { if (!ssid || ssid.length === 0) { - if (callback) callback({ success: false, output: "", error: "No SSID specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No SSID specified", + exitCode: -1 + }); return; } - - const connectionName = root.savedConnections.find(conn => - conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim() - ) || ssid; - - executeCommand([root.nmcliCommandConnection, "delete", connectionName], (result) => { + + const connectionName = root.savedConnections.find(conn => conn && conn.toLowerCase().trim() === ssid.toLowerCase().trim()) || ssid; + + executeCommand([root.nmcliCommandConnection, "delete", connectionName], result => { if (result.success) { Qt.callLater(() => { loadSavedConnections(() => {}); }, 500); } - if (callback) callback(result); + if (callback) + callback(result); }); } function disconnect(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { - if (callback) callback(result.success ? result.output : ""); + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { + if (callback) + callback(result.success ? result.output : ""); }); } else { - executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { - if (callback) callback(result.success ? result.output : ""); + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { + if (callback) + callback(result.success ? result.output : ""); }); } } function disconnectFromNetwork(): void { if (active && active.ssid) { - executeCommand([root.nmcliCommandConnection, "down", active.ssid], (result) => { + executeCommand([root.nmcliCommandConnection, "down", active.ssid], result => { if (result.success) { getNetworks(() => {}); } }); } else { - executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { if (result.success) { getNetworks(() => {}); } @@ -602,13 +613,14 @@ Singleton { } function getDeviceDetails(interfaceName: string, callback: var): void { - executeCommand([root.nmcliCommandDevice, "show", interfaceName], (result) => { - if (callback) callback(result.output); + executeCommand([root.nmcliCommandDevice, "show", interfaceName], result => { + if (callback) + callback(result.output); }); } function refreshStatus(callback: var): void { - getDeviceStatus((output) => { + getDeviceStatus(output => { const lines = output.trim().split("\n"); let connected = false; let activeIf = ""; @@ -631,31 +643,48 @@ Singleton { root.activeInterface = activeIf; root.activeConnection = activeConn; - if (callback) callback({ connected, interface: activeIf, connection: activeConn }); + if (callback) + callback({ + connected, + interface: activeIf, + connection: activeConn + }); }); } function bringInterfaceUp(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "connect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "connect", interfaceName], result => { if (callback) { callback(result); } }); } else { - if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No interface specified", + exitCode: -1 + }); } } function bringInterfaceDown(interfaceName: string, callback: var): void { if (interfaceName && interfaceName.length > 0) { - executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], (result) => { + executeCommand([root.nmcliCommandDevice, "disconnect", interfaceName], result => { if (callback) { callback(result); } }); } else { - if (callback) callback({ success: false, output: "", error: "No interface specified", exitCode: -1 }); + if (callback) + callback({ + success: false, + output: "", + error: "No interface specified", + exitCode: -1 + }); } } @@ -664,7 +693,7 @@ Singleton { if (interfaceName && interfaceName.length > 0) { cmd.push(root.connectionParamIfname, interfaceName); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (callback) { callback(result); } @@ -677,14 +706,16 @@ Singleton { function enableWifi(enabled: bool, callback: var): void { const cmd = enabled ? "on" : "off"; - executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, cmd], result => { if (result.success) { - getWifiStatus((status) => { + getWifiStatus(status => { root.wifiEnabled = status; - if (callback) callback(result); + if (callback) + callback(result); }); } else { - if (callback) callback(result); + if (callback) + callback(result); } }); } @@ -695,33 +726,32 @@ Singleton { } function getWifiStatus(callback: var): void { - executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], (result) => { + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi], result => { if (result.success) { const enabled = result.output.trim() === "enabled"; root.wifiEnabled = enabled; - if (callback) callback(enabled); + if (callback) + callback(enabled); } else { - if (callback) callback(root.wifiEnabled); + if (callback) + callback(root.wifiEnabled); } }); } function getNetworks(callback: var): void { - executeCommand(["-g", root.networkDetailFields, "d", "w"], (result) => { + executeCommand(["-g", root.networkDetailFields, "d", "w"], result => { if (!result.success) { - if (callback) callback([]); + if (callback) + callback([]); return; } - + const allNetworks = parseNetworkOutput(result.output); const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; - - const destroyed = rNetworks.filter(rn => !networks.find(n => - n.frequency === rn.frequency && - n.ssid === rn.ssid && - n.bssid === rn.bssid - )); + + const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); for (const network of destroyed) { const index = rNetworks.indexOf(network); if (index >= 0) { @@ -729,13 +759,9 @@ Singleton { network.destroy(); } } - + for (const network of networks) { - const match = rNetworks.find(n => - n.frequency === network.frequency && - n.ssid === network.ssid && - n.bssid === network.bssid - ); + const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); if (match) { match.lastIpcObject = network; } else { @@ -744,8 +770,9 @@ Singleton { })); } } - - if (callback) callback(root.networks); + + if (callback) + callback(root.networks); checkPendingConnection(); }); } @@ -755,19 +782,21 @@ Singleton { if (interfaceName && interfaceName.length > 0) { cmd.push(root.connectionParamIfname, interfaceName); } - executeCommand(cmd, (result) => { + executeCommand(cmd, result => { if (!result.success) { - if (callback) callback([]); + if (callback) + callback([]); return; } - + const ssids = []; const lines = result.output.trim().split("\n"); const seenSSIDs = new Set(); - + for (const line of lines) { - if (!line || line.length === 0) continue; - + if (!line || line.length === 0) + continue; + const parts = line.split(":"); if (parts.length >= 1) { const ssid = parts[0].trim(); @@ -785,12 +814,13 @@ Singleton { } } } - + ssids.sort((a, b) => { return b.signalValue - a.signalValue; }); - - if (callback) callback(ssids); + + if (callback) + callback(ssids); }); } @@ -798,13 +828,13 @@ Singleton { if (!proc || !error || error.length === 0) { return false; } - + if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { return false; } - + const needsPassword = detectPasswordRequired(error); - + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); @@ -827,77 +857,79 @@ Singleton { } return true; } - + return false; } component CommandProcess: Process { id: proc + property var callback: null property list command: [] property bool callbackCalled: false property int exitCode: 0 + signal processFinished environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) stdout: StdioCollector { id: stdoutCollector - onStreamFinished: { - } } stderr: StdioCollector { id: stderrCollector + onStreamFinished: { const error = text.trim(); if (error && error.length > 0) { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - handlePasswordRequired(proc, error, output, -1); + root.handlePasswordRequired(proc, error, output, -1); } } } - onExited: { - proc.exitCode = exitCode; + onExited: code => { + exitCode = code; + Qt.callLater(() => { - if (proc.callbackCalled) { - proc.processFinished(); + if (callbackCalled) { + processFinished(); return; } - + if (proc.callback) { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; - const success = proc.exitCode === 0; + const success = exitCode === 0; const cmdIsConnection = isConnectionCommand(proc.command); - - if (handlePasswordRequired(proc, error, output, proc.exitCode)) { - proc.processFinished(); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); return; } - - const needsPassword = cmdIsConnection && detectPasswordRequired(error); - + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + if (!success && cmdIsConnection && root.pendingConnection) { const failedSsid = root.pendingConnection.ssid; root.connectionFailed(failedSsid); } - - proc.callbackCalled = true; - proc.callback({ + + callbackCalled = true; + callback({ success: success, output: output, error: error, exitCode: proc.exitCode, needsPassword: needsPassword || false }); - proc.processFinished(); + processFinished(); } else { - proc.processFinished(); + processFinished(); } }); } @@ -905,6 +937,7 @@ Singleton { Component { id: commandProc + CommandProcess {} } @@ -921,16 +954,18 @@ Singleton { Component { id: apComp + AccessPoint {} } Timer { id: connectionCheckTimer + interval: 4000 onTriggered: { if (root.pendingConnection) { const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - + if (!connected && root.pendingConnection.callback) { let foundPasswordError = false; for (let i = 0; i < root.activeProcesses.length; i++) { @@ -938,9 +973,9 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - if (isConnectionCommand(proc.command)) { - const needsPassword = detectPasswordRequired(error); - + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { const pending = root.pendingConnection; root.pendingConnection = null; @@ -967,7 +1002,7 @@ Singleton { } } } - + if (!foundPasswordError) { const pending = root.pendingConnection; const failedSsid = pending.ssid; @@ -975,10 +1010,10 @@ Singleton { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; root.connectionFailed(failedSsid); - pending.callback({ - success: false, - output: "", - error: "Connection timeout", + pending.callback({ + success: false, + output: "", + error: "Connection timeout", exitCode: -1, needsPassword: false }); @@ -994,22 +1029,29 @@ Singleton { Timer { id: immediateCheckTimer + + property int checkCount: 0 + interval: 500 repeat: true triggeredOnStart: false - property int checkCount: 0 onTriggered: { if (root.pendingConnection) { checkCount++; const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - + if (connected) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; if (root.pendingConnection.callback) { - root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); } root.pendingConnection = null; } else { @@ -1018,9 +1060,9 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - if (isConnectionCommand(proc.command)) { - const needsPassword = detectPasswordRequired(error); - + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { connectionCheckTimer.stop(); immediateCheckTimer.stop(); @@ -1047,7 +1089,7 @@ Singleton { } } } - + if (checkCount >= 6) { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; @@ -1069,7 +1111,12 @@ Singleton { immediateCheckTimer.stop(); immediateCheckTimer.checkCount = 0; if (root.pendingConnection.callback) { - root.pendingConnection.callback({ success: true, output: "Connected", error: "", exitCode: 0 }); + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); } root.pendingConnection = null; } else { @@ -1086,13 +1133,13 @@ Singleton { if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { return ""; } - + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; const octet1 = (mask >>> 24) & 0xff; const octet2 = (mask >>> 16) & 0xff; const octet3 = (mask >>> 8) & 0xff; const octet4 = mask & 0xff; - + return `${octet1}.${octet2}.${octet3}.${octet4}`; } @@ -1104,21 +1151,24 @@ Singleton { if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; } else { - if (callback) callback(null); + if (callback) + callback(null); return; } } - - executeCommand(["device", "show", interfaceName], (result) => { + + executeCommand(["device", "show", interfaceName], result => { if (!result.success || !result.output) { root.wirelessDeviceDetails = null; - if (callback) callback(null); + if (callback) + callback(null); return; } - + const details = parseDeviceDetails(result.output, false); root.wirelessDeviceDetails = details; - if (callback) callback(details); + if (callback) + callback(details); }); } @@ -1130,21 +1180,24 @@ Singleton { if (activeInterface && activeInterface.device) { interfaceName = activeInterface.device; } else { - if (callback) callback(null); + if (callback) + callback(null); return; } } - - executeCommand(["device", "show", interfaceName], (result) => { + + executeCommand(["device", "show", interfaceName], result => { if (!result.success || !result.output) { root.ethernetDeviceDetails = null; - if (callback) callback(null); + if (callback) + callback(null); return; } - + const details = parseDeviceDetails(result.output, true); root.ethernetDeviceDetails = details; - if (callback) callback(details); + if (callback) + callback(details); }); } @@ -1157,20 +1210,20 @@ Singleton { macAddress: "", speed: "" }; - + if (!output || output.length === 0) { return details; } - + const lines = output.trim().split("\n"); - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; const parts = line.split(":"); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(":").trim(); - + if (key.startsWith("IP4.ADDRESS")) { const ipParts = value.split("/"); details.ipAddress = ipParts[0] || ""; @@ -1196,46 +1249,36 @@ Singleton { } } } - + return details; } Process { id: rescanProc + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] - onExited: { - getNetworks(() => {}); - } + onExited: root.getNetworks() } Process { id: monitorProc + running: true command: ["nmcli", "monitor"] environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) stdout: SplitParser { - onRead: { - log("Connection state change detected, refreshing..."); - root.refreshOnConnectionChange(); - } - } - - onExited: { - log("Monitor process exited, restarting..."); - Qt.callLater(() => { - monitorProc.running = true; - }, 2000); + onRead: root.refreshOnConnectionChange() } + onExited: Qt.callLater(() => monitorProc.running = true, 2000) } function refreshOnConnectionChange(): void { - getNetworks((networks) => { + getNetworks(networks => { const newActive = root.active; - + if (newActive && newActive.active) { Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { @@ -1246,7 +1289,7 @@ Singleton { getWirelessDeviceDetails(activeWireless.device, () => {}); } } - + if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); @@ -1260,7 +1303,7 @@ Singleton { root.wirelessDeviceDetails = null; root.ethernetDeviceDetails = null; } - + getWirelessInterfaces(() => {}); getEthernetInterfaces(() => { if (root.activeEthernet && root.activeEthernet.connected) { @@ -1277,7 +1320,7 @@ Singleton { getNetworks(() => {}); loadSavedConnections(() => {}); getEthernetInterfaces(() => {}); - + Qt.callLater(() => { if (root.wirelessInterfaces.length > 0) { const activeWireless = root.wirelessInterfaces.find(iface => { @@ -1287,7 +1330,7 @@ Singleton { getWirelessDeviceDetails(activeWireless.device, () => {}); } } - + if (root.ethernetInterfaces.length > 0) { const activeEthernet = root.ethernetInterfaces.find(iface => { return isConnectedState(iface.state); @@ -1299,4 +1342,3 @@ Singleton { }, 2000); } } - -- cgit v1.2.3-freya From 05b0660627586dc7624380e82b818b53004771f5 Mon Sep 17 00:00:00 2001 From: ATMDA Date: Sat, 15 Nov 2025 01:31:53 -0500 Subject: controlcenter: password input errors/wrong pass --- modules/bar/popouts/WirelessPasswordPopout.qml | 85 ++- modules/controlcenter/network/NetworkingPane.qml | 691 +++++++++++---------- .../network/WirelessPasswordDialog.qml | 115 +++- services/Nmcli.qml | 10 +- 4 files changed, 502 insertions(+), 399 deletions(-) (limited to 'services/Nmcli.qml') diff --git a/modules/bar/popouts/WirelessPasswordPopout.qml b/modules/bar/popouts/WirelessPasswordPopout.qml index aa7f40f..59a15b9 100644 --- a/modules/bar/popouts/WirelessPasswordPopout.qml +++ b/modules/bar/popouts/WirelessPasswordPopout.qml @@ -32,14 +32,22 @@ ColumnLayout { } } // Force focus to password container when popout becomes active - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 100); - }, 100); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + }); } } } + Timer { + id: focusTimer + interval: 150 + onTriggered: { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); + } + } + spacing: Appearance.spacing.normal implicitWidth: 400 @@ -51,19 +59,15 @@ ColumnLayout { Component.onCompleted: { if (shouldBeVisible) { - Qt.callLater(() => { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - }, 150); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); } } onShouldBeVisibleChanged: { if (shouldBeVisible) { - Qt.callLater(() => { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - }, 150); + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); } } @@ -243,20 +247,26 @@ ColumnLayout { target: root function onShouldBeVisibleChanged(): void { if (root.shouldBeVisible) { - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 50); + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); passwordContainer.passwordBuffer = ""; connectButton.hasError = false; } } } + Timer { + id: passwordFocusTimer + interval: 50 + onTriggered: { + passwordContainer.forceActiveFocus(); + } + } + Component.onCompleted: { if (root.shouldBeVisible) { - Qt.callLater(() => { - passwordContainer.forceActiveFocus(); - }, 100); + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); } } @@ -489,22 +499,8 @@ ColumnLayout { if (isConnected) { // Successfully connected - give it a moment for network list to update - Qt.callLater(() => { - // Double-check connection is still active - if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - // Return to network popout on successful connection - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - closeDialog(); - } - } - }, 500); + // Use Timer for actual delay + connectionSuccessTimer.start(); return; } @@ -545,6 +541,27 @@ ColumnLayout { } } + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + // Double-check connection is still active + if (root.shouldBeVisible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + // Return to network popout on successful connection + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + closeDialog(); + } + } + } + } + Connections { target: Nmcli function onActiveChanged() { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 56ab7f1..74e0034 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -15,500 +15,507 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts -RowLayout { +Item { id: root required property Session session anchors.fill: parent - spacing: 0 + RowLayout { + id: contentLayout - Item { - Layout.preferredWidth: Math.floor(parent.width * 0.4) - Layout.minimumWidth: 420 - Layout.fillHeight: true + anchors.fill: parent + spacing: 0 - // Left pane - networking list with collapsible sections - StyledFlickable { - id: leftFlickable + Item { + Layout.preferredWidth: Math.floor(parent.width * 0.4) + Layout.minimumWidth: 420 + Layout.fillHeight: true - anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height - clip: true - - StyledScrollBar.vertical: StyledScrollBar { - flickable: leftFlickable - } + // Left pane - networking list with collapsible sections + StyledFlickable { + id: leftFlickable - ColumnLayout { - id: leftContent + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height + clip: true - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.normal + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } - // Settings header above the collapsible sections - RowLayout { - Layout.fillWidth: true - spacing: Appearance.spacing.smaller + ColumnLayout { + id: leftContent - StyledText { - text: qsTr("Settings") - font.pointSize: Appearance.font.size.large - font.weight: 500 - } + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal - Item { + // Settings header above the collapsible sections + RowLayout { Layout.fillWidth: true - } + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Settings") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } - ToggleButton { - toggled: Nmcli.wifiEnabled - icon: "wifi" - accent: "Tertiary" + Item { + Layout.fillWidth: true + } - onClicked: { - Nmcli.toggleWifi(null); + ToggleButton { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" + + onClicked: { + Nmcli.toggleWifi(null); + } } - } - ToggleButton { - toggled: Nmcli.scanning - icon: "wifi_find" - accent: "Secondary" + ToggleButton { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" - onClicked: { - Nmcli.rescanWifi(); + onClicked: { + Nmcli.rescanWifi(); + } } - } - ToggleButton { - toggled: !root.session.ethernet.active && !root.session.network.active - icon: "settings" - accent: "Primary" - - onClicked: { - if (root.session.ethernet.active || root.session.network.active) { - root.session.ethernet.active = null; - root.session.network.active = null; - } else { - // Toggle to show settings - prefer ethernet if available, otherwise wireless - if (Nmcli.ethernetDevices.length > 0) { - root.session.ethernet.active = Nmcli.ethernetDevices[0]; - } else if (Nmcli.networks.length > 0) { - root.session.network.active = Nmcli.networks[0]; + ToggleButton { + toggled: !root.session.ethernet.active && !root.session.network.active + icon: "settings" + accent: "Primary" + + onClicked: { + if (root.session.ethernet.active || root.session.network.active) { + root.session.ethernet.active = null; + root.session.network.active = null; + } else { + // Toggle to show settings - prefer ethernet if available, otherwise wireless + if (Nmcli.ethernetDevices.length > 0) { + root.session.ethernet.active = Nmcli.ethernetDevices[0]; + } else if (Nmcli.networks.length > 0) { + root.session.network.active = Nmcli.networks[0]; + } } } } } - } - - CollapsibleSection { - id: ethernetListSection - Layout.fillWidth: true - title: qsTr("Ethernet") - expanded: true + CollapsibleSection { + id: ethernetListSection - ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + title: qsTr("Ethernet") + expanded: true - RowLayout { + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small - StyledText { - text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } } - } - StyledText { - Layout.fillWidth: true - text: qsTr("All available ethernet devices") - color: Colours.palette.m3outline - } + StyledText { + Layout.fillWidth: true + text: qsTr("All available ethernet devices") + color: Colours.palette.m3outline + } - Repeater { - id: ethernetRepeater + Repeater { + id: ethernetRepeater - Layout.fillWidth: true - model: Nmcli.ethernetDevices + Layout.fillWidth: true + model: Nmcli.ethernetDevices - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - Layout.fillWidth: true + Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.ethernet.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal - StateLayer { - function onClicked(): void { - root.session.network.active = null; - root.session.ethernet.active = modelData; + StateLayer { + function onClicked(): void { + root.session.network.active = null; + root.session.ethernet.active = modelData; + } } - } - RowLayout { - id: rowLayout + RowLayout { + id: rowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledRect { - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - MaterialIcon { - id: icon + MaterialIcon { + id: icon - anchors.centerIn: parent - text: "cable" - font.pointSize: Appearance.font.size.large - fill: modelData.connected ? 1 : 0 - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: "cable" + font.pointSize: Appearance.font.size.large + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } - } - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - text: modelData.interface || qsTr("Unknown") - } + text: modelData.interface || qsTr("Unknown") + } - StyledText { - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.connected ? 500 : 400 - } + StyledText { + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: modelData.connected ? 500 : 400 + } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) - StateLayer { - function onClicked(): void { - if (modelData.connected && modelData.connection) { - Nmcli.disconnectEthernet(modelData.connection, () => {}); - } else { - Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + StateLayer { + function onClicked(): void { + if (modelData.connected && modelData.connection) { + Nmcli.disconnectEthernet(modelData.connection, () => {}); + } else { + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + } } } - } - MaterialIcon { - id: connectIcon + MaterialIcon { + id: connectIcon - anchors.centerIn: parent - text: modelData.connected ? "link_off" : "link" - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: modelData.connected ? "link_off" : "link" + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } } - } - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } } } } - } - - CollapsibleSection { - id: wirelessListSection - Layout.fillWidth: true - title: qsTr("Wireless") - expanded: true + CollapsibleSection { + id: wirelessListSection - ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + title: qsTr("Wireless") + expanded: true - RowLayout { + ColumnLayout { Layout.fillWidth: true spacing: Appearance.spacing.small - StyledText { - text: qsTr("Networks (%1)").arg(Nmcli.networks.length) - font.pointSize: Appearance.font.size.normal - font.weight: 500 + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Networks (%1)").arg(Nmcli.networks.length) + font.pointSize: Appearance.font.size.normal + font.weight: 500 + } + + StyledText { + visible: Nmcli.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.small + } } StyledText { - visible: Nmcli.scanning - text: qsTr("Scanning...") - color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.small + Layout.fillWidth: true + text: qsTr("All available WiFi networks") + color: Colours.palette.m3outline } - } - - StyledText { - Layout.fillWidth: true - text: qsTr("All available WiFi networks") - color: Colours.palette.m3outline - } - Repeater { - id: wirelessRepeater + Repeater { + id: wirelessRepeater - Layout.fillWidth: true - model: ScriptModel { - values: [...Nmcli.networks].sort((a, b) => { - // Put active/connected network first - if (a.active !== b.active) - return b.active - a.active; - // Then sort by signal strength - return b.strength - a.strength; - }) - } + Layout.fillWidth: true + model: ScriptModel { + values: [...Nmcli.networks].sort((a, b) => { + // Put active/connected network first + if (a.active !== b.active) + return b.active - a.active; + // Then sort by signal strength + return b.strength - a.strength; + }) + } - delegate: StyledRect { - required property var modelData + delegate: StyledRect { + required property var modelData - Layout.fillWidth: true + Layout.fillWidth: true - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.session.network.active === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (modelData && root.session.network.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.normal StateLayer { function onClicked(): void { + if (!modelData) { + return; + } root.session.ethernet.active = null; root.session.network.active = modelData; // Check if we need to refresh saved connections when selecting a network - if (modelData && modelData.ssid) { + if (modelData.ssid) { checkSavedProfileForNetwork(modelData.ssid); } } } - RowLayout { - id: wirelessRowLayout + RowLayout { + id: wirelessRowLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + spacing: Appearance.spacing.normal - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessIcon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal - color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + color: (modelData && modelData.active) ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - MaterialIcon { - id: wirelessIcon + MaterialIcon { + id: wirelessIcon - anchors.centerIn: parent - text: Icons.getNetworkIcon(modelData.strength) - font.pointSize: Appearance.font.size.large - fill: modelData.active ? 1 : 0 - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: Icons.getNetworkIcon(modelData && modelData.strength !== undefined ? modelData.strength : 0) + font.pointSize: Appearance.font.size.large + fill: (modelData && modelData.active) ? 1 : 0 + color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } - } - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight - maximumLineCount: 1 + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 - text: modelData.ssid || qsTr("Unknown") - } + text: (modelData && modelData.ssid) ? modelData.ssid : qsTr("Unknown") + } - StyledText { - text: modelData.active ? qsTr("Connected") : (modelData.isSecure ? qsTr("Secured") : qsTr("Open")) - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small - font.weight: modelData.active ? 500 : 400 - } + StyledText { + text: (modelData && modelData.active) ? qsTr("Connected") : ((modelData && modelData.isSecure) ? qsTr("Secured") : qsTr("Open")) + color: (modelData && modelData.active) ? Colours.palette.m3primary : Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + font.weight: (modelData && modelData.active) ? 500 : 400 + } - StyledRect { - implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 + StyledRect { + implicitWidth: implicitHeight + implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.smaller * 2 - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, (modelData && modelData.active) ? 1 : 0) - StateLayer { - function onClicked(): void { - if (modelData.active) { - Nmcli.disconnectFromNetwork(); - } else { - handleWirelessConnect(modelData); + StateLayer { + function onClicked(): void { + if (modelData && modelData.active) { + Nmcli.disconnectFromNetwork(); + } else if (modelData) { + handleWirelessConnect(modelData); + } } } - } - MaterialIcon { - id: wirelessConnectIcon + MaterialIcon { + id: wirelessConnectIcon - anchors.centerIn: parent - text: modelData.active ? "link_off" : "link" - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + anchors.centerIn: parent + text: (modelData && modelData.active) ? "link_off" : "link" + color: (modelData && modelData.active) ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } } } - } - implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: wirelessRowLayout.implicitHeight + Appearance.padding.normal * 2 + } } } } } } - } - InnerBorder { - leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + InnerBorder { + leftThickness: 0 + rightThickness: Appearance.padding.normal / 2 + } } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ClippingRectangle { - anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" - clip: true - - // Right pane - networking details/settings - Loader { - id: loader - property var ethernetPane: root.session.ethernet.active - property var wirelessPane: root.session.network.active - property var pane: ethernetPane || wirelessPane - property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") + Item { + Layout.fillWidth: true + Layout.fillHeight: true + ClippingRectangle { anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - - opacity: 1 - scale: 1 - transformOrigin: Item.Center + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal / 2 + radius: rightBorder.innerRadius + color: "transparent" clip: true - asynchronous: true - sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings - - Behavior on paneId { - SequentialAnimation { - ParallelAnimation { - Anim { - target: loader - property: "opacity" - to: 0 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - Anim { - target: loader - property: "scale" - to: 0.8 - easing.bezierCurve: Appearance.anim.curves.standardAccel - } - } - PropertyAction {} - ParallelAnimation { - Anim { - target: loader - property: "opacity" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + + // Right pane - networking details/settings + Loader { + id: loader + + property var ethernetPane: root.session.ethernet.active + property var wirelessPane: root.session.network.active + property var pane: ethernetPane || wirelessPane + property string paneId: ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : "") + + anchors.fill: parent + anchors.margins: Appearance.padding.large * 2 + + opacity: 1 + scale: 1 + transformOrigin: Item.Center + + clip: true + asynchronous: true + sourceComponent: pane ? (ethernetPane ? ethernetDetails : wirelessDetails) : settings + + Behavior on paneId { + SequentialAnimation { + ParallelAnimation { + Anim { + target: loader + property: "opacity" + to: 0 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + target: loader + property: "scale" + to: 0.8 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } } - Anim { - target: loader - property: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + PropertyAction {} + ParallelAnimation { + Anim { + target: loader + property: "opacity" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: loader + property: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } } } } - } - onPaneChanged: { - paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + onPaneChanged: { + paneId = ethernetPane ? (ethernetPane.interface || "") : (wirelessPane ? (wirelessPane.ssid || wirelessPane.bssid || "") : ""); + } } } - } - InnerBorder { - id: rightBorder + InnerBorder { + id: rightBorder - leftThickness: Appearance.padding.normal / 2 - } + leftThickness: Appearance.padding.normal / 2 + } - Component { - id: settings + Component { + id: settings - StyledFlickable { - id: settingsFlickable + StyledFlickable { + id: settingsFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - clip: true + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + clip: true - StyledScrollBar.vertical: StyledScrollBar { - flickable: settingsFlickable - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: settingsFlickable + } - NetworkSettings { - id: settingsInner + NetworkSettings { + id: settingsInner - anchors.left: parent.left - anchors.right: parent.right - session: root.session + anchors.left: parent.left + anchors.right: parent.right + session: root.session + } } } - } - Component { - id: ethernetDetails + Component { + id: ethernetDetails - EthernetDetails { - session: root.session + EthernetDetails { + session: root.session + } } - } - Component { - id: wirelessDetails + Component { + id: wirelessDetails - WirelessDetails { - session: root.session + WirelessDetails { + session: root.session + } } } } WirelessPasswordDialog { - Layout.fillWidth: true - Layout.fillHeight: true + anchors.fill: parent session: root.session z: 1000 } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index f3381b7..4b350be 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -131,14 +131,17 @@ Item { Layout.alignment: Qt.AlignHCenter Layout.topMargin: Appearance.spacing.small - visible: connectButton.connecting + visible: connectButton.connecting || connectButton.hasError text: { + if (connectButton.hasError) { + return qsTr("Connection failed. Please check your password and try again."); + } if (connectButton.connecting) { return qsTr("Connecting..."); } return ""; } - color: Colours.palette.m3onSurfaceVariant + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small font.weight: 400 wrapMode: Text.WordWrap @@ -153,18 +156,31 @@ Item { focus: true Keys.onPressed: event => { + // Ensure we have focus when receiving keyboard input + if (!activeFocus) { + forceActiveFocus(); + } + + // Clear error when user starts typing + if (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; + } + if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) { if (connectButton.enabled) { connectButton.clicked(); } + event.accepted = true; } else if (event.key === Qt.Key_Backspace) { if (event.modifiers & Qt.ControlModifier) { passwordBuffer = ""; } else { passwordBuffer = passwordBuffer.slice(0, -1); } + event.accepted = true; } else if (event.text && event.text.length > 0) { passwordBuffer += event.text; + event.accepted = true; } } @@ -178,6 +194,7 @@ Item { Qt.callLater(() => { passwordContainer.forceActiveFocus(); passwordContainer.passwordBuffer = ""; + connectButton.hasError = false; }); } } @@ -198,13 +215,29 @@ Item { StyledRect { anchors.fill: parent radius: Appearance.rounding.normal - color: Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus ? 2 : 1 - border.color: passwordContainer.activeFocus ? Colours.palette.m3primary : Colours.palette.m3outline + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) + border.color: { + if (connectButton.hasError) { + return Colours.palette.m3error; + } + if (passwordContainer.activeFocus) { + return Colours.palette.m3primary; + } + return root.visible ? Colours.palette.m3outline : "transparent"; + } Behavior on border.color { CAnim {} } + + Behavior on border.width { + CAnim {} + } + + Behavior on color { + CAnim {} + } } StateLayer { @@ -329,6 +362,9 @@ Item { TextButton { id: connectButton + property bool connecting: false + property bool hasError: false + Layout.fillWidth: true Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 inactiveColour: Colours.palette.m3primary @@ -336,8 +372,6 @@ Item { text: qsTr("Connect") enabled: passwordContainer.passwordBuffer.length > 0 && !connecting - property bool connecting: false - onClicked: { if (!root.network || connecting) { return; @@ -348,6 +382,9 @@ Item { return; } + // Clear any previous error + hasError = false; + // Set connecting state connecting = true; enabled = false; @@ -361,11 +398,27 @@ Item { // Shouldn't happen since we provided password connectionMonitor.stop(); connecting = false; + hasError = true; enabled = true; text = qsTr("Connect"); - } else - // Connection failed, monitor will handle timeout - {} + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } else { + // Connection failed immediately - show error + connectionMonitor.stop(); + connecting = false; + hasError = true; + enabled = true; + text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } }); // Start monitoring connection @@ -386,18 +439,8 @@ Item { if (isConnected) { // Successfully connected - give it a moment for network list to update - Qt.callLater(() => { - // Double-check connection is still active - if (root.visible && Nmcli.active && Nmcli.active.ssid) { - const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - if (stillConnected) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - closeDialog(); - } - } - }, 500); + // Use Timer for actual delay + connectionSuccessTimer.start(); return; } @@ -407,8 +450,14 @@ Item { if (connectionMonitor.repeatCount > 10) { connectionMonitor.stop(); connectButton.connecting = false; + connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } } } } @@ -432,6 +481,23 @@ Item { } } + Timer { + id: connectionSuccessTimer + interval: 500 + onTriggered: { + // Double-check connection is still active + if (root.visible && Nmcli.active && Nmcli.active.ssid) { + const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (stillConnected) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + closeDialog(); + } + } + } + } + Connections { target: Nmcli function onActiveChanged() { @@ -443,8 +509,12 @@ Item { if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); connectButton.connecting = false; + connectButton.hasError = true; connectButton.enabled = true; connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + Nmcli.forgetNetwork(ssid); } } } @@ -457,6 +527,7 @@ Item { isClosing = true; passwordContainer.passwordBuffer = ""; connectButton.connecting = false; + connectButton.hasError = false; connectButton.text = qsTr("Connect"); connectionMonitor.stop(); } diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 24a93da..36bd3e6 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1272,7 +1272,15 @@ Singleton { stdout: SplitParser { onRead: root.refreshOnConnectionChange() } - onExited: Qt.callLater(() => monitorProc.running = true, 2000) + onExited: monitorRestartTimer.start() + } + + Timer { + id: monitorRestartTimer + interval: 2000 + onTriggered: { + monitorProc.running = true; + } } function refreshOnConnectionChange(): void { -- cgit v1.2.3-freya