123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814 |
- """
- Renders the command line on the console.
- (Redraws parts of the input line that were changed.)
- """
- from __future__ import annotations
- from asyncio import FIRST_COMPLETED, Future, ensure_future, sleep, wait
- from collections import deque
- from enum import Enum
- from typing import TYPE_CHECKING, Any, Callable, Dict, Hashable
- from prompt_toolkit.application.current import get_app
- from prompt_toolkit.cursor_shapes import CursorShape
- from prompt_toolkit.data_structures import Point, Size
- from prompt_toolkit.filters import FilterOrBool, to_filter
- from prompt_toolkit.formatted_text import AnyFormattedText, to_formatted_text
- from prompt_toolkit.layout.mouse_handlers import MouseHandlers
- from prompt_toolkit.layout.screen import Char, Screen, WritePosition
- from prompt_toolkit.output import ColorDepth, Output
- from prompt_toolkit.styles import (
- Attrs,
- BaseStyle,
- DummyStyleTransformation,
- StyleTransformation,
- )
- if TYPE_CHECKING:
- from prompt_toolkit.application import Application
- from prompt_toolkit.layout.layout import Layout
- __all__ = [
- "Renderer",
- "print_formatted_text",
- ]
- def _output_screen_diff(
- app: Application[Any],
- output: Output,
- screen: Screen,
- current_pos: Point,
- color_depth: ColorDepth,
- previous_screen: Screen | None,
- last_style: str | None,
- is_done: bool, # XXX: drop is_done
- full_screen: bool,
- attrs_for_style_string: _StyleStringToAttrsCache,
- style_string_has_style: _StyleStringHasStyleCache,
- size: Size,
- previous_width: int,
- ) -> tuple[Point, str | None]:
- """
- Render the diff between this screen and the previous screen.
- This takes two `Screen` instances. The one that represents the output like
- it was during the last rendering and one that represents the current
- output raster. Looking at these two `Screen` instances, this function will
- render the difference by calling the appropriate methods of the `Output`
- object that only paint the changes to the terminal.
- This is some performance-critical code which is heavily optimized.
- Don't change things without profiling first.
- :param current_pos: Current cursor position.
- :param last_style: The style string, used for drawing the last drawn
- character. (Color/attributes.)
- :param attrs_for_style_string: :class:`._StyleStringToAttrsCache` instance.
- :param width: The width of the terminal.
- :param previous_width: The width of the terminal during the last rendering.
- """
- width, height = size.columns, size.rows
- #: Variable for capturing the output.
- write = output.write
- write_raw = output.write_raw
- # Create locals for the most used output methods.
- # (Save expensive attribute lookups.)
- _output_set_attributes = output.set_attributes
- _output_reset_attributes = output.reset_attributes
- _output_cursor_forward = output.cursor_forward
- _output_cursor_up = output.cursor_up
- _output_cursor_backward = output.cursor_backward
- # Hide cursor before rendering. (Avoid flickering.)
- output.hide_cursor()
- def reset_attributes() -> None:
- "Wrapper around Output.reset_attributes."
- nonlocal last_style
- _output_reset_attributes()
- last_style = None # Forget last char after resetting attributes.
- def move_cursor(new: Point) -> Point:
- "Move cursor to this `new` point. Returns the given Point."
- current_x, current_y = current_pos.x, current_pos.y
- if new.y > current_y:
- # Use newlines instead of CURSOR_DOWN, because this might add new lines.
- # CURSOR_DOWN will never create new lines at the bottom.
- # Also reset attributes, otherwise the newline could draw a
- # background color.
- reset_attributes()
- write("\r\n" * (new.y - current_y))
- current_x = 0
- _output_cursor_forward(new.x)
- return new
- elif new.y < current_y:
- _output_cursor_up(current_y - new.y)
- if current_x >= width - 1:
- write("\r")
- _output_cursor_forward(new.x)
- elif new.x < current_x or current_x >= width - 1:
- _output_cursor_backward(current_x - new.x)
- elif new.x > current_x:
- _output_cursor_forward(new.x - current_x)
- return new
- def output_char(char: Char) -> None:
- """
- Write the output of this character.
- """
- nonlocal last_style
- # If the last printed character has the same style, don't output the
- # style again.
- if last_style == char.style:
- write(char.char)
- else:
- # Look up `Attr` for this style string. Only set attributes if different.
- # (Two style strings can still have the same formatting.)
- # Note that an empty style string can have formatting that needs to
- # be applied, because of style transformations.
- new_attrs = attrs_for_style_string[char.style]
- if not last_style or new_attrs != attrs_for_style_string[last_style]:
- _output_set_attributes(new_attrs, color_depth)
- write(char.char)
- last_style = char.style
- def get_max_column_index(row: dict[int, Char]) -> int:
- """
- Return max used column index, ignoring whitespace (without style) at
- the end of the line. This is important for people that copy/paste
- terminal output.
- There are two reasons we are sometimes seeing whitespace at the end:
- - `BufferControl` adds a trailing space to each line, because it's a
- possible cursor position, so that the line wrapping won't change if
- the cursor position moves around.
- - The `Window` adds a style class to the current line for highlighting
- (cursor-line).
- """
- numbers = (
- index
- for index, cell in row.items()
- if cell.char != " " or style_string_has_style[cell.style]
- )
- return max(numbers, default=0)
- # Render for the first time: reset styling.
- if not previous_screen:
- reset_attributes()
- # Disable autowrap. (When entering a the alternate screen, or anytime when
- # we have a prompt. - In the case of a REPL, like IPython, people can have
- # background threads, and it's hard for debugging if their output is not
- # wrapped.)
- if not previous_screen or not full_screen:
- output.disable_autowrap()
- # When the previous screen has a different size, redraw everything anyway.
- # Also when we are done. (We might take up less rows, so clearing is important.)
- if (
- is_done or not previous_screen or previous_width != width
- ): # XXX: also consider height??
- current_pos = move_cursor(Point(x=0, y=0))
- reset_attributes()
- output.erase_down()
- previous_screen = Screen()
- # Get height of the screen.
- # (height changes as we loop over data_buffer, so remember the current value.)
- # (Also make sure to clip the height to the size of the output.)
- current_height = min(screen.height, height)
- # Loop over the rows.
- row_count = min(max(screen.height, previous_screen.height), height)
- for y in range(row_count):
- new_row = screen.data_buffer[y]
- previous_row = previous_screen.data_buffer[y]
- zero_width_escapes_row = screen.zero_width_escapes[y]
- new_max_line_len = min(width - 1, get_max_column_index(new_row))
- previous_max_line_len = min(width - 1, get_max_column_index(previous_row))
- # Loop over the columns.
- c = 0 # Column counter.
- while c <= new_max_line_len:
- new_char = new_row[c]
- old_char = previous_row[c]
- char_width = new_char.width or 1
- # When the old and new character at this position are different,
- # draw the output. (Because of the performance, we don't call
- # `Char.__ne__`, but inline the same expression.)
- if new_char.char != old_char.char or new_char.style != old_char.style:
- current_pos = move_cursor(Point(x=c, y=y))
- # Send injected escape sequences to output.
- if c in zero_width_escapes_row:
- write_raw(zero_width_escapes_row[c])
- output_char(new_char)
- current_pos = Point(x=current_pos.x + char_width, y=current_pos.y)
- c += char_width
- # If the new line is shorter, trim it.
- if previous_screen and new_max_line_len < previous_max_line_len:
- current_pos = move_cursor(Point(x=new_max_line_len + 1, y=y))
- reset_attributes()
- output.erase_end_of_line()
- # Correctly reserve vertical space as required by the layout.
- # When this is a new screen (drawn for the first time), or for some reason
- # higher than the previous one. Move the cursor once to the bottom of the
- # output. That way, we're sure that the terminal scrolls up, even when the
- # lower lines of the canvas just contain whitespace.
- # The most obvious reason that we actually want this behavior is the avoid
- # the artifact of the input scrolling when the completion menu is shown.
- # (If the scrolling is actually wanted, the layout can still be build in a
- # way to behave that way by setting a dynamic height.)
- if current_height > previous_screen.height:
- current_pos = move_cursor(Point(x=0, y=current_height - 1))
- # Move cursor:
- if is_done:
- current_pos = move_cursor(Point(x=0, y=current_height))
- output.erase_down()
- else:
- current_pos = move_cursor(screen.get_cursor_position(app.layout.current_window))
- if is_done or not full_screen:
- output.enable_autowrap()
- # Always reset the color attributes. This is important because a background
- # thread could print data to stdout and we want that to be displayed in the
- # default colors. (Also, if a background color has been set, many terminals
- # give weird artifacts on resize events.)
- reset_attributes()
- if screen.show_cursor or is_done:
- output.show_cursor()
- return current_pos, last_style
- class HeightIsUnknownError(Exception):
- "Information unavailable. Did not yet receive the CPR response."
- class _StyleStringToAttrsCache(Dict[str, Attrs]):
- """
- A cache structure that maps style strings to :class:`.Attr`.
- (This is an important speed up.)
- """
- def __init__(
- self,
- get_attrs_for_style_str: Callable[[str], Attrs],
- style_transformation: StyleTransformation,
- ) -> None:
- self.get_attrs_for_style_str = get_attrs_for_style_str
- self.style_transformation = style_transformation
- def __missing__(self, style_str: str) -> Attrs:
- attrs = self.get_attrs_for_style_str(style_str)
- attrs = self.style_transformation.transform_attrs(attrs)
- self[style_str] = attrs
- return attrs
- class _StyleStringHasStyleCache(Dict[str, bool]):
- """
- Cache for remember which style strings don't render the default output
- style (default fg/bg, no underline and no reverse and no blink). That way
- we know that we should render these cells, even when they're empty (when
- they contain a space).
- Note: we don't consider bold/italic/hidden because they don't change the
- output if there's no text in the cell.
- """
- def __init__(self, style_string_to_attrs: dict[str, Attrs]) -> None:
- self.style_string_to_attrs = style_string_to_attrs
- def __missing__(self, style_str: str) -> bool:
- attrs = self.style_string_to_attrs[style_str]
- is_default = bool(
- attrs.color
- or attrs.bgcolor
- or attrs.underline
- or attrs.strike
- or attrs.blink
- or attrs.reverse
- )
- self[style_str] = is_default
- return is_default
- class CPR_Support(Enum):
- "Enum: whether or not CPR is supported."
- SUPPORTED = "SUPPORTED"
- NOT_SUPPORTED = "NOT_SUPPORTED"
- UNKNOWN = "UNKNOWN"
- class Renderer:
- """
- Typical usage:
- ::
- output = Vt100_Output.from_pty(sys.stdout)
- r = Renderer(style, output)
- r.render(app, layout=...)
- """
- CPR_TIMEOUT = 2 # Time to wait until we consider CPR to be not supported.
- def __init__(
- self,
- style: BaseStyle,
- output: Output,
- full_screen: bool = False,
- mouse_support: FilterOrBool = False,
- cpr_not_supported_callback: Callable[[], None] | None = None,
- ) -> None:
- self.style = style
- self.output = output
- self.full_screen = full_screen
- self.mouse_support = to_filter(mouse_support)
- self.cpr_not_supported_callback = cpr_not_supported_callback
- self._in_alternate_screen = False
- self._mouse_support_enabled = False
- self._bracketed_paste_enabled = False
- self._cursor_key_mode_reset = False
- # Future set when we are waiting for a CPR flag.
- self._waiting_for_cpr_futures: deque[Future[None]] = deque()
- self.cpr_support = CPR_Support.UNKNOWN
- if not output.responds_to_cpr:
- self.cpr_support = CPR_Support.NOT_SUPPORTED
- # Cache for the style.
- self._attrs_for_style: _StyleStringToAttrsCache | None = None
- self._style_string_has_style: _StyleStringHasStyleCache | None = None
- self._last_style_hash: Hashable | None = None
- self._last_transformation_hash: Hashable | None = None
- self._last_color_depth: ColorDepth | None = None
- self.reset(_scroll=True)
- def reset(self, _scroll: bool = False, leave_alternate_screen: bool = True) -> None:
- # Reset position
- self._cursor_pos = Point(x=0, y=0)
- # Remember the last screen instance between renderers. This way,
- # we can create a `diff` between two screens and only output the
- # difference. It's also to remember the last height. (To show for
- # instance a toolbar at the bottom position.)
- self._last_screen: Screen | None = None
- self._last_size: Size | None = None
- self._last_style: str | None = None
- self._last_cursor_shape: CursorShape | None = None
- # Default MouseHandlers. (Just empty.)
- self.mouse_handlers = MouseHandlers()
- #: Space from the top of the layout, until the bottom of the terminal.
- #: We don't know this until a `report_absolute_cursor_row` call.
- self._min_available_height = 0
- # In case of Windows, also make sure to scroll to the current cursor
- # position. (Only when rendering the first time.)
- # It does nothing for vt100 terminals.
- if _scroll:
- self.output.scroll_buffer_to_prompt()
- # Quit alternate screen.
- if self._in_alternate_screen and leave_alternate_screen:
- self.output.quit_alternate_screen()
- self._in_alternate_screen = False
- # Disable mouse support.
- if self._mouse_support_enabled:
- self.output.disable_mouse_support()
- self._mouse_support_enabled = False
- # Disable bracketed paste.
- if self._bracketed_paste_enabled:
- self.output.disable_bracketed_paste()
- self._bracketed_paste_enabled = False
- self.output.reset_cursor_shape()
- # NOTE: No need to set/reset cursor key mode here.
- # Flush output. `disable_mouse_support` needs to write to stdout.
- self.output.flush()
- @property
- def last_rendered_screen(self) -> Screen | None:
- """
- The `Screen` class that was generated during the last rendering.
- This can be `None`.
- """
- return self._last_screen
- @property
- def height_is_known(self) -> bool:
- """
- True when the height from the cursor until the bottom of the terminal
- is known. (It's often nicer to draw bottom toolbars only if the height
- is known, in order to avoid flickering when the CPR response arrives.)
- """
- if self.full_screen or self._min_available_height > 0:
- return True
- try:
- self._min_available_height = self.output.get_rows_below_cursor_position()
- return True
- except NotImplementedError:
- return False
- @property
- def rows_above_layout(self) -> int:
- """
- Return the number of rows visible in the terminal above the layout.
- """
- if self._in_alternate_screen:
- return 0
- elif self._min_available_height > 0:
- total_rows = self.output.get_size().rows
- last_screen_height = self._last_screen.height if self._last_screen else 0
- return total_rows - max(self._min_available_height, last_screen_height)
- else:
- raise HeightIsUnknownError("Rows above layout is unknown.")
- def request_absolute_cursor_position(self) -> None:
- """
- Get current cursor position.
- We do this to calculate the minimum available height that we can
- consume for rendering the prompt. This is the available space below te
- cursor.
- For vt100: Do CPR request. (answer will arrive later.)
- For win32: Do API call. (Answer comes immediately.)
- """
- # Only do this request when the cursor is at the top row. (after a
- # clear or reset). We will rely on that in `report_absolute_cursor_row`.
- assert self._cursor_pos.y == 0
- # In full-screen mode, always use the total height as min-available-height.
- if self.full_screen:
- self._min_available_height = self.output.get_size().rows
- return
- # For Win32, we have an API call to get the number of rows below the
- # cursor.
- try:
- self._min_available_height = self.output.get_rows_below_cursor_position()
- return
- except NotImplementedError:
- pass
- # Use CPR.
- if self.cpr_support == CPR_Support.NOT_SUPPORTED:
- return
- def do_cpr() -> None:
- # Asks for a cursor position report (CPR).
- self._waiting_for_cpr_futures.append(Future())
- self.output.ask_for_cpr()
- if self.cpr_support == CPR_Support.SUPPORTED:
- do_cpr()
- return
- # If we don't know whether CPR is supported, only do a request if
- # none is pending, and test it, using a timer.
- if self.waiting_for_cpr:
- return
- do_cpr()
- async def timer() -> None:
- await sleep(self.CPR_TIMEOUT)
- # Not set in the meantime -> not supported.
- if self.cpr_support == CPR_Support.UNKNOWN:
- self.cpr_support = CPR_Support.NOT_SUPPORTED
- if self.cpr_not_supported_callback:
- # Make sure to call this callback in the main thread.
- self.cpr_not_supported_callback()
- get_app().create_background_task(timer())
- def report_absolute_cursor_row(self, row: int) -> None:
- """
- To be called when we know the absolute cursor position.
- (As an answer of a "Cursor Position Request" response.)
- """
- self.cpr_support = CPR_Support.SUPPORTED
- # Calculate the amount of rows from the cursor position until the
- # bottom of the terminal.
- total_rows = self.output.get_size().rows
- rows_below_cursor = total_rows - row + 1
- # Set the minimum available height.
- self._min_available_height = rows_below_cursor
- # Pop and set waiting for CPR future.
- try:
- f = self._waiting_for_cpr_futures.popleft()
- except IndexError:
- pass # Received CPR response without having a CPR.
- else:
- f.set_result(None)
- @property
- def waiting_for_cpr(self) -> bool:
- """
- Waiting for CPR flag. True when we send the request, but didn't got a
- response.
- """
- return bool(self._waiting_for_cpr_futures)
- async def wait_for_cpr_responses(self, timeout: int = 1) -> None:
- """
- Wait for a CPR response.
- """
- cpr_futures = list(self._waiting_for_cpr_futures) # Make copy.
- # When there are no CPRs in the queue. Don't do anything.
- if not cpr_futures or self.cpr_support == CPR_Support.NOT_SUPPORTED:
- return None
- async def wait_for_responses() -> None:
- for response_f in cpr_futures:
- await response_f
- async def wait_for_timeout() -> None:
- await sleep(timeout)
- # Got timeout, erase queue.
- for response_f in cpr_futures:
- response_f.cancel()
- self._waiting_for_cpr_futures = deque()
- tasks = {
- ensure_future(wait_for_responses()),
- ensure_future(wait_for_timeout()),
- }
- _, pending = await wait(tasks, return_when=FIRST_COMPLETED)
- for task in pending:
- task.cancel()
- def render(
- self, app: Application[Any], layout: Layout, is_done: bool = False
- ) -> None:
- """
- Render the current interface to the output.
- :param is_done: When True, put the cursor at the end of the interface. We
- won't print any changes to this part.
- """
- output = self.output
- # Enter alternate screen.
- if self.full_screen and not self._in_alternate_screen:
- self._in_alternate_screen = True
- output.enter_alternate_screen()
- # Enable bracketed paste.
- if not self._bracketed_paste_enabled:
- self.output.enable_bracketed_paste()
- self._bracketed_paste_enabled = True
- # Reset cursor key mode.
- if not self._cursor_key_mode_reset:
- self.output.reset_cursor_key_mode()
- self._cursor_key_mode_reset = True
- # Enable/disable mouse support.
- needs_mouse_support = self.mouse_support()
- if needs_mouse_support and not self._mouse_support_enabled:
- output.enable_mouse_support()
- self._mouse_support_enabled = True
- elif not needs_mouse_support and self._mouse_support_enabled:
- output.disable_mouse_support()
- self._mouse_support_enabled = False
- # Create screen and write layout to it.
- size = output.get_size()
- screen = Screen()
- screen.show_cursor = False # Hide cursor by default, unless one of the
- # containers decides to display it.
- mouse_handlers = MouseHandlers()
- # Calculate height.
- if self.full_screen:
- height = size.rows
- elif is_done:
- # When we are done, we don't necessary want to fill up until the bottom.
- height = layout.container.preferred_height(
- size.columns, size.rows
- ).preferred
- else:
- last_height = self._last_screen.height if self._last_screen else 0
- height = max(
- self._min_available_height,
- last_height,
- layout.container.preferred_height(size.columns, size.rows).preferred,
- )
- height = min(height, size.rows)
- # When the size changes, don't consider the previous screen.
- if self._last_size != size:
- self._last_screen = None
- # When we render using another style or another color depth, do a full
- # repaint. (Forget about the previous rendered screen.)
- # (But note that we still use _last_screen to calculate the height.)
- if (
- self.style.invalidation_hash() != self._last_style_hash
- or app.style_transformation.invalidation_hash()
- != self._last_transformation_hash
- or app.color_depth != self._last_color_depth
- ):
- self._last_screen = None
- self._attrs_for_style = None
- self._style_string_has_style = None
- if self._attrs_for_style is None:
- self._attrs_for_style = _StyleStringToAttrsCache(
- self.style.get_attrs_for_style_str, app.style_transformation
- )
- if self._style_string_has_style is None:
- self._style_string_has_style = _StyleStringHasStyleCache(
- self._attrs_for_style
- )
- self._last_style_hash = self.style.invalidation_hash()
- self._last_transformation_hash = app.style_transformation.invalidation_hash()
- self._last_color_depth = app.color_depth
- layout.container.write_to_screen(
- screen,
- mouse_handlers,
- WritePosition(xpos=0, ypos=0, width=size.columns, height=height),
- parent_style="",
- erase_bg=False,
- z_index=None,
- )
- screen.draw_all_floats()
- # When grayed. Replace all styles in the new screen.
- if app.exit_style:
- screen.append_style_to_content(app.exit_style)
- # Process diff and write to output.
- self._cursor_pos, self._last_style = _output_screen_diff(
- app,
- output,
- screen,
- self._cursor_pos,
- app.color_depth,
- self._last_screen,
- self._last_style,
- is_done,
- full_screen=self.full_screen,
- attrs_for_style_string=self._attrs_for_style,
- style_string_has_style=self._style_string_has_style,
- size=size,
- previous_width=(self._last_size.columns if self._last_size else 0),
- )
- self._last_screen = screen
- self._last_size = size
- self.mouse_handlers = mouse_handlers
- # Handle cursor shapes.
- new_cursor_shape = app.cursor.get_cursor_shape(app)
- if (
- self._last_cursor_shape is None
- or self._last_cursor_shape != new_cursor_shape
- ):
- output.set_cursor_shape(new_cursor_shape)
- self._last_cursor_shape = new_cursor_shape
- # Flush buffered output.
- output.flush()
- # Set visible windows in layout.
- app.layout.visible_windows = screen.visible_windows
- if is_done:
- self.reset()
- def erase(self, leave_alternate_screen: bool = True) -> None:
- """
- Hide all output and put the cursor back at the first line. This is for
- instance used for running a system command (while hiding the CLI) and
- later resuming the same CLI.)
- :param leave_alternate_screen: When True, and when inside an alternate
- screen buffer, quit the alternate screen.
- """
- output = self.output
- output.cursor_backward(self._cursor_pos.x)
- output.cursor_up(self._cursor_pos.y)
- output.erase_down()
- output.reset_attributes()
- output.enable_autowrap()
- output.flush()
- self.reset(leave_alternate_screen=leave_alternate_screen)
- def clear(self) -> None:
- """
- Clear screen and go to 0,0
- """
- # Erase current output first.
- self.erase()
- # Send "Erase Screen" command and go to (0, 0).
- output = self.output
- output.erase_screen()
- output.cursor_goto(0, 0)
- output.flush()
- self.request_absolute_cursor_position()
- def print_formatted_text(
- output: Output,
- formatted_text: AnyFormattedText,
- style: BaseStyle,
- style_transformation: StyleTransformation | None = None,
- color_depth: ColorDepth | None = None,
- ) -> None:
- """
- Print a list of (style_str, text) tuples in the given style to the output.
- """
- fragments = to_formatted_text(formatted_text)
- style_transformation = style_transformation or DummyStyleTransformation()
- color_depth = color_depth or output.get_default_color_depth()
- # Reset first.
- output.reset_attributes()
- output.enable_autowrap()
- last_attrs: Attrs | None = None
- # Print all (style_str, text) tuples.
- attrs_for_style_string = _StyleStringToAttrsCache(
- style.get_attrs_for_style_str, style_transformation
- )
- for style_str, text, *_ in fragments:
- attrs = attrs_for_style_string[style_str]
- # Set style attributes if something changed.
- if attrs != last_attrs:
- if attrs:
- output.set_attributes(attrs, color_depth)
- else:
- output.reset_attributes()
- last_attrs = attrs
- # Print escape sequences as raw output
- if "[ZeroWidthEscape]" in style_str:
- output.write_raw(text)
- else:
- # Eliminate carriage returns
- text = text.replace("\r", "")
- # Insert a carriage return before every newline (important when the
- # front-end is a telnet client).
- text = text.replace("\n", "\r\n")
- output.write(text)
- # Reset again.
- output.reset_attributes()
- output.flush()
|