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

1""" 

2Renders the command line on the console. 

3(Redraws parts of the input line that were changed.) 

4""" 

5from __future__ import annotations 

6 

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 

11 

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) 

26 

27if TYPE_CHECKING: 

28 from prompt_toolkit.application import Application 

29 from prompt_toolkit.layout.layout import Layout 

30 

31 

32__all__ = [ 

33 "Renderer", 

34 "print_formatted_text", 

35] 

36 

37 

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. 

55 

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. 

61 

62 This is some performance-critical code which is heavily optimized. 

63 Don't change things without profiling first. 

64 

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 

73 

74 #: Variable for capturing the output. 

75 write = output.write 

76 write_raw = output.write_raw 

77 

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 

85 

86 # Hide cursor before rendering. (Avoid flickering.) 

87 output.hide_cursor() 

88 

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. 

94 

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 

98 

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) 

111 

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) 

119 

120 return new 

121 

122 def output_char(char: Char) -> None: 

123 """ 

124 Write the output of this character. 

125 """ 

126 nonlocal last_style 

127 

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) 

140 

141 write(char.char) 

142 last_style = char.style 

143 

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. 

149 

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) 

163 

164 # Render for the first time: reset styling. 

165 if not previous_screen: 

166 reset_attributes() 

167 

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

174 

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

183 

184 previous_screen = Screen() 

185 

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) 

190 

191 # Loop over the rows. 

192 row_count = min(max(screen.height, previous_screen.height), height) 

193 

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] 

198 

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

201 

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 

208 

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

214 

215 # Send injected escape sequences to output. 

216 if c in zero_width_escapes_row: 

217 write_raw(zero_width_escapes_row[c]) 

218 

219 output_char(new_char) 

220 current_pos = Point(x=current_pos.x + char_width, y=current_pos.y) 

221 

222 c += char_width 

223 

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

229 

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. 

235 

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

242 

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

249 

250 if is_done or not full_screen: 

251 output.enable_autowrap() 

252 

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

258 

259 if screen.show_cursor or is_done: 

260 output.show_cursor() 

261 

262 return current_pos, last_style 

263 

264 

265class HeightIsUnknownError(Exception): 

266 "Information unavailable. Did not yet receive the CPR response." 

267 

268 

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 """ 

274 

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 

282 

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) 

286 

287 self[style_str] = attrs 

288 return attrs 

289 

290 

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

297 

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 """ 

301 

302 def __init__(self, style_string_to_attrs: dict[str, Attrs]) -> None: 

303 self.style_string_to_attrs = style_string_to_attrs 

304 

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 ) 

315 

316 self[style_str] = is_default 

317 return is_default 

318 

319 

320class CPR_Support(Enum): 

321 "Enum: whether or not CPR is supported." 

322 SUPPORTED = "SUPPORTED" 

323 NOT_SUPPORTED = "NOT_SUPPORTED" 

324 UNKNOWN = "UNKNOWN" 

325 

326 

327class Renderer: 

328 """ 

329 Typical usage: 

330 

331 :: 

332 

333 output = Vt100_Output.from_pty(sys.stdout) 

334 r = Renderer(style, output) 

335 r.render(app, layout=...) 

336 """ 

337 

338 CPR_TIMEOUT = 2 # Time to wait until we consider CPR to be not supported. 

339 

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 

353 

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 

358 

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 

362 

363 if not output.responds_to_cpr: 

364 self.cpr_support = CPR_Support.NOT_SUPPORTED 

365 

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 

372 

373 self.reset(_scroll=True) 

374 

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) 

378 

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 

387 

388 # Default MouseHandlers. (Just empty.) 

389 self.mouse_handlers = MouseHandlers() 

390 

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 

394 

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

400 

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 

405 

406 # Disable mouse support. 

407 if self._mouse_support_enabled: 

408 self.output.disable_mouse_support() 

409 self._mouse_support_enabled = False 

410 

411 # Disable bracketed paste. 

412 if self._bracketed_paste_enabled: 

413 self.output.disable_bracketed_paste() 

414 self._bracketed_paste_enabled = False 

415 

416 self.output.reset_cursor_shape() 

417 

418 # NOTE: No need to set/reset cursor key mode here. 

419 

420 # Flush output. `disable_mouse_support` needs to write to stdout. 

421 self.output.flush() 

422 

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 

430 

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 

445 

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.") 

459 

460 def request_absolute_cursor_position(self) -> None: 

461 """ 

462 Get current cursor position. 

463 

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. 

467 

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 

474 

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 

479 

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 

487 

488 # Use CPR. 

489 if self.cpr_support == CPR_Support.NOT_SUPPORTED: 

490 return 

491 

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

496 

497 if self.cpr_support == CPR_Support.SUPPORTED: 

498 do_cpr() 

499 return 

500 

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 

505 

506 do_cpr() 

507 

508 async def timer() -> None: 

509 await sleep(self.CPR_TIMEOUT) 

510 

511 # Not set in the meantime -> not supported. 

512 if self.cpr_support == CPR_Support.UNKNOWN: 

513 self.cpr_support = CPR_Support.NOT_SUPPORTED 

514 

515 if self.cpr_not_supported_callback: 

516 # Make sure to call this callback in the main thread. 

517 self.cpr_not_supported_callback() 

518 

519 get_app().create_background_task(timer()) 

520 

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 

527 

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 

532 

533 # Set the minimum available height. 

534 self._min_available_height = rows_below_cursor 

535 

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) 

543 

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) 

551 

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. 

557 

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 

561 

562 async def wait_for_responses() -> None: 

563 for response_f in cpr_futures: 

564 await response_f 

565 

566 async def wait_for_timeout() -> None: 

567 await sleep(timeout) 

568 

569 # Got timeout, erase queue. 

570 for response_f in cpr_futures: 

571 response_f.cancel() 

572 self._waiting_for_cpr_futures = deque() 

573 

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

581 

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. 

587 

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 

592 

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

597 

598 # Enable bracketed paste. 

599 if not self._bracketed_paste_enabled: 

600 self.output.enable_bracketed_paste() 

601 self._bracketed_paste_enabled = True 

602 

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 

607 

608 # Enable/disable mouse support. 

609 needs_mouse_support = self.mouse_support() 

610 

611 if needs_mouse_support and not self._mouse_support_enabled: 

612 output.enable_mouse_support() 

613 self._mouse_support_enabled = True 

614 

615 elif not needs_mouse_support and self._mouse_support_enabled: 

616 output.disable_mouse_support() 

617 self._mouse_support_enabled = False 

618 

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

625 

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 ) 

641 

642 height = min(height, size.rows) 

643 

644 # When the size changes, don't consider the previous screen. 

645 if self._last_size != size: 

646 self._last_screen = None 

647 

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 

660 

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 ) 

669 

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 

673 

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

683 

684 # When grayed. Replace all styles in the new screen. 

685 if app.exit_style: 

686 screen.append_style_to_content(app.exit_style) 

687 

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 

707 

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 

716 

717 # Flush buffered output. 

718 output.flush() 

719 

720 # Set visible windows in layout. 

721 app.layout.visible_windows = screen.visible_windows 

722 

723 if is_done: 

724 self.reset() 

725 

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

731 

732 :param leave_alternate_screen: When True, and when inside an alternate 

733 screen buffer, quit the alternate screen. 

734 """ 

735 output = self.output 

736 

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

742 

743 output.flush() 

744 

745 self.reset(leave_alternate_screen=leave_alternate_screen) 

746 

747 def clear(self) -> None: 

748 """ 

749 Clear screen and go to 0,0 

750 """ 

751 # Erase current output first. 

752 self.erase() 

753 

754 # Send "Erase Screen" command and go to (0, 0). 

755 output = self.output 

756 

757 output.erase_screen() 

758 output.cursor_goto(0, 0) 

759 output.flush() 

760 

761 self.request_absolute_cursor_position() 

762 

763 

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

777 

778 # Reset first. 

779 output.reset_attributes() 

780 output.enable_autowrap() 

781 last_attrs: Attrs | None = None 

782 

783 # Print all (style_str, text) tuples. 

784 attrs_for_style_string = _StyleStringToAttrsCache( 

785 style.get_attrs_for_style_str, style_transformation 

786 ) 

787 

788 for style_str, text, *_ in fragments: 

789 attrs = attrs_for_style_string[style_str] 

790 

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 

798 

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) 

809 

810 # Reset again. 

811 output.reset_attributes() 

812 output.flush()