diff --git a/modules/Bar.qml b/modules/Bar.qml index 4086627..8ab91e0 100644 --- a/modules/Bar.qml +++ b/modules/Bar.qml @@ -102,7 +102,7 @@ PanelWindow { // Connectivity M.BarGroup { borderColor: M.Theme.base0D - M.Network { visible: M.Modules.network.enable } + M.Network { bar: bar; visible: M.Modules.network.enable } M.Bluetooth {} } diff --git a/modules/Network.qml b/modules/Network.qml index beb09e4..66047ae 100644 --- a/modules/Network.qml +++ b/modules/Network.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import Quickshell.Io import "." as M @@ -91,4 +92,20 @@ M.BarSection { anchors.verticalCenter: parent.verticalCenter } + required property var bar + + TapHandler { + cursorShape: Qt.PointingHandCursor + onTapped: menuLoader.active = !menuLoader.active + } + + Loader { + id: menuLoader + active: false + sourceComponent: M.NetworkMenu { + screen: root.bar.screen + anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) + onDismissed: menuLoader.active = false + } + } } diff --git a/modules/NetworkMenu.qml b/modules/NetworkMenu.qml new file mode 100644 index 0000000..e9854bc --- /dev/null +++ b/modules/NetworkMenu.qml @@ -0,0 +1,172 @@ +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 + } +} diff --git a/modules/qmldir b/modules/qmldir index 54c3c05..b179bb3 100644 --- a/modules/qmldir +++ b/modules/qmldir @@ -22,6 +22,7 @@ Battery 1.0 Battery.qml Mpris 1.0 Mpris.qml MprisMenu 1.0 MprisMenu.qml Network 1.0 Network.qml +NetworkMenu 1.0 NetworkMenu.qml Bluetooth 1.0 Bluetooth.qml Backlight 1.0 Backlight.qml Cpu 1.0 Cpu.qml