diff options
Diffstat (limited to 'presentty')
-rw-r--r-- | presentty/__init__.py | 0 | ||||
-rw-r--r-- | presentty/ansiparser.py | 185 | ||||
-rw-r--r-- | presentty/client.py | 66 | ||||
-rw-r--r-- | presentty/console.py | 292 | ||||
-rw-r--r-- | presentty/image.py | 168 | ||||
-rw-r--r-- | presentty/palette.py | 78 | ||||
-rw-r--r-- | presentty/presentty.py | 162 | ||||
-rw-r--r-- | presentty/rst.py | 493 | ||||
-rw-r--r-- | presentty/server.py | 112 | ||||
-rw-r--r-- | presentty/slide.py | 178 | ||||
-rw-r--r-- | presentty/text.py | 81 | ||||
-rw-r--r-- | presentty/transition.py | 153 |
12 files changed, 1968 insertions, 0 deletions
diff --git a/presentty/__init__.py b/presentty/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/presentty/__init__.py diff --git a/presentty/ansiparser.py b/presentty/ansiparser.py new file mode 100644 index 0000000..a780b42 --- /dev/null +++ b/presentty/ansiparser.py @@ -0,0 +1,185 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re + +import urwid + +class ANSIParser(object): + colors = [ + urwid.BLACK, + urwid.DARK_RED, + urwid.DARK_GREEN, + urwid.BROWN, + urwid.DARK_BLUE, + urwid.DARK_MAGENTA, + urwid.DARK_CYAN, + urwid.LIGHT_GRAY, + urwid.DARK_GRAY, + urwid.LIGHT_RED, + urwid.LIGHT_GREEN, + urwid.YELLOW, + urwid.LIGHT_BLUE, + urwid.LIGHT_MAGENTA, + urwid.LIGHT_CYAN, + urwid.WHITE, + ] + + colors256 = ['0', '6', '8', 'a', 'd', 'f'] + colorsgray = ['3', '7', '11', '13', '15', '19', '23', '27', '31', + '35', '38', '42', '46', '50', '52', '58', '62', '66', + '70', '74', '78', '82', '85', '89', '93'] + + def __init__(self): + self.x = 0 + self.y = 0 + self.text_lines = [] + self.attr_lines = [] + self.background = urwid.AttrSpec('light gray', 'black') + self.attr = self.background + self.resetColor() + self.moveTo(0,0) + + def resetColor(self): + self.bold = False + self.blink = False + self.fg = 7 + self.bg = 0 + + def moveTo(self, x, y): + while x>80: + x-=80 + y+=1 + while y+1 > len(self.text_lines): + self.text_lines.append([u' ' for i in range(80)]) + self.attr_lines.append([self.attr for i in range(80)]) + self.x = x + self.y = y + + def parseSequence(self, seq): + values = [] + buf = '' + for c in seq: + if c in ['\x1b', '[']: + continue + if c == ';': + values.append(int(buf)) + buf = '' + continue + if ord(c) < 64: + buf += c + if buf: + values.append(int(buf)) + if c == 'm': + if not values: + values = [0] + fg256 = None + for v in values: + if fg256 is True: + if v <= 0x08: + self.fg = v + elif v <= 0x0f: + self.fg = v - 0x08 + self.bold = True + elif v <= 0xe7: + r, x = divmod(v-16, 36) + g, x = divmod(x, 6) + b = x % 6 + fg256 = ('#' + + self.colors256[r] + + self.colors256[g] + + self.colors256[b]) + else: + fg256 = 'g' + str(self.colorsgray[v-232]) + elif v == 0: + self.resetColor() + elif v == 1: + self.bold = True + elif v == 5: + self.blink = True + elif v>29 and v<38: + self.fg = v-30 + elif v>39 and v<48: + self.bg = v-40 + elif v==38: + fg256=True + fg = self.fg + if self.bold: + fg += 8 + fgattrs = [] + if self.blink: + fgattrs.append('blink') + if fg256: + fgattrs.append(fg256) + else: + fgattrs.append(self.colors[fg]) + self.attr = urwid.AttrSpec(', '.join(fgattrs), self.colors[self.bg]) + if c == 'A': + if not values: + values = [1] + y = max(self.y-values[0], 0) + self.moveTo(self.x, y) + if c == 'C': + if not values: + values = [1] + x = self.x + values[0] + self.moveTo(x, self.y) + if c == 'H': + self.moveTo(values[1]-1, values[0]-1) + + def parse(self, data): + seq = '' + for char in data: + if seq: + seq += char + if ord(char) >= 64 and char != '[': + self.parseSequence(seq) + seq = '' + continue + if char == '\x1a': + continue + if char == '\x1b': + seq = char + continue + if char == '\r': + self.moveTo(0, self.y) + continue + if char == '\n': + self.moveTo(self.x, self.y+1) + continue + if not seq: + self.text_lines[self.y][self.x] = char + self.attr_lines[self.y][self.x] = self.attr + x = self.x + 1 + self.moveTo(x, self.y) + text = [] + current_attr = self.attr_lines[0][0] + current_text = u'' + for y in range(len(self.text_lines)): + for x in range(80): + char = self.text_lines[y][x] + attr = self.attr_lines[y][x] + if (attr.foreground_number != current_attr.foreground_number or + attr.background_number != current_attr.background_number): + text.append((current_attr, current_text)) + current_attr = attr + current_text = u'' + current_text += char + if (current_attr.background_number==0): + current_text = current_text.rstrip(' ') + current_text += u'\n' + current_text = re.sub('\n+$', '\n', current_text) + text.append((current_attr, current_text)) + return text diff --git a/presentty/client.py b/presentty/client.py new file mode 100644 index 0000000..a819dfd --- /dev/null +++ b/presentty/client.py @@ -0,0 +1,66 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import socket + +class Client(object): + def __init__(self, host='127.0.0.1', port=1292): + self.host = host + self.port = port + self.sock = None + self.connect() + + def connect(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.host, self.port)) + self.file = self.sock.makefile('rw', 0) + + def list(self): + self.file.write('list\n') + program = [] + while True: + ln = self.file.readline().strip() + if ln == 'end': + break + x, index, title = ln.split(' ', 2) + program.append(title) + return program + + def size(self): + self.file.write('size\n') + ln = self.file.readline().strip() + x, cols, rows = ln.split(' ', 2) + return (int(cols), int(rows)) + + def parseCurrent(self): + ln = self.file.readline().strip() + x, index, progressive_state, title = ln.split(' ', 3) + return (int(index), int(progressive_state)) + + def current(self): + self.file.write('current\n') + return self.parseCurrent() + + def jump(self, index): + self.file.write('jump %i\n' % index) + return self.parseCurrent() + + def next(self): + self.file.write('next\n') + return self.parseCurrent() + + def prev(self): + self.file.write('prev\n') + return self.parseCurrent() diff --git a/presentty/console.py b/presentty/console.py new file mode 100644 index 0000000..d29b864 --- /dev/null +++ b/presentty/console.py @@ -0,0 +1,292 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import sys +import datetime +import time + +import urwid + +import palette +import client +import slide +import rst + +PALETTE = [ + ('reversed', 'standout', ''), + ('status', 'light red', ''), +] + +class Row(urwid.Button): + def __init__(self, index, title, console): + super(Row, self).__init__('', on_press=console.jump, user_data=index) + col = urwid.Columns([ + ('fixed', 3, urwid.Text('%-2i' % (index+1))), + urwid.Text(title), + ]) + self._w = urwid.AttrMap(col, None, focus_map='reversed') + + def selectable(self): + return True + +class Footer(urwid.WidgetWrap): + def __init__(self): + super(Footer, self).__init__(urwid.Columns([])) + self.position = urwid.Text(u'') + self.timer = urwid.Text(u'') + self._w.contents.append((self.position, ('pack', None, False))) + self._w.contents.append((urwid.Text(u''), ('weight', 1, False))) + self._w.contents.append((self.timer, ('pack', None, False))) + +class Screen(urwid.WidgetWrap): + def __init__(self, console): + super(Screen, self).__init__(urwid.Pile([])) + self.console = console + self.program = [] + self.current = -1 + self.progressive_state = 0 + self.blank_slide = slide.UrwidSlide( + u'', None, urwid.Text(u''), None) + self.timer = 45*60 + self.size = (80, 25) + self.timer_end = None + self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([])) + self.footer = Footer() + footer = urwid.AttrMap(self.footer, 'status') + self.left = urwid.Pile([]) + self.left.contents.append((self.listbox, ('weight', 1))) + self.left.set_focus(0) + + self.right = urwid.Pile([]) + self.setPreviews() + + self.main = urwid.Columns([]) + self.main.contents.append((self.left, ('weight', 1, False))) + self.main.contents.append((self.right, ('given', self.size[0]+2, False))) + self.main.set_focus(0) + + self._w.contents.append((self.main, ('weight', 1))) + self._w.contents.append((footer, ('pack', 1))) + self._w.set_focus(0) + + def setPreviews(self): + current_slide = next_slide = self.blank_slide + if 0 <= self.current < len(self.program): + current_slide = self.program[self.current] + if 0 <= self.current+1 < len(self.program): + next_slide = self.program[self.current+1] + current_slide.setProgressive(self.progressive_state) + current_box = urwid.LineBox(current_slide, "Current") + next_box = urwid.LineBox(next_slide, "Next") + if current_slide.handout: + notes_box = urwid.LineBox(current_slide.handout, "Notes") + else: + notes_box = None + self.right.contents[:] = [] + self.left.contents[:] = self.left.contents[:1] + cols, rows = self.size + self.right.contents.append((current_box, ('given', rows+2))) + self.right.contents.append((next_box, ('given', rows+2))) + self.right.contents.append((urwid.Filler(urwid.Text(u'')), ('weight', 1))) + if notes_box: + self.left.contents.append((notes_box, ('pack', None))) + + def setProgram(self, program): + self.program = program + self.listbox.body[:] = [] + for i, s in enumerate(program): + self.listbox.body.append(Row(i, s.title, self.console)) + + def setSize(self, size): + self.size = size + cols, rows = size + self.right.contents[0] = (self.right.contents[0][0], ('given', rows+2)) + self.right.contents[1] = (self.right.contents[1][0], ('given', rows+2)) + self.main.contents[1] = (self.main.contents[1][0], ('given', cols+2, False)) + + # Implement this method from the urwid screen interface for the ScreenHinter + def get_cols_rows(self): + return self.size + + def setCurrent(self, state): + index, progressive_state = state + changed = False + if index != self.current: + self.current = index + self.listbox.set_focus(index) + self.listbox._invalidate() + self.footer.position.set_text('%i / %i' % (index+1, len(self.program))) + changed = True + if progressive_state != self.progressive_state: + self.progressive_state = progressive_state + changed = True + if changed: + self.setPreviews() + self.footer.timer.set_text(self.getTime()) + + def getTime(self): + now = time.time() + if self.timer_end: + return str(datetime.timedelta(seconds=(int(self.timer_end-now)))) + else: + return str(datetime.timedelta(seconds=int(self.timer))) + + def setTimer(self, secs): + self.timer = secs + if self.timer_end: + now = time.time() + self.timer_end = now + self.timer + + def startStopTimer(self): + now = time.time() + if self.timer_end: + remain = max(self.timer_end - int(now), 0) + self.timer = remain + self.timer_end = None + else: + self.timer_end = now + self.timer + + def keypress(self, size, key): + if key in (' ', 'x'): + self.startStopTimer() + elif key == 'page up': + self.console.prev() + elif key == 'page down': + self.console.next() + elif key == 'right': + self.console.next() + elif key == 'left': + self.console.prev() + elif key == 't': + self.console.timerDialog() + else: + return super(Screen, self).keypress(size, key) + return None + +class FixedButton(urwid.Button): + def sizing(self): + return frozenset([urwid.FIXED]) + + def pack(self, size, focus=False): + return (len(self.get_label())+4, 1) + +class TimerDialog(urwid.WidgetWrap): + signals = ['set', 'cancel'] + def __init__(self): + set_button = FixedButton('Set') + cancel_button = FixedButton('Cancel') + urwid.connect_signal(set_button, 'click', + lambda button:self._emit('set')) + urwid.connect_signal(cancel_button, 'click', + lambda button:self._emit('cancel')) + button_widgets = [('pack', set_button), + ('pack', cancel_button)] + button_columns = urwid.Columns(button_widgets, dividechars=2) + rows = [] + self.entry = urwid.Edit('Timer: ', edit_text='45:00') + rows.append(self.entry) + rows.append(urwid.Divider()) + rows.append(button_columns) + pile = urwid.Pile(rows) + fill = urwid.Filler(pile, valign='top') + super(TimerDialog, self).__init__(urwid.LineBox(fill, 'Timer')) + + def keypress(self, size, key): + r = super(TimerDialog, self).keypress(size, key) + if r == 'enter': + self._emit('set') + return None + elif r == 'esc': + self._emit('cancel') + return None + return r + +class Console(object): + poll_interval = 0.5 + + def __init__(self, program): + self.screen = Screen(self) + self.loop = urwid.MainLoop(self.screen, palette=PALETTE) + self.client = client.Client() + self.screen.setProgram(program) + self.update() + self.loop.set_alarm_in(self.poll_interval, self.updateCallback) + + def run(self): + self.loop.run() + + def jump(self, widget, index): + self.screen.setCurrent(self.client.jump(index)) + + def next(self): + self.screen.setCurrent(self.client.next()) + + def prev(self): + self.screen.setCurrent(self.client.prev()) + + def updateCallback(self, loop=None, data=None): + self.update() + self.loop.set_alarm_in(self.poll_interval, self.updateCallback) + + def update(self): + self.screen.setSize(self.client.size()) + self.screen.setCurrent(self.client.current()) + + def timerDialog(self): + dialog = TimerDialog() + overlay = urwid.Overlay(dialog, self.loop.widget, + 'center', 30, + 'middle', 6) + self.loop.widget = overlay + urwid.connect_signal(dialog, 'cancel', self.cancelDialog) + urwid.connect_signal(dialog, 'set', self.setTimer) + + def cancelDialog(self, widget): + self.loop.widget = self.screen + + def setTimer(self, widget): + parts = widget.entry.edit_text.split(':') + secs = 0 + if len(parts): + secs += int(parts.pop()) + if len(parts): + secs += int(parts.pop())*60 + if len(parts): + secs += int(parts.pop())*60*60 + self.screen.setTimer(secs) + self.loop.widget = self.screen + + +def main(): + parser = argparse.ArgumentParser( + description='Console-based presentation system') + parser.add_argument('--light', dest='light', + default=False, + action='store_true', + help='use a black on white palette') + parser.add_argument('file', + help='presentation file (RST)') + args = parser.parse_args() + if args.light: + plt = palette.LIGHT_PALETTE + else: + plt = palette.DARK_PALETTE + hinter = slide.ScreenHinter() + parser = rst.PresentationParser(plt, hinter) + program = parser.parse(open(args.file).read()) + c = Console(program) + hinter.setScreen(c.screen) + c.run() diff --git a/presentty/image.py b/presentty/image.py new file mode 100644 index 0000000..9aabee8 --- /dev/null +++ b/presentty/image.py @@ -0,0 +1,168 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import subprocess +import HTMLParser +import re + +import PIL +import PIL.ExifTags +import urwid + +import slide + +def nearest_color(x): + if x < 0x30: return '0' + if x < 0x70: return '6' + if x < 0x98: return '8' + if x < 0xc0: return 'a' + if x < 0xe8: return 'd' + return 'f' + +class ANSIImage(urwid.Widget): + def __init__(self, uri, hinter=None): + super(ANSIImage, self).__init__() + self.uri = uri + image = self._loadImage() + self.htmlparser = HTMLParser.HTMLParser() + self.ratio = float(image.size[0])/float(image.size[1]) + self.hinter = hinter + + def _loadImage(self): + image = PIL.Image.open(self.uri) + image.load() + exif = image._getexif() + if exif: + orientation = exif.get(274, 1) + if orientation == 1: + pass + elif orientation == 3: + image = image.rotate(180) + elif orientation == 6: + image = image.rotate(-90) + elif orientation == 8: + image = image.rotate(90) + else: + raise Exception("unknown orientation %s" % orientation) + return image + + def pack(self, size, focus=False): + cols = size[0] + if len(size) > 1: + rows = size[1] + elif self.hinter: + rows = self.hinter.getSize()[1] + else: + rows = None + width = cols + height = int(cols*(1.0/self.ratio)/2.0) + if rows is not None and height > rows: + height = rows + width = int(rows*self.ratio*2.0) + return (width, height) + + def rows(self, size, focus=False): + r = self.pack(size) + return r[1] + + SPAN_RE = re.compile(r"<span style='color:#(......); background-color:#(......);'>(.*)") + def render(self, size, focus=False): + spanre = self.SPAN_RE + htmlparser = self.htmlparser + width, height = self.pack(size, focus) + jp2a = subprocess.Popen(['jp2a', '--colors', '--fill', + '--width=%s' % width, + '--height=%s' % height, + '--html-raw', '-'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + image = self._loadImage() + image.save(jp2a.stdin, 'JPEG') + jp2a.stdin.close() + data = jp2a.stdout.read() + jp2a.stderr.read() + jp2a.wait() + + line_list = [] + attr_list = [] + line_text = '' + line_attrs = [] + current_attr = [None, 0] + current_fg = None + current_bg = None + current_props = None + for line in data.split('<br/>'): + if not line: + continue + for span in line.split('</span>'): + if not span: + continue + m = spanre.match(span) + fg, bg, char = m.groups() + if '&' in char: + char = htmlparser.unescape(char) + char = char.encode('utf8') + line_text += char + props = [] + # TODO: if bold is set, append bold to props + fg = ('#'+ + nearest_color(int(fg[0:2], 16)) + + nearest_color(int(fg[2:4], 16)) + + nearest_color(int(fg[4:6], 16))) + bg = ('#'+ + nearest_color(int(bg[0:2], 16)) + + nearest_color(int(bg[2:4], 16)) + + nearest_color(int(bg[4:6], 16))) + if current_fg == fg and current_bg == bg and current_props == props: + current_attr[1] += len(char) + else: + if current_attr[0]: + line_attrs.append(tuple(current_attr)) + fg = ', '.join(props + [fg]) + attr = urwid.AttrSpec(fg, bg) + current_attr = [attr, len(char)] + current_fg = fg + current_bg = bg + current_props = props + line_attrs.append(tuple(current_attr)) + current_attr = [None, 0] + current_fg = None + current_bg = None + line_list.append(line_text) + line_text = '' + attr_list.append(line_attrs) + line_attrs = [] + canvas = urwid.TextCanvas(line_list, attr_list) + return canvas + +def main(): + import PIL.Image + img = PIL.Image.open('/tmp/p/8.jpg') + img.load() + hinter = slide.ScreenHinter() + hinter.set_cols_rows((80, 25)) + w = ANSIImage(img, hinter) + slpile = slide.SlidePile([]) + slpile.contents.append((w, slpile.options())) + pad = slide.SlidePadding(slpile, align='center', width='pack') + fill = slide.SlideFiller(pad) + #w.render((80,25)) + fill.render((80,25)) + screen = urwid.raw_display.Screen() + if True: + with screen.start(): + screen.draw_screen((80,25), fill.render((80,25))) + raw_input() diff --git a/presentty/palette.py b/presentty/palette.py new file mode 100644 index 0000000..6079a95 --- /dev/null +++ b/presentty/palette.py @@ -0,0 +1,78 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import urwid + +DARK_PALETTE = { + '_default': urwid.AttrSpec('light gray', 'black'), + + 'emphasis': urwid.AttrSpec('bold, light gray', 'black'), + 'title': urwid.AttrSpec('bold, white', 'black'), + + 'progressive': urwid.AttrSpec('dark gray', 'black'), + + # Based on pygments default colors + + 'whitespace': urwid.AttrSpec('light gray', '#aaa'), + 'comment': urwid.AttrSpec('#688', 'black'), + 'comment-preproc': urwid.AttrSpec('#a80', 'black'), + 'keyword': urwid.AttrSpec('bold, #0f0', 'black'), + 'keyword-pseudo': urwid.AttrSpec('#080', 'black'), + 'keyword-type': urwid.AttrSpec('#a06', 'black'), + 'operator': urwid.AttrSpec('#666', 'black'), + 'operator-word': urwid.AttrSpec('bold, #a0f', 'black'), + 'name-builtin': urwid.AttrSpec('#0d0', 'black'), + 'name-function': urwid.AttrSpec('#00f', 'black'), + 'name-class': urwid.AttrSpec('bold, #00f', 'black'), + 'name-namespace': urwid.AttrSpec('bold, #00f', 'black'), + 'name-exception': urwid.AttrSpec('bold, #d66', 'black'), + 'name-variable': urwid.AttrSpec('#008', 'black'), + 'name-constant': urwid.AttrSpec('#800', 'black'), + 'name-label': urwid.AttrSpec('#aa0', 'black'), + 'name-entity': urwid.AttrSpec('bold, #888', 'black'), + 'name-attribute': urwid.AttrSpec('#880', 'black'), + 'name-tag': urwid.AttrSpec('bold, #080', 'black'), + 'name-decorator': urwid.AttrSpec('#a0f', 'black'), + 'string': urwid.AttrSpec('#a00', 'black'), + 'string-doc': urwid.AttrSpec('light gray', 'black'), + 'string-interpol': urwid.AttrSpec('bold, #a68', 'black'), + 'string-escape': urwid.AttrSpec('bold, #a60', 'black'), + 'string-regex': urwid.AttrSpec('#a68', 'black'), + 'string-symbol': urwid.AttrSpec('#008', 'black'), + 'string-other': urwid.AttrSpec('#080', 'black'), + 'number': urwid.AttrSpec('#666', 'black'), + 'generic-heading': urwid.AttrSpec('bold, #008', 'black'), + 'generic-subheading': urwid.AttrSpec('bold, #808', 'black'), + 'generic-deleted': urwid.AttrSpec('#a00', 'black'), + 'generic-inserted': urwid.AttrSpec('#0a0', 'black'), + 'generic-error': urwid.AttrSpec('#f00', 'black'), + 'generic-emph': urwid.AttrSpec('bold, #fff', 'black'), + 'generic-strong': urwid.AttrSpec('bold, #ddd', 'black'), + 'generic-prompt': urwid.AttrSpec('bold, #008', 'black'), + 'generic-output': urwid.AttrSpec('#888', 'black'), + 'generic-traceback': urwid.AttrSpec('#06d', 'black'), + 'error': urwid.AttrSpec('underline, #f00', 'black'), +} + +LIGHT_PALETTE = {} +for k, v in DARK_PALETTE.items(): + LIGHT_PALETTE[k] = urwid.AttrSpec(v.foreground, 'h15') + +LIGHT_PALETTE.update({ + '_default': urwid.AttrSpec('black', 'h15'), + 'emphasis': urwid.AttrSpec('bold, black', 'h15'), + 'title': urwid.AttrSpec('bold, #000', 'h15'), + 'progressive': urwid.AttrSpec('light gray', 'h15'), +}) diff --git a/presentty/presentty.py b/presentty/presentty.py new file mode 100644 index 0000000..35e19c1 --- /dev/null +++ b/presentty/presentty.py @@ -0,0 +1,162 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import os +import sys +import time + +import urwid + +import slide +import server +import rst +import palette + + +class Presenter(object): + def __init__(self, palette): + blank = urwid.Text(u'') + self.blank = slide.UrwidSlide('Blank', None, blank, + palette['_default']) + self.current = self.blank + self.program = [] + self.palette = palette + self.pos = -1 + self.loop = urwid.MainLoop(self.blank, + unhandled_input=self.unhandledInput) + self.loop.screen.set_terminal_properties(colors=256) + + self.server_pipe_in = self.loop.watch_pipe(self.serverData) + r,w = os.pipe() + self.server_pipe_out_read = os.fdopen(r) + self.server_pipe_out_write = w + self.server = server.ConsoleServer(self) + self.server.start() + + def serverData(self, data): + parts = data.split() + if parts[0] == 'jump': + try: + index = int(parts[1]) + except Exception: + os.write(self.server_pipe_out_write, 'err\n') + return + if index < 0 or index > len(self.program)-1: + os.write(self.server_pipe_out_write, 'err\n') + return + self.transitionTo(index) + os.write(self.server_pipe_out_write, 'ok\n') + elif parts[0] == 'next': + self.nextSlide() + os.write(self.server_pipe_out_write, 'ok\n') + elif parts[0] == 'prev': + self.prevSlide() + os.write(self.server_pipe_out_write, 'ok\n') + + def setProgram(self, program): + self.program = program + + def run(self): + self.loop.set_alarm_in(0, self.nextSlide) + self.loop.run() + + def unhandledInput(self, key): + if key in ('right', 'page down'): + self.nextSlide() + elif key in ('left', 'page up'): + self.prevSlide() + + def transitionTo(self, index, forward=True): + self.pos = index + current_slide = self.current + new_slide = self.program[index] + if forward: + transition = new_slide.transition + new_slide.resetProgressive() + else: + transition = current_slide.transition + new_slide.resetProgressive(True) + current_slide.stopAnimation() + if forward: + transition.setTargets(current_slide, new_slide) + else: + transition.setTargets(new_slide, current_slide) + self.loop.widget = transition + duration = transition.getDuration() + start = time.time() + now = start + end = start + duration + while duration: + if forward: + progress = min(1-((end-now)/duration), 1.0) + else: + progress = max(((end-now)/duration), 0.0) + transition.setProgress(progress) + self.loop.draw_screen() + now = time.time() + if now >= end: + break + end = time.time() + self.loop.widget = new_slide + self.current = new_slide + self.loop.draw_screen() + current_slide.resetAnimation() + new_slide.startAnimation(self.loop) + + def nextSlide(self, loop=None, data=None): + if self.current.nextProgressive(): + return + if self.pos+1 == len(self.program): + return + self.transitionTo(self.pos+1) + + def prevSlide(self, loop=None, data=None): + if self.current.prevProgressive(): + return + if self.pos == 0: + return + self.transitionTo(self.pos-1, forward=False) + +def main(): + parser = argparse.ArgumentParser( + description='Console-based presentation system') + parser.add_argument('--light', dest='light', + default=False, + action='store_true', + help='use a black on white palette') + parser.add_argument('--warnings', dest='warnings', + default=False, + action='store_true', + help='print RST parser warnings and exit if any') + parser.add_argument('file', + help='presentation file (RST)') + args = parser.parse_args() + if args.light: + plt = palette.LIGHT_PALETTE + else: + plt = palette.DARK_PALETTE + hinter = slide.ScreenHinter() + parser = rst.PresentationParser(plt, hinter) + program = parser.parse(open(args.file).read(), args.file) + if args.warnings: + w = parser.warnings.getvalue() + if w: + print w + sys.exit(1) + p = Presenter(plt) + p.setProgram(program) + hinter.setScreen(p.loop.screen) + p.run() diff --git a/presentty/rst.py b/presentty/rst.py new file mode 100644 index 0000000..41b3f97 --- /dev/null +++ b/presentty/rst.py @@ -0,0 +1,493 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import docutils +import docutils.frontend +import docutils.parsers.rst +import docutils.nodes +import cStringIO as StringIO + +import urwid + +import slide +import transition as transition_mod +import image +import ansiparser +import text + +try: + import PIL + import PIL.Image +except ImportError: + PIL = None + +DEFAULT_TRANSITION = 'dissolve' +DEFAULT_TRANSITION_DURATION = 0.4 + +class TextAccumulator(object): + def __init__(self): + self.text = [] + + def append(self, text): + self.text.append(text) + + def getFormattedText(self): + return self.text + + wsre = re.compile('\s+') + + def getFlowedText(self): + ret = [] + for part in self.text: + if isinstance(part, tuple): + ret.append((part[0], self.wsre.sub(u' ', part[1]))) + else: + ret.append(self.wsre.sub(u' ', part)) + if not ret: + return u'' + return ret + +class UrwidTranslator(docutils.nodes.GenericNodeVisitor): + transition_map = {'dissolve': transition_mod.DissolveTransition, + 'cut': transition_mod.CutTransition, + 'pan': transition_mod.PanTransition, + } + + def __init__(self, document, palette, hinter=None, basedir='.'): + docutils.nodes.GenericNodeVisitor.__init__(self, document) + self.program = [] + self.stack = [] + self.default_transition = self._make_transition( + DEFAULT_TRANSITION, + DEFAULT_TRANSITION_DURATION) + self.transition = self.default_transition + self.attr = [] + self.table_columns = [] + self.table_column = [] + self.progressives = [] + self.palette = palette + self.hinter = hinter + self.basedir = basedir + self.slide = None + self.default_hide_title = False + self.hide_title = self.default_hide_title + + def _make_transition(self, name, duration): + tr = self.transition_map[name] + return tr(duration) + + def default_visit(self, node): + """Override for generic, uniform traversals.""" + pass + + def default_departure(self, node): + """Override for generic, uniform traversals.""" + pass + + def _append(self, node, widget, *args, **kw): + if self.stack: + if 'handout' in node.get('classes'): + if self.handout_pile not in self.stack: + container = self.handout_pile + else: + # If the handout pile is in the stack, then ignore + # this class -- it has probably needlessly been + # applied to something deeper in the stack. The + # thing further up will end up in the handout. + container = self.stack[-1] + else: + container = self.stack[-1] + container.contents.append((widget, container.options(*args, **kw))) + + def styled(self, style, text): + if style in self.palette: + return (self.palette[style], text) + return text + + def visit_transition(self, node): + name = node['name'] + duration = node.get('duration', DEFAULT_TRANSITION_DURATION) + self.transition = self._make_transition(name, duration) + + def depart_transition(self, node): + pass + + def visit_hidetitle(self, node): + if self.slide: + self.hide_title = True + else: + self.default_hide_title = True + + def depart_hidetitle(self, node): + pass + + def visit_system_message(self, node): + #print node.astext() + raise docutils.nodes.SkipNode() + + def visit_section(self, node): + self.hide_title = self.default_hide_title + self.transition = self.default_transition + title_pile = slide.SlidePile([]) + title_pad = slide.SlidePadding(title_pile, + align='center', width='pack') + + main_pile = slide.SlidePile([]) + main_pad = slide.SlidePadding(main_pile, align='center', width='pack') + outer_pile = slide.SlidePile([ + ('pack', title_pad), + ('pack', main_pad), + ]) + s = slide.UrwidSlide(u'', self.transition, outer_pile, + self.palette['_default']) + self.slide = s + self.stack.append(main_pile) + self.title_pile = title_pile + + pile = slide.SlidePile([]) + s = slide.Handout(pile, self.palette['_default']) + self.handout = s + self.handout_pile = pile + self.slide.handout = s + + def depart_section(self, node): + self.slide.transition = self.transition + if self.hide_title: + self.title_pile.contents[:] = [] + self.program.append(self.slide) + self.stack.pop() + + def visit_block_quote(self, node): + self.stack.append(slide.SlidePile([])) + + def depart_block_quote(self, node): + pile = self.stack.pop() + pad = slide.SlidePadding(pile, left=2) + self._append(node, pad, 'pack') + + def visit_list_item(self, node): + self.stack.append(slide.SlidePile([])) + + def depart_list_item(self, node): + pile = self.stack.pop() + bullet = urwid.Text(u'* ') + cols = slide.SlideColumns([]) + cols.contents.append((bullet, cols.options('pack'))) + cols.contents.append((pile, cols.options('weight', 1))) + if self.progressives: + cols = urwid.AttrMap(cols, self.palette['progressive']) + self.progressives[-1].append(cols) + self._append(node, cols, 'pack') + + def visit_tgroup(self, node): + self.table_columns.append([]) + self.stack.append(slide.SlidePile([])) + + def visit_colspec(self, node): + self.table_columns[-1].append(node['colwidth']) + + def visit_row(self, node): + self.stack.append(slide.SlideColumns([], dividechars=1)) + self.table_column.append(0) + + def depart_row(self, node): + self.table_column.pop() + cols = self.stack.pop() + self._append(node, cols, 'pack') + + def visit_thead(self, node): + pass + + def depart_thead(self, node): + cols = slide.SlideColumns([], dividechars=1) + for width in self.table_columns[-1]: + cols.contents.append((urwid.Text(u'='*width), + cols.options('given', width))) + self._append(node, cols, 'pack') + + def visit_entry(self, node): + self.stack.append(slide.SlidePile([])) + + def depart_entry(self, node): + colindex = self.table_column[-1] + self.table_column[-1] = colindex + 1 + pile = self.stack.pop() + self._append(node, pile, 'given', self.table_columns[-1][colindex]) + + def depart_tgroup(self, node): + self.table_columns.pop() + pile = self.stack.pop() + self._append(node, pile, 'pack') + + def visit_textelement(self, node): + self.stack.append(TextAccumulator()) + + visit_paragraph = visit_textelement + + def depart_paragraph(self, node): + text = self.stack.pop() + self._append(node, urwid.Text(text.getFlowedText()), 'pack') + + visit_literal_block = visit_textelement + + def depart_literal_block(self, node): + text = self.stack.pop() + text = urwid.Text(text.getFormattedText(), wrap='clip') + pad = slide.SlidePadding(text, width='pack') + self._append(node, pad, 'pack') + + visit_line = visit_textelement + + def depart_line(self, node): + text = self.stack.pop() + self._append(node, urwid.Text(text.getFormattedText(), wrap='clip'), + 'pack') + + visit_title = visit_textelement + + def depart_title(self, node): + text = self.stack.pop() + self.slide.title = node.astext() + widget = urwid.Text(self.styled('title', text.getFlowedText()), + align='center') + self.title_pile.contents.append( + (widget, self.title_pile.options('pack'))) + + def visit_Text(self, node): + pass + + def depart_Text(self, node): + if self.stack and isinstance(self.stack[-1], TextAccumulator): + if self.attr: + t = (self.attr[-1], node.astext()) + else: + t = node.astext() + self.stack[-1].append(t) + + def visit_emphasis(self, node): + self.attr.append(self.palette['emphasis']) + + def depart_emphasis(self, node): + self.attr.pop() + + def visit_inline(self, node): + cls = node.get('classes') + if not cls: + raise docutils.nodes.SkipDeparture() + cls = [x for x in cls if x != 'literal'] + for length in range(len(cls), 0, -1): + clsname = '-'.join(cls[:length]) + if clsname in self.palette: + self.attr.append(self.palette[clsname]) + return + raise docutils.nodes.SkipDeparture() + + def depart_inline(self, node): + self.attr.pop() + + def visit_image(self, node): + if not PIL: + return + uri = node['uri'] + fn = os.path.join(self.basedir, uri) + w = image.ANSIImage(fn, self.hinter) + self._append(node, w, 'pack') + + def visit_ansi(self, node): + interval = node.get('interval', 0.5) + oneshot = node.get('oneshot', False) + animation = slide.AnimatedText(interval, oneshot) + for name in node['names']: + p = ansiparser.ANSIParser() + fn = os.path.join(self.basedir, name) + data = unicode(open(fn).read(), 'utf8') + text = p.parse(data) + animation.addFrame(text) + self.slide.animations.append(animation) + self._append(node, animation, 'pack') + + def depart_ansi(self, node): + pass + + def visit_figlet(self, node): + figlet = text.FigletText(node['text']) + self._append(node, figlet, 'pack') + + def depart_figlet(self, node): + pass + + def visit_cowsay(self, node): + cowsay = text.CowsayText(node['text']) + self._append(node, cowsay, 'pack') + + def depart_cowsay(self, node): + pass + + def visit_container(self, node): + self.stack.append(slide.SlidePile([])) + if 'progressive' in node.get('classes'): + self.progressives.append(self.slide.progressives) + self.slide.progressive_attr = self.palette['progressive'] + + def depart_container(self, node): + pile = self.stack.pop() + self._append(node, pile, 'pack') + if 'progressive' in node.get('classes'): + self.progressives.pop() + +class TransitionDirective(docutils.parsers.rst.Directive): + required_arguments = 1 + option_spec = {'duration': float} + has_content = False + + def run(self): + args = {'name': self.arguments[0]} + duration = self.options.get('duration') + if duration: + args['duration'] = duration + node = transition(**args) + return [node] + +class ANSIDirective(docutils.parsers.rst.Directive): + required_arguments = 1 + final_argument_whitespace = True + option_spec = {'interval': float, + 'oneshot': bool} + has_content = False + + def run(self): + args = {'names': self.arguments[0].split()} + args.update(self.options) + node = ansi(**args) + return [node] + +class FigletDirective(docutils.parsers.rst.Directive): + required_arguments = 1 + has_content = False + final_argument_whitespace = True + + def run(self): + args = {'text': self.arguments[0]} + node = figlet(**args) + return [node] + +class CowsayDirective(docutils.parsers.rst.Directive): + required_arguments = 1 + has_content = False + final_argument_whitespace = True + + def run(self): + args = {'text': self.arguments[0]} + node = cowsay(**args) + return [node] + +class HideTitleDirective(docutils.parsers.rst.Directive): + has_content = False + + def run(self): + node = hidetitle() + return [node] + +class transition(docutils.nodes.Special, docutils.nodes.Invisible, + docutils.nodes.Element): + pass + +class ansi(docutils.nodes.General, docutils.nodes.Inline, + docutils.nodes.Element): + pass + +class figlet(docutils.nodes.General, docutils.nodes.Inline, + docutils.nodes.Element): + pass + +class cowsay(docutils.nodes.General, docutils.nodes.Inline, + docutils.nodes.Element): + pass + +class hidetitle(docutils.nodes.Special, docutils.nodes.Invisible, + docutils.nodes.Element): + pass + +class PresentationParser(object): + def __init__(self, palette, hinter=None): + docutils.parsers.rst.directives.register_directive( + 'transition', TransitionDirective) + docutils.parsers.rst.directives.register_directive( + 'ansi', ANSIDirective) + docutils.parsers.rst.directives.register_directive( + 'figlet', FigletDirective) + docutils.parsers.rst.directives.register_directive( + 'cowsay', CowsayDirective) + docutils.parsers.rst.directives.register_directive( + 'hidetitle', HideTitleDirective) + self.warnings = StringIO.StringIO() + self.settings = docutils.frontend.OptionParser( + components=(docutils.parsers.rst.Parser,), + defaults=dict(warning_stream=self.warnings)).get_default_values() + self.parser = docutils.parsers.rst.Parser() + self.palette = palette + self.hinter = hinter + + def _parse(self, input, filename): + document = docutils.utils.new_document(filename, self.settings) + self.parser.parse(input, document) + visitor = UrwidTranslator(document, self.palette, self.hinter, + os.path.dirname(filename)) + document.walkabout(visitor) + return document, visitor + + def parse(self, input, filename='program'): + document, visitor = self._parse(input, filename) + return visitor.program + +def main(): + import argparse + import palette + + argp = argparse.ArgumentParser(description='Test RST parser') + argp.add_argument('file', help='presentation file (RST)') + argp.add_argument('slides', nargs='?', default=[], + help='slides to render') + argp.add_argument('--render', action='store_true', + help='Fully render a slide') + args = argp.parse_args() + + parser = PresentationParser(palette.DARK_PALETTE) + document, visitor = parser._parse(open(args.file).read(), args.file) + + slides = args.slides + if not slides: + slides = range(len(visitor.program)) + slides = [int(x) for x in slides] + + if not args.render: + print document.pformat() + for i in slides: + print '-'*80 + s = visitor.program[i] + for line in s.render((80,25)).text: + print line + else: + screen = urwid.raw_display.Screen() + with screen.start(): + for i in slides: + s = visitor.program[i] + screen.draw_screen((80,25), s.render((80,25))) + raw_input() + +if __name__ == '__main__': + main() diff --git a/presentty/server.py b/presentty/server.py new file mode 100644 index 0000000..74e64b2 --- /dev/null +++ b/presentty/server.py @@ -0,0 +1,112 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import threading +import SocketServer + +class ConsoleHandler(SocketServer.StreamRequestHandler): + def handle(self): + server = self.server.server + while True: + try: + data = self.rfile.readline() + except Exception: + break + if not data: + break + data = data.strip() + if data == 'list': + for i, slide in enumerate(server.list()): + self.wfile.write('slide %i %s\n' % (i, slide.title)) + self.wfile.write('end\n') + elif data == 'current': + i, slide = server.current() + self.wfile.write('current %i %i %s\n' % ( + i, slide.progressive_state, slide.title)) + elif data == 'next': + i, slide = server.next() + self.wfile.write('current %i %i %s\n' % ( + i, slide.progressive_state, slide.title)) + elif data == 'prev': + i, slide = server.prev() + self.wfile.write('current %i %i %s\n' % ( + i, slide.progressive_state, slide.title)) + elif data.startswith('jump'): + parts = data.split() + i, slide = server.jump(int(parts[1].strip())) + self.wfile.write('current %i %i %s\n' % ( + i, slide.progressive_state, slide.title)) + elif data == 'size': + size = server.size() + self.wfile.write('size %s %s\n' % size) + +class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): + allow_reuse_address=True + +class ConsoleServer(object): + def __init__(self, presenter, host='localhost', port=1292): + self.presenter = presenter + self.server = ThreadedTCPServer((host, port), ConsoleHandler) + self.server.server = self + self.lock = threading.Lock() + + def start(self): + self.thread=threading.Thread(target=self._run, name="Console Server") + self.thread.daemon=True + self.thread.start() + + def _run(self): + self.server.serve_forever() + + def stop(self): + self.server.shutdown() + + def list(self): + return self.presenter.program + + def current(self): + s = self.presenter.program[self.presenter.pos] + return (self.presenter.pos, s) + + def size(self): + return self.presenter.loop.screen.get_cols_rows() + + def next(self): + self.lock.acquire() + try: + os.write(self.presenter.server_pipe_in, 'next') + self.presenter.server_pipe_out_read.readline() + return self.current() + finally: + self.lock.release() + + def prev(self): + self.lock.acquire() + try: + os.write(self.presenter.server_pipe_in, 'prev') + self.presenter.server_pipe_out_read.readline() + return self.current() + finally: + self.lock.release() + + def jump(self, pos): + self.lock.acquire() + try: + os.write(self.presenter.server_pipe_in, 'jump %s' % (pos,)) + self.presenter.server_pipe_out_read.readline() + return self.current() + finally: + self.lock.release() diff --git a/presentty/slide.py b/presentty/slide.py new file mode 100644 index 0000000..4894f45 --- /dev/null +++ b/presentty/slide.py @@ -0,0 +1,178 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import urwid + +class SlidePile(urwid.Pile): + def pack(self, size, focus=False): + cols = 0 + rows = 0 + for x in self.contents: + c,r = x[0].pack((size[0],)) + if c>cols: + cols = c + rows += r + return (cols, rows) + +class SlidePadding(urwid.Padding): + def pack(self, size, focus=False): + r = self._original_widget.pack(size, focus) + width = max(r[0] + self.left + self.right, self.min_width) + width = min(size[0], width) + return (width, r[1]) + +class SlideColumns(urwid.Columns): + def pack(self, size, focus=False): + cols = self.dividechars * (len(self.contents)-1) + rows = 0 + for widget, packing in self.contents: + if packing[0] == 'given': + allocated_cols = packing[1] + else: + allocated_cols = size[0] + c,r = widget.pack((allocated_cols,)) + if packing[0] == 'given': + c = allocated_cols + if r>rows: + rows = r + cols += c + return (cols, rows) + +class SlideFiller(urwid.Filler): + pass + +class ScreenHinter(object): + # A terrible hack to try to provide some needed context to the + # image widget. + def __init__(self, screen=None): + self.screen = screen + + def setScreen(self, screen): + self.screen = screen + + def getSize(self): + cols, rows = self.screen.get_cols_rows() + return (cols, rows-1) + +class Handout(urwid.WidgetWrap): + def __init__(self, widget, background): + self.background = background + self.pad = SlidePadding(widget, align='center', width='pack') + self.map = urwid.AttrMap(self.pad, self.background) + super(Handout, self).__init__(self.map) + +class UrwidSlide(urwid.WidgetWrap): + def __init__(self, title, transition, widget, background): + self.title = title + self.transition = transition + self.fill = SlideFiller(widget) + self.background = background + self.map = urwid.AttrMap(self.fill, self.background) + self.handout = None + self.animations = [] + self.progressives = [] + self.progressive_attr = None + self.progressive_state = 0 + super(UrwidSlide, self).__init__(self.map) + + def startAnimation(self, loop): + for x in self.animations: + x.startAnimation(loop) + + def stopAnimation(self): + for x in self.animations: + x.stopAnimation() + + def resetAnimation(self): + for x in self.animations: + x.resetAnimation() + + def resetProgressive(self, on=False): + if on: + self.progressive_state = len(self.progressives) + for x in self.progressives: + x.set_attr_map({None: None}) + else: + self.progressive_state = 0 + for x in self.progressives: + x.set_attr_map({None: self.progressive_attr}) + + def nextProgressive(self): + if self.progressive_state >= len(self.progressives): + return False + self.progressives[self.progressive_state].set_attr_map( + {None: None}) + self.progressive_state += 1 + return True + + def prevProgressive(self): + if self.progressive_state <= 0: + return False + self.progressive_state -= 1 + self.progressives[self.progressive_state].set_attr_map( + {None: self.progressive_attr}) + return True + + def setProgressive(self, state): + self.progressive_state = state + for i, x in enumerate(self.progressives): + if i < self.progressive_state: + x.set_attr_map({None: None}) + else: + x.set_attr_map({None: self.progressive_attr}) + +class AnimatedText(urwid.Text): + def __init__(self, interval=0.5, oneshot=False): + super(AnimatedText, self).__init__(u'') + self.frames = [] + self.current = 0 + self.running = False + self.interval = interval + self.oneshot = oneshot + + def addFrame(self, text): + self.frames.append(text) + if len(self.frames) == self.current+1: + self.set_text(text) + + def startAnimation(self, loop): + if self.running: + return + if len(self.frames) == 1: + return + self.running = True + loop.set_alarm_in(self.interval, self.updateCallback) + + def updateCallback(self, loop=None, data=None): + if not self.running: + return + if self.current+1 >= len(self.frames): + if self.oneshot: + self.running = False + return + self.current = 0 + else: + self.current += 1 + self.set_text(self.frames[self.current]) + loop.set_alarm_in(self.interval, self.updateCallback) + + def stopAnimation(self): + if not self.running: + return + self.running = False + + def resetAnimation(self): + self.current = 0 + self.set_text(self.frames[self.current]) diff --git a/presentty/text.py b/presentty/text.py new file mode 100644 index 0000000..e0a88fc --- /dev/null +++ b/presentty/text.py @@ -0,0 +1,81 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import subprocess + +import urwid + +class FigletText(urwid.WidgetWrap): + def __init__(self, text, attr=None): + self.text = text + self.attr = attr + output = self._run() + if attr: + widget = urwid.Text((attr, output), wrap='clip') + else: + widget = urwid.Text(output, wrap='clip') + super(FigletText, self).__init__(widget) + + def _run(self): + p = subprocess.Popen(['figlet'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + p.stdin.write(self.text) + p.stdin.close() + data = p.stdout.read() + p.stderr.read() + p.wait() + return data + +class CowsayText(urwid.WidgetWrap): + def __init__(self, text, attr=None): + self.text = text + self.attr = attr + output = self._run() + if attr: + widget = urwid.Text((attr, output), wrap='clip') + else: + widget = urwid.Text(output, wrap='clip') + super(CowsayText, self).__init__(widget) + + def _run(self): + p = subprocess.Popen(['cowsay'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + p.stdin.write(self.text) + p.stdin.close() + data = p.stdout.read() + p.stderr.read() + p.wait() + return data + +def main(): + import slide + w = FigletText("Testing") + slpile = slide.SlidePile([]) + slpile.contents.append((w, slpile.options())) + pad = slide.SlidePadding(slpile, align='center', width='pack') + fill = slide.SlideFiller(pad) + #w.render((80,25)) + fill.render((80,25)) + screen = urwid.raw_display.Screen() + if True: + with screen.start(): + screen.draw_screen((80,25), fill.render((80,25))) + raw_input() +if __name__=='__main__': + main() diff --git a/presentty/transition.py b/presentty/transition.py new file mode 100644 index 0000000..9133629 --- /dev/null +++ b/presentty/transition.py @@ -0,0 +1,153 @@ +# Copyright (C) 2015 James E. Blair <corvus@gnu.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import urwid + +class Transition(urwid.Widget): + def __init__(self, duration=0.4): + super(Transition, self).__init__() + self.duration = 0.4 + self.old = None + self.new = None + self.progress = 0.0 + + def getDuration(self): + return self.duration + + def setTargets(self, old, new): + self.old = old + self.new = new + self.setProgress(0.0) + + def setProgress(self, progress): + self.progress = progress + self._invalidate() + +class PanTransition(Transition): + def render(self, size, focus=False): + old = self.old.render((size[0], size[1])) + new = self.new.render((size[0], size[1])) + c = urwid.CanvasJoin([(old, None, False, size[0]), + (new, None, False, size[0])]) + #c = urwid.CanvasOverlay(new, old, 6, 0) + offset = int(size[0] * self.progress) + c.pad_trim_left_right(0-offset, 0-(size[0]-offset)) + return c + +class DissolveTransition(Transition): + def __init__(self, *args, **kw): + super(DissolveTransition, self).__init__(*args, **kw) + self._oldbuf = None + self._newbuf = None + self._cache_size = None + + def setTargets(self, old, new): + if old != self.old: + self._oldbuf = None + self._cache_size = None + if new != self.new: + self._newbuf = None + self._cache_size = None + super(DissolveTransition, self).setTargets(old, new) + + def _to_buf(self, canvas): + buf = [] + for line in canvas.content(): + for (attr, cs, text) in line: + for char in unicode(text, 'utf8'): + buf.append((attr, cs, char)) + return buf + + def render(self, size, focus=False): + if self._cache_size != size: + old = self.old.render((size[0], size[1])) + new = self.new.render((size[0], size[1])) + self._oldbuf = self._to_buf(old) + self._newbuf = self._to_buf(new) + self._cache_size = size + line_list = [] + attr_list = [] + line_text = '' + line_attrs = [] + current_attr = [None, 0] + current_rgb = None + current_props = None + background = urwid.AttrSpec('light gray', 'black') + for i in range(len(self._oldbuf)): + oldattr, oldcs, oldchar = self._oldbuf[i] + newattr, newcs, newchar = self._newbuf[i] + oldrgb = oldattr.get_rgb_values() + newrgb = newattr.get_rgb_values() + if None in oldrgb: + oldrgb = background.get_rgb_values() + if None in newrgb: + newrgb = background.get_rgb_values() + if newchar == ' ': + char = oldchar + charattr = oldattr + newrgb = newrgb[3:]*2 + elif oldchar == ' ': + char = newchar + charattr = newattr + oldrgb = oldrgb[3:]*2 + elif self.progress >= 0.5: + char = newchar + charattr = newattr + else: + char = oldchar + charattr = oldattr + char = char.encode('utf8') + line_text += char + rgb = [] + props = [] + if charattr.bold: + props.append('bold') + if charattr.underline: + props.append('underline') + if charattr.standout: + props.append('standout') + if charattr.blink: + props.append('blink') + for x in range(len(oldrgb)): + rgb.append(int(((newrgb[x]-oldrgb[x])*self.progress)+oldrgb[x])>>4) + if current_rgb == rgb and current_props == props: + current_attr[1] += len(char) + else: + if current_attr[0]: + line_attrs.append(tuple(current_attr)) + fg = ', '.join(props + ['#%x%x%x' % tuple(rgb[:3])]) + bg = '#%x%x%x' % tuple(rgb[3:]) + attr = urwid.AttrSpec(fg, bg) + current_attr = [attr, len(char)] + current_rgb = rgb + current_props = props + if (i+1) % size[0] == 0: + line_attrs.append(tuple(current_attr)) + current_attr = [None, 0] + current_rgb = None + line_list.append(line_text) + line_text = '' + attr_list.append(line_attrs) + line_attrs = [] + canvas = urwid.TextCanvas(line_list, attr_list) + return canvas + +class CutTransition(Transition): + def __init__(self, *args, **kw): + super(CutTransition, self).__init__(*args, **kw) + self.duration = 0.0 + + def render(self, size, focus=False): + return self.new.render(size, focus) |