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('&','&') 188 text = text.replace('<','<') 189 text = text.replace('>','>') 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