import QtQuick import Quickshell import Quickshell.Io import "." as M M.PopupPanel { id: menuWindow panelWidth: 250 property var _networks: [] Process { id: scanner running: true command: ["sh", "-c", "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 sections = text.split("---WIFI---"); const connLines = (sections[0] || "").split("---CONNS---")[1] || ""; const wifiLines = sections[1] || ""; // Visible SSIDs with signal 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; } // Saved connections — filter: show wired always, wifi only if visible 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 }); } // Active first, then by signal (wifi) or name 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; } } } Process { id: connectProc property string uuid: "" command: ["nmcli", "connection", "up", uuid] onRunningChanged: if (!running) scanner.running = true } Process { id: disconnectProc property string uuid: "" command: ["nmcli", "connection", "down", uuid] onRunningChanged: if (!running) scanner.running = true } Repeater { model: menuWindow._networks delegate: Item { id: entry required property var modelData required property int index width: menuWindow.panelWidth height: 32 Rectangle { anchors.fill: parent anchors.leftMargin: 4 anchors.rightMargin: 4 color: entryArea.containsMouse ? M.Theme.base02 : "transparent" radius: M.Theme.radius } Text { id: netIcon anchors.left: parent.left anchors.leftMargin: 12 anchors.verticalCenter: parent.verticalCenter text: entry.modelData.isWifi ? "\uF1EB" : "\uDB80\uDE00" color: entry.modelData.active ? M.Theme.base0D : M.Theme.base05 font.pixelSize: M.Theme.fontSize + 1 font.family: M.Theme.iconFontFamily } Text { anchors.left: netIcon.right anchors.leftMargin: 8 anchors.right: sigLabel.left anchors.rightMargin: 4 anchors.verticalCenter: parent.verticalCenter text: entry.modelData.name color: entry.modelData.active ? M.Theme.base0D : M.Theme.base05 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily font.bold: entry.modelData.active elide: Text.ElideRight } Text { id: sigLabel anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: entry.modelData.signal >= 0 ? entry.modelData.signal + "%" : "" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.fontFamily width: entry.modelData.signal >= 0 ? implicitWidth : 0 } MouseArea { id: entryArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { if (entry.modelData.active) { disconnectProc.uuid = entry.modelData.uuid; disconnectProc.running = true; } else { connectProc.uuid = entry.modelData.uuid; connectProc.running = true; } menuWindow.dismiss(); } } } } // Empty state Text { visible: menuWindow._networks.length === 0 width: menuWindow.panelWidth height: 32 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: "No networks available" color: M.Theme.base04 font.pixelSize: M.Theme.fontSize font.family: M.Theme.fontFamily } }