import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Io import "../common" as Common Item { id: root property QtObject bar: null property string moduleName: "networkPanel" property var settings: ({}) property bool popupOpen: false function closePopout() { popupOpen = false } // Live connection details from `ip` / /sys / iw. property var info: ({}) // { iface, type, ip, prefix, gateway, speed, duplex, ssid, signal, freq, bitrate } property var wifiNetworks: [] property bool scanning: false property bool wifiStationAvailable: false readonly property string kind: bar ? bar.networkKind : "disconnected" readonly property string label: bar ? bar.networkLabel : "" readonly property int signalStrength: bar ? bar.networkSignal : -1 readonly property string icon: { if (kind === "wifi") { var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"] var index = Math.max(0, Math.min(4, Math.ceil(signalStrength / 20) - 1)) return icons[index] } if (kind === "ethernet") return "󰈀" return "󰤮" } function refresh() { if (!detailsProc.running) detailsProc.running = true if (!wifiProc.running) { scanning = true wifiProc.running = true } } function formatSpeed(mbps) { var v = parseInt(mbps, 10) if (!v || v < 0) return "" if (v >= 1000) return (v / 1000).toFixed(v % 1000 === 0 ? 0 : 1) + " Gbps" return v + " Mbps" } function formatFreq(mhz) { var v = parseFloat(mhz) if (!v) return "" return (v / 1000).toFixed(1) + " GHz" } function updateDetails(raw) { var next = {} var lines = String(raw || "").split("\n") for (var i = 0; i < lines.length; i++) { var line = lines[i] if (!line) continue var idx = line.indexOf("\t") if (idx === -1) continue next[line.substring(0, idx)] = line.substring(idx + 1).trim() } info = next } function updateWifi(raw) { var lines = String(raw || "").split("\n") var hasStation = false var nets = [] for (var i = 0; i < lines.length; i++) { var line = lines[i].trim() if (!line) continue if (line === "STATION_AVAILABLE") { hasStation = true; continue } // Format: connectedssidsignalsecurity var parts = line.split("\t") if (parts.length < 3) continue nets.push({ connected: parts[0] === "1", ssid: parts[1], signal: parseInt(parts[2], 10) || 0, security: parts[3] || "" }) } nets.sort(function(a, b) { if (a.connected !== b.connected) return a.connected ? -1 : 1 return b.signal - a.signal }) wifiNetworks = nets wifiStationAvailable = hasStation scanning = false } function wifiIconFor(strength) { var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"] var index = Math.max(0, Math.min(4, Math.ceil(strength / 20) - 1)) return icons[index] } implicitWidth: button.implicitWidth implicitHeight: button.implicitHeight Component.onCompleted: refresh() // Pulls everything we want about the active route's interface in one shot. Process { id: detailsProc command: ["bash", "-c", ` route_json=$(ip -j route get 1.1.1.1 2>/dev/null) [[ -z $route_json ]] && exit 0 iface=$(echo "$route_json" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d[0].get("dev",""))' 2>/dev/null) gw=$(echo "$route_json" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d[0].get("gateway",""))' 2>/dev/null) src=$(echo "$route_json" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d[0].get("prefsrc",""))' 2>/dev/null) [[ -z $iface ]] && exit 0 prefix=$(ip -j addr show "$iface" 2>/dev/null | python3 -c 'import sys,json; d=json.load(sys.stdin); ai=d[0].get("addr_info",[]); print(next((str(a.get("prefixlen","")) for a in ai if a.get("family")=="inet"),""))' 2>/dev/null) printf 'iface\\t%s\\n' "$iface" printf 'ip\\t%s\\n' "$src" printf 'prefix\\t%s\\n' "$prefix" printf 'gateway\\t%s\\n' "$gw" if [[ -d /sys/class/net/$iface/wireless ]]; then printf 'type\\twifi\\n' if command -v iw >/dev/null; then link=$(iw dev "$iface" link 2>/dev/null) [[ -n $link ]] && { printf 'ssid\\t%s\\n' "$(echo "$link" | awk '/SSID:/ { sub(/.*SSID: /, ""); print; exit }')" printf 'signal_dbm\\t%s\\n' "$(echo "$link" | awk '/signal:/ { print $2; exit }')" printf 'freq\\t%s\\n' "$(echo "$link" | awk '/freq:/ { print $2; exit }')" printf 'bitrate\\t%s %s\\n' "$(echo "$link" | awk '/tx bitrate:/ { print $3; exit }')" "$(echo "$link" | awk '/tx bitrate:/ { print $4; exit }')" } fi else printf 'type\\tethernet\\n' [[ -r /sys/class/net/$iface/speed ]] && printf 'speed\\t%s\\n' "$(cat /sys/class/net/$iface/speed)" [[ -r /sys/class/net/$iface/duplex ]] && printf 'duplex\\t%s\\n' "$(cat /sys/class/net/$iface/duplex)" fi `] stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateDetails(text) } } // Wi-Fi scan via iwctl. Emits a STATION_AVAILABLE marker followed by TSV // rows (connected, ssid, signal_pct, security). Strip ANSI escapes early — // iwctl's table layout starts each row with one, which breaks naive parsing. Process { id: wifiProc command: ["bash", "-c", ` station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }') [[ -z $station ]] && exit 0 echo STATION_AVAILABLE iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\ | sed -e 's/\\x1b\\[[0-9;]*m//g' \\ | awk ' NR <= 4 { next } /^[[:space:]]*$/ { next } { connected = (substr($0, 1, 4) ~ />/) ? 1 : 0 line = $0 sub(/^[[:space:]]*>?[[:space:]]+/, "", line) sub(/[[:space:]]+$/, "", line) n = split(line, parts, /[[:space:]]{2,}/) if (n < 3) next ssid = parts[1] security = parts[n-1] dbm = parts[n] / 100 signal = (dbm >= -50) ? 100 : (dbm <= -100) ? 0 : int(2 * (dbm + 100)) printf "%d\\t%s\\t%d\\t%s\\n", connected, ssid, signal, security }' `] stdout: StdioCollector { waitForEnd: true onStreamFinished: root.updateWifi(text) } } Process { id: actionProc } Common.WidgetButton { id: button anchors.fill: parent bar: root.bar text: root.icon horizontalMargin: 8.5 rightExtraMargin: 2 tooltipText: bar ? bar.networkTooltip() : "" onPressed: function(b) { if (b === Qt.RightButton) root.bar.run(root.bar.omarchyPath + "/bin/omarchy-launch-wifi") else { root.popupOpen = !root.popupOpen if (root.popupOpen) root.refresh() } } } Common.PopupCard { anchorItem: button owner: root bar: root.bar open: root.popupOpen contentWidth: 340 contentHeight: column.implicitHeight + 28 Column { id: column anchors.fill: parent spacing: 12 // Header — interface name + type, refresh on the right. Item { width: parent.width height: Math.max(headerInfo.implicitHeight, refreshBtn.implicitHeight) Row { id: headerInfo anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter spacing: 10 Text { text: root.icon color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: 22 anchors.verticalCenter: parent.verticalCenter } Column { spacing: 2 anchors.verticalCenter: parent.verticalCenter Text { text: root.info.iface || (root.kind === "disconnected" ? "Disconnected" : "No connection") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: 13 font.bold: true } Text { text: { if (root.info.type === "wifi") { var s = root.info.ssid || "Wi-Fi" if (root.info.freq) s += " · " + root.formatFreq(root.info.freq) return s } if (root.info.type === "ethernet") return "Ethernet" return "" } visible: text !== "" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: 10 } } } Common.PillButton { id: refreshBtn anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter iconText: "󰑐" tooltipText: "Refresh" tooltipBackground: root.bar.background tooltipForeground: root.bar.foreground foreground: root.bar.foreground horizontalPadding: 8 verticalPadding: 4 iconSize: 14 active: root.scanning onClicked: root.refresh() } } Rectangle { visible: !!root.info.iface width: parent.width height: 1 color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12) } // Connection details: IP, gateway, link speed, etc. Grid { visible: !!root.info.iface width: parent.width columns: 2 columnSpacing: 14 rowSpacing: 4 // IP address. Text { visible: !!root.info.ip text: "IP address" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: 11 } Text { visible: !!root.info.ip text: (root.info.ip || "") + (root.info.prefix ? "/" + root.info.prefix : "") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: 11 } // Gateway. Text { visible: !!root.info.gateway text: "Gateway" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: 11 } Text { visible: !!root.info.gateway text: root.info.gateway || "" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: 11 } // Ethernet link speed / duplex. Text { visible: root.info.type === "ethernet" && !!root.info.speed text: "Link" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: 11 } Text { visible: root.info.type === "ethernet" && !!root.info.speed text: root.formatSpeed(root.info.speed || "") + (root.info.duplex ? " · " + root.info.duplex + " duplex" : "") color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: 11 } // Wi-Fi signal. Text { visible: root.info.type === "wifi" && !!root.info.signal_dbm text: "Signal" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: 11 } Text { visible: root.info.type === "wifi" && !!root.info.signal_dbm text: (root.info.signal_dbm || "") + " dBm" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: 11 } // Wi-Fi tx bitrate. Text { visible: root.info.type === "wifi" && !!root.info.bitrate text: "Link rate" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: 11 } Text { visible: root.info.type === "wifi" && !!root.info.bitrate text: root.info.bitrate || "" color: root.bar.foreground font.family: root.bar.fontFamily font.pixelSize: 11 } } // Wi-Fi networks (only if a Wi-Fi station is available). Rectangle { visible: root.wifiStationAvailable width: parent.width height: 1 color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12) } Text { visible: root.wifiStationAvailable text: root.scanning ? "Scanning Wi-Fi…" : "Wi-Fi networks" color: Qt.darker(root.bar.foreground, 1.4) font.family: root.bar.fontFamily font.pixelSize: 10 font.bold: true } // Scrollable network list — cap the height so a busy neighbourhood // doesn't push the popup off-screen. Bluetooth panel uses the same // pattern. Flickable { visible: root.wifiStationAvailable width: parent.width height: Math.min(networkList.implicitHeight, 240) contentWidth: width contentHeight: networkList.implicitHeight clip: true boundsBehavior: Flickable.StopAtBounds ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } Column { id: networkList width: parent.width spacing: 6 Repeater { model: root.wifiStationAvailable ? root.wifiNetworks : [] Common.PillButton { required property var modelData width: networkList.width leftAlign: true iconText: root.wifiIconFor(modelData.signal) text: (modelData.ssid || "Hidden") + (modelData.security ? " · " + modelData.security : "") foreground: root.bar.foreground horizontalPadding: 10 verticalPadding: 6 active: modelData.connected onClicked: { if (modelData.connected) return if (actionProc.running) return // Known networks (and open networks) can connect non-interactively // via iwctl. Anything else needs Impala for passphrase entry. var ssid = root.bar.shellQuote(modelData.ssid) var security = root.bar.shellQuote(modelData.security || "") var omarchyPath = root.bar.omarchyPath actionProc.command = ["bash", "-c", ` ssid=${ssid} security=${security} station=$(iwctl station list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk '/^[[:space:]]*wl/ { print $1; exit }') known=$(iwctl known-networks list 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g' | awk -v s="$ssid" 'NR > 3 { line = $0 sub(/^[[:space:]]+/, "", line) sub(/[[:space:]]+$/, "", line) n = split(line, p, /[[:space:]]{2,}/) if (n >= 1 && p[1] == s) { print "yes"; exit } }') if [[ -n $station && ( -n $known || $security == "open" ) ]]; then iwctl --dont-ask station "$station" connect "$ssid" else ${omarchyPath}/bin/omarchy-launch-wifi fi `] actionProc.running = true root.popupOpen = false } } } } } Common.PillButton { visible: root.wifiStationAvailable width: parent.width iconText: "󰖩" text: "Open Wi-Fi manager (Impala)" foreground: root.bar.foreground horizontalPadding: 10 verticalPadding: 6 onClicked: { root.bar.run(root.bar.omarchyPath + "/bin/omarchy-launch-wifi"); root.popupOpen = false } } } } }