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