extract NetworkService singleton from NetworkModule and NetworkMenu

This commit is contained in:
Damocles 2026-04-18 10:36:30 +02:00
parent a7eca009e4
commit d646d9b0fe
4 changed files with 194 additions and 183 deletions

View file

@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
@ -18,7 +17,7 @@ M.HoverPanel {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "\uF011" 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.pixelSize: S.Theme.fontSize
font.family: S.Theme.iconFontFamily font.family: S.Theme.iconFontFamily
@ -34,116 +33,16 @@ M.HoverPanel {
} }
TapHandler { TapHandler {
onTapped: { onTapped: S.NetworkService.setWifi(!S.NetworkService.wifiEnabled)
radioProc._state = menuWindow._wifiEnabled ? "off" : "on";
radioProc.running = true;
}
} }
} }
} }
onVisibleChanged: if (visible) onVisibleChanged: if (visible)
scanner.running = true S.NetworkService.refresh()
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);
}
}
Repeater { Repeater {
model: menuWindow._networks model: S.NetworkService.networks
delegate: Item { delegate: Item {
id: entry id: entry
@ -204,25 +103,23 @@ M.HoverPanel {
} }
TapHandler { TapHandler {
onTapped: { onTapped: {
if (entry.modelData.active) { if (entry.modelData.active)
disconnectProc.uuid = entry.modelData.uuid; S.NetworkService.disconnectNetwork(entry.modelData.uuid);
disconnectProc.running = true; else
} else { S.NetworkService.connectNetwork(entry.modelData.uuid);
connectProc.uuid = entry.modelData.uuid; menuWindow.keepOpen(500);
connectProc.running = true;
}
} }
} }
} }
} }
Text { Text {
visible: menuWindow._networks.length === 0 visible: S.NetworkService.networks.length === 0
width: menuWindow.contentWidth width: menuWindow.contentWidth
height: 32 height: 32
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter 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 color: S.Theme.base04
font.pixelSize: S.Theme.fontSize font.pixelSize: S.Theme.fontSize
font.family: S.Theme.fontFamily font.family: S.Theme.fontFamily

View file

@ -1,6 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import "." as M import "." as M
import "../services" as S import "../services" as S
@ -9,73 +8,7 @@ M.BarSection {
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
tooltip: "" tooltip: ""
property string ifname: "" readonly property string state: S.NetworkService.state
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
}
M.BarIcon { M.BarIcon {
icon: { icon: {
@ -92,7 +25,7 @@ M.BarSection {
} }
M.BarLabel { M.BarLabel {
visible: root.state === "wifi" visible: root.state === "wifi"
label: root.essid label: S.NetworkService.essid
color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor color: root.state === "disconnected" ? S.Theme.base08 : root.accentColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }

View file

@ -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()
}
}

View file

@ -9,3 +9,4 @@ NotifItem 1.0 NotifItem.qml
singleton LockService 1.0 LockService.qml singleton LockService 1.0 LockService.qml
singleton BacklightService 1.0 BacklightService.qml singleton BacklightService 1.0 BacklightService.qml
singleton MprisService 1.0 MprisService.qml singleton MprisService 1.0 MprisService.qml
singleton NetworkService 1.0 NetworkService.qml