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.2.2, created at 2023-03-26 06:07 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +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, Deque, Dict, Hashable, Optional, Tuple
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 behaviour 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."
322 SUPPORTED = "SUPPORTED"
323 NOT_SUPPORTED = "NOT_SUPPORTED"
324 UNKNOWN = "UNKNOWN"
327class Renderer:
328 """
329 Typical usage:
331 ::
333 output = Vt100_Output.from_pty(sys.stdout)
334 r = Renderer(style, output)
335 r.render(app, layout=...)
336 """
338 CPR_TIMEOUT = 2 # Time to wait until we consider CPR to be not supported.
340 def __init__(
341 self,
342 style: BaseStyle,
343 output: Output,
344 full_screen: bool = False,
345 mouse_support: FilterOrBool = False,
346 cpr_not_supported_callback: Callable[[], None] | None = None,
347 ) -> None:
348 self.style = style
349 self.output = output
350 self.full_screen = full_screen
351 self.mouse_support = to_filter(mouse_support)
352 self.cpr_not_supported_callback = cpr_not_supported_callback
354 self._in_alternate_screen = False
355 self._mouse_support_enabled = False
356 self._bracketed_paste_enabled = False
357 self._cursor_key_mode_reset = False
359 # Future set when we are waiting for a CPR flag.
360 self._waiting_for_cpr_futures: Deque[Future[None]] = deque()
361 self.cpr_support = CPR_Support.UNKNOWN
363 if not output.responds_to_cpr:
364 self.cpr_support = CPR_Support.NOT_SUPPORTED
366 # Cache for the style.
367 self._attrs_for_style: _StyleStringToAttrsCache | None = None
368 self._style_string_has_style: _StyleStringHasStyleCache | None = None
369 self._last_style_hash: Hashable | None = None
370 self._last_transformation_hash: Hashable | None = None
371 self._last_color_depth: ColorDepth | None = None
373 self.reset(_scroll=True)
375 def reset(self, _scroll: bool = False, leave_alternate_screen: bool = True) -> None:
376 # Reset position
377 self._cursor_pos = Point(x=0, y=0)
379 # Remember the last screen instance between renderers. This way,
380 # we can create a `diff` between two screens and only output the
381 # difference. It's also to remember the last height. (To show for
382 # instance a toolbar at the bottom position.)
383 self._last_screen: Screen | None = None
384 self._last_size: Size | None = None
385 self._last_style: str | None = None
386 self._last_cursor_shape: CursorShape | None = None
388 # Default MouseHandlers. (Just empty.)
389 self.mouse_handlers = MouseHandlers()
391 #: Space from the top of the layout, until the bottom of the terminal.
392 #: We don't know this until a `report_absolute_cursor_row` call.
393 self._min_available_height = 0
395 # In case of Windows, also make sure to scroll to the current cursor
396 # position. (Only when rendering the first time.)
397 # It does nothing for vt100 terminals.
398 if _scroll:
399 self.output.scroll_buffer_to_prompt()
401 # Quit alternate screen.
402 if self._in_alternate_screen and leave_alternate_screen:
403 self.output.quit_alternate_screen()
404 self._in_alternate_screen = False
406 # Disable mouse support.
407 if self._mouse_support_enabled:
408 self.output.disable_mouse_support()
409 self._mouse_support_enabled = False
411 # Disable bracketed paste.
412 if self._bracketed_paste_enabled:
413 self.output.disable_bracketed_paste()
414 self._bracketed_paste_enabled = False
416 self.output.reset_cursor_shape()
418 # NOTE: No need to set/reset cursor key mode here.
420 # Flush output. `disable_mouse_support` needs to write to stdout.
421 self.output.flush()
423 @property
424 def last_rendered_screen(self) -> Screen | None:
425 """
426 The `Screen` class that was generated during the last rendering.
427 This can be `None`.
428 """
429 return self._last_screen
431 @property
432 def height_is_known(self) -> bool:
433 """
434 True when the height from the cursor until the bottom of the terminal
435 is known. (It's often nicer to draw bottom toolbars only if the height
436 is known, in order to avoid flickering when the CPR response arrives.)
437 """
438 if self.full_screen or self._min_available_height > 0:
439 return True
440 try:
441 self._min_available_height = self.output.get_rows_below_cursor_position()
442 return True
443 except NotImplementedError:
444 return False
446 @property
447 def rows_above_layout(self) -> int:
448 """
449 Return the number of rows visible in the terminal above the layout.
450 """
451 if self._in_alternate_screen:
452 return 0
453 elif self._min_available_height > 0:
454 total_rows = self.output.get_size().rows
455 last_screen_height = self._last_screen.height if self._last_screen else 0
456 return total_rows - max(self._min_available_height, last_screen_height)
457 else:
458 raise HeightIsUnknownError("Rows above layout is unknown.")
460 def request_absolute_cursor_position(self) -> None:
461 """
462 Get current cursor position.
464 We do this to calculate the minimum available height that we can
465 consume for rendering the prompt. This is the available space below te
466 cursor.
468 For vt100: Do CPR request. (answer will arrive later.)
469 For win32: Do API call. (Answer comes immediately.)
470 """
471 # Only do this request when the cursor is at the top row. (after a
472 # clear or reset). We will rely on that in `report_absolute_cursor_row`.
473 assert self._cursor_pos.y == 0
475 # In full-screen mode, always use the total height as min-available-height.
476 if self.full_screen:
477 self._min_available_height = self.output.get_size().rows
478 return
480 # For Win32, we have an API call to get the number of rows below the
481 # cursor.
482 try:
483 self._min_available_height = self.output.get_rows_below_cursor_position()
484 return
485 except NotImplementedError:
486 pass
488 # Use CPR.
489 if self.cpr_support == CPR_Support.NOT_SUPPORTED:
490 return
492 def do_cpr() -> None:
493 # Asks for a cursor position report (CPR).
494 self._waiting_for_cpr_futures.append(Future())
495 self.output.ask_for_cpr()
497 if self.cpr_support == CPR_Support.SUPPORTED:
498 do_cpr()
499 return
501 # If we don't know whether CPR is supported, only do a request if
502 # none is pending, and test it, using a timer.
503 if self.waiting_for_cpr:
504 return
506 do_cpr()
508 async def timer() -> None:
509 await sleep(self.CPR_TIMEOUT)
511 # Not set in the meantime -> not supported.
512 if self.cpr_support == CPR_Support.UNKNOWN:
513 self.cpr_support = CPR_Support.NOT_SUPPORTED
515 if self.cpr_not_supported_callback:
516 # Make sure to call this callback in the main thread.
517 self.cpr_not_supported_callback()
519 get_app().create_background_task(timer())
521 def report_absolute_cursor_row(self, row: int) -> None:
522 """
523 To be called when we know the absolute cursor position.
524 (As an answer of a "Cursor Position Request" response.)
525 """
526 self.cpr_support = CPR_Support.SUPPORTED
528 # Calculate the amount of rows from the cursor position until the
529 # bottom of the terminal.
530 total_rows = self.output.get_size().rows
531 rows_below_cursor = total_rows - row + 1
533 # Set the minimum available height.
534 self._min_available_height = rows_below_cursor
536 # Pop and set waiting for CPR future.
537 try:
538 f = self._waiting_for_cpr_futures.popleft()
539 except IndexError:
540 pass # Received CPR response without having a CPR.
541 else:
542 f.set_result(None)
544 @property
545 def waiting_for_cpr(self) -> bool:
546 """
547 Waiting for CPR flag. True when we send the request, but didn't got a
548 response.
549 """
550 return bool(self._waiting_for_cpr_futures)
552 async def wait_for_cpr_responses(self, timeout: int = 1) -> None:
553 """
554 Wait for a CPR response.
555 """
556 cpr_futures = list(self._waiting_for_cpr_futures) # Make copy.
558 # When there are no CPRs in the queue. Don't do anything.
559 if not cpr_futures or self.cpr_support == CPR_Support.NOT_SUPPORTED:
560 return None
562 async def wait_for_responses() -> None:
563 for response_f in cpr_futures:
564 await response_f
566 async def wait_for_timeout() -> None:
567 await sleep(timeout)
569 # Got timeout, erase queue.
570 for response_f in cpr_futures:
571 response_f.cancel()
572 self._waiting_for_cpr_futures = deque()
574 tasks = {
575 ensure_future(wait_for_responses()),
576 ensure_future(wait_for_timeout()),
577 }
578 _, pending = await wait(tasks, return_when=FIRST_COMPLETED)
579 for task in pending:
580 task.cancel()
582 def render(
583 self, app: Application[Any], layout: Layout, is_done: bool = False
584 ) -> None:
585 """
586 Render the current interface to the output.
588 :param is_done: When True, put the cursor at the end of the interface. We
589 won't print any changes to this part.
590 """
591 output = self.output
593 # Enter alternate screen.
594 if self.full_screen and not self._in_alternate_screen:
595 self._in_alternate_screen = True
596 output.enter_alternate_screen()
598 # Enable bracketed paste.
599 if not self._bracketed_paste_enabled:
600 self.output.enable_bracketed_paste()
601 self._bracketed_paste_enabled = True
603 # Reset cursor key mode.
604 if not self._cursor_key_mode_reset:
605 self.output.reset_cursor_key_mode()
606 self._cursor_key_mode_reset = True
608 # Enable/disable mouse support.
609 needs_mouse_support = self.mouse_support()
611 if needs_mouse_support and not self._mouse_support_enabled:
612 output.enable_mouse_support()
613 self._mouse_support_enabled = True
615 elif not needs_mouse_support and self._mouse_support_enabled:
616 output.disable_mouse_support()
617 self._mouse_support_enabled = False
619 # Create screen and write layout to it.
620 size = output.get_size()
621 screen = Screen()
622 screen.show_cursor = False # Hide cursor by default, unless one of the
623 # containers decides to display it.
624 mouse_handlers = MouseHandlers()
626 # Calculate height.
627 if self.full_screen:
628 height = size.rows
629 elif is_done:
630 # When we are done, we don't necessary want to fill up until the bottom.
631 height = layout.container.preferred_height(
632 size.columns, size.rows
633 ).preferred
634 else:
635 last_height = self._last_screen.height if self._last_screen else 0
636 height = max(
637 self._min_available_height,
638 last_height,
639 layout.container.preferred_height(size.columns, size.rows).preferred,
640 )
642 height = min(height, size.rows)
644 # When the size changes, don't consider the previous screen.
645 if self._last_size != size:
646 self._last_screen = None
648 # When we render using another style or another color depth, do a full
649 # repaint. (Forget about the previous rendered screen.)
650 # (But note that we still use _last_screen to calculate the height.)
651 if (
652 self.style.invalidation_hash() != self._last_style_hash
653 or app.style_transformation.invalidation_hash()
654 != self._last_transformation_hash
655 or app.color_depth != self._last_color_depth
656 ):
657 self._last_screen = None
658 self._attrs_for_style = None
659 self._style_string_has_style = None
661 if self._attrs_for_style is None:
662 self._attrs_for_style = _StyleStringToAttrsCache(
663 self.style.get_attrs_for_style_str, app.style_transformation
664 )
665 if self._style_string_has_style is None:
666 self._style_string_has_style = _StyleStringHasStyleCache(
667 self._attrs_for_style
668 )
670 self._last_style_hash = self.style.invalidation_hash()
671 self._last_transformation_hash = app.style_transformation.invalidation_hash()
672 self._last_color_depth = app.color_depth
674 layout.container.write_to_screen(
675 screen,
676 mouse_handlers,
677 WritePosition(xpos=0, ypos=0, width=size.columns, height=height),
678 parent_style="",
679 erase_bg=False,
680 z_index=None,
681 )
682 screen.draw_all_floats()
684 # When grayed. Replace all styles in the new screen.
685 if app.exit_style:
686 screen.append_style_to_content(app.exit_style)
688 # Process diff and write to output.
689 self._cursor_pos, self._last_style = _output_screen_diff(
690 app,
691 output,
692 screen,
693 self._cursor_pos,
694 app.color_depth,
695 self._last_screen,
696 self._last_style,
697 is_done,
698 full_screen=self.full_screen,
699 attrs_for_style_string=self._attrs_for_style,
700 style_string_has_style=self._style_string_has_style,
701 size=size,
702 previous_width=(self._last_size.columns if self._last_size else 0),
703 )
704 self._last_screen = screen
705 self._last_size = size
706 self.mouse_handlers = mouse_handlers
708 # Handle cursor shapes.
709 new_cursor_shape = app.cursor.get_cursor_shape(app)
710 if (
711 self._last_cursor_shape is None
712 or self._last_cursor_shape != new_cursor_shape
713 ):
714 output.set_cursor_shape(new_cursor_shape)
715 self._last_cursor_shape = new_cursor_shape
717 # Flush buffered output.
718 output.flush()
720 # Set visible windows in layout.
721 app.layout.visible_windows = screen.visible_windows
723 if is_done:
724 self.reset()
726 def erase(self, leave_alternate_screen: bool = True) -> None:
727 """
728 Hide all output and put the cursor back at the first line. This is for
729 instance used for running a system command (while hiding the CLI) and
730 later resuming the same CLI.)
732 :param leave_alternate_screen: When True, and when inside an alternate
733 screen buffer, quit the alternate screen.
734 """
735 output = self.output
737 output.cursor_backward(self._cursor_pos.x)
738 output.cursor_up(self._cursor_pos.y)
739 output.erase_down()
740 output.reset_attributes()
741 output.enable_autowrap()
743 output.flush()
745 self.reset(leave_alternate_screen=leave_alternate_screen)
747 def clear(self) -> None:
748 """
749 Clear screen and go to 0,0
750 """
751 # Erase current output first.
752 self.erase()
754 # Send "Erase Screen" command and go to (0, 0).
755 output = self.output
757 output.erase_screen()
758 output.cursor_goto(0, 0)
759 output.flush()
761 self.request_absolute_cursor_position()
764def print_formatted_text(
765 output: Output,
766 formatted_text: AnyFormattedText,
767 style: BaseStyle,
768 style_transformation: StyleTransformation | None = None,
769 color_depth: ColorDepth | None = None,
770) -> None:
771 """
772 Print a list of (style_str, text) tuples in the given style to the output.
773 """
774 fragments = to_formatted_text(formatted_text)
775 style_transformation = style_transformation or DummyStyleTransformation()
776 color_depth = color_depth or output.get_default_color_depth()
778 # Reset first.
779 output.reset_attributes()
780 output.enable_autowrap()
781 last_attrs: Attrs | None = None
783 # Print all (style_str, text) tuples.
784 attrs_for_style_string = _StyleStringToAttrsCache(
785 style.get_attrs_for_style_str, style_transformation
786 )
788 for style_str, text, *_ in fragments:
789 attrs = attrs_for_style_string[style_str]
791 # Set style attributes if something changed.
792 if attrs != last_attrs:
793 if attrs:
794 output.set_attributes(attrs, color_depth)
795 else:
796 output.reset_attributes()
797 last_attrs = attrs
799 # Print escape sequences as raw output
800 if "[ZeroWidthEscape]" in style_str:
801 output.write_raw(text)
802 else:
803 # Eliminate carriage returns
804 text = text.replace("\r", "")
805 # Insert a carriage return before every newline (important when the
806 # front-end is a telnet client).
807 text = text.replace("\n", "\r\n")
808 output.write(text)
810 # Reset again.
811 output.reset_attributes()
812 output.flush()