import curses import os import datetime as dt import threading 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.constants import SCENE, CHASER, AUDIO, MANUAL, INHERIT 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 def wrap_curses(f): def inner(*args, **kwargs): return curses.wrapper(lambda stdscr: f(*args, stdscr, **kwargs)) return inner __version__ = "v0.0.1" 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 lib %s, int %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) self.input.main(self._resize) def handle_enter(self, fid): with self.w_lock: 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.channel_bank.set_scope(f.scope) self.channel_bank.set_values(f.values.items()) self.channel_bank.title = f.type + ' "%s"' % f.name self.channel_bank.highlight = True self.input.context = self.context_scene elif f.type == CHASER: 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 self.input.context = self.context_chaser 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 != CHASER: 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_scene(self, name): with self.w_lock: f = Scene(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, 1): 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)) def scene_clear(self, cr): self.scene_set(cr, None) def scene_exit(self): self.primitive = None self.channel_bank.set_scope(()) self.channel_bank.title = "Channels" 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 self.input.context = self.context_chaser self.chaser = c else: self.input.context = self.context_base self.chaser = None self.current_cv = None 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): quit() 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 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.function is None: return "No function on step" self.handle_enter(c.function.id) 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 __init__(self, path): ## 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), ("delete $num", self.base_delete), ("new scene $quoted_string", self.base_new_scene), ("add $num", self.base_add), ("remove $letter", lambda n: self.base_remove(n, True)), ("remove $num", lambda n: self.base_remove(n, False)), ("save", self.base_save), ("save $quoted_string", self.base_save), ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), ("list chasers", lambda: self.list_functions(CHASER)), ("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.", "save": "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.", }, self.help) self.context_chaser = Input.parse_context(( ("select $num", self.chaser_select), ("edit", self.chaser_edit), ("edit $num", self.chaser_edit), ("fade in $time", self.chaser_fade), ("fade out $time", lambda t: self.chaser_fade(t, True)), ("duration $time", self.chaser_duration), ("unset", self.chaser_unset), ("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.", "duration": "Set the duration for the selected step.", "unset": "Unset the duration of the selected step, inheriting the step's duration from its function.", "quit": "Return to the previous mode.", }, self.help) self.context_scene = Input.parse_context(( ("set $channel_range to $value", self.scene_set), ("reset $channel_range", self.scene_clear), ("list fixtures", self.list_fixtures), ("list scenes", lambda: self.list_functions(SCENE)), ("list chasers", lambda: self.list_functions(CHASER)), ("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.", "quit": "Return to the previous mode.", }, self.help)