From 755d1dda2a1eb1c26fa7bc12328e2bca25256257 Mon Sep 17 00:00:00 2001 From: Ben Connors Date: Fri, 18 Oct 2019 23:02:12 -0400 Subject: Get a decent start on the actual interface - Can edit, create, delete scenes - Basic saving --- interface/__main__.py | 10 ++ interface/channelbank.py | 119 +++++++++++++-------- interface/globals.py | 3 + interface/input/__init__.py | 0 interface/input/parsers.py | 165 +++++++++++++++++++++++++++++ interface/input/tabcomp.py | 247 ++++++++++++++++++++++++++++++++++++++++++++ interface/interface.py | 223 +++++++++++++++++++++++++++++++++++++++ interface/pager.py | 149 ++++++++++++++++++++++++++ 8 files changed, 874 insertions(+), 42 deletions(-) create mode 100644 interface/__main__.py create mode 100644 interface/globals.py create mode 100644 interface/input/__init__.py create mode 100755 interface/input/parsers.py create mode 100755 interface/input/tabcomp.py create mode 100644 interface/interface.py create mode 100755 interface/pager.py diff --git a/interface/__main__.py b/interface/__main__.py new file mode 100644 index 0000000..a8c2738 --- /dev/null +++ b/interface/__main__.py @@ -0,0 +1,10 @@ +import datetime as dt +import os +import sys + +from .interface import Interface + +if len(sys.argv) > 2: + raise ValueError("Usage: %s [workspace file]" % sys.argv[0]) + +Interface(sys.argv[1] if len(sys.argv) == 2 else None).main() diff --git a/interface/channelbank.py b/interface/channelbank.py index c9d971f..c7c7409 100755 --- a/interface/channelbank.py +++ b/interface/channelbank.py @@ -1,5 +1,7 @@ import curses +from .globals import CURSES_LOCK + _progress = [" "] for i in range(8): pos = i // 2 @@ -30,13 +32,15 @@ class ChannelView: height = 3 def _refresh_channel(self): - self.win.addstr(0, 0, "%03d.%03d" % (self.c.f.id, self.c.id), curses.A_UNDERLINE if self._active else 0) - self.win.noutrefresh() + with CURSES_LOCK: + self.win.addstr(0, 0, "%03d.%03d" % (self.c.f.id, self.c.id), curses.A_UNDERLINE if self._active else 0) + self.win.refresh() def _refresh_value(self): - self.win.addstr(1, 0, get_progress(self._value)) - self.win.addstr(1, 4, "%03d" % self._value, (curses.A_ITALIC|curses.A_BOLD if self._held else 0)) - self.win.noutrefresh() + with CURSES_LOCK: + self.win.addstr(1, 0, get_progress(self._value)) + self.win.addstr(1, 4, "%03d" % self._value, (curses.A_ITALIC|curses.A_BOLD if self._held else 0)) + self.win.refresh() def refresh(self): self._refresh_value() @@ -75,8 +79,9 @@ class ChannelView: def set_pos(self, y, x): self.y = self.y self.x = self.x - self.win.noutrefresh() - self.win.mvwin(y, x) + with CURSES_LOCK: + self.win.refresh() + self.win.mvwin(y, x) self.refresh() def __init__(self, root, c, y, x, value=0, held=False, active=False): @@ -89,7 +94,8 @@ class ChannelView: self.x = x self.root = root - self.win = root.subpad(self.height, self.width, self.y, self.x) + with CURSES_LOCK: + self.win = root.subpad(self.height, self.width, self.y, self.x) self._refresh_value() self._refresh_channel() @@ -100,45 +106,50 @@ class ChannelBank: raise NotImplementedError("Screen size to small") - if (height, width) != (self._height, self._width): - self.win.erase() - self.win.noutrefresh() + with CURSES_LOCK: + if (height, width) != (self._height, self._width): + self.win.erase() + self.win.refresh() - self.win.resize(height, width) - self.win.redrawwin() + self.win.resize(height, width) + self.win.redrawwin() - self._height = height - self._width = width - self._refresh_scope() - self.win.border() - self.win.noutrefresh() + self._height = height + self._width = width + self._refresh_scope() + self._put_title() + self.win.refresh() def _refresh_scope(self): - self.win.erase() - ncv = {} - cols = (self._width-4)//ChannelView.width - with open("out.txt", 'w+') as f: - f.write(str(cols)+'\n') - self._sscope = sorted(self.scope, key=lambda a: (a.f.id, a.id)) - for n, c in enumerate(self._sscope): - row = (n // cols)*ChannelView.height + 1 - col = (n % cols)*ChannelView.width + 2 - if c in self._cv: - active = self._cv[c].active - held = self._cv[c].held - value = self._cv[c].value - else: - active = False - held = False - value = 0 - ncv[c] = ChannelView(self.win, c, row, col, value=value, active=active, held=held) - self._cv = ncv + with CURSES_LOCK: + self.win.erase() + ncv = {} + cols = (self._width-4)//ChannelView.width + with open("out.txt", 'w+') as f: + f.write(str(cols)+'\n') + self._sscope = sorted(self.scope, key=lambda a: (a.f.id, a.id)) + for n, c in enumerate(self._sscope): + row = (n // cols)*ChannelView.height + 1 + col = (n % cols)*ChannelView.width + 2 + if c in self._cv: + active = self._cv[c].active + held = self._cv[c].held + value = self._cv[c].value + else: + active = False + held = False + value = 0 + ncv[c] = ChannelView(self.win, c, row, col, value=value, active=active, held=held) + self._cv = ncv + self._put_title() + self.win.refresh() def set_pos(self, y, x): if (y, x) != (self._y, self._x): - self.win.mvwin(y, x) - self.win.border() - self.win.noutrefresh() + with CURSES_LOCK: + self.win.mvwin(y, x) + self._put_title() + self.win.refresh() def set_active(self, channels, v=True): for c in channels: @@ -151,12 +162,36 @@ class ChannelBank: def set_values(self, cv): for c, v in cv: self._cv[c].value = v + + def set_scope(self, scope): + self.scope = frozenset(scope) + self._refresh_scope() + + @property + def title(self): + return self._title + + def _put_title(self): + self.win.border() + pos = min(self._width-2-len(self._title), (3*self._width)//4 - (len(self._title) // 2)) + self.win.addstr(self._height-1, pos, self._title) + + @title.setter + def title(self, v): + if v != self._title: + with CURSES_LOCK: + self._title = v + self.win.border() + self._put_title() + self.win.refresh() def __init__(self, y, x, height, width, scope=frozenset()): - self.win = curses.newwin(height, width, y, x) - self.win.keypad(True) + with CURSES_LOCK: + self.win = curses.newwin(height, width, y, x) + self.win.keypad(True) self.scope = frozenset(scope) self._sscope = [] + self._title = "Channels" self._cv = {} self._height = height self._width = width diff --git a/interface/globals.py b/interface/globals.py new file mode 100644 index 0000000..64e70a6 --- /dev/null +++ b/interface/globals.py @@ -0,0 +1,3 @@ +import threading + +CURSES_LOCK = threading.RLock() diff --git a/interface/input/__init__.py b/interface/input/__init__.py new file mode 100644 index 0000000..e69de29 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) diff --git a/interface/interface.py b/interface/interface.py new file mode 100644 index 0000000..c9f2a6e --- /dev/null +++ b/interface/interface.py @@ -0,0 +1,223 @@ +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 + +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 .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: + @staticmethod + def _compute_sizes(height, width): + hb = height // 2 + ht = height - hb + + wr = width // 2 + wl = width - wr - 1 + + return ( + (ht, width), (0, 0), + (4, wr), (height-4, wl+1), + (hb - 4, wr), (ht, wl+1), + ) + + def _resize(self): + for a, f 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)): + f(*a) + + @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 base_edit(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 != SCENE: + ## FIXME + return "Can only edit scenes so far" + with CURSES_LOCK: + 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.input.context = self.context_scene + + 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.base_edit(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.input.context = self.context_base + self.primitive = None + self.channel_bank.set_scope(()) + self.channel_bank.title = "Channels" + + 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; %3dc: %s" % (f.id, len(f.channels), f.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 __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.chasers = [] + + 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.context_base = Input.parse_context(( + ("edit $num", self.base_edit), + ("delete $num", self.base_delete), + ("new scene $quoted_string", self.base_new_scene), + + ("save", self.base_save), + ("save $quoted_string", self.base_save), + + ("list fixtures", self.list_fixtures), + ("list scenes", lambda: self.list_functions(SCENE)), + + ("page", self.page), + ("clrpage", self.page_clear), + ("quit", self.base_quit), + )) + + 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)), + + ("page", self.page), + ("clrpage", self.page_clear), + ("quit", self.scene_exit), + )) diff --git a/interface/pager.py b/interface/pager.py new file mode 100755 index 0000000..2203e52 --- /dev/null +++ b/interface/pager.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 + +import curses +import threading + +from .globals import CURSES_LOCK + +class Pager: + 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._regen_actual() + self._redraw() + + 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.noutrefresh() + + def _redraw(self): + with self._lock, CURSES_LOCK: + start = max(0, self._bottom_a - self._height + 2) + end = self._bottom_a + todisp = self._actual[start:end] + if len(todisp) < self._height-2 and len(todisp) < len(self._actual): + end = self._bottom_a+self._height-2-len(todisp) + todisp += self._actual[self._bottom_a:end] + self.win.erase() + self.win.border() + if start > 0: + self.win.addch(0, self._width//2, '⯅') + for n, l in enumerate(todisp, 1): + self.win.addstr(n, 1, l) + if end < len(self._actual): + self.win.addch(self._height-1, self._width//2, '⯆') + self.win.refresh() + + def _split_line(self, l): + with self._lock: + lines = [] + + while len(l) >= self._width-2: + lines.append(l[:self._width-3] + '…') + l = "⮡ "+l[self._width-3:] + lines.append(l) + + return lines + + def _regen_actual(self): + with self._lock: + self._actual = [] + for n, l in enumerate(self._lines): + self._actual += self._split_line(l) + if n == self._bottom: + self._bottom_a = len(self._actual) + self._bottom_a = max(self._bottom_a, self._height-2) + + def display_many(self, s, split=False): + with self._lock: + if split: + self._lines.append("") + self._actual.append("") + for l in s: + self._lines.append(l) + self._actual += self._split_line(l) + self._bottom = len(self._lines) - 1 + self._bottom_a = len(self._actual) + self._redraw() + + def display(self, s): + self.display_many((s, )) + + def user_page(self, refresh=None): + curses.curs_set(0) + + while True: + l = self.win.getch() + + with self._lock: + if l == curses.KEY_RESIZE: + if refresh is not None: + refresh() + elif l == ord('q'): + break + elif l in (ord('k'), curses.KEY_UP): + if self._bottom_a > self._height-2: + self._bottom_a -= 1 + self._redraw() + elif l in (ord('j'), curses.KEY_DOWN): + if self._bottom_a < len(self._actual): + self._bottom_a += 1 + self._redraw() + elif l == curses.KEY_NPAGE: + if self._bottom_a < len(self._actual): + self._bottom_a = min(len(self._actual), self._bottom_a + self._height - 2) + self._redraw() + elif l == curses.KEY_PPAGE: + if self._bottom_a > 0: + self._bottom_a = max(self._height - 2, self._bottom_a - self._height + 2) + self._redraw() + elif l == curses.KEY_HOME: + if self._bottom_a > self._height - 2: + self._bottom_a = self._height - 2 + self._redraw() + elif l == curses.KEY_END: + if self._bottom_a < len(self._actual): + self._bottom_a = len(self._actual) + self._redraw() + + curses.curs_set(1) + + def clear(self): + with self._lock: + self._lines = [] + self._actual = [] + self._bottom_a = 0 + self._bottom = -1 + self._redraw() + + 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._lines = [] + self._actual = [] + self._bottom = -1 + self._bottom_a = 0 + + self.set_pos(y, x) + self.set_dim(height, width) -- cgit v1.2.3