#!/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 iter_possible(self): if None in self.children: yield self if [i for i in self.children if i is not None]: for c in self.children: if c is None: continue yield from c.iter_possible() 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]) #help_map = {} 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, help_map=None, help_f=None, parent=None): if parent is None: parent = _Node(None, parse_null) start = {} for i, f in ctx: if isinstance(i, str): i = i.split(' ') if i[0] not in start: if parent.parent is None and i[0][0] == 'h': raise ValueError("No base command may start with h") 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) if parent.parent is None and help_f is not None: if help_map is None: help_map = {} root_commands = tuple((i.name for i in parent.children if i is not None)) help_node = _Node(parent, make_parse_letter('h', "help"), path="help") help_node.children.append(None) if None in help_map: help_node.f = lambda: help_f(None, root_commands, help_map[None]) else: help_node.f = lambda: help_f(None, root_commands, "No help available!") for s in parent.children: if s is None or s.name == "help": continue options = tuple(s.iter_possible()) hn = _Node(help_node, make_parse_letter(s.name[0], s.name), path=s.name) hn.children.append(None) if s.name in help_map: hn.f = lambda options=options, name=s.name: help_f(name, (o.path for o in options), help_map[name]) else: hn.f = lambda options=options, name=s.name: help_f(name, (o.path for o in options), "No help available!") 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)