summaryrefslogtreecommitdiff
path: root/interface/input/tabcomp.py
diff options
context:
space:
mode:
Diffstat (limited to 'interface/input/tabcomp.py')
-rwxr-xr-xinterface/input/tabcomp.py247
1 files changed, 247 insertions, 0 deletions
diff --git a/interface/input/tabcomp.py b/interface/input/tabcomp.py
new file mode 100755
index 0000000..330c948
--- /dev/null
+++ b/interface/input/tabcomp.py
@@ -0,0 +1,247 @@
+#!/usr/bin/env python3
+
+import curses
+import threading
+
+from .parsers import PARSE_MAP, parse_null, make_parse_letter
+from ..globals import CURSES_LOCK
+
+class _Node:
+ def __repr__(self):
+ return self.path
+
+ def __init__(self, parent, parse, var: bool = False, f = None, path = ""):
+ self.f = f
+ self.parent = parent
+ self.parse = parse
+ self.var = var
+
+ self.children = []
+ self.name = path
+ if parent is not None:
+ parent.children.append(self)
+ self.path = parent.path + (' ' if parent.path else "") + path
+ else:
+ self.path = path
+
+#options_help = {
+# "test": "This is a test command",
+# "set": "Set a channel range to the given value",
+# "reset": "Reset the given channels, or all",
+#}
+#
+#root = parse_options([(o.split(' '), f) for o, f in options_list])
+#def iter_possible(start):
+# if None in start.children:
+# yield start
+# if [i for i in start.children if i is not None]:
+# for c in start.children:
+# if c is None:
+# continue
+# yield from iter_possible(c)
+#
+#help_map = {}
+#for s in root.children:
+# if s is None:
+# continue
+# help_map[s.path] = list(iter_possible(s))
+# print("===", s.path.upper())
+# print((options_help[s.path] if s.path in options_help else "No help given"))
+# for p in help_map[s.path]:
+#
+# print('-', p)
+
+options_list = (
+ ("test potato $value one two", lambda *args: ('1')),
+ ("test potato two", lambda *args: ('2')),
+ ("test walnut alpha", lambda *args: ('3')),
+ ("test walnut alpha beta", lambda *args: ('4')),
+ ("set $channel_range to $value", lambda cr, v: ("Channel range %s at %s" % (repr(cr), repr(v)))),
+ ("reset $channel_range", lambda cr: ("reset "+repr(cr))),
+ ("reset", lambda: ("reset all")),
+)
+
+class Input:
+ @staticmethod
+ def parse_context(ctx, parent=None):
+ if parent is None:
+ parent = _Node(None, parse_null, None)
+
+ start = {}
+ for i, f in ctx:
+ if isinstance(i, str):
+ i = i.split(' ')
+ if i[0] not in start:
+ start[i[0]] = []
+ start[i[0]].append((i, f))
+
+ for s, ols in start.items():
+ n = _Node(parent, PARSE_MAP[s] if s[0] == '$' else make_parse_letter(s[0], s),
+ var=(s[0] == '$'), path=s)
+ ols = [(ol[1:], f) for ol, f in ols]
+ for l, f in ols:
+ if not l:
+ n.f = f
+ n.children.append(None)
+ break
+ ols = [i for i in ols if i[0]]
+ if ols:
+ Input.parse_context(ols, parent=n)
+
+ return parent
+
+ def set_dim(self, height, width):
+ if height < 4 or width < 10:
+ raise ValueError("Size too small")
+
+ with self._lock:
+ if (height, width) != (self._height, self._width):
+ self.win.erase()
+ self.win.noutrefresh()
+
+ self.win.resize(height, width)
+ self.win.redrawwin()
+
+ self._height = height
+ self._width = width
+ self.win.border()
+ self._redraw()
+ self.win.noutrefresh()
+
+ def set_pos(self, y, x):
+ with self._lock:
+ if (y, x) != (self._y, self._x):
+ self.win.mvwin(y, x)
+ self._y = y
+ self._x = x
+ self.win.border()
+ self.win.noutrefresh()
+
+ def _redraw(self):
+ with self._lock:
+ if len(self._l2) > self._width-2:
+ l2 = self._l2[:self._width-3] + '…'
+ else:
+ l2 = self._l2
+
+ if len(self._l1) > self._width-5:
+ l1 = ">> …" + self._l1[::-1][:self._width-6][::-1]
+ else:
+ l1 = ">> " + self._l1
+ self.win.addstr(1, 1, ' '*(self._width-2))
+ self.win.addstr(2, 1, ' '*(self._width-2))
+ self.win.addstr(1, 1, l1)
+ self.win.addstr(2, 1, l2, curses.A_ITALIC)
+ self.win.move(1, len(l1)+1)
+ self.win.refresh()
+
+ @property
+ def context(self):
+ return self._context
+
+ @context.setter
+ def context(self, ctx):
+ with self._ctx_lock:
+ self._context = ctx
+ self._ctx_changed = True
+ with CURSES_LOCK:
+ self._l1 = ""
+ self._l2 = ""
+ self._redraw()
+
+ def main(self, resize=None):
+ """Run the input loop.
+
+ If `resize` is given, it will be called should the terminal be resized.
+ """
+ with self._ctx_lock:
+ current = self._context
+ ## In the format:
+ ## (input, parsed, display, is variable?)
+ path = [["", True, "", False]]
+ while True:
+ with self._ctx_lock:
+ self._l1 = "".join((i[2] for i in path if i[2]))
+ with CURSES_LOCK:
+ self._redraw()
+ l = self.win.getch()
+ with self._ctx_lock:
+ if l == curses.KEY_RESIZE:
+ if resize is not None:
+ resize()
+ continue
+ if self._context is None:
+ continue
+ elif self._ctx_changed:
+ path = path[:1]
+ self._ctx_changed = False
+ self._l2 = ""
+ if l in (127, curses.KEY_BACKSPACE): ## Backspace
+ if current == self._context:
+ continue
+ e = path[-1]
+ e[0] = e[0][:-1]
+ if not e[0]:
+ path.pop(-1)
+ current = current.parent
+ continue
+ e[1], _, e[2] = current.parse(e[0])
+ elif l in (10, curses.KEY_ENTER) and None in current.children: ## Enter
+ ret = current.f(*(i[1] for i in path if i[3]))
+ self._l2 = "OK" if ret is None else str(ret)
+ path = path[:1]
+ current = self._context
+ else:
+ e = path[-1]
+ s = e[0] + chr(l)
+ parsed, s, display = current.parse(s)
+ if parsed is None and s: ## Invalid input
+ self._l2 = "Expected \"%s\"" % current.name
+ continue
+ e[1], e[2] = parsed, display
+ if not s: ## We're still working on this one
+ e[0] += chr(l)
+ continue
+ ## We're done with this one, the only remaining option is to move on
+ for n in current.children:
+ if n is None:
+ continue
+ parsed, cs, display = n.parse(s)
+ if not cs:
+ ## Found it
+ if current.parent is not None:
+ e[2] = e[2] + ' '
+ current = n
+ e = [s, parsed, display, n.var]
+ path.append(e)
+ break
+ else:
+ self._l2 = "Available: %s" % ", ".join((("ENTER" if n is None else '"'+n.name+'"') for n in current.children))
+
+ def __init__(self, y, x, height, width):
+ with CURSES_LOCK:
+ self.win = curses.newwin(height, width, y, x)
+ self.win.keypad(True)
+ self._lock = threading.RLock()
+ self._height = height
+ self._width = width
+ self._y = -1
+ self._x = -1
+ self._l1 = ""
+ self._l2 = ""
+ self.set_pos(y, x)
+ self.set_dim(height, width)
+
+ self._ctx_lock = threading.RLock()
+ self._context = None
+ self._ctx_changed = False
+
+ self._redraw()
+
+def main2(stdscr):
+ w = Input(0, 0, 4, 100)
+ w.context = Input.parse_context(options_list)
+ w.main()
+
+if __name__ == "__main__":
+ curses.wrapper(main2)