1"""
2Renders the command line on the console.
3(Redraws parts of the input line that were changed.)
4"""
5from __future__ import unicode_literals
6
7import threading
8import time
9from collections import deque
10
11from six.moves import range
12
13from prompt_toolkit.eventloop import (
14    From,
15    Future,
16    ensure_future,
17    get_event_loop,
18)
19from prompt_toolkit.filters import to_filter
20from prompt_toolkit.formatted_text import to_formatted_text
21from prompt_toolkit.input.base import Input
22from prompt_toolkit.layout.mouse_handlers import MouseHandlers
23from prompt_toolkit.layout.screen import Point, Screen, WritePosition
24from prompt_toolkit.output import ColorDepth, Output
25from prompt_toolkit.styles import (
26    BaseStyle,
27    DummyStyleTransformation,
28    StyleTransformation,
29)
30from prompt_toolkit.utils import is_windows
31
32__all__ = [
33    'Renderer',
34    'print_formatted_text',
35]
36
37
38def _output_screen_diff(app, output, screen, current_pos, color_depth,
39                        previous_screen=None, last_style=None, is_done=False,
40                        full_screen=False, attrs_for_style_string=None,
41                        size=None, previous_width=0):  # XXX: drop is_done
42    """
43    Render the diff between this screen and the previous screen.
44
45    This takes two `Screen` instances. The one that represents the output like
46    it was during the last rendering and one that represents the current
47    output raster. Looking at these two `Screen` instances, this function will
48    render the difference by calling the appropriate methods of the `Output`
49    object that only paint the changes to the terminal.
50
51    This is some performance-critical code which is heavily optimized.
52    Don't change things without profiling first.
53
54    :param current_pos: Current cursor position.
55    :param last_style: The style string, used for drawing the last drawn
56        character.  (Color/attributes.)
57    :param attrs_for_style_string: :class:`._StyleStringToAttrsCache` instance.
58    :param width: The width of the terminal.
59    :param previous_width: The width of the terminal during the last rendering.
60    """
61    width, height = size.columns, size.rows
62
63    #: Remember the last printed character.
64    last_style = [last_style]  # nonlocal
65
66    #: Variable for capturing the output.
67    write = output.write
68    write_raw = output.write_raw
69
70    # Create locals for the most used output methods.
71    # (Save expensive attribute lookups.)
72    _output_set_attributes = output.set_attributes
73    _output_reset_attributes = output.reset_attributes
74    _output_cursor_forward = output.cursor_forward
75    _output_cursor_up = output.cursor_up
76    _output_cursor_backward = output.cursor_backward
77
78    # Hide cursor before rendering. (Avoid flickering.)
79    output.hide_cursor()
80
81    def reset_attributes():
82        " Wrapper around Output.reset_attributes. "
83        _output_reset_attributes()
84        last_style[0] = None  # Forget last char after resetting attributes.
85
86    def move_cursor(new):
87        " Move cursor to this `new` point. Returns the given Point. "
88        current_x, current_y = current_pos.x, current_pos.y
89
90        if new.y > current_y:
91            # Use newlines instead of CURSOR_DOWN, because this might add new lines.
92            # CURSOR_DOWN will never create new lines at the bottom.
93            # Also reset attributes, otherwise the newline could draw a
94            # background color.
95            reset_attributes()
96            write('\r\n' * (new.y - current_y))
97            current_x = 0
98            _output_cursor_forward(new.x)
99            return new
100        elif new.y < current_y:
101            _output_cursor_up(current_y - new.y)
102
103        if current_x >= width - 1:
104            write('\r')
105            _output_cursor_forward(new.x)
106        elif new.x < current_x or current_x >= width - 1:
107            _output_cursor_backward(current_x - new.x)
108        elif new.x > current_x:
109            _output_cursor_forward(new.x - current_x)
110
111        return new
112
113    def output_char(char):
114        """
115        Write the output of this character.
116        """
117        # If the last printed character has the same style, don't output the
118        # style again.
119        the_last_style = last_style[0]  # Either `None` or a style string.
120
121        if the_last_style == char.style:
122            write(char.char)
123        else:
124            # Look up `Attr` for this style string. Only set attributes if different.
125            # (Two style strings can still have the same formatting.)
126            # Note that an empty style string can have formatting that needs to
127            # be applied, because of style transformations.
128            new_attrs = attrs_for_style_string[char.style]
129            if not the_last_style or new_attrs != attrs_for_style_string[the_last_style]:
130                _output_set_attributes(new_attrs, color_depth)
131
132            write(char.char)
133            last_style[0] = char.style
134
135    # Render for the first time: reset styling.
136    if not previous_screen:
137        reset_attributes()
138
139    # Disable autowrap. (When entering a the alternate screen, or anytime when
140    # we have a prompt. - In the case of a REPL, like IPython, people can have
141    # background threads, and it's hard for debugging if their output is not
142    # wrapped.)
143    if not previous_screen or not full_screen:
144        output.disable_autowrap()
145
146    # When the previous screen has a different size, redraw everything anyway.
147    # Also when we are done. (We might take up less rows, so clearing is important.)
148    if is_done or not previous_screen or previous_width != width:  # XXX: also consider height??
149        current_pos = move_cursor(Point(x=0, y=0))
150        reset_attributes()
151        output.erase_down()
152
153        previous_screen = Screen()
154
155    # Get height of the screen.
156    # (height changes as we loop over data_buffer, so remember the current value.)
157    # (Also make sure to clip the height to the size of the output.)
158    current_height = min(screen.height, height)
159
160    # Loop over the rows.
161    row_count = min(max(screen.height, previous_screen.height), height)
162    c = 0  # Column counter.
163
164    for y in range(row_count):
165        new_row = screen.data_buffer[y]
166        previous_row = previous_screen.data_buffer[y]
167        zero_width_escapes_row = screen.zero_width_escapes[y]
168
169        new_max_line_len = min(width - 1, max(new_row.keys()) if new_row else 0)
170        previous_max_line_len = min(width - 1, max(previous_row.keys()) if previous_row else 0)
171
172        # Loop over the columns.
173        c = 0
174        while c < new_max_line_len + 1:
175            new_char = new_row[c]
176            old_char = previous_row[c]
177            char_width = (new_char.width or 1)
178
179            # When the old and new character at this position are different,
180            # draw the output. (Because of the performance, we don't call
181            # `Char.__ne__`, but inline the same expression.)
182            if new_char.char != old_char.char or new_char.style != old_char.style:
183                current_pos = move_cursor(Point(x=c, y=y))
184
185                # Send injected escape sequences to output.
186                if c in zero_width_escapes_row:
187                    write_raw(zero_width_escapes_row[c])
188
189                output_char(new_char)
190                current_pos = Point(x=current_pos.x + char_width, y=current_pos.y)
191
192            c += char_width
193
194        # If the new line is shorter, trim it.
195        if previous_screen and new_max_line_len < previous_max_line_len:
196            current_pos = move_cursor(Point(x=new_max_line_len + 1, y=y))
197            reset_attributes()
198            output.erase_end_of_line()
199
200    # Correctly reserve vertical space as required by the layout.
201    # When this is a new screen (drawn for the first time), or for some reason
202    # higher than the previous one. Move the cursor once to the bottom of the
203    # output. That way, we're sure that the terminal scrolls up, even when the
204    # lower lines of the canvas just contain whitespace.
205
206    # The most obvious reason that we actually want this behaviour is the avoid
207    # the artifact of the input scrolling when the completion menu is shown.
208    # (If the scrolling is actually wanted, the layout can still be build in a
209    # way to behave that way by setting a dynamic height.)
210    if current_height > previous_screen.height:
211        current_pos = move_cursor(Point(x=0, y=current_height - 1))
212
213    # Move cursor:
214    if is_done:
215        current_pos = move_cursor(Point(x=0, y=current_height))
216        output.erase_down()
217    else:
218        current_pos = move_cursor(
219            screen.get_cursor_position(app.layout.current_window))
220
221    if is_done or not full_screen:
222        output.enable_autowrap()
223
224    # Always reset the color attributes. This is important because a background
225    # thread could print data to stdout and we want that to be displayed in the
226    # default colors. (Also, if a background color has been set, many terminals
227    # give weird artifacts on resize events.)
228    reset_attributes()
229
230    if screen.show_cursor or is_done:
231        output.show_cursor()
232
233    return current_pos, last_style[0]
234
235
236class HeightIsUnknownError(Exception):
237    " Information unavailable. Did not yet receive the CPR response. "
238
239
240class _StyleStringToAttrsCache(dict):
241    """
242    A cache structure that maps style strings to :class:`.Attr`.
243    (This is an important speed up.)
244    """
245    def __init__(self, get_attrs_for_style_str, style_transformation):
246        assert callable(get_attrs_for_style_str)
247        assert isinstance(style_transformation, StyleTransformation)
248
249        self.get_attrs_for_style_str = get_attrs_for_style_str
250        self.style_transformation = style_transformation
251
252    def __missing__(self, style_str):
253        attrs = self.get_attrs_for_style_str(style_str)
254        attrs = self.style_transformation.transform_attrs(attrs)
255
256        self[style_str] = attrs
257        return attrs
258
259
260class CPR_Support(object):
261    " Enum: whether or not CPR is supported. "
262    SUPPORTED = 'SUPPORTED'
263    NOT_SUPPORTED = 'NOT_SUPPORTED'
264    UNKNOWN = 'UNKNOWN'
265
266
267class Renderer(object):
268    """
269    Typical usage:
270
271    ::
272
273        output = Vt100_Output.from_pty(sys.stdout)
274        r = Renderer(style, output)
275        r.render(app, layout=...)
276    """
277    CPR_TIMEOUT = 2  # Time to wait until we consider CPR to be not supported.
278
279    def __init__(self, style, output, input, full_screen=False,
280                 mouse_support=False, cpr_not_supported_callback=None):
281
282        assert isinstance(style, BaseStyle)
283        assert isinstance(output, Output)
284        assert isinstance(input, Input)
285        assert callable(cpr_not_supported_callback) or cpr_not_supported_callback is None
286
287        self.style = style
288        self.output = output
289        self.input = input
290        self.full_screen = full_screen
291        self.mouse_support = to_filter(mouse_support)
292        self.cpr_not_supported_callback = cpr_not_supported_callback
293
294        self._in_alternate_screen = False
295        self._mouse_support_enabled = False
296        self._bracketed_paste_enabled = False
297
298        # Future set when we are waiting for a CPR flag.
299        self._waiting_for_cpr_futures = deque()
300        self.cpr_support = CPR_Support.UNKNOWN
301        if not input.responds_to_cpr:
302            self.cpr_support = CPR_Support.NOT_SUPPORTED
303
304        # Cache for the style.
305        self._attrs_for_style = None
306        self._last_style_hash = None
307        self._last_transformation_hash = None
308        self._last_color_depth = None
309
310        self.reset(_scroll=True)
311
312    def reset(self, _scroll=False, leave_alternate_screen=True):
313        # Reset position
314        self._cursor_pos = Point(x=0, y=0)
315
316        # Remember the last screen instance between renderers. This way,
317        # we can create a `diff` between two screens and only output the
318        # difference. It's also to remember the last height. (To show for
319        # instance a toolbar at the bottom position.)
320        self._last_screen = None
321        self._last_size = None
322        self._last_style = None
323
324        # Default MouseHandlers. (Just empty.)
325        self.mouse_handlers = MouseHandlers()
326
327        #: Space from the top of the layout, until the bottom of the terminal.
328        #: We don't know this until a `report_absolute_cursor_row` call.
329        self._min_available_height = 0
330
331        # In case of Windows, also make sure to scroll to the current cursor
332        # position. (Only when rendering the first time.)
333        if is_windows() and _scroll:
334            self.output.scroll_buffer_to_prompt()
335
336        # Quit alternate screen.
337        if self._in_alternate_screen and leave_alternate_screen:
338            self.output.quit_alternate_screen()
339            self._in_alternate_screen = False
340
341        # Disable mouse support.
342        if self._mouse_support_enabled:
343            self.output.disable_mouse_support()
344            self._mouse_support_enabled = False
345
346        # Disable bracketed paste.
347        if self._bracketed_paste_enabled:
348            self.output.disable_bracketed_paste()
349            self._bracketed_paste_enabled = False
350
351        # Flush output. `disable_mouse_support` needs to write to stdout.
352        self.output.flush()
353
354    @property
355    def last_rendered_screen(self):
356        """
357        The `Screen` class that was generated during the last rendering.
358        This can be `None`.
359        """
360        return self._last_screen
361
362    @property
363    def height_is_known(self):
364        """
365        True when the height from the cursor until the bottom of the terminal
366        is known. (It's often nicer to draw bottom toolbars only if the height
367        is known, in order to avoid flickering when the CPR response arrives.)
368        """
369        return self.full_screen or self._min_available_height > 0 or \
370            is_windows()  # On Windows, we don't have to wait for a CPR.
371
372    @property
373    def rows_above_layout(self):
374        """
375        Return the number of rows visible in the terminal above the layout.
376        """
377        if self._in_alternate_screen:
378            return 0
379        elif self._min_available_height > 0:
380            total_rows = self.output.get_size().rows
381            last_screen_height = self._last_screen.height if self._last_screen else 0
382            return total_rows - max(self._min_available_height, last_screen_height)
383        else:
384            raise HeightIsUnknownError('Rows above layout is unknown.')
385
386    def request_absolute_cursor_position(self):
387        """
388        Get current cursor position.
389
390        We do this to calculate the minimum available height that we can
391        consume for rendering the prompt. This is the available space below te
392        cursor.
393
394        For vt100: Do CPR request. (answer will arrive later.)
395        For win32: Do API call. (Answer comes immediately.)
396        """
397        # Only do this request when the cursor is at the top row. (after a
398        # clear or reset). We will rely on that in `report_absolute_cursor_row`.
399        assert self._cursor_pos.y == 0
400
401        # In full-screen mode, always use the total height as min-available-height.
402        if self.full_screen:
403            self._min_available_height = self.output.get_size().rows
404
405        # For Win32, we have an API call to get the number of rows below the
406        # cursor.
407        elif is_windows():
408            self._min_available_height = self.output.get_rows_below_cursor_position()
409
410        # Use CPR.
411        else:
412            if self.cpr_support == CPR_Support.NOT_SUPPORTED:
413                return
414
415            def do_cpr():
416                # Asks for a cursor position report (CPR).
417                self._waiting_for_cpr_futures.append(Future())
418                self.output.ask_for_cpr()
419
420            if self.cpr_support == CPR_Support.SUPPORTED:
421                do_cpr()
422
423            # If we don't know whether CPR is supported, only do a request if
424            # none is pending, and test it, using a timer.
425            elif self.cpr_support == CPR_Support.UNKNOWN and not self.waiting_for_cpr:
426                do_cpr()
427
428                def timer():
429                    time.sleep(self.CPR_TIMEOUT)
430
431                    # Not set in the meantime -> not supported.
432                    if self.cpr_support == CPR_Support.UNKNOWN:
433                        self.cpr_support = CPR_Support.NOT_SUPPORTED
434
435                        if self.cpr_not_supported_callback:
436                            # Make sure to call this callback in the main thread.
437                            get_event_loop().call_from_executor(self.cpr_not_supported_callback)
438
439                t = threading.Thread(target=timer)
440                t.daemon = True
441                t.start()
442
443    def report_absolute_cursor_row(self, row):
444        """
445        To be called when we know the absolute cursor position.
446        (As an answer of a "Cursor Position Request" response.)
447        """
448        self.cpr_support = CPR_Support.SUPPORTED
449
450        # Calculate the amount of rows from the cursor position until the
451        # bottom of the terminal.
452        total_rows = self.output.get_size().rows
453        rows_below_cursor = total_rows - row + 1
454
455        # Set the minimum available height.
456        self._min_available_height = rows_below_cursor
457
458        # Pop and set waiting for CPR future.
459        try:
460            f = self._waiting_for_cpr_futures.popleft()
461        except IndexError:
462            pass  # Received CPR response without having a CPR.
463        else:
464            f.set_result(None)
465
466    @property
467    def waiting_for_cpr(self):
468        """
469        Waiting for CPR flag. True when we send the request, but didn't got a
470        response.
471        """
472        return bool(self._waiting_for_cpr_futures)
473
474    def wait_for_cpr_responses(self, timeout=1):
475        """
476        Wait for a CPR response.
477        """
478        cpr_futures = list(self._waiting_for_cpr_futures)  # Make copy.
479
480        # When there are no CPRs in the queue. Don't do anything.
481        if not cpr_futures or self.cpr_support == CPR_Support.NOT_SUPPORTED:
482            return Future.succeed(None)
483
484        f = Future()
485
486        # When a CPR has been received, set the result.
487        def wait_for_responses():
488            for response_f in cpr_futures:
489                yield From(response_f)
490            if not f.done():
491                f.set_result(None)
492        ensure_future(wait_for_responses())
493
494        # Timeout.
495        def wait_for_timeout():
496            time.sleep(timeout)
497
498            # Got timeout.
499            if not f.done():
500                self._waiting_for_cpr_futures = deque()
501                f.set_result(None)
502
503        t = threading.Thread(target=wait_for_timeout)
504        t.daemon = True
505        t.start()
506
507        return f
508
509    def render(self, app, layout, is_done=False):
510        """
511        Render the current interface to the output.
512
513        :param is_done: When True, put the cursor at the end of the interface. We
514                won't print any changes to this part.
515        """
516        output = self.output
517
518        # Enter alternate screen.
519        if self.full_screen and not self._in_alternate_screen:
520            self._in_alternate_screen = True
521            output.enter_alternate_screen()
522
523        # Enable bracketed paste.
524        if not self._bracketed_paste_enabled:
525            self.output.enable_bracketed_paste()
526            self._bracketed_paste_enabled = True
527
528        # Enable/disable mouse support.
529        needs_mouse_support = self.mouse_support()
530
531        if needs_mouse_support and not self._mouse_support_enabled:
532            output.enable_mouse_support()
533            self._mouse_support_enabled = True
534
535        elif not needs_mouse_support and self._mouse_support_enabled:
536            output.disable_mouse_support()
537            self._mouse_support_enabled = False
538
539        # Create screen and write layout to it.
540        size = output.get_size()
541        screen = Screen()
542        screen.show_cursor = False  # Hide cursor by default, unless one of the
543                                    # containers decides to display it.
544        mouse_handlers = MouseHandlers()
545
546        # Calculate height.
547        if self.full_screen:
548            height = size.rows
549        elif is_done:
550            # When we are done, we don't necessary want to fill up until the bottom.
551            height = layout.container.preferred_height(size.columns, size.rows).preferred
552        else:
553            last_height = self._last_screen.height if self._last_screen else 0
554            height = max(self._min_available_height,
555                         last_height,
556                         layout.container.preferred_height(size.columns, size.rows).preferred)
557
558        height = min(height, size.rows)
559
560        # When te size changes, don't consider the previous screen.
561        if self._last_size != size:
562            self._last_screen = None
563
564        # When we render using another style or another color depth, do a full
565        # repaint. (Forget about the previous rendered screen.)
566        # (But note that we still use _last_screen to calculate the height.)
567        if (self.style.invalidation_hash() != self._last_style_hash or
568                app.style_transformation.invalidation_hash() != self._last_transformation_hash or
569                app.color_depth != self._last_color_depth):
570            self._last_screen = None
571            self._attrs_for_style = None
572
573        if self._attrs_for_style is None:
574            self._attrs_for_style = _StyleStringToAttrsCache(
575                self.style.get_attrs_for_style_str,
576                app.style_transformation)
577
578        self._last_style_hash = self.style.invalidation_hash()
579        self._last_transformation_hash = app.style_transformation.invalidation_hash()
580        self._last_color_depth = app.color_depth
581
582        layout.container.write_to_screen(screen, mouse_handlers, WritePosition(
583            xpos=0,
584            ypos=0,
585            width=size.columns,
586            height=height,
587        ), parent_style='', erase_bg=False, z_index=None)
588        screen.draw_all_floats()
589
590        # When grayed. Replace all styles in the new screen.
591        if app.exit_style:
592            screen.append_style_to_content(app.exit_style)
593
594        # Process diff and write to output.
595        self._cursor_pos, self._last_style = _output_screen_diff(
596            app, output, screen, self._cursor_pos, app.color_depth,
597            self._last_screen, self._last_style, is_done,
598            full_screen=self.full_screen,
599            attrs_for_style_string=self._attrs_for_style, size=size,
600            previous_width=(self._last_size.columns if self._last_size else 0))
601        self._last_screen = screen
602        self._last_size = size
603        self.mouse_handlers = mouse_handlers
604
605        output.flush()
606
607        # Set visible windows in layout.
608        app.layout.visible_windows = screen.visible_windows
609
610        if is_done:
611            self.reset()
612
613    def erase(self, leave_alternate_screen=True):
614        """
615        Hide all output and put the cursor back at the first line. This is for
616        instance used for running a system command (while hiding the CLI) and
617        later resuming the same CLI.)
618
619        :param leave_alternate_screen: When True, and when inside an alternate
620            screen buffer, quit the alternate screen.
621        """
622        output = self.output
623
624        output.cursor_backward(self._cursor_pos.x)
625        output.cursor_up(self._cursor_pos.y)
626        output.erase_down()
627        output.reset_attributes()
628        output.enable_autowrap()
629        output.flush()
630
631        self.reset(leave_alternate_screen=leave_alternate_screen)
632
633    def clear(self):
634        """
635        Clear screen and go to 0,0
636        """
637        # Erase current output first.
638        self.erase()
639
640        # Send "Erase Screen" command and go to (0, 0).
641        output = self.output
642
643        output.erase_screen()
644        output.cursor_goto(0, 0)
645        output.flush()
646
647        self.request_absolute_cursor_position()
648
649
650def print_formatted_text(
651        output, formatted_text, style, style_transformation=None,
652        color_depth=None):
653    """
654    Print a list of (style_str, text) tuples in the given style to the output.
655    """
656    assert isinstance(output, Output)
657    assert isinstance(style, BaseStyle)
658    assert style_transformation is None or isinstance(style_transformation, StyleTransformation)
659    assert color_depth is None or color_depth in ColorDepth._ALL
660
661    fragments = to_formatted_text(formatted_text)
662    style_transformation = style_transformation or DummyStyleTransformation()
663    color_depth = color_depth or ColorDepth.default()
664
665    # Reset first.
666    output.reset_attributes()
667    output.enable_autowrap()
668
669    # Print all (style_str, text) tuples.
670    attrs_for_style_string = _StyleStringToAttrsCache(
671        style.get_attrs_for_style_str,
672        style_transformation)
673
674    for style_str, text in fragments:
675        attrs = attrs_for_style_string[style_str]
676
677        if attrs:
678            output.set_attributes(attrs, color_depth)
679        else:
680            output.reset_attributes()
681
682        # Eliminate carriage returns
683        text = text.replace('\r', '')
684
685        # Assume that the output is raw, and insert a carriage return before
686        # every newline. (Also important when the front-end is a telnet client.)
687        output.write(text.replace('\n', '\r\n'))
688
689    # Reset again.
690    output.reset_attributes()
691    output.flush()
692