1from __future__ import unicode_literals 2 3from collections import defaultdict, namedtuple 4 5from prompt_toolkit.cache import FastDictCache 6from prompt_toolkit.utils import get_cwidth 7 8__all__ = [ 9 'Point', 10 'Size', 11 'Screen', 12 'Char', 13] 14 15 16Point = namedtuple('Point', 'x y') 17Size = namedtuple('Size', 'rows columns') 18 19 20class Char(object): 21 """ 22 Represent a single character in a :class:`.Screen`. 23 24 This should be considered immutable. 25 26 :param char: A single character (can be a double-width character). 27 :param style: A style string. (Can contain classnames.) 28 """ 29 __slots__ = ('char', 'style', 'width') 30 31 # If we end up having one of these special control sequences in the input string, 32 # we should display them as follows: 33 # Usually this happens after a "quoted insert". 34 display_mappings = { 35 '\x00': '^@', # Control space 36 '\x01': '^A', 37 '\x02': '^B', 38 '\x03': '^C', 39 '\x04': '^D', 40 '\x05': '^E', 41 '\x06': '^F', 42 '\x07': '^G', 43 '\x08': '^H', 44 '\x09': '^I', 45 '\x0a': '^J', 46 '\x0b': '^K', 47 '\x0c': '^L', 48 '\x0d': '^M', 49 '\x0e': '^N', 50 '\x0f': '^O', 51 '\x10': '^P', 52 '\x11': '^Q', 53 '\x12': '^R', 54 '\x13': '^S', 55 '\x14': '^T', 56 '\x15': '^U', 57 '\x16': '^V', 58 '\x17': '^W', 59 '\x18': '^X', 60 '\x19': '^Y', 61 '\x1a': '^Z', 62 '\x1b': '^[', # Escape 63 '\x1c': '^\\', 64 '\x1d': '^]', 65 '\x1f': '^_', 66 '\x7f': '^?', # ASCII Delete (backspace). 67 68 # Special characters. All visualized like Vim does. 69 '\x80': '<80>', 70 '\x81': '<81>', 71 '\x82': '<82>', 72 '\x83': '<83>', 73 '\x84': '<84>', 74 '\x85': '<85>', 75 '\x86': '<86>', 76 '\x87': '<87>', 77 '\x88': '<88>', 78 '\x89': '<89>', 79 '\x8a': '<8a>', 80 '\x8b': '<8b>', 81 '\x8c': '<8c>', 82 '\x8d': '<8d>', 83 '\x8e': '<8e>', 84 '\x8f': '<8f>', 85 86 '\x90': '<90>', 87 '\x91': '<91>', 88 '\x92': '<92>', 89 '\x93': '<93>', 90 '\x94': '<94>', 91 '\x95': '<95>', 92 '\x96': '<96>', 93 '\x97': '<97>', 94 '\x98': '<98>', 95 '\x99': '<99>', 96 '\x9a': '<9a>', 97 '\x9b': '<9b>', 98 '\x9c': '<9c>', 99 '\x9d': '<9d>', 100 '\x9e': '<9e>', 101 '\x9f': '<9f>', 102 103 # For the non-breaking space: visualize like Emacs does by default. 104 # (Print a space, but attach the 'nbsp' class that applies the 105 # underline style.) 106 '\xa0': ' ', 107 } 108 109 def __init__(self, char=' ', style=''): 110 # If this character has to be displayed otherwise, take that one. 111 if char in self.display_mappings: 112 if char == '\xa0': 113 style += ' class:nbsp ' # Will be underlined. 114 else: 115 style += ' class:control-character ' 116 117 char = self.display_mappings[char] 118 119 self.char = char 120 self.style = style 121 122 # Calculate width. (We always need this, so better to store it directly 123 # as a member for performance.) 124 self.width = get_cwidth(char) 125 126 def __eq__(self, other): 127 return self.char == other.char and self.style == other.style 128 129 def __ne__(self, other): 130 # Not equal: We don't do `not char.__eq__` here, because of the 131 # performance of calling yet another function. 132 return self.char != other.char or self.style != other.style 133 134 def __repr__(self): 135 return '%s(%r, %r)' % (self.__class__.__name__, self.char, self.style) 136 137 138_CHAR_CACHE = FastDictCache(Char, size=1000 * 1000) 139Transparent = '[transparent]' 140 141 142class Screen(object): 143 """ 144 Two dimensional buffer of :class:`.Char` instances. 145 """ 146 def __init__(self, default_char=None, initial_width=0, initial_height=0): 147 if default_char is None: 148 default_char = _CHAR_CACHE[' ', Transparent] 149 150 self.data_buffer = defaultdict(lambda: defaultdict(lambda: default_char)) 151 152 #: Escape sequences to be injected. 153 self.zero_width_escapes = defaultdict(lambda: defaultdict(lambda: '')) 154 155 #: Position of the cursor. 156 self.cursor_positions = {} # Map `Window` objects to `Point` objects. 157 158 #: Visibility of the cursor. 159 self.show_cursor = True 160 161 #: (Optional) Where to position the menu. E.g. at the start of a completion. 162 #: (We can't use the cursor position, because we don't want the 163 #: completion menu to change its position when we browse through all the 164 #: completions.) 165 self.menu_positions = {} # Map `Window` objects to `Point` objects. 166 167 #: Currently used width/height of the screen. This will increase when 168 #: data is written to the screen. 169 self.width = initial_width or 0 170 self.height = initial_height or 0 171 172 # Windows that have been drawn. (Each `Window` class will add itself to 173 # this list.) 174 self.visible_windows = [] 175 176 self._draw_float_functions = [] # List of (z_index, draw_func) 177 178 def set_cursor_position(self, window, position): 179 " Set the cursor position for a given window. " 180 self.cursor_positions[window] = position 181 182 def set_menu_position(self, window, position): 183 " Set the cursor position for a given window. " 184 self.menu_positions[window] = position 185 186 def get_cursor_position(self, window): 187 """ 188 Get the cursor position for a given window. 189 Returns a `Point`. 190 """ 191 try: 192 return self.cursor_positions[window] 193 except KeyError: 194 return Point(x=0, y=0) 195 196 def get_menu_position(self, window): 197 """ 198 Get the menu position for a given window. 199 (This falls back to the cursor position if no menu position was set.) 200 """ 201 try: 202 return self.menu_positions[window] 203 except KeyError: 204 try: 205 return self.cursor_positions[window] 206 except KeyError: 207 return Point(x=0, y=0) 208 209 def draw_with_z_index(self, z_index, draw_func): 210 """ 211 Add a draw-function for a `Window` which has a >= 0 z_index. 212 This will be postponed until `draw_all_floats` is called. 213 """ 214 assert isinstance(z_index, int), z_index 215 assert callable(draw_func) 216 217 self._draw_float_functions.append((z_index, draw_func)) 218 219 def draw_all_floats(self): 220 """ 221 Draw all float functions in order of z-index. 222 """ 223 # We keep looping because some draw functions could add new functions 224 # to this list. See `FloatContainer`. 225 while self._draw_float_functions: 226 # Sort the floats that we have so far by z_index. 227 functions = sorted(self._draw_float_functions, key=lambda item: item[0]) 228 229 # Draw only one at a time, then sort everything again. Now floats 230 # might have been added. 231 self._draw_float_functions = functions[1:] 232 functions[0][1]() 233 234 def append_style_to_content(self, style_str): 235 """ 236 For all the characters in the screen. 237 Set the style string to the given `style_str`. 238 """ 239 b = self.data_buffer 240 char_cache = _CHAR_CACHE 241 242 append_style = ' ' + style_str 243 244 for y, row in b.items(): 245 for x, char in row.items(): 246 b[y][x] = char_cache[char.char, char.style + append_style] 247 248 def fill_area(self, write_position, style='', after=False): 249 """ 250 Fill the content of this area, using the given `style`. 251 The style is prepended before whatever was here before. 252 """ 253 if not style.strip(): 254 return 255 256 xmin = write_position.xpos 257 xmax = write_position.xpos + write_position.width 258 char_cache = _CHAR_CACHE 259 data_buffer = self.data_buffer 260 261 if after: 262 append_style = ' ' + style 263 prepend_style = '' 264 else: 265 append_style = '' 266 prepend_style = style + ' ' 267 268 for y in range(write_position.ypos, write_position.ypos + write_position.height): 269 row = data_buffer[y] 270 for x in range(xmin, xmax): 271 cell = row[x] 272 row[x] = char_cache[cell.char, prepend_style + cell.style + append_style] 273 274 275class WritePosition(object): 276 def __init__(self, xpos, ypos, width, height): 277 assert height >= 0 278 assert width >= 0 279 # xpos and ypos can be negative. (A float can be partially visible.) 280 281 self.xpos = xpos 282 self.ypos = ypos 283 self.width = width 284 self.height = height 285 286 def __repr__(self): 287 return '%s(x=%r, y=%r, width=%r, height=%r)' % ( 288 self.__class__.__name__, 289 self.xpos, self.ypos, self.width, self.height) 290