summaryrefslogtreecommitdiff
path: root/interface/input
diff options
context:
space:
mode:
authorBen Connors <benconnors@outlook.com>2019-10-18 23:02:12 -0400
committerBen Connors <benconnors@outlook.com>2019-10-18 23:02:12 -0400
commit755d1dda2a1eb1c26fa7bc12328e2bca25256257 (patch)
tree272a917fea560e93f28602806727f0d2b315d185 /interface/input
parentbd3feef1499353af4410a52ee88dcb07f5dd08c0 (diff)
Get a decent start on the actual interface
- Can edit, create, delete scenes - Basic saving
Diffstat (limited to 'interface/input')
-rw-r--r--interface/input/__init__.py0
-rwxr-xr-xinterface/input/parsers.py165
-rwxr-xr-xinterface/input/tabcomp.py247
3 files changed, 412 insertions, 0 deletions
diff --git a/interface/input/__init__.py b/interface/input/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/interface/input/__init__.py
diff --git a/interface/input/parsers.py b/interface/input/parsers.py
new file mode 100755
index 0000000..5e53e03
--- /dev/null
+++ b/interface/input/parsers.py
@@ -0,0 +1,165 @@
+def parse_interval(s):
+ if not s:
+ return None, s
+ dash = False
+ init_s = s
+ buff = ""
+ r = [-1, -1]
+ i = 0
+ while s:
+ if not dash and s[0] == '-':
+ if buff:
+ r[0] = int(buff)
+ buff = ""
+ dash = True
+ elif s[0] in "0123456789":
+ buff += s[0]
+ else:
+ break
+ s = s[1:]
+ i += 1
+
+ if buff:
+ r[1 if dash else 0] = int(buff)
+ elif not buff and not dash:
+ return None, s, None
+
+ if not dash:
+ r[1] = r[0]
+
+ return tuple(r), s, init_s[:i]
+
+def parse_range(s):
+ if not s:
+ return None, s, None
+
+ rs = []
+ disp = []
+ tc = False
+ while s:
+ r, s, d = parse_interval(s)
+ if r is None:
+ break
+ tc = False
+ rs.append(r)
+ disp.append(d)
+ if s and s[0] == ',':
+ s = s[1:]
+ tc = True
+ else:
+ break
+
+ if not rs:
+ return None, s, None
+ return tuple(rs), s, ", ".join(disp) + (", " if tc else "")
+
+def parse_channelrange(s):
+ if not s:
+ return None, s, None
+
+ f, s, d1 = parse_range(s)
+ if f is None:
+ return None, s, None
+ elif not s or s[0] != ';':
+ return (f, ((-1,-1),)), s, d1
+
+
+ s = s[1:]
+ c, s, d2 = parse_range(s)
+ if c is None:
+ return (f, ((-1, -1),)), s, d1 + "; "
+
+ return (f, c), s, d1+"; "+d2
+
+def parse_value(s):
+ if not s:
+ return None, s
+
+ buff = ""
+ while s:
+ if s[0] in "0123456789" and int(buff+s[0]) < 256:
+ buff += s[0]
+ s = s[1:]
+ else:
+ break
+
+ return (None if not buff else int(buff)), s, buff
+
+def parse_num(s):
+ if not s:
+ return None, s
+
+ buff = ""
+ while s:
+ if s[0] in "0123456789":
+ buff += s[0]
+ s = s[1:]
+ else:
+ break
+
+ return (None if not buff else int(buff)), s, buff
+
+def make_parse_letter(letter, display):
+ def inner(s):
+ if not s or s[0] != letter:
+ return None, s, None
+ return True, s[1:], display
+
+ return inner
+
+def parse_null(s):
+ return True, s, ""
+
+def parse_string(s):
+ if not s:
+ return None, s, None
+
+ buff = ""
+ while s:
+ if s[0] != ' ':
+ buff += s[0]
+ s = s[1:]
+ else:
+ if not buff:
+ return None, s, None
+ break
+
+ return buff, s, buff
+
+def parse_quotedstring(s):
+ if not s:
+ return None, s, None
+
+ if s[0] != "'":
+ return None, s, None
+ s = s[1:]
+
+ buff = ""
+ bs = False
+ while s:
+ if s[0] == '\\':
+ buff += s[0]
+ bs = not bs
+ elif s[0] == "'":
+ if bs:
+ buff += s[0]
+ bs = False
+ else:
+ s = s[1:]
+ break
+ else:
+ buff += s[0]
+ bs = False
+ s = s[1:]
+ else:
+ return buff, s, "'"+buff
+ return buff, s, "'"+buff+"'"
+
+PARSE_MAP = {
+ "$channel_range": parse_channelrange,
+ "$value": parse_value,
+ "$null": parse_null,
+ "$string": parse_string,
+ "$num": parse_num,
+ "$quoted_string": parse_quotedstring,
+}
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)