#!/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)