| 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 |
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)