diff --git a/shell/modules/NetworkMenu.qml b/shell/modules/NetworkMenu.qml index 36e41dd..f9c656f 100644 --- a/shell/modules/NetworkMenu.qml +++ b/shell/modules/NetworkMenu.qml @@ -1,6 +1,5 @@ import QtQuick import Quickshell -import Quickshell.Io import "." as M import "../services" as S @@ -18,7 +17,7 @@ M.HoverPanel { Text { anchors.centerIn: parent text: "\uF011" - color: menuWindow._wifiEnabled ? menuWindow.accentColor : S.Theme.base04 + color: S.NetworkService.wifiEnabled ? menuWindow.accentColor : S.Theme.base04 font.pixelSize: S.Theme.fontSize font.family: S.Theme.iconFontFamily @@ -34,116 +33,16 @@ M.HoverPanel { } TapHandler { - onTapped: { - radioProc._state = menuWindow._wifiEnabled ? "off" : "on"; - radioProc.running = true; - } + onTapped: S.NetworkService.setWifi(!S.NetworkService.wifiEnabled) } } } onVisibleChanged: if (visible) - scanner.running = true - - function triggerRefresh() { - if (visible) - scanner.running = true; - } - - property var _networks: [] - property bool _wifiEnabled: true - - property Process _scanner: Process { - id: scanner - running: true - command: ["sh", "-c", "echo '---RADIO---';" + "nmcli radio wifi 2>/dev/null;" + "echo '---CONNS---';" + "nmcli -t -f NAME,UUID,TYPE,ACTIVE connection show 2>/dev/null;" + "echo '---WIFI---';" + "nmcli -t -f SSID,SIGNAL device wifi list --rescan no 2>/dev/null"] - stdout: StdioCollector { - onStreamFinished: { - const radioSection = text.split("---CONNS---")[0].split("---RADIO---")[1] || ""; - menuWindow._wifiEnabled = radioSection.trim() === "enabled"; - - const sections = text.split("---WIFI---"); - const connLines = (sections[0] || "").split("---CONNS---")[1] || ""; - const wifiLines = sections[1] || ""; - - const visible = {}; - for (const l of wifiLines.trim().split("\n")) { - if (!l) - continue; - const parts = l.split(":"); - const ssid = parts[0]; - if (ssid) - visible[ssid] = parseInt(parts[1]) || 0; - } - - const nets = []; - for (const l of connLines.trim().split("\n")) { - if (!l) - continue; - const parts = l.split(":"); - const name = parts[0]; - const uuid = parts[1]; - const type = parts[2] || ""; - const active = parts[3] === "yes"; - const isWifi = type.includes("wireless"); - - if (isWifi && !(name in visible)) - continue; - - nets.push({ - "name": name, - "uuid": uuid, - "isWifi": isWifi, - "active": active, - "signal": isWifi ? (visible[name] || 0) : -1 - }); - } - - nets.sort((a, b) => { - if (a.active !== b.active) - return a.active ? -1 : 1; - if (a.signal >= 0 && b.signal >= 0) - return b.signal - a.signal; - return a.name.localeCompare(b.name); - }); - - menuWindow._networks = nets; - } - } - } - - property Process _radioProc: Process { - id: radioProc - property string _state: "" - command: ["nmcli", "radio", "wifi", _state] - onRunningChanged: if (!running) { - scanner.running = true; - menuWindow.keepOpen(500); - } - } - - property Process _connectProc: Process { - id: connectProc - property string uuid: "" - command: ["nmcli", "connection", "up", uuid] - onRunningChanged: if (!running) { - scanner.running = true; - menuWindow.keepOpen(500); - } - } - - property Process _disconnectProc: Process { - id: disconnectProc - property string uuid: "" - command: ["nmcli", "connection", "down", uuid] - onRunningChanged: if (!running) { - scanner.running = true; - menuWindow.keepOpen(500); - } - } + S.NetworkService.refresh() Repeater { - model: menuWindow._networks + model: S.NetworkService.networks delegate: Item { id: entry @@ -204,25 +103,23 @@ M.HoverPanel { } TapHandler { onTapped: { - if (entry.modelData.active) { - disconnectProc.uuid = entry.modelData.uuid; - disconnectProc.running = true; - } else { - connectProc.uuid = entry.modelData.uuid; - connectProc.running = true; - } + if (entry.modelData.active) + S.NetworkService.disconnectNetwork(entry.modelData.uuid); + else + S.NetworkService.connectNetwork(entry.modelData.uuid); + menuWindow.keepOpen(500); } } } } Text { - visible: menuWindow._networks.length === 0 + visible: S.NetworkService.networks.length === 0 width: menuWindow.contentWidth height: 32 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - text: menuWindow._wifiEnabled ? "No networks available" : "Wi-Fi is off" + text: S.NetworkService.wifiEnabled ? "No networks available" : "Wi-Fi is off" color: S.Theme.base04 font.pixelSize: S.Theme.fontSize font.family: S.Theme.fontFamily diff --git a/shell/modules/NetworkModule.qml b/shell/modules/NetworkModule.qml index 0de5532..d37eff8 100644 --- a/shell/modules/NetworkModule.qml +++ b/shell/modules/NetworkModule.qml @@ -1,6 +1,5 @@ import QtQuick import Quickshell -import Quickshell.Io import "." as M import "../services" as S @@ -9,73 +8,7 @@ M.BarSection { spacing: S.Theme.moduleSpacing tooltip: "" - property string ifname: "" - property string essid: "" - property string state: "disconnected" - property string ipAddr: "" - property string signal: "" - - Process { - id: proc - running: S.Modules.network.enable - command: ["sh", "-c", "line=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active 2>/dev/null | head -1); if [ -z \"$line\" ]; then dev=$(nmcli -t -f DEVICE,STATE device 2>/dev/null | grep ':connected' | grep -v ':unmanaged\\|:unavailable\\|:disconnected\\|:connecting' | head -1 | cut -d: -f1); [ -n \"$dev\" ] && line=\"linked:linked:$dev\"; fi; [ -z \"$line\" ] && exit 0; echo \"$line\"; dev=$(echo \"$line\" | cut -d: -f3); ip=$(nmcli -t -f IP4.ADDRESS device show \"$dev\" 2>/dev/null | head -1 | cut -d: -f2); echo \"ip:${ip:-}\"; sig=$(nmcli -t -f GENERAL.SIGNAL device show \"$dev\" 2>/dev/null | head -1 | cut -d: -f2); echo \"sig:${sig:-}\""] - stdout: StdioCollector { - onStreamFinished: { - const lines = text.trim().split("\n"); - if (!lines[0]) { - root.state = "disconnected"; - root.essid = ""; - root.ifname = ""; - root.ipAddr = ""; - root.signal = ""; - return; - } - const parts = lines[0].split(":"); - root.essid = parts[0] || ""; - root.ifname = parts[2] || ""; - if ((parts[1] || "").includes("wireless")) - root.state = "wifi"; - else if (parts[0] === "linked") - root.state = "linked"; - else - root.state = "eth"; - // Parse extra info lines - root.ipAddr = ""; - root.signal = ""; - for (let i = 1; i < lines.length; i++) { - if (lines[i].startsWith("ip:")) - root.ipAddr = lines[i].slice(3); - else if (lines[i].startsWith("sig:")) - root.signal = lines[i].slice(4); - } - } - } - } - // Event-driven: re-poll on any network change - Process { - id: monitor - running: S.Modules.network.enable - command: ["nmcli", "monitor"] - stdout: SplitParser { - splitMarker: "\n" - onRead: _debounce.restart() - } - } - Timer { - id: _debounce - interval: 300 - onTriggered: { - proc.running = true; - networkMenu.triggerRefresh(); - } - } - // Fallback poll - Timer { - interval: 60000 - running: S.Modules.network.enable - repeat: true - onTriggered: proc.running = true - } + readonly property string state: S.NetworkService.state M.BarIcon { icon: { @@ -92,7 +25,7 @@ M.BarSection { } M.BarLabel { visible: root.state === "wifi" - label: root.essid + label: S.NetworkService.essid color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor anchors.verticalCenter: parent.verticalCenter } diff --git a/shell/services/NetworkService.qml b/shell/services/NetworkService.qml new file mode 100644 index 0000000..2c2b531 --- /dev/null +++ b/shell/services/NetworkService.qml @@ -0,0 +1,180 @@ +pragma Singleton + +import QtQuick +import Quickshell.Io +import "." as S + +QtObject { + id: root + + // Connection state + property string ifname: "" + property string essid: "" + property string state: "disconnected" // "disconnected" | "wifi" | "eth" | "linked" + property string ipAddr: "" + property string signal: "" + + // Wi-Fi networks and radio state + property var networks: [] + property bool wifiEnabled: true + + function refresh() { + _statusProc.running = true; + _scannerProc.running = true; + } + + function setWifi(enabled) { + _radioProc._state = enabled ? "on" : "off"; + _radioProc.running = true; + } + + function connectNetwork(uuid) { + _connectProc._uuid = uuid; + _connectProc.running = true; + } + + function disconnectNetwork(uuid) { + _disconnectProc._uuid = uuid; + _disconnectProc.running = true; + } + + // Status polling + property Process _statusProc: Process { + running: S.Modules.network.enable + command: ["sh", "-c", "line=$(nmcli -t -f NAME,TYPE,DEVICE connection show --active 2>/dev/null | head -1); if [ -z \"$line\" ]; then dev=$(nmcli -t -f DEVICE,STATE device 2>/dev/null | grep ':connected' | grep -v ':unmanaged\\|:unavailable\\|:disconnected\\|:connecting' | head -1 | cut -d: -f1); [ -n \"$dev\" ] && line=\"linked:linked:$dev\"; fi; [ -z \"$line\" ] && exit 0; echo \"$line\"; dev=$(echo \"$line\" | cut -d: -f3); ip=$(nmcli -t -f IP4.ADDRESS device show \"$dev\" 2>/dev/null | head -1 | cut -d: -f2); echo \"ip:${ip:-}\"; sig=$(nmcli -t -f GENERAL.SIGNAL device show \"$dev\" 2>/dev/null | head -1 | cut -d: -f2); echo \"sig:${sig:-}\""] + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().split("\n"); + if (!lines[0]) { + root.state = "disconnected"; + root.essid = ""; + root.ifname = ""; + root.ipAddr = ""; + root.signal = ""; + return; + } + const parts = lines[0].split(":"); + root.essid = parts[0] || ""; + root.ifname = parts[2] || ""; + if ((parts[1] || "").includes("wireless")) + root.state = "wifi"; + else if (parts[0] === "linked") + root.state = "linked"; + else + root.state = "eth"; + root.ipAddr = ""; + root.signal = ""; + for (let i = 1; i < lines.length; i++) { + if (lines[i].startsWith("ip:")) + root.ipAddr = lines[i].slice(3); + else if (lines[i].startsWith("sig:")) + root.signal = lines[i].slice(4); + } + } + } + } + + // Event-driven monitor + property Process _monitor: Process { + running: S.Modules.network.enable + command: ["nmcli", "monitor"] + stdout: SplitParser { + splitMarker: "\n" + onRead: _debounce.restart() + } + } + + property Timer _debounce: Timer { + interval: 300 + onTriggered: root.refresh() + } + + // Fallback poll + property Timer _fallbackPoll: Timer { + interval: 60000 + running: S.Modules.network.enable + repeat: true + onTriggered: root.refresh() + } + + // Wi-Fi scanner (connections + available SSIDs) + property Process _scannerProc: Process { + running: true + command: ["sh", "-c", "echo '---RADIO---';" + "nmcli radio wifi 2>/dev/null;" + "echo '---CONNS---';" + "nmcli -t -f NAME,UUID,TYPE,ACTIVE connection show 2>/dev/null;" + "echo '---WIFI---';" + "nmcli -t -f SSID,SIGNAL device wifi list --rescan no 2>/dev/null"] + stdout: StdioCollector { + onStreamFinished: { + const radioSection = text.split("---CONNS---")[0].split("---RADIO---")[1] || ""; + root.wifiEnabled = radioSection.trim() === "enabled"; + + const sections = text.split("---WIFI---"); + const connLines = (sections[0] || "").split("---CONNS---")[1] || ""; + const wifiLines = sections[1] || ""; + + const visible = {}; + for (const l of wifiLines.trim().split("\n")) { + if (!l) + continue; + const parts = l.split(":"); + const ssid = parts[0]; + if (ssid) + visible[ssid] = parseInt(parts[1]) || 0; + } + + const nets = []; + for (const l of connLines.trim().split("\n")) { + if (!l) + continue; + const parts = l.split(":"); + const name = parts[0]; + const uuid = parts[1]; + const type = parts[2] || ""; + const active = parts[3] === "yes"; + const isWifi = type.includes("wireless"); + + if (isWifi && !(name in visible)) + continue; + + nets.push({ + name: name, + uuid: uuid, + isWifi: isWifi, + active: active, + signal: isWifi ? (visible[name] || 0) : -1 + }); + } + + nets.sort((a, b) => { + if (a.active !== b.active) + return a.active ? -1 : 1; + if (a.signal >= 0 && b.signal >= 0) + return b.signal - a.signal; + return a.name.localeCompare(b.name); + }); + + root.networks = nets; + } + } + } + + // Action processes + property Process _radioProc: Process { + property string _state: "" + command: ["nmcli", "radio", "wifi", _state] + onRunningChanged: if (!running) + root.refresh() + } + + property Process _connectProc: Process { + property string _uuid: "" + command: ["nmcli", "connection", "up", _uuid] + onRunningChanged: if (!running) + root.refresh() + } + + property Process _disconnectProc: Process { + property string _uuid: "" + command: ["nmcli", "connection", "down", _uuid] + onRunningChanged: if (!running) + root.refresh() + } +} diff --git a/shell/services/qmldir b/shell/services/qmldir index 37969bb..c7765f3 100644 --- a/shell/services/qmldir +++ b/shell/services/qmldir @@ -9,3 +9,4 @@ NotifItem 1.0 NotifItem.qml singleton LockService 1.0 LockService.qml singleton BacklightService 1.0 BacklightService.qml singleton MprisService 1.0 MprisService.qml +singleton NetworkService 1.0 NetworkService.qml