rofi/scripts/rofi-wifi 7.8 K raw
1
#!/bin/sh
2
# Requires: rofi, libnotify, iw, wpa_supplicant, udhcpc, openrc, doas
3
# WiFi menu: scan, connect, disconnect. This is the rofi rewrite of the old
4
# dm-wifi-connect, with two changes worth knowing about — the passphrase goes
5
# through `rofi -password` instead of the black-on-black dmenu trick, and a
6
# new network is merged into wpa_supplicant.conf beside the ones already
7
# there rather than replacing them, so the machine can still roam back to a
8
# network it has seen before.
9
#
10
# Prompting runs as the calling user so rofi keeps the X session and
11
# notifications keep the user's bus; only scanning, the config write and the
12
# bring-up are handed to doas.
13
set -u
14
15
CONF=/etc/wpa_supplicant/wpa_supplicant.conf
16
NOTIFY_ID=7301
17
18
menu() { rofi -dmenu -i "$@"; }
19
20
notify() {
21
    # dunstify can replace an earlier notification in place, so the progress
22
    # line becomes the result line instead of stacking up.
23
    if command -v dunstify >/dev/null 2>&1; then
24
        dunstify -r "$NOTIFY_ID" ${2:+-u "$2"} "WiFi" "$1"
25
    else
26
        notify-send ${2:+-u "$2"} "WiFi" "$1"
27
    fi
28
}
29
30
fail() { notify "$1" critical; exit 1; }
31
32
# doas asks for a passphrase on a tty, and launched from a keybinding there
33
# is none — fail with something readable instead of hanging invisibly.
34
doas -n true 2>/dev/null || fail "doas needs a password; add a nopass rule for this user."
35
36
# Ask the kernel which interfaces are wireless rather than assuming wlan0.
37
IFACES=$(iw dev | awk '$1 == "Interface" { print $2 }')
38
case $(printf '%s' "$IFACES" | grep -c .) in
39
    0) fail "No wireless interfaces found." ;;
40
    1) IFACE=$IFACES ;;
41
    *) IFACE=$(printf '%s\n' "$IFACES" | menu -l 5 -p "interface") ;;
42
esac
43
[ -n "$IFACE" ] || exit 1
44
45
link_ssid() { iw dev "$IFACE" link 2>/dev/null | sed -n 's/^[[:space:]]*SSID: //p'; }
46
ipv4() { ip -4 addr show "$IFACE" 2>/dev/null | awk '/inet /{ print $2; exit }'; }
47
48
saved_ssids() { doas -n grep -o 'ssid="[^"]*"' "$CONF" 2>/dev/null | sed 's/^ssid="//; s/"$//'; }
49
50
disconnect() {
51
    notify "󰤬  Disabling $IFACE..."
52
    doas sh -c '
53
        # wpa_supplicant runs under supervise-daemon, which respawns the
54
        # daemon a couple of seconds after any kill. Stopping the service is
55
        # what takes the supervisor down with it; pkill only kills the child.
56
        rc-service -s -q wpa_supplicant stop
57
        ifdown "$1" 2>/dev/null
58
59
        PIDFILE=/var/run/udhcpc.$1.pid
60
        if [ -f "$PIDFILE" ]; then
61
            kill "$(cat "$PIDFILE")" 2>/dev/null
62
            rm -f "$PIDFILE"
63
        fi
64
65
        ip addr flush dev "$1" 2>/dev/null
66
        ip link set "$1" down
67
    ' _ "$IFACE"
68
    notify "󰤭  $IFACE disabled"
69
    exit 0
70
}
71
72
bring_up() {
73
    ssid=$1
74
    notify "󰤨  Connecting to $ssid..."
75
    doas sh -c '
76
        # wpa_supplicant will not reread its config on its own, so restart it
77
        # rather than signalling it. This also covers the case where the link
78
        # was disabled outright. ifup is no help: wlan0 has no stanza in
79
        # /etc/network/interfaces.
80
        rc-service wpa_supplicant restart >/dev/null 2>&1
81
82
        # Give the supplicant time to associate before asking for a lease — a
83
        # DHCP request sent before association just burns its whole timeout.
84
        i=0
85
        while [ "$i" -lt 20 ]; do
86
            iw dev "$1" link 2>/dev/null | grep -q "^Connected to" && break
87
            sleep 1
88
            i=$((i + 1))
89
        done
90
91
        # Replace any DHCP client left over from a previous connect; without
92
        # this they stack up, one per run, all renewing the same lease.
93
        PIDFILE=/var/run/udhcpc.$1.pid
94
        if [ -f "$PIDFILE" ]; then
95
            kill "$(cat "$PIDFILE")" 2>/dev/null
96
            rm -f "$PIDFILE"
97
        fi
98
        ip addr flush dev "$1" 2>/dev/null
99
100
        # Same invocation ifupdown-ng uses for a dhcp stanza. udhcpc
101
        # daemonises once it has the lease, so this returns with the address
102
        # already set.
103
        udhcpc -b -R -p "$PIDFILE" -i "$1" -x "hostname:$(hostname)" >/dev/null 2>&1
104
    ' _ "$IFACE"
105
106
    if [ -n "$(ipv4)" ]; then
107
        notify "󰤨  $ssid on $IFACE — $(ipv4)"
108
    else
109
        fail "󰤮  Failed to connect to $ssid"
110
    fi
111
    exit 0
112
}
113
114
connect() {
115
    ssid=$1
116
117
    # A network already in the config has a working passphrase on file; only
118
    # ask again if there is nothing saved for it.
119
    if saved_ssids | grep -Fxq "$ssid"; then
120
        bring_up "$ssid"
121
    fi
122
123
    pass=$(rofi -dmenu -password -p "password for $ssid" -mesg "Leave empty for an open network" -l 0 </dev/null) || exit 0
124
125
    # Build the new stanza before touching anything: a rejected passphrase
126
    # must not disturb the config that is currently working. The PSK never
127
    # lands on disk unprivileged — it goes to the root shell over a pipe.
128
    if [ -z "$pass" ]; then
129
        NEW=$(printf 'network={\n\tssid="%s"\n\tkey_mgmt=NONE\n}\n' "$ssid")
130
    else
131
        NEW=$(wpa_passphrase "$ssid" "$pass" 2>/dev/null | grep -v '^[[:space:]]*#psk=') ||
132
            fail "Rejected by wpa_passphrase — password must be 8-63 characters."
133
    fi
134
    [ -n "$NEW" ] || fail "Could not generate a config for $ssid."
135
136
    # Merge: keep the global settings and every other network block, drop
137
    # any earlier block for this SSID, then append the new one. awk does the
138
    # dropping because a network={...} block spans lines; its program travels
139
    # as an argument so it needs no escaping inside the root shell.
140
    DROP_BLOCK='
141
        /^[[:space:]]*network[[:space:]]*=[[:space:]]*\{/ {
142
            block = $0 ORS; inblock = 1; matched = 0; next
143
        }
144
        inblock {
145
            block = block $0 ORS
146
            if (index($0, "ssid=\"" ssid "\"")) matched = 1
147
            if ($0 ~ /^[[:space:]]*\}/) {
148
                if (!matched) printf "%s", block
149
                inblock = 0
150
            }
151
            next
152
        }
153
        { print }
154
    '
155
156
    printf '%s\n' "$NEW" | doas sh -c '
157
        CONF=$1; SSID=$2; PROG=$3
158
        NEW=$(cat)
159
        [ -f "$CONF" ] && cp "$CONF" "$CONF.bak"
160
        {
161
            [ -f "$CONF.bak" ] && awk -v ssid="$SSID" "$PROG" "$CONF.bak"
162
            printf "%s\n" "$NEW"
163
        } > "$CONF"
164
        chmod 600 "$CONF"
165
    ' _ "$CONF" "$ssid" "$DROP_BLOCK" || fail "Could not write $CONF."
166
167
    bring_up "$ssid"
168
}
169
170
# --- menu -------------------------------------------------------------------
171
172
CURRENT=$(link_ssid)
173
IP=$(ipv4)
174
if [ -n "$CURRENT" ]; then
175
    STATUS="󰤨  connected to $CURRENT${IP:+ — $IP}"
176
else
177
    STATUS="󰤭  not connected on $IFACE"
178
fi
179
180
doas ip link set "$IFACE" up 2>/dev/null
181
notify "󰤬  Scanning for networks on $IFACE..."
182
183
# `scan dump` reuses cached results if the card is busy (already associated),
184
# which is when a live scan tends to fail.
185
SCAN=$(doas iw dev "$IFACE" scan 2>/dev/null || doas iw dev "$IFACE" scan dump 2>/dev/null)
186
187
# Only real SSID lines: bare "SSID: " is a hidden network, and "SSID List"
188
# under extended capabilities is not a network at all.
189
SSIDS=$(printf '%s\n' "$SCAN" | sed -n 's/^[[:space:]]*SSID: \(..*\)$/\1/p' | sort -u)
190
191
SAVED=$(saved_ssids)
192
LIST=$(printf '%s\n' "$SSIDS" | awk -v cur="$CURRENT" -v saved="$SAVED" '
193
    BEGIN { n = split(saved, s, "\n"); for (i = 1; i <= n; i++) known[s[i]] = 1 }
194
    NF {
195
        # 󰤪 marks a network this machine already has credentials for, 󰄬 the
196
        # one it is on right now.
197
        icon = ($0 == cur) ? "󰄬" : (known[$0] ? "󰤪" : "󰤟")
198
        printf "%s  %s\n", icon, $0
199
    }')
200
201
ACTIONS="󰑐  rescan
202
󰛳  hidden network"
203
[ -n "$CURRENT" ] && ACTIONS="$ACTIONS
204
󰤮  disconnect"
205
206
CHOICE=$(printf '%s\n%s\n' "$LIST" "$ACTIONS" | grep -v '^$' | menu -l 12 -p "wifi" -mesg "$STATUS") || exit 0
207
[ -n "$CHOICE" ] || exit 0
208
209
case ${CHOICE##*  } in
210
    rescan)           exec "$0" ;;
211
    disconnect)       disconnect ;;
212
    "hidden network") ssid=$(menu -p "ssid" -l 0 </dev/null); [ -n "$ssid" ] && connect "$ssid" ;;
213
    *)                connect "${CHOICE##*  }" ;;
214
esac