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"] class Section: def __init__(self, title, keys, updater): self.items = None self.title = title self.keys = keys self.link_section = None # sinks for sink-inputs, sources for source-outputs self._updater = updater self.update() def update(self): self.items = (self._updater)() class Selection: def __init__(self, section, item): self.section = section self.item = item sections = [ Section("Sink Inputs", "qvm", lambda: pa.list_sink_inputs()), Section("Sinks", "qvd", lambda: pa.list_sinks()), Section("Source Outputs", "qvm", lambda: pa.list_source_outputs()), Section("Sources", "qvd", lambda: pa.list_sources()), ] sections[0].link_section = sections[1] sections[2].link_section = sections[3] sel = Selection(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": "(q)uit", "v": "(v)olume", "d": "(d)efault", "m": "(m)ove", } 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() sel.item = max(0, min(len(sections[sel.section].items) - 1, sel.item)) if len(sections[sel.section].items) == 0: for i, s in enumerate(sections): if len(s.items) != 0: sel.section = i sel.item = 0 break else: sel = Selection(0, 0) # We changed the selection, let's return to the main menu menu = MENU_MAIN redraw() 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: sel = selection_down(sel) elif key == T.KEY_UP: sel = selection_up(sel) elif key == ord('q'): break elif "v" in sections[sel.section].keys and key == ord("v"): thing = get_selected() kind = thing.kind() 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 "d" in sections[sel.section].keys and key == ord("d"): wrap_pacmd(lambda: get_selected().set_default()) elif "m" in sections[sel.section].keys and key == ord("m"): thing = get_selected() idx = show_prompt( "Enter {} number to link {} {} to" .format(thing.linked_kind(), thing.kind(), thing.index())) if idx is None: continue try: idx = int(idx) except: show_message("Invalid number!") continue link_section = sections[sel.section].link_section for i in range(len(link_section.items)): if link_section.items[i].index() == idx: wrap_pacmd(lambda: thing.move_to(idx)) break else: show_message("No {} found with that index!" .format(thing.linked_kind())) elif menu == MENU_VOLUME: thing = get_selected() 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 selection_down(sel): if sel.item >= len(sections[sel.section].items) - 1: if sel.section >= len(sections) - 1: T.bel() return sel else: i = sel.section + 1 while i < len(sections) and len(sections[i].items) == 0: i += 1 if i < len(sections): return Selection(i, 0) else: T.bel() return sel else: return Selection(sel.section, sel.item + 1) def selection_up(sel): if sel.item <= 0: if sel.section <= 0: T.bel() return sel else: i = sel.section - 1 while i >= 0 and len(sections[i].items) == 0: i -= 1 if i >= 0: return Selection(i, len(sections[i].items) - 1) else: T.bel() return sel else: return Selection(sel.section, sel.item - 1) def get_selected(): return sections[sel.section].items[sel.item] 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 # Returns y after end of section on screen def draw_section(y, items, title, section_selected): T.moveto(0, y) T.setbold(True) T.tprint(title) T.setbold(False) y += 1 for i, item in enumerate(items): T.moveto(1, y) print_item(item, section_selected and sel.item == i) y += 1 return y def redraw(): global prompt_y if sections[0].items is None: update() sz = T.gettermsize() T.fillrect(0, 0, sz.w, sz.h, ' ') T.setstyle(T.Style(9, 9, False, False)) y = 0 for i, section in enumerate(sections): y = draw_section(y, section.items, section.title, sel.section == i) y += 1 y += 1 prompt_y = y T.moveto(0, y) T.tprint(menutext[menu]) first = True for key in sections[sel.section].keys: if key not in menuopts[menu]: continue if first: first = False else: T.tprint(" ") T.tprint(menuopts[menu][key]) 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_item(item, selected): print_prefix(item, selected) T.setfg(3) T.tprint("[{}]".format(item.index())) T.setfg(9) T.tprint(" ") T.tprint(item.name()) if isinstance(item, pa.SinkSource): T.tprint(" ") print_state(item) T.tprint(" ") print_volume(item) if isinstance(item, pa.SinkSource): if item.default(): T.tprint(" ") T.setbold(True) T.tprint("[default]") T.setbold(False) else: T.setfg(3) T.tprint(" -> " if isinstance(item, pa.SinkInput) else " <- ") T.tprint("[{}]".format(item.linked_index())) T.setfg(9) def print_state(thing): T.setfg(6) T.tprint("({})".format(thing.state())) T.setfg(9) def update(): for section in sections: section.update() orig_excepthook = sys.excepthook def excepthook(exctype, value, traceback): T.endkeyboard() T.endscreen() orig_excepthook(exctype, value, traceback) sys.excepthook = excepthook