import curses import os import datetime as dt import threading import traceback as tb import blc2 from blc2.functions.audio import Audio from blc2.functions.scene import Scene from blc2.functions.chaser import Chaser from blc2.functions.chaserstep import ChaserStep from blc2.functions.join import Join from blc2.constants import SCENE, CHASER, AUDIO, MANUAL, INHERIT, INFTY, CHASERSTEP, JOIN, ONESHOT, LOOP from blc2.topology import Fixture from blc2.workspace import Workspace from .globals import CURSES_LOCK from .input.tabcomp import Input from .channelbank import ChannelBank from .chaserview import ChaserView from .pager import Pager from .dialog import askyesnocancel from .audioview import AudioView from .render import Renderer from .dummy import DummyOutput def wrap_curses(f): def inner(*args, **kwargs): return curses.wrapper(lambda stdscr: f(*args, stdscr, **kwargs)) return inner __version__ = "v0.0.3" class Interface: def _compute_sizes(self, height, width): hb = height // 2 ht = height - hb wr = width // 2 wl = width - wr - 1 chasers = [] if self.chaser_views: current = 0 cw = (wl // len(self.chaser_views)) - 1 for i in range(len(self.chaser_views)): curw = (wl - current) if (i+1) == len(self.chaser_views) else cw chasers.append(((hb, curw), (ht, current))) current += curw + 1 return ( ((ht, width), (0, 0),), ((4, wr), (height-4, wl+1),), ((hb - 4, wr), (ht, wl+1),), *chasers ) def _resize(self): ## FIXME self._actual_resize() self._actual_resize() def _actual_resize(self): for (a1, a2), (f1, f2) in zip(self._compute_sizes(*self.stdscr.getmaxyx()), ((self.channel_bank.set_dim, self.channel_bank.set_pos), (self.input.set_dim, self.input.set_pos), (self.pager.set_dim, self.pager.set_pos), *((c.set_dim, c.set_pos) for c in self.chaser_views))): f1(*a1) f2(*a2) @wrap_curses def main(self, stdscr): height, width = stdscr.getmaxyx() self.stdscr = stdscr (cbd, cbp), (ind, inp), (pgd, pgp) = self._compute_sizes(height, width) self.channel_bank = ChannelBank(*cbp, *cbd) self.input = Input(*inp, *ind) self.input.context = self.context_base self.pager = Pager(*pgp, *pgd) todisp = [ "Welcome to BLC2!", "Currently running library %s, interface %s" % (blc2.__version__, __version__), "", ] if self._w_created: todisp.append("Created a new workspace") else: todisp.append("Loaded workspace \"%s\" from \"%s\"" % (self.w.name, self.path)) todisp.append("Authored by %s, last modified at %s" % (self.w.author, self.w.modified.strftime("%Y-%m-%d %H:%M:%S"))) self.pager.display_many(todisp) if not self.output.ok: self.pager.display_many(("WARNING: Output is not OK!",), True) self.input.main(self._resize) def handle_show(self, f): with self.w_lock: if f is None or f.type == SCENE: if not isinstance(self.channel_bank, ChannelBank): (hw, yx), *_ = self._compute_sizes(*self.stdscr.getmaxyx()) self.channel_bank = ChannelBank(*yx, *hw) if f is None: return self.channel_bank.set_scope(f.scope) self.channel_bank.set_values(f.values.items()) elif f.type == AUDIO: if not isinstance(self.channel_bank, AudioView): (hw, yx), *_ = self._compute_sizes(*self.stdscr.getmaxyx()) self.channel_bank = AudioView(*yx, *hw) self.channel_bank.audio = f self.channel_bank.title = f.type + ' "%s"' % f.name self.renderer.clear_hold() if f.type == SCENE: self.renderer.hold(f.values) def handle_enter(self, fid): with self.w_lock: if fid is None: if not self.chaser_views: return "No chaser loaded" f = self.chaser_views[0].chaser else: if fid not in self.w.functions: return "No such function" f = self.w.functions[fid] if self.current_cv is not None: self.current_cv.highlight = False if self.chaser is not None: cv = [c for c in self.chaser_views if c.chaser == self.chaser][0].selected self.chaser_stack.append((self.chaser, cv)) self.chaser = None if f.type == SCENE: self.primitive = f self.handle_show(f) self.channel_bank.highlight = True self.input.context = self.context_scene elif f.type in (CHASER, JOIN): self.chaser = f cv = [c for c in self.chaser_views if c.chaser == f] if not cv: if len(self.chaser_views) == 2: self.chaser_views[1].set_chaser(f, None) else: self.base_add(f.id) self.current_cv = self.chaser_views[-1] else: cv = cv[0] self.current_cv = cv self.current_cv.highlight = True if f.type == CHASER: self.input.context = self.context_chaser elif f.type == JOIN: self.input.context = self.context_join elif f.type == AUDIO: self.primitive = f self.handle_show(f) self.channel_bank.highlight = True self.input.context = self.context_audio else: ## FIXME return "No other types allowed yet" def base_add(self, fid): with self.w_lock: if fid not in self.w.functions: return "No such function" f = self.w.functions[fid] if f.type not in (CHASER, JOIN): return "Can only add chasers!" with CURSES_LOCK: if True in (c.chaser.id for c in self.chaser_views): return "Already added!" elif len(self.chaser_views) == 2: ## FIXME? return "Can only use two for now" self.chaser_views.append(None) ld, lp = self._compute_sizes(*self.stdscr.getmaxyx())[-1] self.chaser_views[-1] = ChaserView(*lp, *ld) self._resize() self.chaser_views[-1].set_chaser(f, None) def base_remove(self, n, index=True): with self.w_lock: if not index: n = [j for j, i in enumerate(self.chaser_views) if i.chaser.id == n] if not n: return "No such chaser loaded" n = n[0] if n >= len(self.chaser_views) or n < 0: return "Index out of range" with CURSES_LOCK: self.chaser_views[n].win.erase() self.chaser_views[n].win.refresh() self.chaser_views.pop(n) self._resize() def base_delete(self, fid): with self.w_lock: if fid not in self.w.functions: return "No such function" f = self.w.functions[fid] f.delete() def base_new(self, name, cls): with self.w_lock: f = cls(self.w, name=name) self.handle_enter(f.id) def _gather_channels(self, cr): with self.w_lock: ## Gather the affected channels channels = [] for f in self.w.fixtures.values(): for s, e in cr[0]: if (s <= f.id <= e) or (e == -1 and f.id >= s): break else: continue for n, c in enumerate(f.channels): for s, e in cr[1]: if (s <= n <= e) or (e == -1 and n >= s): channels.append(c) break return channels def scene_set(self, cr, v): with self.w_lock: channels = self._gather_channels(cr) ## Set the values self.primitive.update({c: v for c in channels}) ## Update the display self.channel_bank.set_scope(self.primitive.scope) if v is not None: self.channel_bank.set_values(((c, v) for c in channels)) self.renderer.clear_hold() self.renderer.hold(self.primitive.values) def scene_edit(self, cr, force = False): with self.w_lock: channels = self._gather_channels(cr) if not force: channels = [c for c in channels if c in self.primitive.scope] else: self.primitive.update({c: 0 for c in channels if c not in self.primitive.scope}) if not channels: return "No channels given" curses.curs_set(0) values = self.primitive.values self.channel_bank.set_active(channels, True) while True: self.channel_bank.set_scope(self.primitive.scope) self.channel_bank.set_values(((c, v) for c, v in values.items())) self.renderer.clear_hold() self.renderer.hold(self.primitive.values) l = self.pager.win.getch() delta = 0 if l == curses.KEY_RESIZE: self._resize() elif l == ord('q'): break elif l in (ord('j'), curses.KEY_DOWN): delta = -5 elif l in (ord('k'), curses.KEY_UP): delta = 5 elif l == ord('h'): delta = -1 elif l == ord('l'): delta = 1 elif l == curses.KEY_NPAGE: delta = 25 elif l == curses.KEY_PPAGE: delta = -25 elif l == curses.KEY_HOME: delta = 255 elif l == curses.KEY_END: delta = -255 if delta != 0: for c in channels: values[c] = max(0, min(255, values[c]+delta)) self.primitive.update(values) self.channel_bank.set_active(channels, False) curses.curs_set(1) def scene_clear(self, cr): self.scene_set(cr, None) def scene_exit(self): self.primitive = None self.handle_exit() def audio_exit(self): self.primitive = None self.handle_exit() def handle_exit(self): for c in self.chaser_views: c.highlight = False if self.current_cv is not None: self.current_cv.selected = None self.channel_bank.highlight = False if self.chaser_stack: ## Load the previous chaser c, sel = self.chaser_stack.pop(-1) if True not in (True for i in self.chaser_views if i.chaser.id == c.id): ## We need to load it ## Check if we have one for the current chaser try: idx = [i.chaser.id for i in self.chaser_views].index(self.chaser.id) except (ValueError, AttributeError): ## We don't have it if len(self.chaser_views) == 2: ## We don't have room for it, just replace the last one self.chaser_views[1].set_chaser(c, sel) else: self.base_add(c.id) self.current_cv = self.chaser_views[-1] else: self.chaser_views[idx].set_chaser(c, sel) self.current_cv = self.chaser_views[idx] self.current_cv.highlight = True self.current_cv.selected = sel if c.type == CHASER: self.input.context = self.context_chaser else: self.input.context = self.context_join self.chaser = c else: if isinstance(self.channel_bank, AudioView): (hw, yx), *_ = self._compute_sizes(*self.stdscr.getmaxyx()) self.channel_bank = ChannelBank(*yx, *hw) self.channel_bank.set_scope(()) self.channel_bank.title = "Channels" self.input.context = self.context_base self.chaser = None self.current_cv = None self.renderer.clear_hold() def base_save(self, path=None): with self.w_lock: if path is not None: self.path = path if self.path is None: return "No path set" self.w.modified = dt.datetime.now() self.w.save(self.path) return "Saved to "+self.path def base_quit(self): if askyesnocancel(self.stdscr, "Really exit?"): quit() else: self._resize() def list_fixtures(self): with self.w_lock: td = ["FIXTURES:"] for f in sorted(self.w.fixtures.values(), key=lambda a: a.id): td.append("- %03d: %s" % (f.id, f.name)) for c in f.channels: td.append(" %03d-%03d: %s" % (f.id, c.id, c.name)) self.pager.display_many(td, split=True) def list_functions(self, typ): with self.w_lock: td = [typ.upper()+"S:"] for f in sorted(self.w.functions.values(), key=lambda a: a.id): if f.type == typ: td.append("- %03d: %s" % (f.id, f.name)) self.pager.display_many(td, split=True) def page(self): self.pager.user_page() def page_clear(self): self.pager.clear() def help(self, name, commands, help_): if name is None: dname = "MODE HELP:" else: dname = name.upper() + " COMMAND:" todisp = [dname, ""] if commands: if name is None: todisp.append("Available commands:") else: todisp.append("Available forms:") todisp.extend(("- "+i for i in commands)) todisp.append("") todisp.extend(help_.split('\n')) self.pager.display_many(todisp, split=True) def chaser_select(self, num): with self.w_lock: if num < 1 or num > len(self.current_cv.chaser.steps): return "Out of range" self.current_cv.selected = num - 1 s = self.current_cv.chaser.steps[num-1] if (s.type == CHASERSTEP and s.function is not None and s.function.type in (SCENE, AUDIO,)) or s.type in (SCENE, AUDIO,): self.handle_show(s.function if s.type == CHASERSTEP else s) def chaser_edit(self, num=None): with self.w_lock: if num is not None: r = self.chaser_select(num) if r is not None: return r elif self.current_cv.selected is None: return "No step selected" c = self.current_cv.chaser.steps[self.current_cv.selected] if c.type == CHASERSTEP and c.function is None: return "No function on step" self.handle_enter(c.function.id if c.type == CHASERSTEP else c.id) def chaser_delete(self, num=None): with self.w_lock: if num is not None: r = self.chaser_select(num) if r is not None: return r elif self.current_cv.selected is None: return "No step selected" c = self.current_cv.chaser s = c.steps[self.current_cv.selected] sel = self.current_cv.selected with CURSES_LOCK: if c.type == CHASER: if askyesnocancel(self.stdscr, "Really delete step %d?" % (s.index+1), resize=self._resize): sel = s.index s.delete() else: return else: c.delete_step(s) if not c.steps: sel = None else: sel = max(0, sel-1) self.current_cv.set_chaser(c, sel) self._resize() def chaser_fade(self, t, out=False): with self.w_lock: if self.current_cv.selected is None: return "No step selected" c = self.current_cv.chaser s = c.steps[self.current_cv.selected] if out: s.fade_out = t else: s.fade_in = t self.current_cv.set_chaser(c, s.index) def chaser_duration(self, t): with self.w_lock: if self.current_cv.selected is None: return "No step selected" c = self.current_cv.chaser s = c.steps[self.current_cv.selected] if s.duration_mode != MANUAL: s.duration_mode = MANUAL s.duration = t self.current_cv.set_chaser(c, s.index) def chaser_unset(self): with self.w_lock: if self.current_cv.selected is None: return "No step selected" c = self.current_cv.chaser s = c.steps[self.current_cv.selected] s.duration_mode = INHERIT self.current_cv.set_chaser(c, s.index) def chaser_new(self, index, name, fid=None): with self.w_lock: if fid is not None: if fid not in self.w.functions: return "No such function" f = self.w.functions[fid] if f.type not in (SCENE, AUDIO, CHASER, JOIN): return "Invalid function" else: f = None c = self.current_cv.chaser if c.type == CHASER: if name is None and f is not None: name = "Step \"%s\"" % f.name s = ChaserStep(c, index=index, name=name, function=f) self.current_cv.set_chaser(c, s.index) else: if f.id in (i.id for i in c.steps): return "Already added" c.add_step(f) self.current_cv.set_chaser(c, len(c.steps)-1) #self.chaser_edit(s.index+1) def chaser_new_new(self, index, name, fname, type_): with self.w_lock: if name is None and f is not None: name = "Step \"%s\"" % f.name f = type_(self.w, name=fname) return self.chaser_new(index, name, fid=f.id) def chaser_rename(self, name): with self.w_lock: if self.current_cv.selected is None: return "No step selected" c = self.current_cv.chaser s = c.steps[self.current_cv.selected] s.name = name self.current_cv.set_chaser(c, s.index) def chaser_bind(self, fid): with self.w_lock: if self.current_cv.selected is None: return "No step selected" c = self.current_cv.chaser s = c.steps[self.current_cv.selected] if fid not in self.w.functions: return "No such function" f = self.w.functions[fid] if f.type not in (SCENE, AUDIO, CHASER, JOIN): return "Invalid function" s.function = f self.current_cv.set_chaser(c, s.index) self.chaser_select(s.index+1) def chaser_move(self, a, b = None): with self.w_lock: cursel = self.current_cv.selected if cursel is not None: cursel = self.current_cv.chaser.steps[cursel] if b is not None: sel = a - 1 if sel < 0 or sel >= len(self.current_cv.chaser.steps): return "Invalid selection" to = b - 1 else: to = a - 1 if self.current_cv.selected is None: return "No step selected" sel = self.current_cv.selected if to < 0 or to >= len(self.current_cv.chaser.steps): return "Invalid destination" c = self.current_cv.chaser s = c.steps[sel] s.index = to self.current_cv.set_chaser(c, cursel.index if cursel is not None else None) def primitive_rename(self, name): with self.w_lock: self.primitive.name = name self.channel_bank.title = self.primitive.type + ' "%s"' % name def chaser_rename_self(self, name): with self.w_lock: c = self.current_cv.chaser c.name = name self.current_cv.set_chaser(c, self.current_cv.selected) def audio_fade(self, t, out=False): with self.w_lock: if out: self.primitive.fade_out = t else: self.primitive.fade_in = t self.channel_bank.audio = self.primitive def audio_filename(self, fname): with self.w_lock: self.primitive.filename = fname self.channel_bank.audio = self.primitive def _render_callback(self, t, values): if not self.rendering: return with self.w_lock, CURSES_LOCK: syx = self.input.win.getyx() self.channel_bank.set_values(values) self.channel_bank.title = "LIVE: %7.2fs" % t for d, cv in zip(self.renderer._data, (i for i in self.chaser_views if i.chaser.type == CHASER)): cv.selected = d.steps[-1].index if d.steps else None self.input.win.move(*syx) def base_run(self): if not self.chaser_views: return "No chasers loaded" self.channel_bank.set_scope(self._channels) self.channel_bank.title = "LIVE: %7.2fs" % 0 self.channel_bank.highlight = True self.rendering = True self.renderer.set_functions(*((cv.chaser, cv.chaser.get_data(cv.selected)) for cv in self.chaser_views if cv.chaser.type == CHASER)) self.renderer.start() self.input.context = self.context_run return "Started running" def chaser_run(self): self.current_cv.highlight = False self.renderer.clear_hold() self.chaser_stack.append([c.selected for c in self.chaser_views]) self.handle_show(None) self.base_run() def run_exit(self): self.rendering = False self.renderer.stop() with self.w_lock: self.channel_bank.title = "Channels" self.channel_bank.highlight = False self.channel_bank.set_scope(()) if self.chaser_stack: for i, c in zip(self.chaser_stack.pop(-1), self.chaser_views): c.selected = i self.current_cv.highlight = True if self.current_cv.selected is not None: s = self.current_cv.chaser.steps[self.current_cv.selected] if s.function is not None and s.function.type in (SCENE, AUDIO,): self.handle_show(s.function) self.input.context = self.context_chaser else: self.input.context = self.context_base for cv in self.chaser_views: cv.selected = None def run_jump(self, n, p, index): if not index: n = [j for j, i in enumerate(self.chaser_views) if i.chaser.id == n] if not n: return "No such chaser loaded" n = n[0] if n >= len(self.chaser_views) or n < 0: return "Chaser index out of range" if p is not None and (p < 1 or p > len(self.chaser_views[n].chaser.steps)): return "Step index out of range" self.renderer.advance((n, (p-1) if p is not None else p)) def run_advance_all(self): for c in self.chaser_views: self.renderer.advance((c.chaser.id, None)) def current_status(self): self.pager.display_many(( "CURRENT STATUS", "Output is %sOK: %s" % ("" if self.output.ok else "NOT ", self.output.status), ), True) def chaser_info(self, n): with self.w_lock: if n is None: n = self.current_cv.selected if n is None: return "No step selected" else: n -= 1 if n < 0 or n >= len(self.current_cv.chaser.steps): return "Invalid step" f = self.current_cv.chaser.steps[n] self.pager.display_many(( f.name, "- Fade in: %7.3f" % (f.fade_in/1000), "- Duration: " + (("%7.3f" % (f.duration/1000)) if f.duration != INFTY else "infty"), "- Fade out: %7.3f" % (f.fade_out/1000), )) def chaser_mode(self, mode): with self.w_lock: self.current_cv.chaser.advance_mode = mode self.current_cv.set_chaser(self.current_cv.chaser, self.current_cv.selected) def base_copy(self, name, num): with self.w_lock: ## TODO: Implement this in BLC if num not in self.w.functions: return "No such function" f = self.w.functions[num] if f.type != SCENE: return "Can only close scenes" f2 = Scene(self.w, name=name, values=f.values) self.handle_enter(f2.id) def __init__(self, path, output): ## Have to do most of the actual initialization in the main method, as curses isn't ## ready yet. self.channel_bank = None self.input = None self.pager = None self.stdscr = None self.primitive = None self.chaser = None self.current_cv = None self.path = path self._w_created = False if path is None or not os.path.isfile(path): self.w = Workspace("", "", 0, dt.datetime.now()) self._w_created = True else: self.w = Workspace.load(path) self.w_lock = threading.RLock() self.chaser_views = [] self.chaser_stack = [] self.context_base = Input.parse_context(( ("edit $num", self.handle_enter), ("edit", lambda: self.handle_enter(None)), ("delete $num", self.base_delete), ("new scene $quoted_string", lambda n: self.base_new(n, Scene)), ("new chaser $quoted_string", lambda n: self.base_new(n, Chaser)), ("new audio $quoted_string", lambda n: self.base_new(n, Audio)), ("new join $quoted_string", lambda n: self.base_new(n, Join)), ("add $num", self.base_add), ("subtract $letter", lambda n: self.base_remove(n, True)), ("subtract $num", lambda n: self.base_remove(n, False)), ("run", self.base_run), ("write", self.base_save), ("write $quoted_string", self.base_save), ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), ("list chasers", lambda: self.list_functions(CHASER)), ("list audio", lambda: self.list_functions(AUDIO)), ("list joins", lambda: self.list_functions(JOIN)), ("new scene $quoted_string from $num", self.base_copy), ("currentstatus", self.current_status), ("pager page", self.page), ("pager clear", self.page_clear), ("quit", self.base_quit), ), { None: "This is the base edit mode for editing functions.", "edit": "Edit the specified function.", "delete": "Delete the specified function.", "new": "Create a new function of the given type.", "write": "Save the workspace. The path is implicitly the one loaded from if not given.", "list": "List available fixtures or functions.", "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", "quit": "Exit BLC.", "remove": "Remove the specified chaser from the display. If a letter is used, remove the chaser at the given position, where 'a' is the left-most chaser and so forth. If a number is used, treat it as a chaser ID.", "add": "Add a chaser to the display. Currently a limit of 2 visible.", "currentstatus" : "Display information about the system's current status." }, self.help) self.context_chaser = Input.parse_context(( ("choose $num", self.chaser_select), ("edit", self.chaser_edit), ("edit $num", self.chaser_edit), ("delete", self.chaser_delete), ("delete $num", self.chaser_delete), ("append $quoted_string", lambda n: self.chaser_new(-1, n)), ("append $quoted_string from $num", lambda n, s: self.chaser_new(-1, n, s)), ("append $quoted_string from new scene $quoted_string", lambda n, s: self.chaser_new_new(-1, n, s, Scene)), ("append $quoted_string from new audio $quoted_string", lambda n, s: self.chaser_new_new(-1, n, s, Audio)), ("append", lambda: self.chaser_new(-1, "")), ("append from $num", lambda s: self.chaser_new(-1, "", s)), ("append from new scene $quoted_string", lambda s: self.chaser_new_new(-1, "", s, Scene)), ("append from new audio $quoted_string", lambda s: self.chaser_new_new(-1, "", s, Audio)), ("new $num $quoted_string", self.chaser_new), ("new $num $quoted_string from $num", self.chaser_new), ("new $num $quoted_string from new scene $quoted_string", lambda i, n, s: self.chaser_new_new(i, n, s, Scene)), ("new $num $quoted_string from new audio $quoted_string", lambda i, n, s: self.chaser_new_new(i, n, s, Audio)), ("rename $quoted_string", self.chaser_rename), ("rename chaser $quoted_string", self.chaser_rename_self), ("move $num to $num", self.chaser_move), ("move $num", self.chaser_move), ("set fade in $time", self.chaser_fade), ("set fade out $time", lambda t: self.chaser_fade(t, True)), ("set mode oneshot", lambda: self.chaser_mode(ONESHOT)), ("set mode loop", lambda: self.chaser_mode(LOOP)), ("set length $time", self.chaser_duration), ("unbind", self.chaser_unset), ("bind $num", self.chaser_bind), ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), ("list chasers", lambda: self.list_functions(CHASER)), ("list audio", lambda: self.list_functions(AUDIO)), ("list joins", lambda: self.list_functions(JOIN)), ("info", lambda: self.chaser_info(None)), ("info $num", self.chaser_info), ("trailer", self.chaser_run), ("pager page", self.page), ("pager clear", self.page_clear), ("quit", self.handle_exit), ), { None: "This mode is for editing chasers. All functions (excepting select) which take a number as an argument may be called without to act on the currently selected step.", "edit": "Edit the given step.", "select": "Select a step.", "fade": "Change the fade durations for the selected step.", "length": "Set the duration for the selected step.", "new": "Create a new step at the given position with the given name. Can also create a new function to use for the scene. Immediately enters edit mode for that step.", "delete": "Remove the given step.", "rename": "Rename the current step.", "append": "Create a new step at the end of the chaser. See 'new' for details.", "unset": "Unset the duration of the selected step, inheriting the step's duration from its function.", "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", "move": "Move the given step to the given position", "bind": "Bind the step to the given function.", "quit": "Return to the previous mode.", "trailer": "Preview the chaser in run mode.", }, self.help) self.context_scene = Input.parse_context(( ("set $channel_range to $value", self.scene_set), ("reset $channel_range", self.scene_clear), ("edit $channel_range", self.scene_edit), ("edit $channel_range force", lambda cr: self.scene_edit(cr, True)), ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), ("list chasers", lambda: self.list_functions(CHASER)), ("list audio", lambda: self.list_functions(AUDIO)), ("list joins", lambda: self.list_functions(JOIN)), ("rename $quoted_string", self.primitive_rename), ("pager page", self.page), ("pager clear", self.page_clear), ("quit", self.scene_exit), ), { None: "This mode is for editing scene primitives for fixed lighting.", "set": "Set the given channel range to the given value (0 <= value <= 255).", "reset": "Remove the given channel range from the scene", "list": "List available fixtures or functions.", "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", "edit": "Live edit scenes using the arrow keys, page up/down, home/end, and h/j/k/l. If \"force\" is used, all matching channels are edited, instead of just the matching ones already in the scope.", "quit": "Return to the previous mode.", }, self.help) self.context_audio = Input.parse_context(( ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), ("list chasers", lambda: self.list_functions(CHASER)), ("rename $quoted_string", self.primitive_rename), ("filename $quoted_string", self.audio_filename), ("fade in $time", self.audio_fade), ("fade out $time", lambda t: self.audio_fade(t, True)), ("pager page", self.page), ("pager clear", self.page_clear), ("quit", self.audio_exit), ), { None: "This mode is for editing audio primitives for single audio files.", "list": "List available fixtures or functions.", "rename": "Rename the function.", "filename": "Set the filename for the function.", "fade": "Set fade times for the function. Note that unlike lighting, fade out is done during the audio's run.", "pager": "Control the pager. In page mode, arrow keys, page up/down, home/end, and j/k can be used to scroll, q exits.", "quit": "Return to the previous mode.", }, self.help) self.context_run = Input.parse_context(( ("quit", self.run_exit), ("jump $letter to $num", lambda n, p: self.run_jump(n, p, True)), ("jump $num to $num", lambda n, p: self.run_jump(n, p, False)), ("advance $letter", lambda n: self.run_jump(n, None, True)), ("advance $num", lambda n: self.run_jump(n, None, False)), ("advance", lambda: self.run_jump(0, None, True)), ("badvance", lambda: self.run_jump(1, None, True)), ("everythingadvance", self.run_advance_all()), ("currentstatus", self.current_status), ), { None: "This mode is for running a show or previewing a chaser.", "quit": "Return to the previous mode.", "jump": "Jump the given chaser to the given location.", "advance": "Advance a chaser a single step. If no letter is given, this is the first from the left.", "badvance": "Advance the second chaser from the left a single step.", "everythingadvance": "Advance all chasers a single step each.", "currentstatus" : "Display information about the system's current status." }, self.help) self.context_join = Input.parse_context(( ("choose $num", self.chaser_select), ("edit", self.chaser_edit), ("edit $num", self.chaser_edit), ("delete", self.chaser_delete), ("delete $num", self.chaser_delete), ("add", lambda: self.chaser_new(-1, "")), ("add from $num", lambda s: self.chaser_new(-1, "", s)), ("add from new scene $quoted_string", lambda s: self.chaser_new_new(-1, "", s, Scene)), ("add from new audio $quoted_string", lambda s: self.chaser_new_new(-1, "", s, Audio)), ("rename $quoted_string", self.chaser_rename_self), ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), ("list chasers", lambda: self.list_functions(CHASER)), ("list audio", lambda: self.list_functions(AUDIO)), ("list joins", lambda: self.list_functions(JOIN)), ("pager page", self.page), ("pager clear", self.page_clear), ("quit", self.handle_exit), )) self.output = output self.renderer = Renderer(self.w, self.w_lock, self.output, self._render_callback) self.rendering = False self._channels = sum((tuple(f.channels) for f in sorted(self.w.fixtures.values(), key=lambda i: i.id)), ())