diff --git a/Modules/Anim.qml b/Components/Anim.qml similarity index 100% rename from Modules/Anim.qml rename to Components/Anim.qml diff --git a/Modules/CAnim.qml b/Components/CAnim.qml similarity index 100% rename from Modules/CAnim.qml rename to Components/CAnim.qml diff --git a/Daemons/Network.qml b/Daemons/Network.qml new file mode 100644 index 0000000..be89dbf --- /dev/null +++ b/Daemons/Network.qml @@ -0,0 +1,323 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + Component.onCompleted: { + // Trigger ethernet device detection after initialization + Qt.callLater(() => { + getEthernetDevices(); + }); + // Load saved connections on startup + Nmcli.loadSavedConnections(() => { + root.savedConnections = Nmcli.savedConnections; + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + }); + // Get initial WiFi status + Nmcli.getWifiStatus(enabled => { + root.wifiEnabled = enabled; + }); + // Sync networks from Nmcli on startup + Qt.callLater(() => { + syncNetworksFromNmcli(); + }, 100); + } + + readonly property list networks: [] + readonly property AccessPoint active: networks.find(n => n.active) ?? null + property bool wifiEnabled: true + readonly property bool scanning: Nmcli.scanning + + property list ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + property int ethernetDeviceCount: 0 + property bool ethernetProcessRunning: false + property var ethernetDeviceDetails: null + property var wirelessDeviceDetails: null + + function enableWifi(enabled: bool): void { + Nmcli.enableWifi(enabled, result => { + if (result.success) { + root.getWifiStatus(); + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + } + }); + } + + function toggleWifi(): void { + Nmcli.toggleWifi(result => { + if (result.success) { + root.getWifiStatus(); + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + } + }); + } + + function rescanWifi(): void { + Nmcli.rescanWifi(); + } + + property var pendingConnection: null + signal connectionFailed(string ssid) + + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + // Set up pending connection tracking if callback provided + if (callback) { + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + root.pendingConnection = { + ssid: ssid, + bssid: hasBssid ? bssid : "", + callback: callback + }; + } + + Nmcli.connectToNetwork(ssid, password, bssid, result => { + if (result && result.success) { + // Connection successful + if (callback) + callback(result); + root.pendingConnection = null; + } else if (result && result.needsPassword) { + // Password needed - callback will handle showing dialog + if (callback) + callback(result); + } else { + // Connection failed + if (result && result.error) { + root.connectionFailed(ssid); + } + if (callback) + callback(result); + root.pendingConnection = null; + } + }); + } + + function connectToNetworkWithPasswordCheck(ssid: string, isSecure: bool, callback: var, bssid: string): void { + // Set up pending connection tracking + const hasBssid = bssid !== undefined && bssid !== null && bssid.length > 0; + root.pendingConnection = { + ssid: ssid, + bssid: hasBssid ? bssid : "", + callback: callback + }; + + Nmcli.connectToNetworkWithPasswordCheck(ssid, isSecure, result => { + if (result && result.success) { + // Connection successful + if (callback) + callback(result); + root.pendingConnection = null; + } else if (result && result.needsPassword) { + // Password needed - callback will handle showing dialog + if (callback) + callback(result); + } else { + // Connection failed + if (result && result.error) { + root.connectionFailed(ssid); + } + if (callback) + callback(result); + root.pendingConnection = null; + } + }, bssid); + } + + function disconnectFromNetwork(): void { + // Try to disconnect - use connection name if available, otherwise use device + Nmcli.disconnectFromNetwork(); + // Refresh network list after disconnection + Qt.callLater(() => { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + }, 500); + } + + function forgetNetwork(ssid: string): void { + // Delete the connection profile for this network + // This will remove the saved password and connection settings + Nmcli.forgetNetwork(ssid, result => { + if (result.success) { + // Refresh network list after deletion + Qt.callLater(() => { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + }, 500); + } + }); + } + + property list savedConnections: [] + property list savedConnectionSsids: [] + + // Sync saved connections from Nmcli when they're updated + Connections { + target: Nmcli + function onSavedConnectionsChanged() { + root.savedConnections = Nmcli.savedConnections; + } + function onSavedConnectionSsidsChanged() { + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + } + } + + function syncNetworksFromNmcli(): void { + const rNetworks = root.networks; + const nNetworks = Nmcli.networks; + + // Build a map of existing networks by key + const existingMap = new Map(); + for (const rn of rNetworks) { + const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; + existingMap.set(key, rn); + } + + // Build a map of new networks by key + const newMap = new Map(); + for (const nn of nNetworks) { + const key = `${nn.frequency}:${nn.ssid}:${nn.bssid}`; + newMap.set(key, nn); + } + + // Remove networks that no longer exist + for (const [key, network] of existingMap) { + if (!newMap.has(key)) { + const index = rNetworks.indexOf(network); + if (index >= 0) { + rNetworks.splice(index, 1); + network.destroy(); + } + } + } + + // Add or update networks from Nmcli + for (const [key, nNetwork] of newMap) { + const existing = existingMap.get(key); + if (existing) { + // Update existing network's lastIpcObject + existing.lastIpcObject = nNetwork.lastIpcObject; + } else { + // Create new AccessPoint from Nmcli's data + rNetworks.push(apComp.createObject(root, { + lastIpcObject: nNetwork.lastIpcObject + })); + } + } + } + + 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 {} + } + + function hasSavedProfile(ssid: string): bool { + // Use Nmcli's hasSavedProfile which has the same logic + return Nmcli.hasSavedProfile(ssid); + } + + function getWifiStatus(): void { + Nmcli.getWifiStatus(enabled => { + root.wifiEnabled = enabled; + }); + } + + function getEthernetDevices(): void { + root.ethernetProcessRunning = true; + Nmcli.getEthernetInterfaces(interfaces => { + root.ethernetDevices = Nmcli.ethernetDevices; + root.ethernetDeviceCount = Nmcli.ethernetDevices.length; + root.ethernetProcessRunning = false; + }); + } + + function connectEthernet(connectionName: string, interfaceName: string): void { + Nmcli.connectEthernet(connectionName, interfaceName, result => { + if (result.success) { + getEthernetDevices(); + // Refresh device details after connection + Qt.callLater(() => { + const activeDevice = root.ethernetDevices.find(function (d) { + return d.connected; + }); + if (activeDevice && activeDevice.interface) { + updateEthernetDeviceDetails(activeDevice.interface); + } + }, 1000); + } + }); + } + + function disconnectEthernet(connectionName: string): void { + Nmcli.disconnectEthernet(connectionName, result => { + if (result.success) { + getEthernetDevices(); + // Clear device details after disconnection + Qt.callLater(() => { + root.ethernetDeviceDetails = null; + }); + } + }); + } + + function updateEthernetDeviceDetails(interfaceName: string): void { + Nmcli.getEthernetDeviceDetails(interfaceName, details => { + root.ethernetDeviceDetails = details; + }); + } + + function updateWirelessDeviceDetails(): void { + // Find the wireless interface by looking for wifi devices + // Pass empty string to let Nmcli find the active interface automatically + Nmcli.getWirelessDeviceDetails("", details => { + root.wirelessDeviceDetails = details; + }); + } + + function cidrToSubnetMask(cidr: string): string { + // Convert CIDR notation (e.g., "24") to subnet mask (e.g., "255.255.255.0") + const cidrNum = parseInt(cidr); + if (isNaN(cidrNum) || cidrNum < 0 || cidrNum > 32) { + return ""; + } + + const mask = (0xffffffff << (32 - cidrNum)) >>> 0; + const octets = [(mask >>> 24) & 0xff, (mask >>> 16) & 0xff, (mask >>> 8) & 0xff, mask & 0xff]; + + return octets.join("."); + } + + Process { + running: true + command: ["nmcli", "m"] + stdout: SplitParser { + onRead: { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + getEthernetDevices(); + } + } + } +} diff --git a/Daemons/Nmcli.qml b/Daemons/Nmcli.qml new file mode 100644 index 0000000..36bd3e6 --- /dev/null +++ b/Daemons/Nmcli.qml @@ -0,0 +1,1352 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +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 bool scanning: rescanProc.running + 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: [] + + // 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 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]; + 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); + }); + } + + function getWirelessInterfaces(callback: var): void { + 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", 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, + state: iface.state, + connection: iface.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([root.nmcliCommandConnection, "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([root.nmcliCommandDevice, "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([root.nmcliCommandConnection, "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", root.deviceStatusFields, root.nmcliCommandDevice, "status"], result => { + const interfaces = parseDeviceStatusOutput(result.output, "both"); + if (callback) + callback(interfaces); + }); + } + + function isInterfaceConnected(interfaceName: string, callback: var): void { + 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); + 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 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; + 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 = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid]; + if (password && password.length > 0) { + cmd.push(root.connectionParamPassword, password); + } + executeCommand(cmd, result => { + if (result.needsPassword && callback) { + if (callback) + callback(result); + return; + } + + if (!result.success && root.pendingConnection && retries < 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); + } + }); + } + + 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 => { + 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 { + 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); + }); + } + } + }); + }); + } + + function checkAndDeleteConnection(ssid: string, callback: var): void { + executeCommand([root.nmcliCommandConnection, "show", ssid], result => { + if (result.success) { + executeCommand([root.nmcliCommandConnection, "delete", ssid], deleteResult => { + Qt.callLater(() => { + if (callback) + callback(); + }, 300); + }); + } else { + if (callback) + callback(); + } + }); + } + + function activateConnection(connectionName: string, callback: var): void { + executeCommand([root.nmcliCommandConnection, "up", connectionName], result => { + if (callback) + callback(result); + }); + } + + function loadSavedConnections(callback: var): void { + executeCommand(["-t", "-f", root.connectionListFields, root.nmcliCommandConnection, "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 === root.connectionTypeWireless) { + 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", root.wirelessSsidField, root.nmcliCommandConnection, "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([root.nmcliCommandConnection, "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([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 : ""); + }); + } + } + + function disconnectFromNetwork(): void { + if (active && active.ssid) { + executeCommand([root.nmcliCommandConnection, "down", active.ssid], result => { + if (result.success) { + getNetworks(() => {}); + } + }); + } else { + executeCommand([root.nmcliCommandDevice, "disconnect", root.deviceTypeWifi], result => { + if (result.success) { + getNetworks(() => {}); + } + }); + } + } + + function getDeviceDetails(interfaceName: string, callback: var): void { + executeCommand([root.nmcliCommandDevice, "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 (isConnectedState(state)) { + 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([root.nmcliCommandDevice, "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([root.nmcliCommandDevice, "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 = [root.nmcliCommandDevice, root.nmcliCommandWifi, "rescan"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push(root.connectionParamIfname, interfaceName); + } + executeCommand(cmd, result => { + if (callback) { + callback(result); + } + }); + } + + function rescanWifi(): void { + rescanProc.running = true; + } + + function enableWifi(enabled: bool, callback: var): void { + const cmd = enabled ? "on" : "off"; + executeCommand([root.nmcliCommandRadio, root.nmcliCommandWifi, 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([root.nmcliCommandRadio, root.nmcliCommandWifi], 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", root.networkDetailFields, "d", "w"], result => { + if (!result.success) { + 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)); + 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", root.networkListFields, root.nmcliCommandDevice, root.nmcliCommandWifi, "list"]; + if (interfaceName && interfaceName.length > 0) { + cmd.push(root.connectionParamIfname, 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); + }); + } + + 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 + 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 + } + + stderr: StdioCollector { + id: stderrCollector + + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + root.handlePasswordRequired(proc, error, output, -1); + } + } + } + + onExited: code => { + exitCode = code; + + Qt.callLater(() => { + if (callbackCalled) { + processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.command); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); + return; + } + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + callbackCalled = true; + callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + processFinished(); + } else { + 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) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + 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 + + property int checkCount: 0 + + interval: 500 + repeat: true + triggeredOnStart: false + + 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) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + 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 isConnectedState(iface.state); + }); + 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 isConnectedState(iface.state); + }); + 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: rescanProc + + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] + onExited: root.getNetworks() + } + + Process { + id: monitorProc + + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: SplitParser { + onRead: root.refreshOnConnectionChange() + } + onExited: monitorRestartTimer.start() + } + + Timer { + id: monitorRestartTimer + interval: 2000 + onTriggered: { + monitorProc.running = true; + } + } + + 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 isConnectedState(iface.state); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return isConnectedState(iface.state); + }); + 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 isConnectedState(iface.state); + }); + if (activeWireless && activeWireless.device) { + getWirelessDeviceDetails(activeWireless.device, () => {}); + } + } + + if (root.ethernetInterfaces.length > 0) { + const activeEthernet = root.ethernetInterfaces.find(iface => { + return isConnectedState(iface.state); + }); + if (activeEthernet && activeEthernet.device) { + getEthernetDeviceDetails(activeEthernet.device, () => {}); + } + } + }, 2000); + } +} diff --git a/Bar.qml b/Drawers/Bar.qml similarity index 100% rename from Bar.qml rename to Drawers/Bar.qml diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 4282c84..b020a1e 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -1,6 +1,6 @@ import Quickshell import QtQuick -import QtQuick.Shapes +import qs.Components import qs.Modules as Modules import qs.Modules.Notifications as Notifications import qs.Modules.Notifications.Sidebar as Sidebar @@ -31,7 +31,7 @@ Item { // anchors.margins: 8 anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight Behavior on anchors.topMargin { - Modules.Anim {} + Anim {} } Osd.Wrapper { diff --git a/Modules/GetIcons.qml b/Helpers/GetIcons.qml similarity index 92% rename from Modules/GetIcons.qml rename to Helpers/GetIcons.qml index dad9408..46cc993 100644 --- a/Modules/GetIcons.qml +++ b/Helpers/GetIcons.qml @@ -1,7 +1,6 @@ pragma Singleton import Quickshell -import Quickshell.Services.Notifications Singleton { id: root diff --git a/Helpers/Picker.qml b/Helpers/Picker.qml index c475f87..1878000 100644 --- a/Helpers/Picker.qml +++ b/Helpers/Picker.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Wayland import QtQuick import QtQuick.Effects -import qs.Modules +import qs.Components import qs.Config import qs.Helpers diff --git a/Modules/Wallust.qml b/Helpers/Wallust.qml similarity index 100% rename from Modules/Wallust.qml rename to Helpers/Wallust.qml diff --git a/Modules/Background.qml b/Modules/Background.qml index 560503d..a920cc1 100644 --- a/Modules/Background.qml +++ b/Modules/Background.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Shapes +import qs.Components import qs.Config ShapePath { diff --git a/Modules/Content.qml b/Modules/Content.qml index 3f0b30e..2ab064c 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -4,10 +4,9 @@ import Quickshell import Quickshell.Services.SystemTray import QtQuick import qs.Config +import qs.Components import qs.Modules.Calendar import qs.Modules.WSOverview -import qs.Modules.Polkit -import qs.Modules.Dashboard import qs.Modules.Network import qs.Modules.UPower diff --git a/Modules/Dashboard/Background.qml b/Modules/Dashboard/Background.qml index 9dd284b..1c150e6 100644 --- a/Modules/Dashboard/Background.qml +++ b/Modules/Dashboard/Background.qml @@ -1,7 +1,5 @@ import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules import QtQuick import QtQuick.Shapes @@ -62,6 +60,6 @@ ShapePath { } Behavior on fillColor { - Modules.CAnim {} + CAnim {} } } diff --git a/Modules/Dashboard/Content.qml b/Modules/Dashboard/Content.qml index 4ff2028..2e6866b 100644 --- a/Modules/Dashboard/Content.qml +++ b/Modules/Dashboard/Content.qml @@ -5,7 +5,7 @@ import Quickshell.Widgets import QtQuick import QtQuick.Layouts import qs.Config -import qs.Modules +import qs.Components Item { id: root diff --git a/Modules/Dashboard/Dash/Resources.qml b/Modules/Dashboard/Dash/Resources.qml index acaf12a..969aca7 100644 --- a/Modules/Dashboard/Dash/Resources.qml +++ b/Modules/Dashboard/Dash/Resources.qml @@ -2,7 +2,6 @@ import QtQuick import qs.Components import qs.Helpers import qs.Config -import qs.Modules as Modules Row { id: root @@ -91,7 +90,7 @@ Row { } Behavior on value { - Modules.Anim { + Anim { duration: Appearance.anim.durations.large } } diff --git a/Modules/Dashboard/Wrapper.qml b/Modules/Dashboard/Wrapper.qml index 123972f..4c1d272 100644 --- a/Modules/Dashboard/Wrapper.qml +++ b/Modules/Dashboard/Wrapper.qml @@ -1,12 +1,9 @@ pragma ComponentBehavior: Bound -import ZShell import Quickshell import QtQuick import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules Item { id: root @@ -46,7 +43,7 @@ Item { from: "" to: "visible" - Modules.Anim { + Anim { target: root property: "implicitHeight" duration: MaterialEasing.expressiveEffectsTime @@ -57,7 +54,7 @@ Item { from: "visible" to: "" - Modules.Anim { + Anim { target: root property: "implicitHeight" easing.bezierCurve: MaterialEasing.expressiveEffects diff --git a/Modules/Launcher/AppList.qml b/Modules/Launcher/AppList.qml index 1c3fdee..e80e635 100644 --- a/Modules/Launcher/AppList.qml +++ b/Modules/Launcher/AppList.qml @@ -5,9 +5,7 @@ import QtQuick import qs.Modules.Launcher.Services import qs.Modules.Launcher.Items import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules CustomListView { id: root @@ -41,7 +39,7 @@ CustomListView { implicitHeight: root.currentItem?.implicitHeight ?? 0 Behavior on y { - Modules.Anim { + Anim { duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.expressiveEffects } @@ -92,7 +90,7 @@ CustomListView { transitions: Transition { SequentialAnimation { ParallelAnimation { - Modules.Anim { + Anim { target: root property: "opacity" from: 1 @@ -100,7 +98,7 @@ CustomListView { duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.expressiveEffects } - Modules.Anim { + Anim { target: root property: "scale" from: 1 @@ -114,7 +112,7 @@ CustomListView { properties: "values,delegate" } ParallelAnimation { - Modules.Anim { + Anim { target: root property: "opacity" from: 0 @@ -122,7 +120,7 @@ CustomListView { duration: Appearance.anim.durations.small easing.bezierCurve: Appearance.anim.curves.expressiveEffects } - Modules.Anim { + Anim { target: root property: "scale" from: 0.9 @@ -146,7 +144,7 @@ CustomListView { add: Transition { enabled: !root.state - Modules.Anim { + Anim { properties: "opacity,scale" from: 0 to: 1 @@ -156,7 +154,7 @@ CustomListView { remove: Transition { enabled: !root.state - Modules.Anim { + Anim { properties: "opacity,scale" from: 1 to: 0 @@ -164,31 +162,31 @@ CustomListView { } move: Transition { - Modules.Anim { + Anim { property: "y" } - Modules.Anim { + Anim { properties: "opacity,scale" to: 1 } } addDisplaced: Transition { - Modules.Anim { + Anim { property: "y" duration: Appearance.anim.durations.small } - Modules.Anim { + Anim { properties: "opacity,scale" to: 1 } } displaced: Transition { - Modules.Anim { + Anim { property: "y" } - Modules.Anim { + Anim { properties: "opacity,scale" to: 1 } diff --git a/Modules/Launcher/Background.qml b/Modules/Launcher/Background.qml index ac13d4f..5845020 100644 --- a/Modules/Launcher/Background.qml +++ b/Modules/Launcher/Background.qml @@ -1,9 +1,7 @@ import QtQuick import QtQuick.Shapes import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules ShapePath { id: root @@ -56,6 +54,6 @@ ShapePath { } Behavior on fillColor { - Modules.CAnim {} + CAnim {} } } diff --git a/Modules/Launcher/Content.qml b/Modules/Launcher/Content.qml index 252f15a..660406f 100644 --- a/Modules/Launcher/Content.qml +++ b/Modules/Launcher/Content.qml @@ -6,7 +6,6 @@ import qs.Modules.Launcher.Services import qs.Components import qs.Helpers import qs.Config -import qs.Modules as Modules Item { id: root @@ -176,13 +175,13 @@ Item { } Behavior on width { - Modules.Anim { + Anim { duration: Appearance.anim.durations.small } } Behavior on opacity { - Modules.Anim { + Anim { duration: Appearance.anim.durations.small } } diff --git a/Modules/Launcher/Wrapper.qml b/Modules/Launcher/Wrapper.qml index cba412a..35b7932 100644 --- a/Modules/Launcher/Wrapper.qml +++ b/Modules/Launcher/Wrapper.qml @@ -4,7 +4,6 @@ import Quickshell import QtQuick import qs.Components import qs.Config -import qs.Modules as Modules Item { id: root @@ -43,7 +42,7 @@ Item { SequentialAnimation { id: showAnim - Modules.Anim { + Anim { target: root property: "implicitHeight" to: root.contentHeight @@ -61,7 +60,7 @@ Item { ScriptAction { script: root.implicitHeight = root.implicitHeight } - Modules.Anim { + Anim { target: root property: "implicitHeight" to: 0 diff --git a/Modules/Lock/LockSurface.qml b/Modules/Lock/LockSurface.qml index c32e3f2..fe3d77c 100644 --- a/Modules/Lock/LockSurface.qml +++ b/Modules/Lock/LockSurface.qml @@ -1,16 +1,11 @@ pragma ComponentBehavior: Bound -import Quickshell import Quickshell.Wayland import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import QtQuick.Effects import qs.Config import qs.Helpers -import qs.Effects import qs.Components -import qs.Modules as Modules WlSessionLockSurface { id: root @@ -34,32 +29,32 @@ WlSessionLockSurface { id: unlockAnim ParallelAnimation { - Modules.Anim { + Anim { target: lockContent properties: "implicitWidth,implicitHeight" to: lockContent.size duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - Modules.Anim { + Anim { target: lockBg property: "radius" to: lockContent.radius } - Modules.Anim { + Anim { target: content property: "scale" to: 0 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - Modules.Anim { + Anim { target: content property: "opacity" to: 0 duration: Appearance.anim.durations.small } - Modules.Anim { + Anim { target: lockIcon property: "opacity" to: 1 @@ -69,7 +64,7 @@ WlSessionLockSurface { PauseAnimation { duration: Appearance.anim.durations.small } - Modules.Anim { + Anim { target: lockContent property: "opacity" to: 0 @@ -90,7 +85,7 @@ WlSessionLockSurface { SequentialAnimation { ParallelAnimation { - Modules.Anim { + Anim { target: lockContent property: "scale" to: 1 @@ -99,36 +94,36 @@ WlSessionLockSurface { } } ParallelAnimation { - Modules.Anim { + Anim { target: lockIcon property: "opacity" to: 0 } - Modules.Anim { + Anim { target: content property: "opacity" to: 1 } - Modules.Anim { + Anim { target: content property: "scale" to: 1 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - Modules.Anim { + Anim { target: lockBg property: "radius" to: Appearance.rounding.large * 1.5 } - Modules.Anim { + Anim { target: lockContent property: "implicitWidth" to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - Modules.Anim { + Anim { target: lockContent property: "implicitHeight" to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult diff --git a/Modules/Notifications/Background.qml b/Modules/Notifications/Background.qml index 07e45f1..54af8d1 100644 --- a/Modules/Notifications/Background.qml +++ b/Modules/Notifications/Background.qml @@ -1,6 +1,5 @@ import qs.Components import qs.Config -import qs.Modules as Modules import QtQuick import QtQuick.Shapes @@ -49,6 +48,6 @@ ShapePath { } Behavior on fillColor { - Modules.CAnim {} + CAnim {} } } diff --git a/Modules/Notifications/Sidebar/Background.qml b/Modules/Notifications/Sidebar/Background.qml index 3ca0a60..710d4e5 100644 --- a/Modules/Notifications/Sidebar/Background.qml +++ b/Modules/Notifications/Sidebar/Background.qml @@ -1,6 +1,5 @@ import qs.Components import qs.Config -import qs.Modules as Modules import QtQuick import QtQuick.Shapes @@ -49,6 +48,6 @@ ShapePath { } Behavior on fillColor { - Modules.CAnim {} + CAnim {} } } diff --git a/Modules/Notifications/Sidebar/Utils/Background.qml b/Modules/Notifications/Sidebar/Utils/Background.qml index a081c24..875f35a 100644 --- a/Modules/Notifications/Sidebar/Utils/Background.qml +++ b/Modules/Notifications/Sidebar/Utils/Background.qml @@ -1,6 +1,5 @@ import qs.Components import qs.Config -import qs.Modules as Modules import QtQuick import QtQuick.Shapes @@ -50,6 +49,6 @@ ShapePath { } Behavior on fillColor { - Modules.CAnim {} + CAnim {} } } diff --git a/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml b/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml index 1244c91..5867fc2 100644 --- a/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml +++ b/Modules/Notifications/Sidebar/Utils/Cards/Toggles.qml @@ -1,9 +1,11 @@ +import Quickshell.Bluetooth +import Quickshell.Networking as QSNetwork +import QtQuick +import QtQuick.Layouts import qs.Components import qs.Config import qs.Modules import qs.Daemons -import QtQuick -import QtQuick.Layouts CustomRect { id: root @@ -28,12 +30,50 @@ CustomRect { Layout.alignment: Qt.AlignHCenter spacing: 7 + Toggle { + visible: QSNetwork.Networking.devices.values.length > 0 + icon: Network.wifiEnabled ? "wifi" : "wifi_off" + checked: Network.wifiEnabled + onClicked: Network.toggleWifi() + } + Toggle { id: toggle - icon: "notifications_off" - checked: NotifServer.dnd + icon: NotifServer.dnd ? "notifications_off" : "notifications" + checked: !NotifServer.dnd onClicked: NotifServer.dnd = !NotifServer.dnd } + + Toggle { + icon: Audio.sourceMuted ? "mic_off" : "mic" + checked: !Audio.sourceMuted + onClicked: { + const audio = Audio.source?.audio; + if ( audio ) + audio.muted = !audio.muted; + } + } + + Toggle { + icon: Audio.muted ? "volume_off" : "volume_up" + checked: !Audio.muted + onClicked: { + const audio = Audio.sink?.audio; + if ( audio ) + audio.muted = !audio.muted; + } + } + + Toggle { + visible: Bluetooth.defaultAdapter?.enabled ?? false + icon: Bluetooth.defaultAdapter?.enabled ? "bluetooth" : "bluetooth_disabled" + checked: Bluetooth.defaultAdapter?.enabled ?? false + onClicked: { + const adapter = Bluetooth.defaultAdapter + if ( adapter ) + adapter.enabled = !adapter.enabled; + } + } } } diff --git a/Modules/Notifications/Sidebar/Utils/IdleInhibit.qml b/Modules/Notifications/Sidebar/Utils/IdleInhibit.qml index 1bd2a18..4595d1e 100644 --- a/Modules/Notifications/Sidebar/Utils/IdleInhibit.qml +++ b/Modules/Notifications/Sidebar/Utils/IdleInhibit.qml @@ -1,6 +1,5 @@ import qs.Components import qs.Config -import qs.Modules as Modules import qs.Helpers import QtQuick import QtQuick.Layouts @@ -99,25 +98,25 @@ CustomRect { } Behavior on anchors.bottomMargin { - Modules.Anim { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects } } Behavior on opacity { - Modules.Anim { + Anim { duration: MaterialEasing.expressiveEffectsTime } } Behavior on scale { - Modules.Anim {} + Anim {} } } Behavior on implicitHeight { - Modules.Anim { + Anim { duration: MaterialEasing.expressiveEffectsTime easing.bezierCurve: MaterialEasing.expressiveEffects } diff --git a/Modules/Notifications/Sidebar/Utils/Wrapper.qml b/Modules/Notifications/Sidebar/Utils/Wrapper.qml index 2889a99..4f82920 100644 --- a/Modules/Notifications/Sidebar/Utils/Wrapper.qml +++ b/Modules/Notifications/Sidebar/Utils/Wrapper.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import qs.Components import qs.Config -import qs.Modules as Modules import Quickshell import QtQuick @@ -47,7 +46,7 @@ Item { from: "" to: "visible" - Modules.Anim { + Anim { target: root property: "implicitHeight" duration: MaterialEasing.expressiveEffectsTime @@ -58,7 +57,7 @@ Item { from: "visible" to: "" - Modules.Anim { + Anim { target: root property: "implicitHeight" easing.bezierCurve: MaterialEasing.expressiveEffects diff --git a/Modules/Notifications/Sidebar/Wrapper.qml b/Modules/Notifications/Sidebar/Wrapper.qml index 4fb280b..8a5de8b 100644 --- a/Modules/Notifications/Sidebar/Wrapper.qml +++ b/Modules/Notifications/Sidebar/Wrapper.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import qs.Components import qs.Config -import qs.Modules as Modules import QtQuick Item { @@ -29,7 +28,7 @@ Item { from: "" to: "visible" - Modules.Anim { + Anim { target: root property: "implicitWidth" duration: MaterialEasing.expressiveEffectsTime @@ -40,7 +39,7 @@ Item { from: "visible" to: "" - Modules.Anim { + Anim { target: root property: "implicitWidth" easing.bezierCurve: MaterialEasing.expressiveEffects diff --git a/Modules/Notifications/Wrapper.qml b/Modules/Notifications/Wrapper.qml index 46e1763..10d5729 100644 --- a/Modules/Notifications/Wrapper.qml +++ b/Modules/Notifications/Wrapper.qml @@ -1,7 +1,6 @@ import QtQuick import qs.Components import qs.Config -import qs.Modules as Modules Item { id: root @@ -23,7 +22,7 @@ Item { } transitions: Transition { - Modules.Anim { + Anim { target: root property: "implicitHeight" duration: MaterialEasing.expressiveEffectsTime diff --git a/Modules/Osd/Background.qml b/Modules/Osd/Background.qml index 90f7711..b492d16 100644 --- a/Modules/Osd/Background.qml +++ b/Modules/Osd/Background.qml @@ -1,9 +1,7 @@ import QtQuick import QtQuick.Shapes import qs.Components -import qs.Helpers import qs.Config -import qs.Modules as Modules ShapePath { id: root @@ -56,6 +54,6 @@ ShapePath { } Behavior on fillColor { - Modules.CAnim {} + CAnim {} } } diff --git a/Modules/Osd/Content.qml b/Modules/Osd/Content.qml index bddc67f..15fc3a8 100644 --- a/Modules/Osd/Content.qml +++ b/Modules/Osd/Content.qml @@ -6,7 +6,6 @@ import qs.Components import qs.Helpers import qs.Config import qs.Daemons -import qs.Modules as Modules Item { id: root @@ -125,13 +124,13 @@ Item { visible: active Behavior on Layout.preferredHeight { - Modules.Anim { + Anim { easing.bezierCurve: Appearance.anim.curves.emphasized } } Behavior on opacity { - Modules.Anim {} + Anim {} } } } diff --git a/Modules/Osd/Wrapper.qml b/Modules/Osd/Wrapper.qml index 6943809..b8a52ea 100644 --- a/Modules/Osd/Wrapper.qml +++ b/Modules/Osd/Wrapper.qml @@ -5,7 +5,6 @@ import QtQuick import qs.Components import qs.Helpers import qs.Config -import qs.Modules as Modules import qs.Daemons Item { @@ -54,7 +53,7 @@ Item { from: "" to: "visible" - Modules.Anim { + Anim { target: root property: "implicitWidth" easing.bezierCurve: MaterialEasing.expressiveEffects @@ -64,7 +63,7 @@ Item { from: "visible" to: "" - Modules.Anim { + Anim { target: root property: "implicitWidth" easing.bezierCurve: MaterialEasing.expressiveEffects diff --git a/Modules/Resource.qml b/Modules/Resource.qml index e4f0182..4d888f0 100644 --- a/Modules/Resource.qml +++ b/Modules/Resource.qml @@ -1,8 +1,7 @@ -import qs.Modules import QtQuick import QtQuick.Layouts import QtQuick.Shapes -import Quickshell +import qs.Components import qs.Config Item { diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index e2eb390..ef6d531 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -1,10 +1,7 @@ import QtQuick.Layouts -import QtQuick.Effects import QtQuick import Quickshell import Quickshell.Services.SystemTray -import Quickshell.Io -import Quickshell.Widgets import qs.Modules import qs.Components import qs.Config diff --git a/Modules/TrayMenu.qml b/Modules/TrayMenu.qml index 60d29e6..2540a2d 100644 --- a/Modules/TrayMenu.qml +++ b/Modules/TrayMenu.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import Quickshell -import Quickshell.DBusMenu import QtQuick import QtQuick.Layouts import Qt5Compat.GraphicalEffects @@ -9,7 +8,6 @@ import Quickshell.Hyprland import QtQml import qs.Effects import qs.Config -import qs.Modules PanelWindow { id: root diff --git a/Modules/UPower/UPowerWidget.qml b/Modules/UPower/UPowerWidget.qml index c4c11e0..a0dfcfe 100644 --- a/Modules/UPower/UPowerWidget.qml +++ b/Modules/UPower/UPowerWidget.qml @@ -1,11 +1,9 @@ -import Quickshell import Quickshell.Services.UPower import QtQuick import QtQuick.Layouts import qs.Components import qs.Config import qs.Helpers as Helpers -import qs.Modules Item { id: root diff --git a/Modules/UpdatesWidget.qml b/Modules/UpdatesWidget.qml index 6fce70a..0431f71 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/UpdatesWidget.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Layouts +import qs.Components import qs.Modules import qs.Config diff --git a/Modules/WallBackground.qml b/Modules/Wallpaper/WallBackground.qml similarity index 98% rename from Modules/WallBackground.qml rename to Modules/Wallpaper/WallBackground.qml index a6aacfb..a5b080a 100644 --- a/Modules/WallBackground.qml +++ b/Modules/Wallpaper/WallBackground.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick +import qs.Components import qs.Helpers import qs.Config diff --git a/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml similarity index 93% rename from Wallpaper.qml rename to Modules/Wallpaper/Wallpaper.qml index a0368b0..76fdf64 100644 --- a/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -1,8 +1,6 @@ import Quickshell import QtQuick import Quickshell.Wayland -import qs.Helpers -import qs.Modules import qs.Config Loader { diff --git a/Modules/Wrapper.qml b/Modules/Wrapper.qml index a42c9ef..6c395ce 100644 --- a/Modules/Wrapper.qml +++ b/Modules/Wrapper.qml @@ -2,8 +2,8 @@ import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import QtQuick +import qs.Components import qs.Config -import qs.Helpers Item { id: root diff --git a/shell.qml b/shell.qml index f65c607..61e3f61 100644 --- a/shell.qml +++ b/shell.qml @@ -3,7 +3,9 @@ //@ pragma Env QS_NO_RELOAD_POPUP=1 import Quickshell import qs.Modules +import qs.Modules.Wallpaper import qs.Modules.Lock as Lock +import qs.Drawers import qs.Helpers import qs.Modules.Polkit