From 755d1dda2a1eb1c26fa7bc12328e2bca25256257 Mon Sep 17 00:00:00 2001
From: Ben Connors <benconnors@outlook.com>
Date: Fri, 18 Oct 2019 23:02:12 -0400
Subject: Get a decent start on the actual interface

- Can edit, create, delete scenes
- Basic saving
---
 interface/input/tabcomp.py | 247 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 247 insertions(+)
 create mode 100755 interface/input/tabcomp.py

(limited to 'interface/input/tabcomp.py')

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)
-- 
cgit v1.2.3