diff options
Diffstat (limited to 'presentty/console.py')
-rw-r--r-- | presentty/console.py | 292 |
1 files changed, 292 insertions, 0 deletions
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 | |||
16 | import argparse | ||
17 | import sys | ||
18 | import datetime | ||
19 | import time | ||
20 | |||
21 | import urwid | ||
22 | |||
23 | import palette | ||
24 | import client | ||
25 | import slide | ||
26 | import rst | ||
27 | |||
28 | PALETTE = [ | ||
29 | ('reversed', 'standout', ''), | ||
30 | ('status', 'light red', ''), | ||
31 | ] | ||
32 | |||
33 | class 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 | |||
45 | class 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 | |||
54 | class 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 | |||
179 | class 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 | |||
186 | class 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 | |||
217 | class 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 | |||
273 | def 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() | ||