1#!/usr/bin/python
2#
3# Urwid html fragment output wrapper for "screen shots"
4#    Copyright (C) 2004-2007  Ian Ward
5#
6#    This library is free software; you can redistribute it and/or
7#    modify it under the terms of the GNU Lesser General Public
8#    License as published by the Free Software Foundation; either
9#    version 2.1 of the License, or (at your option) any later version.
10#
11#    This library is distributed in the hope that it will be useful,
12#    but WITHOUT ANY WARRANTY; without even the implied warranty of
13#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14#    Lesser General Public License for more details.
15#
16#    You should have received a copy of the GNU Lesser General Public
17#    License along with this library; if not, write to the Free Software
18#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19#
20# Urwid web site: http://excess.org/urwid/
21
22from __future__ import division, print_function
23
24"""
25HTML PRE-based UI implementation
26"""
27
28from urwid import util
29from urwid.main_loop import ExitMainLoop
30from urwid.display_common import AttrSpec, BaseScreen
31
32
33# replace control characters with ?'s
34_trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)])
35
36_default_foreground = 'black'
37_default_background = 'light gray'
38
39class HtmlGeneratorSimulationError(Exception):
40    pass
41
42class HtmlGenerator(BaseScreen):
43    # class variables
44    fragments = []
45    sizes = []
46    keys = []
47    started = True
48
49    def __init__(self):
50        super(HtmlGenerator, self).__init__()
51        self.colors = 16
52        self.bright_is_bold = False # ignored
53        self.has_underline = True # ignored
54        self.register_palette_entry(None,
55            _default_foreground, _default_background)
56
57    def set_terminal_properties(self, colors=None, bright_is_bold=None,
58        has_underline=None):
59
60        if colors is None:
61            colors = self.colors
62        if bright_is_bold is None:
63            bright_is_bold = self.bright_is_bold
64        if has_underline is None:
65            has_underline = self.has_underline
66
67        self.colors = colors
68        self.bright_is_bold = bright_is_bold
69        self.has_underline = has_underline
70
71    def set_mouse_tracking(self, enable=True):
72        """Not yet implemented"""
73        pass
74
75    def set_input_timeouts(self, *args):
76        pass
77
78    def reset_default_terminal_palette(self, *args):
79        pass
80
81    def draw_screen(self, size, r ):
82        """Create an html fragment from the render object.
83        Append it to HtmlGenerator.fragments list.
84        """
85        # collect output in l
86        l = []
87
88        cols, rows = size
89
90        assert r.rows() == rows
91
92        if r.cursor is not None:
93            cx, cy = r.cursor
94        else:
95            cx = cy = None
96
97        y = -1
98        for row in r.content():
99            y += 1
100            col = 0
101
102            for a, cs, run in row:
103                if not str is bytes:
104                    run = run.decode()
105                run = run.translate(_trans_table)
106                if isinstance(a, AttrSpec):
107                    aspec = a
108                else:
109                    aspec = self._palette[a][
110                        {1: 1, 16: 0, 88:2, 256:3}[self.colors]]
111
112                if y == cy and col <= cx:
113                    run_width = util.calc_width(run, 0,
114                        len(run))
115                    if col+run_width > cx:
116                        l.append(html_span(run,
117                            aspec, cx-col))
118                    else:
119                        l.append(html_span(run, aspec))
120                    col += run_width
121                else:
122                    l.append(html_span(run, aspec))
123
124            l.append("\n")
125
126        # add the fragment to the list
127        self.fragments.append( "<pre>%s</pre>" % "".join(l) )
128
129    def clear(self):
130        """
131        Force the screen to be completely repainted on the next
132        call to draw_screen().
133
134        (does nothing for html_fragment)
135        """
136        pass
137
138    def get_cols_rows(self):
139        """Return the next screen size in HtmlGenerator.sizes."""
140        if not self.sizes:
141            raise HtmlGeneratorSimulationError("Ran out of screen sizes to return!")
142        return self.sizes.pop(0)
143
144    def get_input(self, raw_keys=False):
145        """Return the next list of keypresses in HtmlGenerator.keys."""
146        if not self.keys:
147            raise ExitMainLoop()
148        if raw_keys:
149            return (self.keys.pop(0), [])
150        return self.keys.pop(0)
151
152_default_aspec = AttrSpec(_default_foreground, _default_background)
153(_d_fg_r, _d_fg_g, _d_fg_b, _d_bg_r, _d_bg_g, _d_bg_b) = (
154    _default_aspec.get_rgb_values())
155
156def html_span(s, aspec, cursor = -1):
157    fg_r, fg_g, fg_b, bg_r, bg_g, bg_b = aspec.get_rgb_values()
158    # use real colours instead of default fg/bg
159    if fg_r is None:
160        fg_r, fg_g, fg_b = _d_fg_r, _d_fg_g, _d_fg_b
161    if bg_r is None:
162        bg_r, bg_g, bg_b = _d_bg_r, _d_bg_g, _d_bg_b
163    html_fg = "#%02x%02x%02x" % (fg_r, fg_g, fg_b)
164    html_bg = "#%02x%02x%02x" % (bg_r, bg_g, bg_b)
165    if aspec.standout:
166        html_fg, html_bg = html_bg, html_fg
167    extra = (";text-decoration:underline" * aspec.underline +
168        ";font-weight:bold" * aspec.bold)
169    def html_span(fg, bg, s):
170        if not s: return ""
171        return ('<span style="color:%s;'
172            'background:%s%s">%s</span>' %
173            (fg, bg, extra, html_escape(s)))
174
175    if cursor >= 0:
176        c_off, _ign = util.calc_text_pos(s, 0, len(s), cursor)
177        c2_off = util.move_next_char(s, c_off, len(s))
178        return (html_span(html_fg, html_bg, s[:c_off]) +
179            html_span(html_bg, html_fg, s[c_off:c2_off]) +
180            html_span(html_fg, html_bg, s[c2_off:]))
181    else:
182        return html_span(html_fg, html_bg, s)
183
184
185def html_escape(text):
186    """Escape text so that it will be displayed safely within HTML"""
187    text = text.replace('&','&amp;')
188    text = text.replace('<','&lt;')
189    text = text.replace('>','&gt;')
190    return text
191
192def screenshot_init( sizes, keys ):
193    """
194    Replace curses_display.Screen and raw_display.Screen class with
195    HtmlGenerator.
196
197    Call this function before executing an application that uses
198    curses_display.Screen to have that code use HtmlGenerator instead.
199
200    sizes -- list of ( columns, rows ) tuples to be returned by each call
201             to HtmlGenerator.get_cols_rows()
202    keys -- list of lists of keys to be returned by each call to
203            HtmlGenerator.get_input()
204
205    Lists of keys may include "window resize" to force the application to
206    call get_cols_rows and read a new screen size.
207
208    For example, the following call will prepare an application to:
209     1. start in 80x25 with its first call to get_cols_rows()
210     2. take a screenshot when it calls draw_screen(..)
211     3. simulate 5 "down" keys from get_input()
212     4. take a screenshot when it calls draw_screen(..)
213     5. simulate keys "a", "b", "c" and a "window resize"
214     6. resize to 20x10 on its second call to get_cols_rows()
215     7. take a screenshot when it calls draw_screen(..)
216     8. simulate a "Q" keypress to quit the application
217
218    screenshot_init( [ (80,25), (20,10) ],
219        [ ["down"]*5, ["a","b","c","window resize"], ["Q"] ] )
220    """
221    try:
222        for (row,col) in sizes:
223            assert type(row) == int
224            assert row>0 and col>0
225    except (AssertionError, ValueError):
226        raise Exception("sizes must be in the form [ (col1,row1), (col2,row2), ...]")
227
228    try:
229        for l in keys:
230            assert type(l) == list
231            for k in l:
232                assert type(k) == str
233    except (AssertionError, ValueError):
234        raise Exception("keys must be in the form [ [keyA1, keyA2, ..], [keyB1, ..], ...]")
235
236    from . import curses_display
237    curses_display.Screen = HtmlGenerator
238    from . import raw_display
239    raw_display.Screen = HtmlGenerator
240
241    HtmlGenerator.sizes = sizes
242    HtmlGenerator.keys = keys
243
244
245def screenshot_collect():
246    """Return screenshots as a list of HTML fragments."""
247    l = HtmlGenerator.fragments
248    HtmlGenerator.fragments = []
249    return l
250
251
252