Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/prompt_toolkit/renderer.py: 14%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

347 statements  

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()