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