summaryrefslogtreecommitdiff
path: root/presentty
diff options
context:
space:
mode:
authorJames E. Blair <corvus@gnu.org>2014-12-20 07:35:13 -0800
committerJames E. Blair <corvus@gnu.org>2015-01-09 10:39:14 -0800
commitef9fb76de9ef299fbdc8f87f1dd05bdd1eda649e (patch)
treedc85b1dfb180f427b3686e59d6b7a8254397c1e2 /presentty
Initial commit
Diffstat (limited to 'presentty')
-rw-r--r--presentty/__init__.py0
-rw-r--r--presentty/ansiparser.py185
-rw-r--r--presentty/client.py66
-rw-r--r--presentty/console.py292
-rw-r--r--presentty/image.py168
-rw-r--r--presentty/palette.py78
-rw-r--r--presentty/presentty.py162
-rw-r--r--presentty/rst.py493
-rw-r--r--presentty/server.py112
-rw-r--r--presentty/slide.py178
-rw-r--r--presentty/text.py81
-rw-r--r--presentty/transition.py153
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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import re
17
18import urwid
19
20class ANSIParser(object):
21 colors = [
22 urwid.BLACK,
23 urwid.DARK_RED,
24 urwid.DARK_GREEN,
25 urwid.BROWN,
26 urwid.DARK_BLUE,
27 urwid.DARK_MAGENTA,
28 urwid.DARK_CYAN,
29 urwid.LIGHT_GRAY,
30 urwid.DARK_GRAY,
31 urwid.LIGHT_RED,
32 urwid.LIGHT_GREEN,
33 urwid.YELLOW,
34 urwid.LIGHT_BLUE,
35 urwid.LIGHT_MAGENTA,
36 urwid.LIGHT_CYAN,
37 urwid.WHITE,
38 ]
39
40 colors256 = ['0', '6', '8', 'a', 'd', 'f']
41 colorsgray = ['3', '7', '11', '13', '15', '19', '23', '27', '31',
42 '35', '38', '42', '46', '50', '52', '58', '62', '66',
43 '70', '74', '78', '82', '85', '89', '93']
44
45 def __init__(self):
46 self.x = 0
47 self.y = 0
48 self.text_lines = []
49 self.attr_lines = []
50 self.background = urwid.AttrSpec('light gray', 'black')
51 self.attr = self.background
52 self.resetColor()
53 self.moveTo(0,0)
54
55 def resetColor(self):
56 self.bold = False
57 self.blink = False
58 self.fg = 7
59 self.bg = 0
60
61 def moveTo(self, x, y):
62 while x>80:
63 x-=80
64 y+=1
65 while y+1 > len(self.text_lines):
66 self.text_lines.append([u' ' for i in range(80)])
67 self.attr_lines.append([self.attr for i in range(80)])
68 self.x = x
69 self.y = y
70
71 def parseSequence(self, seq):
72 values = []
73 buf = ''
74 for c in seq:
75 if c in ['\x1b', '[']:
76 continue
77 if c == ';':
78 values.append(int(buf))
79 buf = ''
80 continue
81 if ord(c) < 64:
82 buf += c
83 if buf:
84 values.append(int(buf))
85 if c == 'm':
86 if not values:
87 values = [0]
88 fg256 = None
89 for v in values:
90 if fg256 is True:
91 if v <= 0x08:
92 self.fg = v
93 elif v <= 0x0f:
94 self.fg = v - 0x08
95 self.bold = True
96 elif v <= 0xe7:
97 r, x = divmod(v-16, 36)
98 g, x = divmod(x, 6)
99 b = x % 6
100 fg256 = ('#' +
101 self.colors256[r] +
102 self.colors256[g] +
103 self.colors256[b])
104 else:
105 fg256 = 'g' + str(self.colorsgray[v-232])
106 elif v == 0:
107 self.resetColor()
108 elif v == 1:
109 self.bold = True
110 elif v == 5:
111 self.blink = True
112 elif v>29 and v<38:
113 self.fg = v-30
114 elif v>39 and v<48:
115 self.bg = v-40
116 elif v==38:
117 fg256=True
118 fg = self.fg
119 if self.bold:
120 fg += 8
121 fgattrs = []
122 if self.blink:
123 fgattrs.append('blink')
124 if fg256:
125 fgattrs.append(fg256)
126 else:
127 fgattrs.append(self.colors[fg])
128 self.attr = urwid.AttrSpec(', '.join(fgattrs), self.colors[self.bg])
129 if c == 'A':
130 if not values:
131 values = [1]
132 y = max(self.y-values[0], 0)
133 self.moveTo(self.x, y)
134 if c == 'C':
135 if not values:
136 values = [1]
137 x = self.x + values[0]
138 self.moveTo(x, self.y)
139 if c == 'H':
140 self.moveTo(values[1]-1, values[0]-1)
141
142 def parse(self, data):
143 seq = ''
144 for char in data:
145 if seq:
146 seq += char
147 if ord(char) >= 64 and char != '[':
148 self.parseSequence(seq)
149 seq = ''
150 continue
151 if char == '\x1a':
152 continue
153 if char == '\x1b':
154 seq = char
155 continue
156 if char == '\r':
157 self.moveTo(0, self.y)
158 continue
159 if char == '\n':
160 self.moveTo(self.x, self.y+1)
161 continue
162 if not seq:
163 self.text_lines[self.y][self.x] = char
164 self.attr_lines[self.y][self.x] = self.attr
165 x = self.x + 1
166 self.moveTo(x, self.y)
167 text = []
168 current_attr = self.attr_lines[0][0]
169 current_text = u''
170 for y in range(len(self.text_lines)):
171 for x in range(80):
172 char = self.text_lines[y][x]
173 attr = self.attr_lines[y][x]
174 if (attr.foreground_number != current_attr.foreground_number or
175 attr.background_number != current_attr.background_number):
176 text.append((current_attr, current_text))
177 current_attr = attr
178 current_text = u''
179 current_text += char
180 if (current_attr.background_number==0):
181 current_text = current_text.rstrip(' ')
182 current_text += u'\n'
183 current_text = re.sub('\n+$', '\n', current_text)
184 text.append((current_attr, current_text))
185 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import socket
17
18class Client(object):
19 def __init__(self, host='127.0.0.1', port=1292):
20 self.host = host
21 self.port = port
22 self.sock = None
23 self.connect()
24
25 def connect(self):
26 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
27 self.sock.connect((self.host, self.port))
28 self.file = self.sock.makefile('rw', 0)
29
30 def list(self):
31 self.file.write('list\n')
32 program = []
33 while True:
34 ln = self.file.readline().strip()
35 if ln == 'end':
36 break
37 x, index, title = ln.split(' ', 2)
38 program.append(title)
39 return program
40
41 def size(self):
42 self.file.write('size\n')
43 ln = self.file.readline().strip()
44 x, cols, rows = ln.split(' ', 2)
45 return (int(cols), int(rows))
46
47 def parseCurrent(self):
48 ln = self.file.readline().strip()
49 x, index, progressive_state, title = ln.split(' ', 3)
50 return (int(index), int(progressive_state))
51
52 def current(self):
53 self.file.write('current\n')
54 return self.parseCurrent()
55
56 def jump(self, index):
57 self.file.write('jump %i\n' % index)
58 return self.parseCurrent()
59
60 def next(self):
61 self.file.write('next\n')
62 return self.parseCurrent()
63
64 def prev(self):
65 self.file.write('prev\n')
66 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import argparse
17import sys
18import datetime
19import time
20
21import urwid
22
23import palette
24import client
25import slide
26import rst
27
28PALETTE = [
29 ('reversed', 'standout', ''),
30 ('status', 'light red', ''),
31]
32
33class Row(urwid.Button):
34 def __init__(self, index, title, console):
35 super(Row, self).__init__('', on_press=console.jump, user_data=index)
36 col = urwid.Columns([
37 ('fixed', 3, urwid.Text('%-2i' % (index+1))),
38 urwid.Text(title),
39 ])
40 self._w = urwid.AttrMap(col, None, focus_map='reversed')
41
42 def selectable(self):
43 return True
44
45class Footer(urwid.WidgetWrap):
46 def __init__(self):
47 super(Footer, self).__init__(urwid.Columns([]))
48 self.position = urwid.Text(u'')
49 self.timer = urwid.Text(u'')
50 self._w.contents.append((self.position, ('pack', None, False)))
51 self._w.contents.append((urwid.Text(u''), ('weight', 1, False)))
52 self._w.contents.append((self.timer, ('pack', None, False)))
53
54class Screen(urwid.WidgetWrap):
55 def __init__(self, console):
56 super(Screen, self).__init__(urwid.Pile([]))
57 self.console = console
58 self.program = []
59 self.current = -1
60 self.progressive_state = 0
61 self.blank_slide = slide.UrwidSlide(
62 u'', None, urwid.Text(u''), None)
63 self.timer = 45*60
64 self.size = (80, 25)
65 self.timer_end = None
66 self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
67 self.footer = Footer()
68 footer = urwid.AttrMap(self.footer, 'status')
69 self.left = urwid.Pile([])
70 self.left.contents.append((self.listbox, ('weight', 1)))
71 self.left.set_focus(0)
72
73 self.right = urwid.Pile([])
74 self.setPreviews()
75
76 self.main = urwid.Columns([])
77 self.main.contents.append((self.left, ('weight', 1, False)))
78 self.main.contents.append((self.right, ('given', self.size[0]+2, False)))
79 self.main.set_focus(0)
80
81 self._w.contents.append((self.main, ('weight', 1)))
82 self._w.contents.append((footer, ('pack', 1)))
83 self._w.set_focus(0)
84
85 def setPreviews(self):
86 current_slide = next_slide = self.blank_slide
87 if 0 <= self.current < len(self.program):
88 current_slide = self.program[self.current]
89 if 0 <= self.current+1 < len(self.program):
90 next_slide = self.program[self.current+1]
91 current_slide.setProgressive(self.progressive_state)
92 current_box = urwid.LineBox(current_slide, "Current")
93 next_box = urwid.LineBox(next_slide, "Next")
94 if current_slide.handout:
95 notes_box = urwid.LineBox(current_slide.handout, "Notes")
96 else:
97 notes_box = None
98 self.right.contents[:] = []
99 self.left.contents[:] = self.left.contents[:1]
100 cols, rows = self.size
101 self.right.contents.append((current_box, ('given', rows+2)))
102 self.right.contents.append((next_box, ('given', rows+2)))
103 self.right.contents.append((urwid.Filler(urwid.Text(u'')), ('weight', 1)))
104 if notes_box:
105 self.left.contents.append((notes_box, ('pack', None)))
106
107 def setProgram(self, program):
108 self.program = program
109 self.listbox.body[:] = []
110 for i, s in enumerate(program):
111 self.listbox.body.append(Row(i, s.title, self.console))
112
113 def setSize(self, size):
114 self.size = size
115 cols, rows = size
116 self.right.contents[0] = (self.right.contents[0][0], ('given', rows+2))
117 self.right.contents[1] = (self.right.contents[1][0], ('given', rows+2))
118 self.main.contents[1] = (self.main.contents[1][0], ('given', cols+2, False))
119
120 # Implement this method from the urwid screen interface for the ScreenHinter
121 def get_cols_rows(self):
122 return self.size
123
124 def setCurrent(self, state):
125 index, progressive_state = state
126 changed = False
127 if index != self.current:
128 self.current = index
129 self.listbox.set_focus(index)
130 self.listbox._invalidate()
131 self.footer.position.set_text('%i / %i' % (index+1, len(self.program)))
132 changed = True
133 if progressive_state != self.progressive_state:
134 self.progressive_state = progressive_state
135 changed = True
136 if changed:
137 self.setPreviews()
138 self.footer.timer.set_text(self.getTime())
139
140 def getTime(self):
141 now = time.time()
142 if self.timer_end:
143 return str(datetime.timedelta(seconds=(int(self.timer_end-now))))
144 else:
145 return str(datetime.timedelta(seconds=int(self.timer)))
146
147 def setTimer(self, secs):
148 self.timer = secs
149 if self.timer_end:
150 now = time.time()
151 self.timer_end = now + self.timer
152
153 def startStopTimer(self):
154 now = time.time()
155 if self.timer_end:
156 remain = max(self.timer_end - int(now), 0)
157 self.timer = remain
158 self.timer_end = None
159 else:
160 self.timer_end = now + self.timer
161
162 def keypress(self, size, key):
163 if key in (' ', 'x'):
164 self.startStopTimer()
165 elif key == 'page up':
166 self.console.prev()
167 elif key == 'page down':
168 self.console.next()
169 elif key == 'right':
170 self.console.next()
171 elif key == 'left':
172 self.console.prev()
173 elif key == 't':
174 self.console.timerDialog()
175 else:
176 return super(Screen, self).keypress(size, key)
177 return None
178
179class FixedButton(urwid.Button):
180 def sizing(self):
181 return frozenset([urwid.FIXED])
182
183 def pack(self, size, focus=False):
184 return (len(self.get_label())+4, 1)
185
186class TimerDialog(urwid.WidgetWrap):
187 signals = ['set', 'cancel']
188 def __init__(self):
189 set_button = FixedButton('Set')
190 cancel_button = FixedButton('Cancel')
191 urwid.connect_signal(set_button, 'click',
192 lambda button:self._emit('set'))
193 urwid.connect_signal(cancel_button, 'click',
194 lambda button:self._emit('cancel'))
195 button_widgets = [('pack', set_button),
196 ('pack', cancel_button)]
197 button_columns = urwid.Columns(button_widgets, dividechars=2)
198 rows = []
199 self.entry = urwid.Edit('Timer: ', edit_text='45:00')
200 rows.append(self.entry)
201 rows.append(urwid.Divider())
202 rows.append(button_columns)
203 pile = urwid.Pile(rows)
204 fill = urwid.Filler(pile, valign='top')
205 super(TimerDialog, self).__init__(urwid.LineBox(fill, 'Timer'))
206
207 def keypress(self, size, key):
208 r = super(TimerDialog, self).keypress(size, key)
209 if r == 'enter':
210 self._emit('set')
211 return None
212 elif r == 'esc':
213 self._emit('cancel')
214 return None
215 return r
216
217class Console(object):
218 poll_interval = 0.5
219
220 def __init__(self, program):
221 self.screen = Screen(self)
222 self.loop = urwid.MainLoop(self.screen, palette=PALETTE)
223 self.client = client.Client()
224 self.screen.setProgram(program)
225 self.update()
226 self.loop.set_alarm_in(self.poll_interval, self.updateCallback)
227
228 def run(self):
229 self.loop.run()
230
231 def jump(self, widget, index):
232 self.screen.setCurrent(self.client.jump(index))
233
234 def next(self):
235 self.screen.setCurrent(self.client.next())
236
237 def prev(self):
238 self.screen.setCurrent(self.client.prev())
239
240 def updateCallback(self, loop=None, data=None):
241 self.update()
242 self.loop.set_alarm_in(self.poll_interval, self.updateCallback)
243
244 def update(self):
245 self.screen.setSize(self.client.size())
246 self.screen.setCurrent(self.client.current())
247
248 def timerDialog(self):
249 dialog = TimerDialog()
250 overlay = urwid.Overlay(dialog, self.loop.widget,
251 'center', 30,
252 'middle', 6)
253 self.loop.widget = overlay
254 urwid.connect_signal(dialog, 'cancel', self.cancelDialog)
255 urwid.connect_signal(dialog, 'set', self.setTimer)
256
257 def cancelDialog(self, widget):
258 self.loop.widget = self.screen
259
260 def setTimer(self, widget):
261 parts = widget.entry.edit_text.split(':')
262 secs = 0
263 if len(parts):
264 secs += int(parts.pop())
265 if len(parts):
266 secs += int(parts.pop())*60
267 if len(parts):
268 secs += int(parts.pop())*60*60
269 self.screen.setTimer(secs)
270 self.loop.widget = self.screen
271
272
273def main():
274 parser = argparse.ArgumentParser(
275 description='Console-based presentation system')
276 parser.add_argument('--light', dest='light',
277 default=False,
278 action='store_true',
279 help='use a black on white palette')
280 parser.add_argument('file',
281 help='presentation file (RST)')
282 args = parser.parse_args()
283 if args.light:
284 plt = palette.LIGHT_PALETTE
285 else:
286 plt = palette.DARK_PALETTE
287 hinter = slide.ScreenHinter()
288 parser = rst.PresentationParser(plt, hinter)
289 program = parser.parse(open(args.file).read())
290 c = Console(program)
291 hinter.setScreen(c.screen)
292 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import subprocess
17import HTMLParser
18import re
19
20import PIL
21import PIL.ExifTags
22import urwid
23
24import slide
25
26def nearest_color(x):
27 if x < 0x30: return '0'
28 if x < 0x70: return '6'
29 if x < 0x98: return '8'
30 if x < 0xc0: return 'a'
31 if x < 0xe8: return 'd'
32 return 'f'
33
34class ANSIImage(urwid.Widget):
35 def __init__(self, uri, hinter=None):
36 super(ANSIImage, self).__init__()
37 self.uri = uri
38 image = self._loadImage()
39 self.htmlparser = HTMLParser.HTMLParser()
40 self.ratio = float(image.size[0])/float(image.size[1])
41 self.hinter = hinter
42
43 def _loadImage(self):
44 image = PIL.Image.open(self.uri)
45 image.load()
46 exif = image._getexif()
47 if exif:
48 orientation = exif.get(274, 1)
49 if orientation == 1:
50 pass
51 elif orientation == 3:
52 image = image.rotate(180)
53 elif orientation == 6:
54 image = image.rotate(-90)
55 elif orientation == 8:
56 image = image.rotate(90)
57 else:
58 raise Exception("unknown orientation %s" % orientation)
59 return image
60
61 def pack(self, size, focus=False):
62 cols = size[0]
63 if len(size) > 1:
64 rows = size[1]
65 elif self.hinter:
66 rows = self.hinter.getSize()[1]
67 else:
68 rows = None
69 width = cols
70 height = int(cols*(1.0/self.ratio)/2.0)
71 if rows is not None and height > rows:
72 height = rows
73 width = int(rows*self.ratio*2.0)
74 return (width, height)
75
76 def rows(self, size, focus=False):
77 r = self.pack(size)
78 return r[1]
79
80 SPAN_RE = re.compile(r"<span style='color:#(......); background-color:#(......);'>(.*)")
81 def render(self, size, focus=False):
82 spanre = self.SPAN_RE
83 htmlparser = self.htmlparser
84 width, height = self.pack(size, focus)
85 jp2a = subprocess.Popen(['jp2a', '--colors', '--fill',
86 '--width=%s' % width,
87 '--height=%s' % height,
88 '--html-raw', '-'],
89 stdin=subprocess.PIPE,
90 stdout=subprocess.PIPE,
91 stderr=subprocess.PIPE)
92 image = self._loadImage()
93 image.save(jp2a.stdin, 'JPEG')
94 jp2a.stdin.close()
95 data = jp2a.stdout.read()
96 jp2a.stderr.read()
97 jp2a.wait()
98
99 line_list = []
100 attr_list = []
101 line_text = ''
102 line_attrs = []
103 current_attr = [None, 0]
104 current_fg = None
105 current_bg = None
106 current_props = None
107 for line in data.split('<br/>'):
108 if not line:
109 continue
110 for span in line.split('</span>'):
111 if not span:
112 continue
113 m = spanre.match(span)
114 fg, bg, char = m.groups()
115 if '&' in char:
116 char = htmlparser.unescape(char)
117 char = char.encode('utf8')
118 line_text += char
119 props = []
120 # TODO: if bold is set, append bold to props
121 fg = ('#'+
122 nearest_color(int(fg[0:2], 16)) +
123 nearest_color(int(fg[2:4], 16)) +
124 nearest_color(int(fg[4:6], 16)))
125 bg = ('#'+
126 nearest_color(int(bg[0:2], 16)) +
127 nearest_color(int(bg[2:4], 16)) +
128 nearest_color(int(bg[4:6], 16)))
129 if current_fg == fg and current_bg == bg and current_props == props:
130 current_attr[1] += len(char)
131 else:
132 if current_attr[0]:
133 line_attrs.append(tuple(current_attr))
134 fg = ', '.join(props + [fg])
135 attr = urwid.AttrSpec(fg, bg)
136 current_attr = [attr, len(char)]
137 current_fg = fg
138 current_bg = bg
139 current_props = props
140 line_attrs.append(tuple(current_attr))
141 current_attr = [None, 0]
142 current_fg = None
143 current_bg = None
144 line_list.append(line_text)
145 line_text = ''
146 attr_list.append(line_attrs)
147 line_attrs = []
148 canvas = urwid.TextCanvas(line_list, attr_list)
149 return canvas
150
151def main():
152 import PIL.Image
153 img = PIL.Image.open('/tmp/p/8.jpg')
154 img.load()
155 hinter = slide.ScreenHinter()
156 hinter.set_cols_rows((80, 25))
157 w = ANSIImage(img, hinter)
158 slpile = slide.SlidePile([])
159 slpile.contents.append((w, slpile.options()))
160 pad = slide.SlidePadding(slpile, align='center', width='pack')
161 fill = slide.SlideFiller(pad)
162 #w.render((80,25))
163 fill.render((80,25))
164 screen = urwid.raw_display.Screen()
165 if True:
166 with screen.start():
167 screen.draw_screen((80,25), fill.render((80,25)))
168 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import urwid
17
18DARK_PALETTE = {
19 '_default': urwid.AttrSpec('light gray', 'black'),
20
21 'emphasis': urwid.AttrSpec('bold, light gray', 'black'),
22 'title': urwid.AttrSpec('bold, white', 'black'),
23
24 'progressive': urwid.AttrSpec('dark gray', 'black'),
25
26 # Based on pygments default colors
27
28 'whitespace': urwid.AttrSpec('light gray', '#aaa'),
29 'comment': urwid.AttrSpec('#688', 'black'),
30 'comment-preproc': urwid.AttrSpec('#a80', 'black'),
31 'keyword': urwid.AttrSpec('bold, #0f0', 'black'),
32 'keyword-pseudo': urwid.AttrSpec('#080', 'black'),
33 'keyword-type': urwid.AttrSpec('#a06', 'black'),
34 'operator': urwid.AttrSpec('#666', 'black'),
35 'operator-word': urwid.AttrSpec('bold, #a0f', 'black'),
36 'name-builtin': urwid.AttrSpec('#0d0', 'black'),
37 'name-function': urwid.AttrSpec('#00f', 'black'),
38 'name-class': urwid.AttrSpec('bold, #00f', 'black'),
39 'name-namespace': urwid.AttrSpec('bold, #00f', 'black'),
40 'name-exception': urwid.AttrSpec('bold, #d66', 'black'),
41 'name-variable': urwid.AttrSpec('#008', 'black'),
42 'name-constant': urwid.AttrSpec('#800', 'black'),
43 'name-label': urwid.AttrSpec('#aa0', 'black'),
44 'name-entity': urwid.AttrSpec('bold, #888', 'black'),
45 'name-attribute': urwid.AttrSpec('#880', 'black'),
46 'name-tag': urwid.AttrSpec('bold, #080', 'black'),
47 'name-decorator': urwid.AttrSpec('#a0f', 'black'),
48 'string': urwid.AttrSpec('#a00', 'black'),
49 'string-doc': urwid.AttrSpec('light gray', 'black'),
50 'string-interpol': urwid.AttrSpec('bold, #a68', 'black'),
51 'string-escape': urwid.AttrSpec('bold, #a60', 'black'),
52 'string-regex': urwid.AttrSpec('#a68', 'black'),
53 'string-symbol': urwid.AttrSpec('#008', 'black'),
54 'string-other': urwid.AttrSpec('#080', 'black'),
55 'number': urwid.AttrSpec('#666', 'black'),
56 'generic-heading': urwid.AttrSpec('bold, #008', 'black'),
57 'generic-subheading': urwid.AttrSpec('bold, #808', 'black'),
58 'generic-deleted': urwid.AttrSpec('#a00', 'black'),
59 'generic-inserted': urwid.AttrSpec('#0a0', 'black'),
60 'generic-error': urwid.AttrSpec('#f00', 'black'),
61 'generic-emph': urwid.AttrSpec('bold, #fff', 'black'),
62 'generic-strong': urwid.AttrSpec('bold, #ddd', 'black'),
63 'generic-prompt': urwid.AttrSpec('bold, #008', 'black'),
64 'generic-output': urwid.AttrSpec('#888', 'black'),
65 'generic-traceback': urwid.AttrSpec('#06d', 'black'),
66 'error': urwid.AttrSpec('underline, #f00', 'black'),
67}
68
69LIGHT_PALETTE = {}
70for k, v in DARK_PALETTE.items():
71 LIGHT_PALETTE[k] = urwid.AttrSpec(v.foreground, 'h15')
72
73LIGHT_PALETTE.update({
74 '_default': urwid.AttrSpec('black', 'h15'),
75 'emphasis': urwid.AttrSpec('bold, black', 'h15'),
76 'title': urwid.AttrSpec('bold, #000', 'h15'),
77 'progressive': urwid.AttrSpec('light gray', 'h15'),
78})
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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import argparse
17import os
18import sys
19import time
20
21import urwid
22
23import slide
24import server
25import rst
26import palette
27
28
29class Presenter(object):
30 def __init__(self, palette):
31 blank = urwid.Text(u'')
32 self.blank = slide.UrwidSlide('Blank', None, blank,
33 palette['_default'])
34 self.current = self.blank
35 self.program = []
36 self.palette = palette
37 self.pos = -1
38 self.loop = urwid.MainLoop(self.blank,
39 unhandled_input=self.unhandledInput)
40 self.loop.screen.set_terminal_properties(colors=256)
41
42 self.server_pipe_in = self.loop.watch_pipe(self.serverData)
43 r,w = os.pipe()
44 self.server_pipe_out_read = os.fdopen(r)
45 self.server_pipe_out_write = w
46 self.server = server.ConsoleServer(self)
47 self.server.start()
48
49 def serverData(self, data):
50 parts = data.split()
51 if parts[0] == 'jump':
52 try:
53 index = int(parts[1])
54 except Exception:
55 os.write(self.server_pipe_out_write, 'err\n')
56 return
57 if index < 0 or index > len(self.program)-1:
58 os.write(self.server_pipe_out_write, 'err\n')
59 return
60 self.transitionTo(index)
61 os.write(self.server_pipe_out_write, 'ok\n')
62 elif parts[0] == 'next':
63 self.nextSlide()
64 os.write(self.server_pipe_out_write, 'ok\n')
65 elif parts[0] == 'prev':
66 self.prevSlide()
67 os.write(self.server_pipe_out_write, 'ok\n')
68
69 def setProgram(self, program):
70 self.program = program
71
72 def run(self):
73 self.loop.set_alarm_in(0, self.nextSlide)
74 self.loop.run()
75
76 def unhandledInput(self, key):
77 if key in ('right', 'page down'):
78 self.nextSlide()
79 elif key in ('left', 'page up'):
80 self.prevSlide()
81
82 def transitionTo(self, index, forward=True):
83 self.pos = index
84 current_slide = self.current
85 new_slide = self.program[index]
86 if forward:
87 transition = new_slide.transition
88 new_slide.resetProgressive()
89 else:
90 transition = current_slide.transition
91 new_slide.resetProgressive(True)
92 current_slide.stopAnimation()
93 if forward:
94 transition.setTargets(current_slide, new_slide)
95 else:
96 transition.setTargets(new_slide, current_slide)
97 self.loop.widget = transition
98 duration = transition.getDuration()
99 start = time.time()
100 now = start
101 end = start + duration
102 while duration:
103 if forward:
104 progress = min(1-((end-now)/duration), 1.0)
105 else:
106 progress = max(((end-now)/duration), 0.0)
107 transition.setProgress(progress)
108 self.loop.draw_screen()
109 now = time.time()
110 if now >= end:
111 break
112 end = time.time()
113 self.loop.widget = new_slide
114 self.current = new_slide
115 self.loop.draw_screen()
116 current_slide.resetAnimation()
117 new_slide.startAnimation(self.loop)
118
119 def nextSlide(self, loop=None, data=None):
120 if self.current.nextProgressive():
121 return
122 if self.pos+1 == len(self.program):
123 return
124 self.transitionTo(self.pos+1)
125
126 def prevSlide(self, loop=None, data=None):
127 if self.current.prevProgressive():
128 return
129 if self.pos == 0:
130 return
131 self.transitionTo(self.pos-1, forward=False)
132
133def main():
134 parser = argparse.ArgumentParser(
135 description='Console-based presentation system')
136 parser.add_argument('--light', dest='light',
137 default=False,
138 action='store_true',
139 help='use a black on white palette')
140 parser.add_argument('--warnings', dest='warnings',
141 default=False,
142 action='store_true',
143 help='print RST parser warnings and exit if any')
144 parser.add_argument('file',
145 help='presentation file (RST)')
146 args = parser.parse_args()
147 if args.light:
148 plt = palette.LIGHT_PALETTE
149 else:
150 plt = palette.DARK_PALETTE
151 hinter = slide.ScreenHinter()
152 parser = rst.PresentationParser(plt, hinter)
153 program = parser.parse(open(args.file).read(), args.file)
154 if args.warnings:
155 w = parser.warnings.getvalue()
156 if w:
157 print w
158 sys.exit(1)
159 p = Presenter(plt)
160 p.setProgram(program)
161 hinter.setScreen(p.loop.screen)
162 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import os
17import re
18import docutils
19import docutils.frontend
20import docutils.parsers.rst
21import docutils.nodes
22import cStringIO as StringIO
23
24import urwid
25
26import slide
27import transition as transition_mod
28import image
29import ansiparser
30import text
31
32try:
33 import PIL
34 import PIL.Image
35except ImportError:
36 PIL = None
37
38DEFAULT_TRANSITION = 'dissolve'
39DEFAULT_TRANSITION_DURATION = 0.4
40
41class TextAccumulator(object):
42 def __init__(self):
43 self.text = []
44
45 def append(self, text):
46 self.text.append(text)
47
48 def getFormattedText(self):
49 return self.text
50
51 wsre = re.compile('\s+')
52
53 def getFlowedText(self):
54 ret = []
55 for part in self.text:
56 if isinstance(part, tuple):
57 ret.append((part[0], self.wsre.sub(u' ', part[1])))
58 else:
59 ret.append(self.wsre.sub(u' ', part))
60 if not ret:
61 return u''
62 return ret
63
64class UrwidTranslator(docutils.nodes.GenericNodeVisitor):
65 transition_map = {'dissolve': transition_mod.DissolveTransition,
66 'cut': transition_mod.CutTransition,
67 'pan': transition_mod.PanTransition,
68 }
69
70 def __init__(self, document, palette, hinter=None, basedir='.'):
71 docutils.nodes.GenericNodeVisitor.__init__(self, document)
72 self.program = []
73 self.stack = []
74 self.default_transition = self._make_transition(
75 DEFAULT_TRANSITION,
76 DEFAULT_TRANSITION_DURATION)
77 self.transition = self.default_transition
78 self.attr = []
79 self.table_columns = []
80 self.table_column = []
81 self.progressives = []
82 self.palette = palette
83 self.hinter = hinter
84 self.basedir = basedir
85 self.slide = None
86 self.default_hide_title = False
87 self.hide_title = self.default_hide_title
88
89 def _make_transition(self, name, duration):
90 tr = self.transition_map[name]
91 return tr(duration)
92
93 def default_visit(self, node):
94 """Override for generic, uniform traversals."""
95 pass
96
97 def default_departure(self, node):
98 """Override for generic, uniform traversals."""
99 pass
100
101 def _append(self, node, widget, *args, **kw):
102 if self.stack:
103 if 'handout' in node.get('classes'):
104 if self.handout_pile not in self.stack:
105 container = self.handout_pile
106 else:
107 # If the handout pile is in the stack, then ignore
108 # this class -- it has probably needlessly been
109 # applied to something deeper in the stack. The
110 # thing further up will end up in the handout.
111 container = self.stack[-1]
112 else:
113 container = self.stack[-1]
114 container.contents.append((widget, container.options(*args, **kw)))
115
116 def styled(self, style, text):
117 if style in self.palette:
118 return (self.palette[style], text)
119 return text
120
121 def visit_transition(self, node):
122 name = node['name']
123 duration = node.get('duration', DEFAULT_TRANSITION_DURATION)
124 self.transition = self._make_transition(name, duration)
125
126 def depart_transition(self, node):
127 pass
128
129 def visit_hidetitle(self, node):
130 if self.slide:
131 self.hide_title = True
132 else:
133 self.default_hide_title = True
134
135 def depart_hidetitle(self, node):
136 pass
137
138 def visit_system_message(self, node):
139 #print node.astext()
140 raise docutils.nodes.SkipNode()
141
142 def visit_section(self, node):
143 self.hide_title = self.default_hide_title
144 self.transition = self.default_transition
145 title_pile = slide.SlidePile([])
146 title_pad = slide.SlidePadding(title_pile,
147 align='center', width='pack')
148
149 main_pile = slide.SlidePile([])
150 main_pad = slide.SlidePadding(main_pile, align='center', width='pack')
151 outer_pile = slide.SlidePile([
152 ('pack', title_pad),
153 ('pack', main_pad),
154 ])
155 s = slide.UrwidSlide(u'', self.transition, outer_pile,
156 self.palette['_default'])
157 self.slide = s
158 self.stack.append(main_pile)
159 self.title_pile = title_pile
160
161 pile = slide.SlidePile([])
162 s = slide.Handout(pile, self.palette['_default'])
163 self.handout = s
164 self.handout_pile = pile
165 self.slide.handout = s
166
167 def depart_section(self, node):
168 self.slide.transition = self.transition
169 if self.hide_title:
170 self.title_pile.contents[:] = []
171 self.program.append(self.slide)
172 self.stack.pop()
173
174 def visit_block_quote(self, node):
175 self.stack.append(slide.SlidePile([]))
176
177 def depart_block_quote(self, node):
178 pile = self.stack.pop()
179 pad = slide.SlidePadding(pile, left=2)
180 self._append(node, pad, 'pack')
181
182 def visit_list_item(self, node):
183 self.stack.append(slide.SlidePile([]))
184
185 def depart_list_item(self, node):
186 pile = self.stack.pop()
187 bullet = urwid.Text(u'* ')
188 cols = slide.SlideColumns([])
189 cols.contents.append((bullet, cols.options('pack')))
190 cols.contents.append((pile, cols.options('weight', 1)))
191 if self.progressives:
192 cols = urwid.AttrMap(cols, self.palette['progressive'])
193 self.progressives[-1].append(cols)
194 self._append(node, cols, 'pack')
195
196 def visit_tgroup(self, node):
197 self.table_columns.append([])
198 self.stack.append(slide.SlidePile([]))
199
200 def visit_colspec(self, node):
201 self.table_columns[-1].append(node['colwidth'])
202
203 def visit_row(self, node):
204 self.stack.append(slide.SlideColumns([], dividechars=1))
205 self.table_column.append(0)
206
207 def depart_row(self, node):
208 self.table_column.pop()
209 cols = self.stack.pop()
210 self._append(node, cols, 'pack')
211
212 def visit_thead(self, node):
213 pass
214
215 def depart_thead(self, node):
216 cols = slide.SlideColumns([], dividechars=1)
217 for width in self.table_columns[-1]:
218 cols.contents.append((urwid.Text(u'='*width),
219 cols.options('given', width)))
220 self._append(node, cols, 'pack')
221
222 def visit_entry(self, node):
223 self.stack.append(slide.SlidePile([]))
224
225 def depart_entry(self, node):
226 colindex = self.table_column[-1]
227 self.table_column[-1] = colindex + 1
228 pile = self.stack.pop()
229 self._append(node, pile, 'given', self.table_columns[-1][colindex])
230
231 def depart_tgroup(self, node):
232 self.table_columns.pop()
233 pile = self.stack.pop()
234 self._append(node, pile, 'pack')
235
236 def visit_textelement(self, node):
237 self.stack.append(TextAccumulator())
238
239 visit_paragraph = visit_textelement
240
241 def depart_paragraph(self, node):
242 text = self.stack.pop()
243 self._append(node, urwid.Text(text.getFlowedText()), 'pack')
244
245 visit_literal_block = visit_textelement
246
247 def depart_literal_block(self, node):
248 text = self.stack.pop()
249 text = urwid.Text(text.getFormattedText(), wrap='clip')
250 pad = slide.SlidePadding(text, width='pack')
251 self._append(node, pad, 'pack')
252
253 visit_line = visit_textelement
254
255 def depart_line(self, node):
256 text = self.stack.pop()
257 self._append(node, urwid.Text(text.getFormattedText(), wrap='clip'),
258 'pack')
259
260 visit_title = visit_textelement
261
262 def depart_title(self, node):
263 text = self.stack.pop()
264 self.slide.title = node.astext()
265 widget = urwid.Text(self.styled('title', text.getFlowedText()),
266 align='center')
267 self.title_pile.contents.append(
268 (widget, self.title_pile.options('pack')))
269
270 def visit_Text(self, node):
271 pass
272
273 def depart_Text(self, node):
274 if self.stack and isinstance(self.stack[-1], TextAccumulator):
275 if self.attr:
276 t = (self.attr[-1], node.astext())
277 else:
278 t = node.astext()
279 self.stack[-1].append(t)
280
281 def visit_emphasis(self, node):
282 self.attr.append(self.palette['emphasis'])
283
284 def depart_emphasis(self, node):
285 self.attr.pop()
286
287 def visit_inline(self, node):
288 cls = node.get('classes')
289 if not cls:
290 raise docutils.nodes.SkipDeparture()
291 cls = [x for x in cls if x != 'literal']
292 for length in range(len(cls), 0, -1):
293 clsname = '-'.join(cls[:length])
294 if clsname in self.palette:
295 self.attr.append(self.palette[clsname])
296 return
297 raise docutils.nodes.SkipDeparture()
298
299 def depart_inline(self, node):
300 self.attr.pop()
301
302 def visit_image(self, node):
303 if not PIL:
304 return
305 uri = node['uri']
306 fn = os.path.join(self.basedir, uri)
307 w = image.ANSIImage(fn, self.hinter)
308 self._append(node, w, 'pack')
309
310 def visit_ansi(self, node):
311 interval = node.get('interval', 0.5)
312 oneshot = node.get('oneshot', False)
313 animation = slide.AnimatedText(interval, oneshot)
314 for name in node['names']:
315 p = ansiparser.ANSIParser()
316 fn = os.path.join(self.basedir, name)
317 data = unicode(open(fn).read(), 'utf8')
318 text = p.parse(data)
319 animation.addFrame(text)
320 self.slide.animations.append(animation)
321 self._append(node, animation, 'pack')
322
323 def depart_ansi(self, node):
324 pass
325
326 def visit_figlet(self, node):
327 figlet = text.FigletText(node['text'])
328 self._append(node, figlet, 'pack')
329
330 def depart_figlet(self, node):
331 pass
332
333 def visit_cowsay(self, node):
334 cowsay = text.CowsayText(node['text'])
335 self._append(node, cowsay, 'pack')
336
337 def depart_cowsay(self, node):
338 pass
339
340 def visit_container(self, node):
341 self.stack.append(slide.SlidePile([]))
342 if 'progressive' in node.get('classes'):
343 self.progressives.append(self.slide.progressives)
344 self.slide.progressive_attr = self.palette['progressive']
345
346 def depart_container(self, node):
347 pile = self.stack.pop()
348 self._append(node, pile, 'pack')
349 if 'progressive' in node.get('classes'):
350 self.progressives.pop()
351
352class TransitionDirective(docutils.parsers.rst.Directive):
353 required_arguments = 1
354 option_spec = {'duration': float}
355 has_content = False
356
357 def run(self):
358 args = {'name': self.arguments[0]}
359 duration = self.options.get('duration')
360 if duration:
361 args['duration'] = duration
362 node = transition(**args)
363 return [node]
364
365class ANSIDirective(docutils.parsers.rst.Directive):
366 required_arguments = 1
367 final_argument_whitespace = True
368 option_spec = {'interval': float,
369 'oneshot': bool}
370 has_content = False
371
372 def run(self):
373 args = {'names': self.arguments[0].split()}
374 args.update(self.options)
375 node = ansi(**args)
376 return [node]
377
378class FigletDirective(docutils.parsers.rst.Directive):
379 required_arguments = 1
380 has_content = False
381 final_argument_whitespace = True
382
383 def run(self):
384 args = {'text': self.arguments[0]}
385 node = figlet(**args)
386 return [node]
387
388class CowsayDirective(docutils.parsers.rst.Directive):
389 required_arguments = 1
390 has_content = False
391 final_argument_whitespace = True
392
393 def run(self):
394 args = {'text': self.arguments[0]}
395 node = cowsay(**args)
396 return [node]
397
398class HideTitleDirective(docutils.parsers.rst.Directive):
399 has_content = False
400
401 def run(self):
402 node = hidetitle()
403 return [node]
404
405class transition(docutils.nodes.Special, docutils.nodes.Invisible,
406 docutils.nodes.Element):
407 pass
408
409class ansi(docutils.nodes.General, docutils.nodes.Inline,
410 docutils.nodes.Element):
411 pass
412
413class figlet(docutils.nodes.General, docutils.nodes.Inline,
414 docutils.nodes.Element):
415 pass
416
417class cowsay(docutils.nodes.General, docutils.nodes.Inline,
418 docutils.nodes.Element):
419 pass
420
421class hidetitle(docutils.nodes.Special, docutils.nodes.Invisible,
422 docutils.nodes.Element):
423 pass
424
425class PresentationParser(object):
426 def __init__(self, palette, hinter=None):
427 docutils.parsers.rst.directives.register_directive(
428 'transition', TransitionDirective)
429 docutils.parsers.rst.directives.register_directive(
430 'ansi', ANSIDirective)
431 docutils.parsers.rst.directives.register_directive(
432 'figlet', FigletDirective)
433 docutils.parsers.rst.directives.register_directive(
434 'cowsay', CowsayDirective)
435 docutils.parsers.rst.directives.register_directive(
436 'hidetitle', HideTitleDirective)
437 self.warnings = StringIO.StringIO()
438 self.settings = docutils.frontend.OptionParser(
439 components=(docutils.parsers.rst.Parser,),
440 defaults=dict(warning_stream=self.warnings)).get_default_values()
441 self.parser = docutils.parsers.rst.Parser()
442 self.palette = palette
443 self.hinter = hinter
444
445 def _parse(self, input, filename):
446 document = docutils.utils.new_document(filename, self.settings)
447 self.parser.parse(input, document)
448 visitor = UrwidTranslator(document, self.palette, self.hinter,
449 os.path.dirname(filename))
450 document.walkabout(visitor)
451 return document, visitor
452
453 def parse(self, input, filename='program'):
454 document, visitor = self._parse(input, filename)
455 return visitor.program
456
457def main():
458 import argparse
459 import palette
460
461 argp = argparse.ArgumentParser(description='Test RST parser')
462 argp.add_argument('file', help='presentation file (RST)')
463 argp.add_argument('slides', nargs='?', default=[],
464 help='slides to render')
465 argp.add_argument('--render', action='store_true',
466 help='Fully render a slide')
467 args = argp.parse_args()
468
469 parser = PresentationParser(palette.DARK_PALETTE)
470 document, visitor = parser._parse(open(args.file).read(), args.file)
471
472 slides = args.slides
473 if not slides:
474 slides = range(len(visitor.program))
475 slides = [int(x) for x in slides]
476
477 if not args.render:
478 print document.pformat()
479 for i in slides:
480 print '-'*80
481 s = visitor.program[i]
482 for line in s.render((80,25)).text:
483 print line
484 else:
485 screen = urwid.raw_display.Screen()
486 with screen.start():
487 for i in slides:
488 s = visitor.program[i]
489 screen.draw_screen((80,25), s.render((80,25)))
490 raw_input()
491
492if __name__ == '__main__':
493 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import os
17import threading
18import SocketServer
19
20class ConsoleHandler(SocketServer.StreamRequestHandler):
21 def handle(self):
22 server = self.server.server
23 while True:
24 try:
25 data = self.rfile.readline()
26 except Exception:
27 break
28 if not data:
29 break
30 data = data.strip()
31 if data == 'list':
32 for i, slide in enumerate(server.list()):
33 self.wfile.write('slide %i %s\n' % (i, slide.title))
34 self.wfile.write('end\n')
35 elif data == 'current':
36 i, slide = server.current()
37 self.wfile.write('current %i %i %s\n' % (
38 i, slide.progressive_state, slide.title))
39 elif data == 'next':
40 i, slide = server.next()
41 self.wfile.write('current %i %i %s\n' % (
42 i, slide.progressive_state, slide.title))
43 elif data == 'prev':
44 i, slide = server.prev()
45 self.wfile.write('current %i %i %s\n' % (
46 i, slide.progressive_state, slide.title))
47 elif data.startswith('jump'):
48 parts = data.split()
49 i, slide = server.jump(int(parts[1].strip()))
50 self.wfile.write('current %i %i %s\n' % (
51 i, slide.progressive_state, slide.title))
52 elif data == 'size':
53 size = server.size()
54 self.wfile.write('size %s %s\n' % size)
55
56class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
57 allow_reuse_address=True
58
59class ConsoleServer(object):
60 def __init__(self, presenter, host='localhost', port=1292):
61 self.presenter = presenter
62 self.server = ThreadedTCPServer((host, port), ConsoleHandler)
63 self.server.server = self
64 self.lock = threading.Lock()
65
66 def start(self):
67 self.thread=threading.Thread(target=self._run, name="Console Server")
68 self.thread.daemon=True
69 self.thread.start()
70
71 def _run(self):
72 self.server.serve_forever()
73
74 def stop(self):
75 self.server.shutdown()
76
77 def list(self):
78 return self.presenter.program
79
80 def current(self):
81 s = self.presenter.program[self.presenter.pos]
82 return (self.presenter.pos, s)
83
84 def size(self):
85 return self.presenter.loop.screen.get_cols_rows()
86
87 def next(self):
88 self.lock.acquire()
89 try:
90 os.write(self.presenter.server_pipe_in, 'next')
91 self.presenter.server_pipe_out_read.readline()
92 return self.current()
93 finally:
94 self.lock.release()
95
96 def prev(self):
97 self.lock.acquire()
98 try:
99 os.write(self.presenter.server_pipe_in, 'prev')
100 self.presenter.server_pipe_out_read.readline()
101 return self.current()
102 finally:
103 self.lock.release()
104
105 def jump(self, pos):
106 self.lock.acquire()
107 try:
108 os.write(self.presenter.server_pipe_in, 'jump %s' % (pos,))
109 self.presenter.server_pipe_out_read.readline()
110 return self.current()
111 finally:
112 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import urwid
17
18class SlidePile(urwid.Pile):
19 def pack(self, size, focus=False):
20 cols = 0
21 rows = 0
22 for x in self.contents:
23 c,r = x[0].pack((size[0],))
24 if c>cols:
25 cols = c
26 rows += r
27 return (cols, rows)
28
29class SlidePadding(urwid.Padding):
30 def pack(self, size, focus=False):
31 r = self._original_widget.pack(size, focus)
32 width = max(r[0] + self.left + self.right, self.min_width)
33 width = min(size[0], width)
34 return (width, r[1])
35
36class SlideColumns(urwid.Columns):
37 def pack(self, size, focus=False):
38 cols = self.dividechars * (len(self.contents)-1)
39 rows = 0
40 for widget, packing in self.contents:
41 if packing[0] == 'given':
42 allocated_cols = packing[1]
43 else:
44 allocated_cols = size[0]
45 c,r = widget.pack((allocated_cols,))
46 if packing[0] == 'given':
47 c = allocated_cols
48 if r>rows:
49 rows = r
50 cols += c
51 return (cols, rows)
52
53class SlideFiller(urwid.Filler):
54 pass
55
56class ScreenHinter(object):
57 # A terrible hack to try to provide some needed context to the
58 # image widget.
59 def __init__(self, screen=None):
60 self.screen = screen
61
62 def setScreen(self, screen):
63 self.screen = screen
64
65 def getSize(self):
66 cols, rows = self.screen.get_cols_rows()
67 return (cols, rows-1)
68
69class Handout(urwid.WidgetWrap):
70 def __init__(self, widget, background):
71 self.background = background
72 self.pad = SlidePadding(widget, align='center', width='pack')
73 self.map = urwid.AttrMap(self.pad, self.background)
74 super(Handout, self).__init__(self.map)
75
76class UrwidSlide(urwid.WidgetWrap):
77 def __init__(self, title, transition, widget, background):
78 self.title = title
79 self.transition = transition
80 self.fill = SlideFiller(widget)
81 self.background = background
82 self.map = urwid.AttrMap(self.fill, self.background)
83 self.handout = None
84 self.animations = []
85 self.progressives = []
86 self.progressive_attr = None
87 self.progressive_state = 0
88 super(UrwidSlide, self).__init__(self.map)
89
90 def startAnimation(self, loop):
91 for x in self.animations:
92 x.startAnimation(loop)
93
94 def stopAnimation(self):
95 for x in self.animations:
96 x.stopAnimation()
97
98 def resetAnimation(self):
99 for x in self.animations:
100 x.resetAnimation()
101
102 def resetProgressive(self, on=False):
103 if on:
104 self.progressive_state = len(self.progressives)
105 for x in self.progressives:
106 x.set_attr_map({None: None})
107 else:
108 self.progressive_state = 0
109 for x in self.progressives:
110 x.set_attr_map({None: self.progressive_attr})
111
112 def nextProgressive(self):
113 if self.progressive_state >= len(self.progressives):
114 return False
115 self.progressives[self.progressive_state].set_attr_map(
116 {None: None})
117 self.progressive_state += 1
118 return True
119
120 def prevProgressive(self):
121 if self.progressive_state <= 0:
122 return False
123 self.progressive_state -= 1
124 self.progressives[self.progressive_state].set_attr_map(
125 {None: self.progressive_attr})
126 return True
127
128 def setProgressive(self, state):
129 self.progressive_state = state
130 for i, x in enumerate(self.progressives):
131 if i < self.progressive_state:
132 x.set_attr_map({None: None})
133 else:
134 x.set_attr_map({None: self.progressive_attr})
135
136class AnimatedText(urwid.Text):
137 def __init__(self, interval=0.5, oneshot=False):
138 super(AnimatedText, self).__init__(u'')
139 self.frames = []
140 self.current = 0
141 self.running = False
142 self.interval = interval
143 self.oneshot = oneshot
144
145 def addFrame(self, text):
146 self.frames.append(text)
147 if len(self.frames) == self.current+1:
148 self.set_text(text)
149
150 def startAnimation(self, loop):
151 if self.running:
152 return
153 if len(self.frames) == 1:
154 return
155 self.running = True
156 loop.set_alarm_in(self.interval, self.updateCallback)
157
158 def updateCallback(self, loop=None, data=None):
159 if not self.running:
160 return
161 if self.current+1 >= len(self.frames):
162 if self.oneshot:
163 self.running = False
164 return
165 self.current = 0
166 else:
167 self.current += 1
168 self.set_text(self.frames[self.current])
169 loop.set_alarm_in(self.interval, self.updateCallback)
170
171 def stopAnimation(self):
172 if not self.running:
173 return
174 self.running = False
175
176 def resetAnimation(self):
177 self.current = 0
178 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import subprocess
17
18import urwid
19
20class FigletText(urwid.WidgetWrap):
21 def __init__(self, text, attr=None):
22 self.text = text
23 self.attr = attr
24 output = self._run()
25 if attr:
26 widget = urwid.Text((attr, output), wrap='clip')
27 else:
28 widget = urwid.Text(output, wrap='clip')
29 super(FigletText, self).__init__(widget)
30
31 def _run(self):
32 p = subprocess.Popen(['figlet'],
33 stdin=subprocess.PIPE,
34 stdout=subprocess.PIPE,
35 stderr=subprocess.PIPE)
36 p.stdin.write(self.text)
37 p.stdin.close()
38 data = p.stdout.read()
39 p.stderr.read()
40 p.wait()
41 return data
42
43class CowsayText(urwid.WidgetWrap):
44 def __init__(self, text, attr=None):
45 self.text = text
46 self.attr = attr
47 output = self._run()
48 if attr:
49 widget = urwid.Text((attr, output), wrap='clip')
50 else:
51 widget = urwid.Text(output, wrap='clip')
52 super(CowsayText, self).__init__(widget)
53
54 def _run(self):
55 p = subprocess.Popen(['cowsay'],
56 stdin=subprocess.PIPE,
57 stdout=subprocess.PIPE,
58 stderr=subprocess.PIPE)
59 p.stdin.write(self.text)
60 p.stdin.close()
61 data = p.stdout.read()
62 p.stderr.read()
63 p.wait()
64 return data
65
66def main():
67 import slide
68 w = FigletText("Testing")
69 slpile = slide.SlidePile([])
70 slpile.contents.append((w, slpile.options()))
71 pad = slide.SlidePadding(slpile, align='center', width='pack')
72 fill = slide.SlideFiller(pad)
73 #w.render((80,25))
74 fill.render((80,25))
75 screen = urwid.raw_display.Screen()
76 if True:
77 with screen.start():
78 screen.draw_screen((80,25), fill.render((80,25)))
79 raw_input()
80if __name__=='__main__':
81 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 @@
1# Copyright (C) 2015 James E. Blair <corvus@gnu.org>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import urwid
17
18class Transition(urwid.Widget):
19 def __init__(self, duration=0.4):
20 super(Transition, self).__init__()
21 self.duration = 0.4
22 self.old = None
23 self.new = None
24 self.progress = 0.0
25
26 def getDuration(self):
27 return self.duration
28
29 def setTargets(self, old, new):
30 self.old = old
31 self.new = new
32 self.setProgress(0.0)
33
34 def setProgress(self, progress):
35 self.progress = progress
36 self._invalidate()
37
38class PanTransition(Transition):
39 def render(self, size, focus=False):
40 old = self.old.render((size[0], size[1]))
41 new = self.new.render((size[0], size[1]))
42 c = urwid.CanvasJoin([(old, None, False, size[0]),
43 (new, None, False, size[0])])
44 #c = urwid.CanvasOverlay(new, old, 6, 0)
45 offset = int(size[0] * self.progress)
46 c.pad_trim_left_right(0-offset, 0-(size[0]-offset))
47 return c
48
49class DissolveTransition(Transition):
50 def __init__(self, *args, **kw):
51 super(DissolveTransition, self).__init__(*args, **kw)
52 self._oldbuf = None
53 self._newbuf = None
54 self._cache_size = None
55
56 def setTargets(self, old, new):
57 if old != self.old:
58 self._oldbuf = None
59 self._cache_size = None
60 if new != self.new:
61 self._newbuf = None
62 self._cache_size = None
63 super(DissolveTransition, self).setTargets(old, new)
64
65 def _to_buf(self, canvas):
66 buf = []
67 for line in canvas.content():
68 for (attr, cs, text) in line:
69 for char in unicode(text, 'utf8'):
70 buf.append((attr, cs, char))
71 return buf
72
73 def render(self, size, focus=False):
74 if self._cache_size != size:
75 old = self.old.render((size[0], size[1]))
76 new = self.new.render((size[0], size[1]))
77 self._oldbuf = self._to_buf(old)
78 self._newbuf = self._to_buf(new)
79 self._cache_size = size
80 line_list = []
81 attr_list = []
82 line_text = ''
83 line_attrs = []
84 current_attr = [None, 0]
85 current_rgb = None
86 current_props = None
87 background = urwid.AttrSpec('light gray', 'black')
88 for i in range(len(self._oldbuf)):
89 oldattr, oldcs, oldchar = self._oldbuf[i]
90 newattr, newcs, newchar = self._newbuf[i]
91 oldrgb = oldattr.get_rgb_values()
92 newrgb = newattr.get_rgb_values()
93 if None in oldrgb:
94 oldrgb = background.get_rgb_values()
95 if None in newrgb:
96 newrgb = background.get_rgb_values()
97 if newchar == ' ':
98 char = oldchar
99 charattr = oldattr
100 newrgb = newrgb[3:]*2
101 elif oldchar == ' ':
102 char = newchar
103 charattr = newattr
104 oldrgb = oldrgb[3:]*2
105 elif self.progress >= 0.5:
106 char = newchar
107 charattr = newattr
108 else:
109 char = oldchar
110 charattr = oldattr
111 char = char.encode('utf8')
112 line_text += char
113 rgb = []
114 props = []
115 if charattr.bold:
116 props.append('bold')
117 if charattr.underline:
118 props.append('underline')
119 if charattr.standout:
120 props.append('standout')
121 if charattr.blink:
122 props.append('blink')
123 for x in range(len(oldrgb)):
124 rgb.append(int(((newrgb[x]-oldrgb[x])*self.progress)+oldrgb[x])>>4)
125 if current_rgb == rgb and current_props == props:
126 current_attr[1] += len(char)
127 else:
128 if current_attr[0]:
129 line_attrs.append(tuple(current_attr))
130 fg = ', '.join(props + ['#%x%x%x' % tuple(rgb[:3])])
131 bg = '#%x%x%x' % tuple(rgb[3:])
132 attr = urwid.AttrSpec(fg, bg)
133 current_attr = [attr, len(char)]
134 current_rgb = rgb
135 current_props = props
136 if (i+1) % size[0] == 0:
137 line_attrs.append(tuple(current_attr))
138 current_attr = [None, 0]
139 current_rgb = None
140 line_list.append(line_text)
141 line_text = ''
142 attr_list.append(line_attrs)
143 line_attrs = []
144 canvas = urwid.TextCanvas(line_list, attr_list)
145 return canvas
146
147class CutTransition(Transition):
148 def __init__(self, *args, **kw):
149 super(CutTransition, self).__init__(*args, **kw)
150 self.duration = 0.0
151
152 def render(self, size, focus=False):
153 return self.new.render(size, focus)