From 96c5e048cb080bfc9f67593be7b2908907bc5064 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Fri, 15 May 2026 02:40:26 -0400 Subject: [PATCH] Drive Wi-Fi widget from iwctl with scrollable network list --- .../omarchy-shell/plugins/bar/Bar.qml | 40 ++++-- .../plugins/bar/common/PillButton.qml | 6 +- .../plugins/bar/widgets/networkPanel.qml | 135 +++++++++++------- 3 files changed, 114 insertions(+), 67 deletions(-) diff --git a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml index 45f00596..23a9df35 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/Bar.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/Bar.qml @@ -367,22 +367,32 @@ Item { function networkCommand() { return [ - "route_device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == \"dev\") { print $(i + 1); exit } }')", - "connection_name() { command -v nmcli >/dev/null 2>&1 && nmcli -t -f GENERAL.CONNECTION device show \"$1\" 2>/dev/null | awk -F: '$1 == \"GENERAL.CONNECTION\" { print $2; exit }'; }", - "wifi_details() { command -v nmcli >/dev/null 2>&1 && nmcli -t -f IN-USE,SIGNAL,FREQ device wifi list --rescan no 2>/dev/null | awk -F: '$1 == \"*\" { print $2 \"\\t\" $3; exit }'; }", - "if [[ -n $route_device && ! -d /sys/class/net/$route_device/wireless ]]; then", - " label=$(connection_name \"$route_device\"); label=${label:-$route_device}", - " printf 'ethernet\\t%s\\t\\t\\n' \"$label\"", - "elif [[ -n $route_device ]]; then", - " label=$(connection_name \"$route_device\"); label=${label:-$route_device}", - " details=$(wifi_details)", - " signal=${details%%$'\\t'*}", - " frequency=${details#*$'\\t'}", - " [[ $frequency == \"$details\" ]] && frequency=\"\"", - " printf 'wifi\\t%s\\t%s\\t%s\\n' \"$label\" \"$signal\" \"$frequency\"", - "else", + "device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == \"dev\") { print $(i + 1); exit } }')", + "if [[ -z $device ]]; then", " printf 'disconnected\\t\\t\\t\\n'", - "fi" + " exit 0", + "fi", + "if [[ ! -d /sys/class/net/$device/wireless ]]; then", + " printf 'ethernet\\t%s\\t\\t\\n' \"$device\"", + " exit 0", + "fi", + "show=$(iwctl station \"$device\" show 2>/dev/null | sed -e 's/\\x1b\\[[0-9;]*m//g')", + "state=$(awk '/^[[:space:]]*State[[:space:]]/ { sub(/.*State[[:space:]]+/, \"\"); sub(/[[:space:]]+$/, \"\"); print; exit }' <<<\"$show\")", + "ssid=$(awk '/^[[:space:]]*Connected network[[:space:]]/ { sub(/.*Connected network[[:space:]]+/, \"\"); sub(/[[:space:]]+$/, \"\"); print; exit }' <<<\"$show\")", + "freq=$(awk '/^[[:space:]]*Frequency[[:space:]]/ { sub(/.*Frequency[[:space:]]+/, \"\"); sub(/[[:space:]]+$/, \"\"); print; exit }' <<<\"$show\")", + "rssi=$(awk '/^[[:space:]]*RSSI[[:space:]]/ { sub(/.*RSSI[[:space:]]+/, \"\"); sub(/[[:space:]]+$/, \"\"); print; exit }' <<<\"$show\")", + "dbm=${rssi%% *}", + "signal=\"\"", + "if [[ -n $dbm ]]; then", + " if (( dbm >= -50 )); then signal=100", + " elif (( dbm <= -100 )); then signal=0", + " else signal=$(( 2 * (dbm + 100) )); fi", + "fi", + "if [[ -n $state && $state != connected ]]; then", + " printf 'disconnected\\t\\t\\t\\n'", + " exit 0", + "fi", + "printf 'wifi\\t%s\\t%s\\t%s\\n' \"${ssid:-$device}\" \"$signal\" \"$freq\"" ].join("\n") } diff --git a/default/quickshell/omarchy-shell/plugins/bar/common/PillButton.qml b/default/quickshell/omarchy-shell/plugins/bar/common/PillButton.qml index 17b05fad..21fdf5d3 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/common/PillButton.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/common/PillButton.qml @@ -19,6 +19,7 @@ Rectangle { property real horizontalPadding: 10 property real verticalPadding: 6 property bool active: false + property bool leftAlign: false property color activeBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18) ToolTip { @@ -61,7 +62,10 @@ Rectangle { Row { id: row - anchors.centerIn: parent + anchors.verticalCenter: parent.verticalCenter + anchors.left: root.leftAlign ? parent.left : undefined + anchors.leftMargin: root.leftAlign ? root.horizontalPadding : 0 + anchors.horizontalCenter: root.leftAlign ? undefined : parent.horizontalCenter spacing: 8 Text { diff --git a/default/quickshell/omarchy-shell/plugins/bar/widgets/networkPanel.qml b/default/quickshell/omarchy-shell/plugins/bar/widgets/networkPanel.qml index febca2f3..47f45a98 100644 --- a/default/quickshell/omarchy-shell/plugins/bar/widgets/networkPanel.qml +++ b/default/quickshell/omarchy-shell/plugins/bar/widgets/networkPanel.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Controls import Quickshell import Quickshell.Io import "../common" as Common @@ -143,36 +144,33 @@ fi } } - // Wi-Fi scan via nmcli OR iwctl, whichever is available. Emits TSV plus - // a STATION_AVAILABLE marker so the panel knows whether to render the list. + // 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", ` -if command -v nmcli >/dev/null; then - echo STATION_AVAILABLE - nmcli -t -f IN-USE,SSID,SIGNAL,SECURITY device wifi list --rescan no 2>/dev/null \ - | awk -F: '{ printf "%s\\t%s\\t%s\\t%s\\n", ($1=="*" ? "1" : "0"), $2, $3, $4 }' -elif command -v iwctl >/dev/null; then - station=$(iwctl station list 2>/dev/null | sed -n 's/^[[:space:]]*\\(wl[a-z0-9]*\\)[[:space:]].*/\\1/p' | head -1) - [[ -n $station ]] && { - echo STATION_AVAILABLE - iwctl station "$station" get-networks 2>/dev/null \ - | sed -e 's/\\x1b\\[[0-9;]*m//g' \ - | awk 'NR > 4 && NF >= 2 { - connected = ($0 ~ /^>/) ? 1 : 0 - line = $0 - sub(/^[ >]+/, "", line) - # Last two columns are security and signal indicator (asterisks) - n = split(line, fields, /[ \\t]{2,}/) - ssid = fields[1] - security = (n >= 2) ? fields[2] : "" - signal_marks = (n >= 3) ? fields[3] : "" - gsub(/[^*]/, "", signal_marks) - signal_pct = length(signal_marks) * 25 - printf "%d\\t%s\\t%d\\t%s\\n", connected, ssid, signal_pct, security - }' - } -fi +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 @@ -390,34 +388,69 @@ fi font.bold: true } - Repeater { - model: root.wifiStationAvailable ? root.wifiNetworks.slice(0, 6) : [] + // 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 - Common.PillButton { - required property var modelData + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + Column { + id: networkList width: parent.width - iconText: root.wifiIconFor(modelData.signal) - text: (modelData.ssid || "Hidden") + (modelData.security ? " · " + modelData.security : "") - foreground: root.bar.foreground - horizontalPadding: 10 - verticalPadding: 6 - active: modelData.connected + spacing: 6 - onClicked: { - if (modelData.connected) return - if (actionProc.running) return - if (Qt.platform.os) { /* keep linter happy */ } - // Prefer nmcli; fall back to iwctl. Both work as one-shot connects - // for open or already-known networks; secured networks pop the TUI. - actionProc.command = ["bash", "-c", - "if command -v nmcli >/dev/null; then" + - " nmcli device wifi connect " + root.bar.shellQuote(modelData.ssid) + - "; else" + - " " + root.bar.omarchyPath + "/bin/omarchy-launch-wifi" + - "; fi"] - actionProc.running = true - root.popupOpen = false + 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 + } + } } } }