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