From 5afcef2af59e7f8725bfab41d53cb74dc7a3e5d5 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Thu, 21 May 2020 22:55:32 +0200 Subject: DRY/genericity refactor; also sources + source-outputs --- inter.py | 262 +++++++++++++++++++++++++++++++++++++-------------------------- pa.py | 101 ++++++++++++++++++------ pacmd.py | 6 ++ 3 files changed, 235 insertions(+), 134 deletions(-) diff --git a/inter.py b/inter.py index a665540..a2f3456 100644 --- a/inter.py +++ b/inter.py @@ -11,9 +11,33 @@ except Exception as e: __all__ = ["start", "end", "mainloop", "update"] -inps = None -sinks = None -sel = (0, 0) +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) @@ -23,15 +47,15 @@ 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]) -] +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] = [] +menuopts[MENU_VOLUME] = {} def start(): @@ -52,45 +76,40 @@ 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) + 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')): + 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) + sel = selection_down(sel) 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) + sel = selection_up(sel) elif key == ord('q'): break - elif key == ord('v'): + elif "v" in sections[sel.section].keys and key == ord("v"): thing = get_selected() - kind = "sink" if type(thing) == pa.Sink else "input" + kind = thing.kind() vol = thing.volume() if len(vol) >= 2 and vol[0] != vol[1]: show_message( @@ -98,13 +117,14 @@ def mainloop(): menu = MENU_VOLUME - elif sel[0] == 1 and key == ord('d'): - wrap_pacmd(lambda: sinks[sel[1]].set_default()) + elif "d" in sections[sel.section].keys and key == ord("d"): + wrap_pacmd(lambda: get_selected().set_default()) - elif sel[0] == 0 and key == ord('m'): + elif "m" in sections[sel.section].keys and key == ord("m"): + thing = get_selected() idx = show_prompt( - "Enter sink number to link input {} to" - .format(inps[sel[1]].index())) + "Enter {} number to link {} {} to" + .format(thing.linked_kind(), thing.kind(), thing.index())) if idx is None: continue @@ -115,15 +135,18 @@ def mainloop(): 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)) + 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 sink found with that index!") + show_message("No {} found with that index!" + .format(thing.linked_kind())) elif menu == MENU_VOLUME: - thing = [inps, sinks][sel[0]][sel[1]] + thing = get_selected() incr = 0 if key == T.KEY_RIGHT: @@ -134,7 +157,7 @@ def mainloop(): incr = 0.01 elif key == T.KEY_ALT + T.KEY_LEFT: incr = -0.01 - elif key == ord('m'): + elif key == ord("m"): thing.set_muted(not thing.muted()) continue else: @@ -149,8 +172,42 @@ def mainloop(): 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 [inps, sinks][sel[0]][sel[1]] + return sections[sel.section].items[sel.item] def wrap_pacmd(lam): try: @@ -184,9 +241,24 @@ def show_prompt(msg): 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 inps is None: + if sections[0].items is None: update() sz = T.gettermsize() @@ -195,45 +267,23 @@ def redraw(): 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) + for i, section in enumerate(sections): + y = draw_section(y, section.items, section.title, sel.section == 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) + 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 @@ -271,49 +321,43 @@ def print_volume(thing): " (MUTED)" if thing.muted() else "")) T.setfg(9) -def print_input(inp, selected): - print_prefix(inp, selected) +def print_item(item, selected): + print_prefix(item, selected) T.setfg(3) - T.tprint("[{}]".format(inp.index())) + T.tprint("[{}]".format(item.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(item.name()) - T.tprint(" ") - T.tprint(sink.name()) + if isinstance(item, pa.SinkSource): + T.tprint(" ") + print_state(item) T.tprint(" ") - print_volume(sink) + 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) - if sink.default(): - T.tprint(" ") - T.setbold(True) - T.tprint("[default]") - T.setbold(False) +def print_state(thing): + T.setfg(6) + T.tprint("({})".format(thing.state())) + T.setfg(9) def update(): - global inps, sinks - - inps = pa.list_sink_inputs() - sinks = pa.list_sinks() - redraw() + for section in sections: + section.update() orig_excepthook = sys.excepthook diff --git a/pa.py b/pa.py index 014a234..e09565e 100644 --- a/pa.py +++ b/pa.py @@ -1,10 +1,14 @@ from collections import namedtuple import pacmd -class Sink: - def __init__(self, pitem): +class SinkSource: + def __init__(self, pitem, kind_of_thing): assert type(pitem) == pacmd.Item self._i = pitem + self._kind_of_thing = kind_of_thing + + def kind(self): + return self._kind_of_thing def name(self): return self._i.ch["name"].value @@ -30,28 +34,57 @@ class Sink: def set_volume(self, vol): assert type(vol) == float or type(vol) == int vol = round(vol * _get_maxvol(self._i)) - pacmd.pacmd("set-sink-volume", str(self.index()), str(vol)) + pacmd.pacmd("set-{}-volume".format(self._kind_of_thing), + str(self.index()), str(vol)) def set_muted(self, yes): - pacmd.pacmd("set-sink-mute", str(self.index()), "true" if yes else "false") + pacmd.pacmd("set-{}-mute".format(self._kind_of_thing), + str(self.index()), "true" if yes else "false") def set_default(self): - pacmd.pacmd("set-default-sink", str(self.index())) + pacmd.pacmd("set-default-{}".format(self._kind_of_thing), + str(self.index())) + +class Sink(SinkSource): + def __init__(self, pitem): + super().__init__(pitem, "sink") -class SinkInput: +class Source(SinkSource): def __init__(self, pitem): + super().__init__(pitem, "source") + +class InputOutput: + def __init__(self, pitem, kind_of_thing, linked_kind): assert type(pitem) == pacmd.Item self._i = pitem + self._kind_of_thing = kind_of_thing + self._linked_kind = linked_kind + + # sink-input or source-output + def kind(self): + return self._kind_of_thing + + # sink or source + def linked_kind(self): + return self._linked_kind def name(self): try: name = self._i.ch["properties"].ch["media.name"].value - if "application.process.binary" in self._i.ch["properties"].ch: - name += " (" + self._i.ch["properties"].ch["application.process.binary"].value + ")" + # Append the first that is present, if any + for key in ["application.name", "application.process.binary"]: + if key in self._i.ch["properties"].ch: + name += " (" + self._i.ch["properties"].ch[key].value + ")" + break except Exception as e: return "???" return name + def linked_index(self): + return int(self._i.ch[self._linked_kind].value.split()[0]) + + # Don't know if this is ever true; it wouldn't make much sense to have a + # 'default' input or output. def default(self): return self._i.default @@ -61,9 +94,6 @@ class SinkInput: def driver(self): return self._i.ch["driver"].value - def sink(self): - return int(self._i.ch["sink"].value.split()[0]) - def muted(self): return _parse_muted(self._i) @@ -73,19 +103,35 @@ class SinkInput: def set_volume(self, vol): assert type(vol) == float or type(vol) == int vol = round(vol * _get_maxvol(self._i)) - pacmd.pacmd("set-sink-input-volume", str(self.index()), str(vol)) + pacmd.pacmd("set-{}-volume".format(self._kind_of_thing), str(self.index()), str(vol)) def set_muted(self, yes): - pacmd.pacmd("set-sink-input-mute", str(self.index()), "true" if yes else "false") + pacmd.pacmd("set-{}-mute".format(self._kind_of_thing), str(self.index()), "true" if yes else "false") - def move_to_sink(self, idx): + def move_to(self, idx): assert type(idx) == int - pacmd.pacmd("move-sink-input", str(self.index()), str(idx)) + pacmd.pacmd("move-{}".format(self._kind_of_thing), str(self.index()), str(idx)) + +class SinkInput(InputOutput): + def __init__(self, pitem): + super().__init__(pitem, "sink-input", "sink") + +class SourceOutput(InputOutput): + def __init__(self, pitem): + super().__init__(pitem, "source-output", "source") +# TODO: How should one really do this? This is just a collection of hacks that +# accidentally works for all my devices, but will probably fail the moment you +# try another. def _get_maxvol(item): - if "base volume" in item.ch: - return int(item.ch["base volume"].value.split("/")[0].strip()) + if "volume steps" in item.ch: + return int(item.ch["volume steps"].value.strip()) - 1 + elif "base volume" in item.ch: + num, perc = item.ch["base volume"].value.split("/")[0:2] + num = int(num.strip()) + perc = int(num.replace("%", "").strip()) + return num * (100 / perc) else: return 65536 @@ -100,14 +146,19 @@ def _parse_muted(item): elif item.ch["muted"].value == "no": return False else: assert False +def _list_things(pacmd_obj, section_name, klass): + assert len(pacmd_obj.sections) == 1 + assert pacmd_obj.sections[0].name == section_name + return [klass(item) for item in pacmd_obj.sections[0].items] + def list_sinks(): - res = pacmd.list_sinks() - assert len(res.sections) == 1 - assert res.sections[0].name == "sink" - return [Sink(item) for item in res.sections[0].items] + return _list_things(pacmd.list_sinks(), "sink", Sink) def list_sink_inputs(): - res = pacmd.list_sink_inputs() - assert len(res.sections) == 1 - assert res.sections[0].name == "input" - return [SinkInput(item) for item in res.sections[0].items] + return _list_things(pacmd.list_sink_inputs(), "input", SinkInput) + +def list_sources(): + return _list_things(pacmd.list_sources(), "source", Source) + +def list_source_outputs(): + return _list_things(pacmd.list_source_outputs(), "output", SourceOutput) diff --git a/pacmd.py b/pacmd.py index 433e6f1..135c341 100644 --- a/pacmd.py +++ b/pacmd.py @@ -138,6 +138,12 @@ def list_sinks(): def list_sink_inputs(): return _pacmd_parsed("list-sink-inputs") +def list_sources(): + return _pacmd_parsed("list-sources") + +def list_source_outputs(): + return _pacmd_parsed("list-source-outputs") + def _make_indent(n): return " " * n -- cgit v1.2.3