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