diff --git a/pkgs/sane-scripts/default.nix b/pkgs/sane-scripts/default.nix index 44c7e946..e5678820 100644 --- a/pkgs/sane-scripts/default.nix +++ b/pkgs/sane-scripts/default.nix @@ -95,12 +95,11 @@ let }; }; - patchPhase = '' - # remove python scripts (we package them further below) - rm sane-bt-search - rm sane-date-math - rm sane-reclaim-boot-space - ''; + # remove python scripts (we package them further below) + patchPhase = builtins.concatStringsSep + "\n" + (lib.mapAttrsToList (name: pkg: "rm ${pkg.pname}") py-scripts) + ; installPhase = '' mkdir -p $out/bin @@ -108,24 +107,29 @@ let ''; }; - bt-search = static-nix-shell.mkPython3Bin { - pname = "sane-bt-search"; - src = ./src; - pyPkgs = [ "natsort" "requests" ]; + py-scripts = { + bt-search = static-nix-shell.mkPython3Bin { + pname = "sane-bt-search"; + src = ./src; + pyPkgs = [ "natsort" "requests" ]; + }; + date-math = static-nix-shell.mkPython3Bin { + pname = "sane-date-math"; + src = ./src; + }; + reclaim-boot-space = static-nix-shell.mkPython3Bin { + pname = "sane-reclaim-boot-space"; + src = ./src; + }; + ip-reconnect = static-nix-shell.mkPython3Bin { + pname = "sane-ip-reconnect"; + src = ./src; + }; }; - date-math = static-nix-shell.mkPython3Bin { - pname = "sane-date-math"; - src = ./src; - }; - reclaim-boot-space = static-nix-shell.mkPython3Bin { - pname = "sane-reclaim-boot-space"; - src = ./src; - }; - in symlinkJoin { name = "sane-scripts"; - paths = [ shell-scripts bt-search date-math reclaim-boot-space ]; + paths = [ shell-scripts ] ++ lib.attrValues py-scripts; meta = { description = "collection of scripts associated with uninsane systems"; homepage = "https://git.uninsane.org"; diff --git a/pkgs/sane-scripts/src/sane-ip-reconnect b/pkgs/sane-scripts/src/sane-ip-reconnect index 73ea6540..ecf2940f 100755 --- a/pkgs/sane-scripts/src/sane-ip-reconnect +++ b/pkgs/sane-scripts/src/sane-ip-reconnect @@ -1,20 +1,96 @@ -#!/usr/bin/env bash +#!/usr/bin/env nix-shell +#!nix-shell -i python3 -p "python3.withPackages (ps: [ ])" -# reconnect to best wifi network. -# lappy frequently DCs from the ideal network +import re +import subprocess +import time -set -ex +def rm_color(stdout: str) -> str: + " remove terminal control codes -- used by iwctl to colorize the output " + return re.sub("\\[[0-9;]*m", "", stdout) -sudo iwctl station wlan0 scan -sleep 5 +def rm_heading(stdout: str) -> str: + return "\n".join(stdout.split("\n")[4:]) -# get networks. remove control characters (colors), then leading info, then take the top-rated network -networks=$(iwctl station wlan0 get-networks rssi-dbms | sed 's/\[[0-9;]*m//g' | sed 's/ [ >]*/ /g' | sed 's/^ //' | tail -n +5) +def extract_columns(stdout: str) -> list: + " split each line into two fields " + lines = stdout.split("\n") + items = [] + for l in lines: + # XXX: this will fail for non-psk networks + # but i don't split on space because that would fail for networks that have names with spaces + if " psk " not in l: continue + split_at = l.find(" psk ") + first, second = l[:split_at], l[split_at+5:] + first, second = first.strip(), second.strip() + if first.startswith('> '): + # the `>` is formatting, indicating that it's the active network + first = first[2:].strip() + items.append((first, second)) + return items -strengths=$(echo "$networks" | grep -o '\-[0-9][0-9]* *$') -best_strength=$(echo "$strengths" | sort -h | tail -n 1) -best_line=$(echo "$networks" | grep -- "$best_strength$") -# network names could have spaces in them if someone's evil, so rather than `cut`, we trim the `psk` and `db` columnds -best_network=$(echo "$best_line" | sed 's/ [a-z][a-z]* -[0-9][0-9]* *$//g') +def iwctl(args: list, sudo: bool = False) -> str: + cmd = [ "iwctl" ] + args + if sudo: + cmd = [ "sudo" ] + cmd + res = subprocess.run(cmd, capture_output=True) + if res.returncode != 0: + print(f"iwctl failed:\n{res.stderr}") + res.check_returncode() # raise + return res.stdout.decode() -sudo iwctl station wlan0 connect "$best_network" +def scan(): + iwctl(["station", "wlan0", "scan"], sudo=True) + time.sleep(5) # give time for adapter to see networks + +def get_known() -> list: + stdout = iwctl(["known-networks", "list"]) + stdout = rm_color(stdout) + stdout = rm_heading(stdout) + return [name for (name, date) in extract_columns(stdout)] + +def get_visible() -> list: + stdout = iwctl(["station", "wlan0", "get-networks", "rssi-dbms"]) + stdout = rm_color(stdout) + stdout = rm_heading(stdout) + return [(name, int(strength)) for (name, strength) in extract_columns(stdout)] + +def choose_best(visible: list, known: list) -> str: + candidates = [(name, strength) for (name, strength) in visible if name in known] + # the least-negative RSSI is the best + return max(candidates, key=lambda c: c[1])[0] + +def connect(network: str) -> str: + return iwctl(["station", "wlan0", "connect", network], sudo=True) + +def restart() -> str: + return subprocess.check_output([ + "sudo", + "systemctl", + "restart", + "iwd", + ]) + +print("scanning for networks... ", end="", flush=True) +scan() +print("done") +print() + +known = get_known() +print("known networks:", "".join(f"\n\t{name}" for name in known)) +print() + +visible = get_visible() +print("visible networks:", "".join(f"\n\t{name}: {rss}" for (name, rss) in visible)) +print() + +best = choose_best(visible, known) + +try: + print(f"connecting to {best}") + result = connect(best) +except subprocess.CalledProcessError as e: + print("restarting iwd daemon") + restart() +else: + print("success", result)