import sys, os, atexit from pacmd import PacmdError import pa try: sys.path.append(os.path.dirname(os.path.realpath(__file__))) import termio as T except Exception as e: print("Place termio.py and libtermio.so from the github.com/tomsmeding/termio project in this directory") print(e) sys.exit(1) __all__ = ["start", "end", "mainloop", "update"] inps = None sinks = None sel = (0, 0) prompt_y = 0 # updated by redraw() MENU_MAIN, MENU_VOLUME, NUM_MENUS = range(3) menu = MENU_MAIN menutext = [0] * NUM_MENUS menuopts = [0] * NUM_MENUS menutext[MENU_MAIN] = "" menuopts[MENU_MAIN] = [ ("(q)uit", [0,1]), ("(v)olume", [0,1]), ("(d)efault", [1]), ("(m)ove", [0]) ] menutext[MENU_VOLUME] = "Change volume: <- / -> (alt: 1%) (m)ute [q/esc: return]" menuopts[MENU_VOLUME] = [] def start(): T.initscreen() atexit.register(T.endscreen) T.initkeyboard(False) atexit.register(T.endkeyboard) redraw() def end(): T.endkeyboard() atexit.unregister(T.endkeyboard) T.endscreen() atexit.unregister(T.endscreen) def mainloop(): global sel, menu while True: update() selected_things = [inps, sinks][sel[0]] other_things = [sinks, inps][sel[0]] if len(selected_things) == 0 and len(other_things) != 0: sel = (1 - sel[0], sel[1]) redraw() if sel[1] < 0 or sel[1] >= len(selected_things): sel = (sel[0], 0) key = T.tgetkey() if menu != MENU_MAIN and (key == T.KEY_ESC or key == ord('q')): menu = MENU_MAIN elif menu == MENU_MAIN: if key == T.KEY_DOWN: if sel[0] == 0 and sel[1] >= len(inps) - 1: if len(sinks) > 0: sel = (1, 0) elif sel[0] == 1 and sel[1] >= len(sinks) - 1: pass else: sel = (sel[0], sel[1] + 1) elif key == T.KEY_UP: if sel[0] == 0 and sel[1] == 0: pass elif sel[0] == 1 and sel[1] == 0: if len(inps) > 0: sel = (0, len(inps) - 1) else: sel = (sel[0], sel[1] - 1) elif key == ord('q'): break elif key == ord('v'): thing = get_selected() kind = "sink" if type(thing) == pa.Sink else "input" vol = thing.volume() if len(vol) >= 2 and vol[0] != vol[1]: show_message( "Warning: current volume of this " + kind + " is asymmetric!") menu = MENU_VOLUME elif sel[0] == 1 and key == ord('d'): wrap_pacmd(lambda: sinks[sel[1]].set_default()) elif sel[0] == 0 and key == ord('m'): idx = show_prompt( "Enter sink number to link input {} to" .format(inps[sel[1]].index())) if idx is None: continue try: idx = int(idx) except: show_message("Invalid number!") continue for i in range(len(sinks)): if sinks[i].index() == idx: wrap_pacmd(lambda: inps[sel[1]].move_to_sink(idx)) break else: show_message("No sink found with that index!") elif menu == MENU_VOLUME: thing = [inps, sinks][sel[0]][sel[1]] incr = 0 if key == T.KEY_RIGHT: incr = 0.05 elif key == T.KEY_LEFT: incr = -0.05 elif key == T.KEY_ALT + T.KEY_RIGHT: incr = 0.01 elif key == T.KEY_ALT + T.KEY_LEFT: incr = -0.01 elif key == ord('m'): thing.set_muted(not thing.muted()) continue else: T.bel() continue vol = thing.volume() vol = sum(vol) / len(vol) vol = min(1, max(0, vol + incr)) wrap_pacmd(lambda: thing.set_volume(vol)) else: assert False def get_selected(): return [inps, sinks][sel[0]][sel[1]] def wrap_pacmd(lam): try: lam() except pacmd.PacmdError as e: show_message("An error occurred:\n" + str(e)) def show_message(msg): sz = T.gettermsize() T.fillrect(0, prompt_y, sz.w, sz.h - prompt_y, ' ') T.moveto(0, prompt_y) T.tprint("! " + msg + "\n[press return]") T.redraw() while True: key = T.tgetkey() if key == T.KEY_CR or key == T.KEY_LF: break T.fillrect(0, prompt_y, sz.w, sz.h - prompt_y, ' ') def show_prompt(msg): sz = T.gettermsize() T.fillrect(0, prompt_y, sz.w, sz.h - prompt_y, ' ') T.moveto(0, prompt_y) T.tprint(msg + "\n> ") T.redraw() line = T.tgetline() T.fillrect(0, prompt_y, sz.w, sz.h - prompt_y, ' ') return line def redraw(): global prompt_y if inps is None: update() sz = T.gettermsize() T.fillrect(0, 0, sz.w, sz.h, ' ') T.setstyle(T.Style(9, 9, False, False)) y = 0 T.moveto(0, y) T.setbold(True) T.tprint("Sink Inputs") T.setbold(False) y += 1 for i, inp in enumerate(inps): T.moveto(4, y) print_input(inp, sel[0] == 0 and sel[1] == i) y += 1 y += 1 T.moveto(0, y) T.setbold(True) T.tprint("Sinks") T.setbold(False) y += 1 for i, sink in enumerate(sinks): T.moveto(4, y) print_sink(sink, sel[0] == 1 and sel[1] == i) y += 1 y += 2 prompt_y = y T.moveto(0, y) T.tprint(menutext[menu]) first = True for (text, groups) in menuopts[menu]: if sel[0] in groups: if first: first = False else: T.tprint(" ") T.tprint(text) # if sel[0] not in groups: # T.setfg(6) # T.tprint(text) # T.setfg(9) y += 1 T.moveto(0, y) T.tprint("> ") T.redraw() def print_prefix(thing, selected): if selected: T.setbold(True) T.tprint("> ") T.setbold(False) else: T.tprint(" ") def fmt_volume(vol): res = [str(round(100 * value)) + "%" for value in vol] if len(vol) > 2: return res[0] + "/" + res[1] + " (? len(vol)==" + str(len(vol)) + ")" elif len(vol) == 2: if vol[0] == vol[1]: return res[0] else: return res[0] + "/" + res[1] elif len(vol) == 1: return res[0] + " (mono)" else: return "? (len(vol)==0)" def print_volume(thing): T.setfg(6) T.tprint("(vol: {}{})".format( fmt_volume(thing.volume()), " (MUTED)" if thing.muted() else "")) T.setfg(9) def print_input(inp, selected): print_prefix(inp, selected) T.setfg(3) T.tprint("[{}]".format(inp.index())) T.setfg(9) T.tprint(" ") T.tprint(inp.name()) T.tprint(" ") print_volume(inp) T.setfg(3) T.tprint(" -> ") T.tprint("[{}]".format(inp.sink())) T.setfg(9) def print_sink(sink, selected): print_prefix(sink, selected) T.setfg(3) T.tprint("[{}]".format(sink.index())) T.setfg(9) T.tprint(" ") T.tprint(sink.name()) T.tprint(" ") print_volume(sink) if sink.default(): T.tprint(" ") T.setbold(True) T.tprint("[default]") T.setbold(False) def update(): global inps, sinks inps = pa.list_sink_inputs() sinks = pa.list_sinks() redraw() orig_excepthook = sys.excepthook def excepthook(exctype, value, traceback): T.endkeyboard() T.endscreen() orig_excepthook(exctype, value, traceback) sys.excepthook = excepthook