#! /usr/bin/env python3 import argparse import json import pprint import subprocess def get_sinks(): """ Consult pactl and get a sorted list of all sinks as dicts """ sinks = json.loads( subprocess.run( ["pactl", "-f", "json", "list", "sinks"], capture_output=True, text=True, check=True, ).stdout ) return sorted(sinks, key=lambda s: s["index"]) def get_status(): status = json.loads( subprocess.run( ["pactl", "-f", "json", "info"], capture_output=True, text=True, check=True, ).stdout ) return status def change_sink(sink): """ Provided a sink object, use pactl to change over to it """ assert sink.get("name") return subprocess.run(["pactl", "set-default-sink", sink.get("name")], check=True) def get_current_sink(sinks, status): """ Given a list of all sinks and the status of the system, return the dict for the sink that's currently the default output device """ return next( ( sink for sink in sinks if sink.get("name", "") == status.get("default_sink_name", "") ), None, ) def get_offset_sink(sinks, currentsink, offset=1): def find_sink_by_index(sinks, index): for sink in sinks: if sink["index"] == index: return sink return None """ Given a list of sinks and the current default sink, get some offset in the list of all sinks away from it. Useful for getting next/previous sinks """ indexes = [s["index"] for s in sinks] i = indexes.index(currentsink.get("index", 0)) next_index = indexes[(i + offset) % len(indexes)] return find_sink_by_index(sinks, next_index) def get_sink_icon(sinks, currentsink): """ Given the list of all sinks and the current one, figure out how we should display that as a small icon """ icon = { "hdmi": "\uf26c", # fa-tv "analog": "\uf028", # fa-volume-up "headphones": "\uf025", # fa-headphones "bluetooth": "\uf294", # fa-bluetooth-b } def classify(sink): name = sink.get("name", "").lower() port = sink.get("active_port", "") desc = sink.get("description", "").lower() if "hdmi" in name or "hdmi" in desc: return "hdmi" if "headphones" in port or "headset" in desc: return "headphones" if "analog" in name or "lineout" in port: return "analog" if "bluez" in name or "bluetooth" in desc: return "bluetooth" return "analog" return icon[classify(currentsink)] def main(): parser = argparse.ArgumentParser( description="Display and cycle through PulseAudio and PipeWire sinks" ) parser.add_argument( "-i", "--iconify", action="store_true", help="Print an icon instead of the full name of the sink", ) parser.add_argument( "-n", "--next", action="store_true", help="Advance to the next sink" ) parser.add_argument( "-p", "--previous", action="store_true", help="Advance to the previous sink" ) args = parser.parse_args() # Get pactl's status sinks = get_sinks() status = get_status() currentsink = get_current_sink(sinks, status) # Figure out our offset into the list offset = 1 if args.next else -1 if args.previous else 0 # Figure out where we wanna go newsink = get_offset_sink(sinks, currentsink, offset) change_sink(newsink) if args.iconify: print(get_sink_icon(sinks, newsink)) else: print(newsink.get("description")) if __name__ == "__main__": main()