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