aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2020-05-21 22:55:32 +0200
committerTom Smeding <tom.smeding@gmail.com>2020-05-21 22:56:55 +0200
commit5afcef2af59e7f8725bfab41d53cb74dc7a3e5d5 (patch)
tree1a80d6230732c8cffec72686133b5a0ab0271ee7
parent821b756d26648ba22b33e8a5d97af962d817825b (diff)
DRY/genericity refactor; also sources + source-outputs
-rw-r--r--inter.py262
-rw-r--r--pa.py101
-rw-r--r--pacmd.py6
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