blc2 - Python library and frontend for running theatrical lighting and SFX. Written for the English 2041F fall 2019 theatre production of "The Cenci".

git clone https://benconnors.ca/git-repos/blc2

Log | Files | Refs

tabcomp.py (9659B) - raw


      1 #!/usr/bin/env python3
      2 
      3 import curses
      4 import threading
      5 
      6 from .parsers import PARSE_MAP, parse_null, make_parse_letter
      7 from ..globals import CURSES_LOCK
      8 
      9 class _Node:
     10     def iter_possible(self):
     11         if None in self.children:
     12             yield self
     13         if [i for i in self.children if i is not None]:
     14             for c in self.children:
     15                 if c is None:
     16                     continue
     17                 yield from c.iter_possible()
     18 
     19     def __repr__(self):
     20         return self.path
     21 
     22     def __init__(self, parent, parse, var: bool = False, f = None, path = ""):
     23         self.f = f
     24         self.parent = parent
     25         self.parse = parse
     26         self.var = var
     27 
     28         self.children = []
     29         self.name = path
     30         if parent is not None:
     31             parent.children.append(self)
     32             self.path = parent.path + (' ' if parent.path else "") + path
     33         else:
     34             self.path = path
     35 
     36 #options_help = {
     37 #    "test": "This is a test command",
     38 #    "set": "Set a channel range to the given value",
     39 #    "reset": "Reset the given channels, or all",
     40 #}
     41 #
     42 #root = parse_options([(o.split(' '), f) for o, f in options_list])
     43 
     44 #help_map = {}
     45 
     46 options_list = (
     47     ("test potato $value one two", lambda *args: ('1')),
     48     ("test potato two", lambda *args: ('2')),
     49     ("test walnut alpha", lambda *args: ('3')),
     50     ("test walnut alpha beta", lambda *args: ('4')),
     51     ("set $channel_range to $value", lambda cr, v: ("Channel range %s at %s" % (repr(cr), repr(v)))),
     52     ("reset $channel_range", lambda cr: ("reset "+repr(cr))),
     53     ("reset", lambda: ("reset all")),
     54 )
     55 
     56 class Input:
     57     @staticmethod
     58     def parse_context(ctx, help_map=None, help_f=None, parent=None):
     59         if parent is None:
     60             parent = _Node(None, parse_null)
     61 
     62         start = {}
     63         for i, f in ctx:
     64             if not i:
     65                 if None in parent.children:
     66                     raise ValueError("Duplicate base command")
     67                 parent.children.append(None)
     68                 parent.f = f
     69                 continue
     70 
     71             if isinstance(i, str):
     72                 i = i.split(' ')
     73             if i[0] not in start:
     74                 if parent.parent is None and i[0][0] == 'h':
     75                     raise ValueError("No base command may start with h")
     76                 start[i[0]] = []
     77             start[i[0]].append((i, f))
     78 
     79         for s, ols in start.items():
     80             n = _Node(parent, PARSE_MAP[s] if s[0] == '$' else make_parse_letter(s[0], s),
     81                       var=(s[0] == '$'), path=s)
     82             ols = [(ol[1:], f) for ol, f in ols]
     83             for l, f in ols:
     84                 if not l:
     85                     n.f = f
     86                     n.children.append(None)
     87                     break
     88             ols = [i for i in ols if i[0]]
     89             if ols:
     90                 Input.parse_context(ols, parent=n)
     91 
     92         if parent.parent is None and help_f is not None:
     93             if help_map is None:
     94                 help_map = {}
     95 
     96             root_commands = tuple((i.name for i in parent.children if i is not None))
     97 
     98             help_node = _Node(parent, make_parse_letter('h', "help"), path="help")
     99             help_node.children.append(None)
    100 
    101             if None in help_map:
    102                 help_node.f = lambda: help_f(None, root_commands, help_map[None])
    103             else:
    104                 help_node.f = lambda: help_f(None, root_commands, "No help available!")
    105 
    106             for s in parent.children:
    107                 if s is None or s.name == "help":
    108                     continue
    109 
    110                 options = tuple(s.iter_possible())
    111                 hn = _Node(help_node, make_parse_letter(s.name[0], s.name), path=s.name)
    112                 hn.children.append(None)
    113 
    114                 if s.name in help_map:
    115                     hn.f = lambda options=options, name=s.name: help_f(name, (o.path for o in options), help_map[name])
    116                 else:
    117                     hn.f = lambda options=options, name=s.name: help_f(name, (o.path for o in options), "No help available!")
    118 
    119         return parent
    120 
    121     def set_dim(self, height, width):
    122         if height < 4 or width < 10:
    123             raise ValueError("Size too small")
    124 
    125         with self._lock:
    126             if (height, width) != (self._height, self._width):
    127                 self.win.erase()
    128                 self.win.noutrefresh()
    129 
    130                 self.win.resize(height, width)
    131                 self.win.redrawwin()
    132 
    133             self._height = height
    134             self._width = width
    135             self.win.border()
    136             self._redraw()
    137             self.win.noutrefresh()
    138         
    139     def set_pos(self, y, x):
    140         with self._lock:
    141             if (y, x) != (self._y, self._x):
    142                 self.win.mvwin(y, x)
    143                 self._y = y
    144                 self._x = x
    145                 self.win.border()
    146                 self.win.noutrefresh()
    147 
    148     def _redraw(self):
    149         with self._lock:
    150             if len(self._l2) > self._width-2:
    151                 l2 = self._l2[:self._width-3] + '…'
    152             else:
    153                 l2 = self._l2
    154 
    155             if len(self._l1) > self._width-5:
    156                 l1 = ">> …" + self._l1[::-1][:self._width-6][::-1]
    157             else:
    158                 l1 = ">> " + self._l1
    159             self.win.addstr(1, 1, ' '*(self._width-2))
    160             self.win.addstr(2, 1, ' '*(self._width-2))
    161             self.win.addstr(1, 1, l1)
    162             self.win.addstr(2, 1, l2, (curses.A_ITALIC if hasattr(curses, "A_ITALIC") else 0))
    163             self.win.move(1, len(l1)+1)
    164             self.win.refresh()
    165 
    166     @property
    167     def context(self):
    168         return self._context
    169 
    170     @context.setter
    171     def context(self, ctx):
    172         with self._ctx_lock:
    173             self._context = ctx
    174             self._ctx_changed = True
    175             with CURSES_LOCK:
    176                 self._l1 = ""
    177                 self._l2 = ""
    178                 self._redraw()
    179 
    180     def main(self, resize=None):
    181         """Run the input loop.
    182 
    183         If `resize` is given, it will be called should the terminal be resized. 
    184         """
    185         with self._ctx_lock:
    186             current = self._context
    187         ## In the format:
    188         ## (input, parsed, display, is variable?)
    189         path = [["", True, "", False]]
    190         size_ok = True
    191         while True:
    192             with self._ctx_lock:
    193                 self._l1 = "".join((i[2] for i in path if i[2]))
    194             with CURSES_LOCK:
    195                 if size_ok:
    196                     self._redraw()
    197             l = self.win.getch()
    198             with self._ctx_lock:
    199                 if l == curses.KEY_RESIZE:
    200                     if resize is not None:
    201                         size_ok = resize()
    202                     continue
    203                 if not size_ok:
    204                     continue
    205                 if self._context is None:
    206                     continue
    207                 elif self._ctx_changed:
    208                     path = path[:1]
    209                     self._ctx_changed = False
    210                 self._l2 = ""
    211                 if l in (127, curses.KEY_BACKSPACE): ## Backspace
    212                     if current == self._context:
    213                         continue
    214                     e = path[-1]
    215                     e[0] = e[0][:-1]
    216                     if not e[0]:
    217                         path.pop(-1)
    218                         current = current.parent
    219                         continue
    220                     e[1], _, e[2] = current.parse(e[0])
    221                 elif l in (10, curses.KEY_ENTER) and None in current.children: ## Enter 
    222                     ret = current.f(*(i[1] for i in path if i[3]))
    223                     self._l2 = "OK" if ret is None else str(ret)
    224                     path = path[:1]
    225                     current = self._context
    226                 else:
    227                     e = path[-1]
    228                     s = e[0] + chr(l)
    229                     parsed, s, display = current.parse(s)
    230                     if parsed is None and s: ## Invalid input
    231                         self._l2 = "Expected \"%s\"" % current.name
    232                         continue
    233                     e[1], e[2] = parsed, display
    234                     if not s: ## We're still working on this one 
    235                         e[0] += chr(l)
    236                         continue
    237                     ## We're done with this one, the only remaining option is to move on
    238                     for n in current.children:
    239                         if n is None:
    240                             continue
    241                         parsed, cs, display = n.parse(s)
    242                         if not cs:
    243                             ## Found it
    244                             if current.parent is not None:
    245                                 e[2] = e[2] + ' '
    246                             current = n
    247                             e = [s, parsed, display, n.var]
    248                             path.append(e)
    249                             break
    250                     else:
    251                         self._l2 = "Available: %s" % ", ".join((("ENTER" if n is None else '"'+n.name+'"') for n in current.children))
    252 
    253     def __init__(self, y, x, height, width):
    254         with CURSES_LOCK:
    255             self.win = curses.newwin(height, width, y, x)
    256             self.win.keypad(True)
    257         self._lock =  threading.RLock()
    258         self._height = height
    259         self._width = width
    260         self._y = -1
    261         self._x = -1
    262         self._l1 = ""
    263         self._l2 = ""
    264         self.set_pos(y, x)
    265         self.set_dim(height, width)
    266 
    267         self._ctx_lock = threading.RLock()
    268         self._context = None
    269         self._ctx_changed = False
    270 
    271         self._redraw()
    272 
    273 def main2(stdscr):
    274     w = Input(0, 0, 4, 100)
    275     w.context = Input.parse_context(options_list)
    276     w.main()
    277 
    278 if __name__ == "__main__":
    279     curses.wrapper(main2)