#!/bin/bash # omarchy:summary=Show or pin the Wi-Fi band for the active connection # omarchy:group=network # omarchy:args=[auto|2.4|5|6] # omarchy:examples=omarchy network band | omarchy network band 5 | omarchy network band auto set -euo pipefail # NetworkManager 1.44+ accepts a third band value, so 5GHz and 6GHz can be # pinned apart. Pinning the band rather than a BSSID keeps working when the AP # rotates BSSIDs, and leaves roaming between APs intact. nm_band_for() { case "$1" in 2.4) echo "bg" ;; 5) echo "a" ;; 6) echo "6GHz" ;; *) return 1 ;; esac } band_from_nm() { case "$1" in bg) echo "2.4" ;; a) echo "5" ;; 6GHz) echo "6" ;; *) echo "auto" ;; esac } # Accepts either nmcli's "2412 MHz" or iw's "5745.0": keep the leading digits # and ignore whatever unit or fraction follows. The boundaries mirror Model.js # formatHeaderFreq so the panel's label and this command cannot disagree. band_for_freq() { local mhz=${1%%[!0-9]*} [[ -n $mhz ]] || return 1 if ((mhz >= 2400 && mhz < 2500)); then echo "2.4" elif ((mhz >= 4900 && mhz < 5925)); then echo "5" elif ((mhz >= 5925 && mhz < 7125)); then echo "6" else return 1 fi } # Every value read back from nmcli goes through here. LC_ALL=C because nmcli # translates state words such as "connected", which would silently stop matching # under a non-English session; `-e no` because `-g` otherwise escapes ':' and # '\' in values, and an escaped SSID would never match iw's raw one. nm_get() { LC_ALL=C nmcli -e no -g "$@" 2>/dev/null } wifi_device() { nm_get DEVICE,TYPE,STATE device status | awk -F: '$2 == "wifi" && $3 == "connected" { print $1; exit }' } wifi_profile() { nm_get GENERAL.CONNECTION device show "$1" } selected_band() { band_from_nm "$(nm_get 802-11-wireless.band connection show "$1")" } # Sets $ssid and $freq from one `iw dev link`. iw prints the SSID as # the rest of its line, so an SSID containing ':' needs no special handling. read_link() { local link link=$(iw dev "$1" link 2>/dev/null) ssid=$(awk '/SSID:/ { sub(/.*SSID: /, ""); print; exit }' <<<"$link") freq=$(awk '/freq:/ { print $2; exit }' <<<"$link") } # Every band the SSID is reachable on, low to high, always including the one # already in use -- a weak radio gets missed by plenty of scans, and the band we # are sitting on must never be absent from its own list of options. A band the # AP does not answer on is never offered: pinning to it would drop the # connection with nothing to reassociate to. # # --rescan no reads NetworkManager's cache, which the panel's own scanner keeps # warm; forcing a scan here would stall every poll. The kernel's own cache (iw # scan dump) is pruned far harder and would make bands flicker. available_bands() { local device=$1 ssid=$2 current=$3 { if [[ -n $current ]]; then echo "$current"; fi # SSID is queried last so one containing ':' can be reassembled verbatim. # It reaches awk through the environment, not -v, which would expand # backslash escapes in an SSID that contains one. nm_get FREQ,SSID dev wifi list ifname "$device" --rescan no | want="$ssid" awk -F: ' BEGIN { want = ENVIRON["want"] } { name = $2 for (i = 3; i <= NF; i++) name = name ":" $i if (name == want) print $1 }' | while read -r freq; do band_for_freq "$freq" || true; done } | sort -u -g | tr '\n' ' ' | sed 's/ $//' } print_status() { local device profile band available device=$(wifi_device) [[ -n $device ]] || return 0 read_link "$device" [[ -n $ssid ]] || return 0 band=$(band_for_freq "$freq" || true) available=$(available_bands "$device" "$ssid" "$band") profile=$(wifi_profile "$device") printf 'band\t%s\n' "$band" printf 'available\t%s\n' "$available" if [[ -n $profile ]]; then printf 'selected\t%s\n' "$(selected_band "$profile")"; fi } set_band() { local target=$1 local device profile previous desired device=$(wifi_device) if [[ -z $device ]]; then echo "Error: no connected Wi-Fi device." >&2 exit 1 fi profile=$(wifi_profile "$device") if [[ -z $profile ]]; then echo "Error: no active Wi-Fi connection profile." >&2 exit 1 fi if [[ $target == "auto" ]]; then desired="" else read_link "$device" if [[ " $(available_bands "$device" "$ssid" "$(band_for_freq "$freq" || true)") " != *" $target "* ]]; then echo "Error: ${target}GHz is not available on this network." >&2 exit 1 fi desired=$(nm_band_for "$target") fi previous=$(nm_get 802-11-wireless.band connection show "$profile") [[ $previous == "$desired" ]] && exit 0 nmcli connection modify "$profile" 802-11-wireless.band "$desired" >/dev/null # A band change only takes effect on reassociation. If the radio cannot come # back up on the requested band, put the previous setting back and reconnect # rather than leaving the machine stranded offline. if ! nmcli connection up "$profile" >/dev/null 2>&1; then nmcli connection modify "$profile" 802-11-wireless.band "$previous" >/dev/null nmcli connection up "$profile" >/dev/null 2>&1 || true echo "Error: could not connect on ${target}; reverted to previous band." >&2 exit 1 fi } usage() { echo "Usage: omarchy-network-band [auto|2.4|5|6]" >&2 } if (($# == 0)); then print_status exit 0 fi if (($# > 1)); then usage exit 1 fi case "$1" in auto | 2.4 | 5 | 6) set_band "$1" ;; *) usage exit 1 ;; esac