{ pkgs, config, ...}: { kassouni.palette.commands = [ { name = "bluetooth"; icon = "󰂯"; # `jc --bluetoothctl` turns `bluetoothctl devices` into JSON, so the menu is # built from parsed records rather than by splitting "Device " # lines. That subcommand leaves every status field empty, so connectedness # comes from a second `devices Connected` call intersected on address. exe = pkgs.writers.writePython3 "bluetooth-toggle" { # The interpolated store paths below are longer than 79 columns. flakeIgnore = [ "E501" ]; } '' import html import json import subprocess import sys BT = "${pkgs.bluez}/bin/bluetoothctl" JC = "${pkgs.jc}/bin/jc" ROFI = "${pkgs.rofi}/bin/rofi" def devices(*args): """Parse `bluetoothctl devices [args]` into a list of records.""" listed = subprocess.run( [BT, "devices", *args], capture_output=True, text=True ) parsed = subprocess.run( [JC, "--bluetoothctl"], input=listed.stdout, capture_output=True, text=True, ) return json.loads(parsed.stdout or "[]") def label(device, connected): """Pango markup row. Names are escaped, they are not our text.""" name = html.escape(device["name"]) if device["address"] in connected: name += ' connected' return name def main(): known = devices() if not known: return 0 # Older bluez has no `devices Connected`; that call just comes back # empty there, which degrades to "nothing is connected". connected = {d["address"] for d in devices("Connected")} rows = [label(d, connected) for d in known] choice = subprocess.run( [ROFI, "-dmenu", "-i", "-markup-rows", "-format", "i", "-p", "bluetooth"], input="\n".join(rows), capture_output=True, text=True, ) if choice.returncode != 0: return 1 device = known[int(choice.stdout.strip())] action = ( "disconnect" if device["address"] in connected else "connect" ) return subprocess.run([BT, action, device["address"]]).returncode if __name__ == "__main__": sys.exit(main()) ''; } ]; }