diff options
Diffstat (limited to 'interface/input/tabcomp.py')
-rwxr-xr-x | interface/input/tabcomp.py | 247 |
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) |