Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/prompt_toolkit/widgets/base.py: 29%

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

334 statements  

1""" 

2Collection of reusable components for building full screen applications. 

3 

4All of these widgets implement the ``__pt_container__`` method, which makes 

5them usable in any situation where we are expecting a `prompt_toolkit` 

6container object. 

7 

8.. warning:: 

9 

10 At this point, the API for these widgets is considered unstable, and can 

11 potentially change between minor releases (we try not too, but no 

12 guarantees are made yet). The public API in 

13 `prompt_toolkit.shortcuts.dialogs` on the other hand is considered stable. 

14""" 

15 

16from __future__ import annotations 

17 

18from functools import partial 

19from typing import Callable, Generic, Sequence, TypeVar 

20 

21from prompt_toolkit.application.current import get_app 

22from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest 

23from prompt_toolkit.buffer import Buffer, BufferAcceptHandler 

24from prompt_toolkit.completion import Completer, DynamicCompleter 

25from prompt_toolkit.document import Document 

26from prompt_toolkit.filters import ( 

27 Condition, 

28 FilterOrBool, 

29 has_focus, 

30 is_done, 

31 is_true, 

32 to_filter, 

33) 

34from prompt_toolkit.formatted_text import ( 

35 AnyFormattedText, 

36 StyleAndTextTuples, 

37 Template, 

38 to_formatted_text, 

39) 

40from prompt_toolkit.formatted_text.utils import fragment_list_to_text 

41from prompt_toolkit.history import History 

42from prompt_toolkit.key_binding.key_bindings import KeyBindings 

43from prompt_toolkit.key_binding.key_processor import KeyPressEvent 

44from prompt_toolkit.keys import Keys 

45from prompt_toolkit.layout.containers import ( 

46 AnyContainer, 

47 ConditionalContainer, 

48 Container, 

49 DynamicContainer, 

50 Float, 

51 FloatContainer, 

52 HSplit, 

53 VSplit, 

54 Window, 

55 WindowAlign, 

56) 

57from prompt_toolkit.layout.controls import ( 

58 BufferControl, 

59 FormattedTextControl, 

60 GetLinePrefixCallable, 

61) 

62from prompt_toolkit.layout.dimension import AnyDimension 

63from prompt_toolkit.layout.dimension import Dimension as D 

64from prompt_toolkit.layout.margins import ( 

65 ConditionalMargin, 

66 NumberedMargin, 

67 ScrollbarMargin, 

68) 

69from prompt_toolkit.layout.processors import ( 

70 AppendAutoSuggestion, 

71 BeforeInput, 

72 ConditionalProcessor, 

73 PasswordProcessor, 

74 Processor, 

75) 

76from prompt_toolkit.lexers import DynamicLexer, Lexer 

77from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 

78from prompt_toolkit.utils import get_cwidth 

79from prompt_toolkit.validation import DynamicValidator, Validator 

80 

81from .toolbars import SearchToolbar 

82 

83__all__ = [ 

84 "TextArea", 

85 "Label", 

86 "Button", 

87 "Frame", 

88 "Shadow", 

89 "Box", 

90 "VerticalLine", 

91 "HorizontalLine", 

92 "RadioList", 

93 "CheckboxList", 

94 "Checkbox", # backward compatibility 

95 "ProgressBar", 

96] 

97 

98E = KeyPressEvent 

99 

100 

101class Border: 

102 "Box drawing characters. (Thin)" 

103 

104 HORIZONTAL = "\u2500" 

105 VERTICAL = "\u2502" 

106 TOP_LEFT = "\u250c" 

107 TOP_RIGHT = "\u2510" 

108 BOTTOM_LEFT = "\u2514" 

109 BOTTOM_RIGHT = "\u2518" 

110 

111 

112class TextArea: 

113 """ 

114 A simple input field. 

115 

116 This is a higher level abstraction on top of several other classes with 

117 sane defaults. 

118 

119 This widget does have the most common options, but it does not intend to 

120 cover every single use case. For more configurations options, you can 

121 always build a text area manually, using a 

122 :class:`~prompt_toolkit.buffer.Buffer`, 

123 :class:`~prompt_toolkit.layout.BufferControl` and 

124 :class:`~prompt_toolkit.layout.Window`. 

125 

126 Buffer attributes: 

127 

128 :param text: The initial text. 

129 :param multiline: If True, allow multiline input. 

130 :param completer: :class:`~prompt_toolkit.completion.Completer` instance 

131 for auto completion. 

132 :param complete_while_typing: Boolean. 

133 :param accept_handler: Called when `Enter` is pressed (This should be a 

134 callable that takes a buffer as input). 

135 :param history: :class:`~prompt_toolkit.history.History` instance. 

136 :param auto_suggest: :class:`~prompt_toolkit.auto_suggest.AutoSuggest` 

137 instance for input suggestions. 

138 

139 BufferControl attributes: 

140 

141 :param password: When `True`, display using asterisks. 

142 :param focusable: When `True`, allow this widget to receive the focus. 

143 :param focus_on_click: When `True`, focus after mouse click. 

144 :param input_processors: `None` or a list of 

145 :class:`~prompt_toolkit.layout.Processor` objects. 

146 :param validator: `None` or a :class:`~prompt_toolkit.validation.Validator` 

147 object. 

148 

149 Window attributes: 

150 

151 :param lexer: :class:`~prompt_toolkit.lexers.Lexer` instance for syntax 

152 highlighting. 

153 :param wrap_lines: When `True`, don't scroll horizontally, but wrap lines. 

154 :param width: Window width. (:class:`~prompt_toolkit.layout.Dimension` object.) 

155 :param height: Window height. (:class:`~prompt_toolkit.layout.Dimension` object.) 

156 :param scrollbar: When `True`, display a scroll bar. 

157 :param style: A style string. 

158 :param dont_extend_width: When `True`, don't take up more width then the 

159 preferred width reported by the control. 

160 :param dont_extend_height: When `True`, don't take up more width then the 

161 preferred height reported by the control. 

162 :param get_line_prefix: None or a callable that returns formatted text to 

163 be inserted before a line. It takes a line number (int) and a 

164 wrap_count and returns formatted text. This can be used for 

165 implementation of line continuations, things like Vim "breakindent" and 

166 so on. 

167 

168 Other attributes: 

169 

170 :param search_field: An optional `SearchToolbar` object. 

171 """ 

172 

173 def __init__( 

174 self, 

175 text: str = "", 

176 multiline: FilterOrBool = True, 

177 password: FilterOrBool = False, 

178 lexer: Lexer | None = None, 

179 auto_suggest: AutoSuggest | None = None, 

180 completer: Completer | None = None, 

181 complete_while_typing: FilterOrBool = True, 

182 validator: Validator | None = None, 

183 accept_handler: BufferAcceptHandler | None = None, 

184 history: History | None = None, 

185 focusable: FilterOrBool = True, 

186 focus_on_click: FilterOrBool = False, 

187 wrap_lines: FilterOrBool = True, 

188 read_only: FilterOrBool = False, 

189 width: AnyDimension = None, 

190 height: AnyDimension = None, 

191 dont_extend_height: FilterOrBool = False, 

192 dont_extend_width: FilterOrBool = False, 

193 line_numbers: bool = False, 

194 get_line_prefix: GetLinePrefixCallable | None = None, 

195 scrollbar: bool = False, 

196 style: str = "", 

197 search_field: SearchToolbar | None = None, 

198 preview_search: FilterOrBool = True, 

199 prompt: AnyFormattedText = "", 

200 input_processors: list[Processor] | None = None, 

201 name: str = "", 

202 ) -> None: 

203 if search_field is None: 

204 search_control = None 

205 elif isinstance(search_field, SearchToolbar): 

206 search_control = search_field.control 

207 

208 if input_processors is None: 

209 input_processors = [] 

210 

211 # Writeable attributes. 

212 self.completer = completer 

213 self.complete_while_typing = complete_while_typing 

214 self.lexer = lexer 

215 self.auto_suggest = auto_suggest 

216 self.read_only = read_only 

217 self.wrap_lines = wrap_lines 

218 self.validator = validator 

219 

220 self.buffer = Buffer( 

221 document=Document(text, 0), 

222 multiline=multiline, 

223 read_only=Condition(lambda: is_true(self.read_only)), 

224 completer=DynamicCompleter(lambda: self.completer), 

225 complete_while_typing=Condition( 

226 lambda: is_true(self.complete_while_typing) 

227 ), 

228 validator=DynamicValidator(lambda: self.validator), 

229 auto_suggest=DynamicAutoSuggest(lambda: self.auto_suggest), 

230 accept_handler=accept_handler, 

231 history=history, 

232 name=name, 

233 ) 

234 

235 self.control = BufferControl( 

236 buffer=self.buffer, 

237 lexer=DynamicLexer(lambda: self.lexer), 

238 input_processors=[ 

239 ConditionalProcessor( 

240 AppendAutoSuggestion(), has_focus(self.buffer) & ~is_done 

241 ), 

242 ConditionalProcessor( 

243 processor=PasswordProcessor(), filter=to_filter(password) 

244 ), 

245 BeforeInput(prompt, style="class:text-area.prompt"), 

246 ] 

247 + input_processors, 

248 search_buffer_control=search_control, 

249 preview_search=preview_search, 

250 focusable=focusable, 

251 focus_on_click=focus_on_click, 

252 ) 

253 

254 if multiline: 

255 if scrollbar: 

256 right_margins = [ScrollbarMargin(display_arrows=True)] 

257 else: 

258 right_margins = [] 

259 if line_numbers: 

260 left_margins = [NumberedMargin()] 

261 else: 

262 left_margins = [] 

263 else: 

264 height = D.exact(1) 

265 left_margins = [] 

266 right_margins = [] 

267 

268 style = "class:text-area " + style 

269 

270 # If no height was given, guarantee height of at least 1. 

271 if height is None: 

272 height = D(min=1) 

273 

274 self.window = Window( 

275 height=height, 

276 width=width, 

277 dont_extend_height=dont_extend_height, 

278 dont_extend_width=dont_extend_width, 

279 content=self.control, 

280 style=style, 

281 wrap_lines=Condition(lambda: is_true(self.wrap_lines)), 

282 left_margins=left_margins, 

283 right_margins=right_margins, 

284 get_line_prefix=get_line_prefix, 

285 ) 

286 

287 @property 

288 def text(self) -> str: 

289 """ 

290 The `Buffer` text. 

291 """ 

292 return self.buffer.text 

293 

294 @text.setter 

295 def text(self, value: str) -> None: 

296 self.document = Document(value, 0) 

297 

298 @property 

299 def document(self) -> Document: 

300 """ 

301 The `Buffer` document (text + cursor position). 

302 """ 

303 return self.buffer.document 

304 

305 @document.setter 

306 def document(self, value: Document) -> None: 

307 self.buffer.set_document(value, bypass_readonly=True) 

308 

309 @property 

310 def accept_handler(self) -> BufferAcceptHandler | None: 

311 """ 

312 The accept handler. Called when the user accepts the input. 

313 """ 

314 return self.buffer.accept_handler 

315 

316 @accept_handler.setter 

317 def accept_handler(self, value: BufferAcceptHandler) -> None: 

318 self.buffer.accept_handler = value 

319 

320 def __pt_container__(self) -> Container: 

321 return self.window 

322 

323 

324class Label: 

325 """ 

326 Widget that displays the given text. It is not editable or focusable. 

327 

328 :param text: Text to display. Can be multiline. All value types accepted by 

329 :class:`prompt_toolkit.layout.FormattedTextControl` are allowed, 

330 including a callable. 

331 :param style: A style string. 

332 :param width: When given, use this width, rather than calculating it from 

333 the text size. 

334 :param dont_extend_width: When `True`, don't take up more width than 

335 preferred, i.e. the length of the longest line of 

336 the text, or value of `width` parameter, if 

337 given. `True` by default 

338 :param dont_extend_height: When `True`, don't take up more width than the 

339 preferred height, i.e. the number of lines of 

340 the text. `False` by default. 

341 """ 

342 

343 def __init__( 

344 self, 

345 text: AnyFormattedText, 

346 style: str = "", 

347 width: AnyDimension = None, 

348 dont_extend_height: bool = True, 

349 dont_extend_width: bool = False, 

350 align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT, 

351 # There is no cursor navigation in a label, so it makes sense to always 

352 # wrap lines by default. 

353 wrap_lines: FilterOrBool = True, 

354 ) -> None: 

355 self.text = text 

356 

357 def get_width() -> AnyDimension: 

358 if width is None: 

359 text_fragments = to_formatted_text(self.text) 

360 text = fragment_list_to_text(text_fragments) 

361 if text: 

362 longest_line = max(get_cwidth(line) for line in text.splitlines()) 

363 else: 

364 return D(preferred=0) 

365 return D(preferred=longest_line) 

366 else: 

367 return width 

368 

369 self.formatted_text_control = FormattedTextControl(text=lambda: self.text) 

370 

371 self.window = Window( 

372 content=self.formatted_text_control, 

373 width=get_width, 

374 height=D(min=1), 

375 style="class:label " + style, 

376 dont_extend_height=dont_extend_height, 

377 dont_extend_width=dont_extend_width, 

378 align=align, 

379 wrap_lines=wrap_lines, 

380 ) 

381 

382 def __pt_container__(self) -> Container: 

383 return self.window 

384 

385 

386class Button: 

387 """ 

388 Clickable button. 

389 

390 :param text: The caption for the button. 

391 :param handler: `None` or callable. Called when the button is clicked. No 

392 parameters are passed to this callable. Use for instance Python's 

393 `functools.partial` to pass parameters to this callable if needed. 

394 :param width: Width of the button. 

395 """ 

396 

397 def __init__( 

398 self, 

399 text: str, 

400 handler: Callable[[], None] | None = None, 

401 width: int = 12, 

402 left_symbol: str = "<", 

403 right_symbol: str = ">", 

404 ) -> None: 

405 self.text = text 

406 self.left_symbol = left_symbol 

407 self.right_symbol = right_symbol 

408 self.handler = handler 

409 self.width = width 

410 self.control = FormattedTextControl( 

411 self._get_text_fragments, 

412 key_bindings=self._get_key_bindings(), 

413 focusable=True, 

414 ) 

415 

416 def get_style() -> str: 

417 if get_app().layout.has_focus(self): 

418 return "class:button.focused" 

419 else: 

420 return "class:button" 

421 

422 # Note: `dont_extend_width` is False, because we want to allow buttons 

423 # to take more space if the parent container provides more space. 

424 # Otherwise, we will also truncate the text. 

425 # Probably we need a better way here to adjust to width of the 

426 # button to the text. 

427 

428 self.window = Window( 

429 self.control, 

430 align=WindowAlign.CENTER, 

431 height=1, 

432 width=width, 

433 style=get_style, 

434 dont_extend_width=False, 

435 dont_extend_height=True, 

436 ) 

437 

438 def _get_text_fragments(self) -> StyleAndTextTuples: 

439 width = ( 

440 self.width 

441 - (get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)) 

442 + (len(self.text) - get_cwidth(self.text)) 

443 ) 

444 text = (f"{{:^{max(0, width)}}}").format(self.text) 

445 

446 def handler(mouse_event: MouseEvent) -> None: 

447 if ( 

448 self.handler is not None 

449 and mouse_event.event_type == MouseEventType.MOUSE_UP 

450 ): 

451 self.handler() 

452 

453 return [ 

454 ("class:button.arrow", self.left_symbol, handler), 

455 ("[SetCursorPosition]", ""), 

456 ("class:button.text", text, handler), 

457 ("class:button.arrow", self.right_symbol, handler), 

458 ] 

459 

460 def _get_key_bindings(self) -> KeyBindings: 

461 "Key bindings for the Button." 

462 kb = KeyBindings() 

463 

464 @kb.add(" ") 

465 @kb.add("enter") 

466 def _(event: E) -> None: 

467 if self.handler is not None: 

468 self.handler() 

469 

470 return kb 

471 

472 def __pt_container__(self) -> Container: 

473 return self.window 

474 

475 

476class Frame: 

477 """ 

478 Draw a border around any container, optionally with a title text. 

479 

480 Changing the title and body of the frame is possible at runtime by 

481 assigning to the `body` and `title` attributes of this class. 

482 

483 :param body: Another container object. 

484 :param title: Text to be displayed in the top of the frame (can be formatted text). 

485 :param style: Style string to be applied to this widget. 

486 """ 

487 

488 def __init__( 

489 self, 

490 body: AnyContainer, 

491 title: AnyFormattedText = "", 

492 style: str = "", 

493 width: AnyDimension = None, 

494 height: AnyDimension = None, 

495 key_bindings: KeyBindings | None = None, 

496 modal: bool = False, 

497 ) -> None: 

498 self.title = title 

499 self.body = body 

500 

501 fill = partial(Window, style="class:frame.border") 

502 style = "class:frame " + style 

503 

504 top_row_with_title = VSplit( 

505 [ 

506 fill(width=1, height=1, char=Border.TOP_LEFT), 

507 fill(char=Border.HORIZONTAL), 

508 fill(width=1, height=1, char="|"), 

509 # Notice: we use `Template` here, because `self.title` can be an 

510 # `HTML` object for instance. 

511 Label( 

512 lambda: Template(" {} ").format(self.title), 

513 style="class:frame.label", 

514 dont_extend_width=True, 

515 ), 

516 fill(width=1, height=1, char="|"), 

517 fill(char=Border.HORIZONTAL), 

518 fill(width=1, height=1, char=Border.TOP_RIGHT), 

519 ], 

520 height=1, 

521 ) 

522 

523 top_row_without_title = VSplit( 

524 [ 

525 fill(width=1, height=1, char=Border.TOP_LEFT), 

526 fill(char=Border.HORIZONTAL), 

527 fill(width=1, height=1, char=Border.TOP_RIGHT), 

528 ], 

529 height=1, 

530 ) 

531 

532 @Condition 

533 def has_title() -> bool: 

534 return bool(self.title) 

535 

536 self.container = HSplit( 

537 [ 

538 ConditionalContainer( 

539 content=top_row_with_title, 

540 filter=has_title, 

541 alternative_content=top_row_without_title, 

542 ), 

543 VSplit( 

544 [ 

545 fill(width=1, char=Border.VERTICAL), 

546 DynamicContainer(lambda: self.body), 

547 fill(width=1, char=Border.VERTICAL), 

548 # Padding is required to make sure that if the content is 

549 # too small, the right frame border is still aligned. 

550 ], 

551 padding=0, 

552 ), 

553 VSplit( 

554 [ 

555 fill(width=1, height=1, char=Border.BOTTOM_LEFT), 

556 fill(char=Border.HORIZONTAL), 

557 fill(width=1, height=1, char=Border.BOTTOM_RIGHT), 

558 ], 

559 # specifying height here will increase the rendering speed. 

560 height=1, 

561 ), 

562 ], 

563 width=width, 

564 height=height, 

565 style=style, 

566 key_bindings=key_bindings, 

567 modal=modal, 

568 ) 

569 

570 def __pt_container__(self) -> Container: 

571 return self.container 

572 

573 

574class Shadow: 

575 """ 

576 Draw a shadow underneath/behind this container. 

577 (This applies `class:shadow` the the cells under the shadow. The Style 

578 should define the colors for the shadow.) 

579 

580 :param body: Another container object. 

581 """ 

582 

583 def __init__(self, body: AnyContainer) -> None: 

584 self.container = FloatContainer( 

585 content=body, 

586 floats=[ 

587 Float( 

588 bottom=-1, 

589 height=1, 

590 left=1, 

591 right=-1, 

592 transparent=True, 

593 content=Window(style="class:shadow"), 

594 ), 

595 Float( 

596 bottom=-1, 

597 top=1, 

598 width=1, 

599 right=-1, 

600 transparent=True, 

601 content=Window(style="class:shadow"), 

602 ), 

603 ], 

604 ) 

605 

606 def __pt_container__(self) -> Container: 

607 return self.container 

608 

609 

610class Box: 

611 """ 

612 Add padding around a container. 

613 

614 This also makes sure that the parent can provide more space than required by 

615 the child. This is very useful when wrapping a small element with a fixed 

616 size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit`` 

617 try to make sure to adapt respectively the width and height, possibly 

618 shrinking other elements. Wrapping something in a ``Box`` makes it flexible. 

619 

620 :param body: Another container object. 

621 :param padding: The margin to be used around the body. This can be 

622 overridden by `padding_left`, padding_right`, `padding_top` and 

623 `padding_bottom`. 

624 :param style: A style string. 

625 :param char: Character to be used for filling the space around the body. 

626 (This is supposed to be a character with a terminal width of 1.) 

627 """ 

628 

629 def __init__( 

630 self, 

631 body: AnyContainer, 

632 padding: AnyDimension = None, 

633 padding_left: AnyDimension = None, 

634 padding_right: AnyDimension = None, 

635 padding_top: AnyDimension = None, 

636 padding_bottom: AnyDimension = None, 

637 width: AnyDimension = None, 

638 height: AnyDimension = None, 

639 style: str = "", 

640 char: None | str | Callable[[], str] = None, 

641 modal: bool = False, 

642 key_bindings: KeyBindings | None = None, 

643 ) -> None: 

644 self.padding = padding 

645 self.padding_left = padding_left 

646 self.padding_right = padding_right 

647 self.padding_top = padding_top 

648 self.padding_bottom = padding_bottom 

649 self.body = body 

650 

651 def left() -> AnyDimension: 

652 if self.padding_left is None: 

653 return self.padding 

654 return self.padding_left 

655 

656 def right() -> AnyDimension: 

657 if self.padding_right is None: 

658 return self.padding 

659 return self.padding_right 

660 

661 def top() -> AnyDimension: 

662 if self.padding_top is None: 

663 return self.padding 

664 return self.padding_top 

665 

666 def bottom() -> AnyDimension: 

667 if self.padding_bottom is None: 

668 return self.padding 

669 return self.padding_bottom 

670 

671 self.container = HSplit( 

672 [ 

673 Window(height=top, char=char), 

674 VSplit( 

675 [ 

676 Window(width=left, char=char), 

677 body, 

678 Window(width=right, char=char), 

679 ] 

680 ), 

681 Window(height=bottom, char=char), 

682 ], 

683 width=width, 

684 height=height, 

685 style=style, 

686 modal=modal, 

687 key_bindings=None, 

688 ) 

689 

690 def __pt_container__(self) -> Container: 

691 return self.container 

692 

693 

694_T = TypeVar("_T") 

695 

696 

697class _DialogList(Generic[_T]): 

698 """ 

699 Common code for `RadioList` and `CheckboxList`. 

700 """ 

701 

702 def __init__( 

703 self, 

704 values: Sequence[tuple[_T, AnyFormattedText]], 

705 default_values: Sequence[_T] | None = None, 

706 select_on_focus: bool = False, 

707 open_character: str = "", 

708 select_character: str = "*", 

709 close_character: str = "", 

710 container_style: str = "", 

711 default_style: str = "", 

712 number_style: str = "", 

713 selected_style: str = "", 

714 checked_style: str = "", 

715 multiple_selection: bool = False, 

716 show_scrollbar: bool = True, 

717 show_cursor: bool = True, 

718 show_numbers: bool = False, 

719 ) -> None: 

720 assert len(values) > 0 

721 default_values = default_values or [] 

722 

723 self.values = values 

724 self.show_numbers = show_numbers 

725 

726 self.open_character = open_character 

727 self.select_character = select_character 

728 self.close_character = close_character 

729 self.container_style = container_style 

730 self.default_style = default_style 

731 self.number_style = number_style 

732 self.selected_style = selected_style 

733 self.checked_style = checked_style 

734 self.multiple_selection = multiple_selection 

735 self.show_scrollbar = show_scrollbar 

736 

737 # current_values will be used in multiple_selection, 

738 # current_value will be used otherwise. 

739 keys: list[_T] = [value for (value, _) in values] 

740 self.current_values: list[_T] = [ 

741 value for value in default_values if value in keys 

742 ] 

743 self.current_value: _T = ( 

744 default_values[0] 

745 if len(default_values) and default_values[0] in keys 

746 else values[0][0] 

747 ) 

748 

749 # Cursor index: take first selected item or first item otherwise. 

750 if len(self.current_values) > 0: 

751 self._selected_index = keys.index(self.current_values[0]) 

752 else: 

753 self._selected_index = 0 

754 

755 # Key bindings. 

756 kb = KeyBindings() 

757 

758 @kb.add("up") 

759 @kb.add("k") # Vi-like. 

760 def _up(event: E) -> None: 

761 self._selected_index = max(0, self._selected_index - 1) 

762 if select_on_focus: 

763 self._handle_enter() 

764 

765 @kb.add("down") 

766 @kb.add("j") # Vi-like. 

767 def _down(event: E) -> None: 

768 self._selected_index = min(len(self.values) - 1, self._selected_index + 1) 

769 if select_on_focus: 

770 self._handle_enter() 

771 

772 @kb.add("pageup") 

773 def _pageup(event: E) -> None: 

774 w = event.app.layout.current_window 

775 if w.render_info: 

776 self._selected_index = max( 

777 0, self._selected_index - len(w.render_info.displayed_lines) 

778 ) 

779 

780 @kb.add("pagedown") 

781 def _pagedown(event: E) -> None: 

782 w = event.app.layout.current_window 

783 if w.render_info: 

784 self._selected_index = min( 

785 len(self.values) - 1, 

786 self._selected_index + len(w.render_info.displayed_lines), 

787 ) 

788 

789 @kb.add("enter") 

790 @kb.add(" ") 

791 def _click(event: E) -> None: 

792 self._handle_enter() 

793 

794 @kb.add(Keys.Any) 

795 def _find(event: E) -> None: 

796 # We first check values after the selected value, then all values. 

797 values = list(self.values) 

798 for value in values[self._selected_index + 1 :] + values: 

799 text = fragment_list_to_text(to_formatted_text(value[1])).lower() 

800 

801 if text.startswith(event.data.lower()): 

802 self._selected_index = self.values.index(value) 

803 return 

804 

805 numbers_visible = Condition(lambda: self.show_numbers) 

806 

807 for i in range(1, 10): 

808 

809 @kb.add(str(i), filter=numbers_visible) 

810 def _select_i(event: E, index: int = i) -> None: 

811 self._selected_index = min(len(self.values) - 1, index - 1) 

812 if select_on_focus: 

813 self._handle_enter() 

814 

815 # Control and window. 

816 self.control = FormattedTextControl( 

817 self._get_text_fragments, 

818 key_bindings=kb, 

819 focusable=True, 

820 show_cursor=show_cursor, 

821 ) 

822 

823 self.window = Window( 

824 content=self.control, 

825 style=self.container_style, 

826 right_margins=[ 

827 ConditionalMargin( 

828 margin=ScrollbarMargin(display_arrows=True), 

829 filter=Condition(lambda: self.show_scrollbar), 

830 ), 

831 ], 

832 dont_extend_height=True, 

833 ) 

834 

835 def _handle_enter(self) -> None: 

836 if self.multiple_selection: 

837 val = self.values[self._selected_index][0] 

838 if val in self.current_values: 

839 self.current_values.remove(val) 

840 else: 

841 self.current_values.append(val) 

842 else: 

843 self.current_value = self.values[self._selected_index][0] 

844 

845 def _get_text_fragments(self) -> StyleAndTextTuples: 

846 def mouse_handler(mouse_event: MouseEvent) -> None: 

847 """ 

848 Set `_selected_index` and `current_value` according to the y 

849 position of the mouse click event. 

850 """ 

851 if mouse_event.event_type == MouseEventType.MOUSE_UP: 

852 self._selected_index = mouse_event.position.y 

853 self._handle_enter() 

854 

855 result: StyleAndTextTuples = [] 

856 for i, value in enumerate(self.values): 

857 if self.multiple_selection: 

858 checked = value[0] in self.current_values 

859 else: 

860 checked = value[0] == self.current_value 

861 selected = i == self._selected_index 

862 

863 style = "" 

864 if checked: 

865 style += " " + self.checked_style 

866 if selected: 

867 style += " " + self.selected_style 

868 

869 result.append((style, self.open_character)) 

870 

871 if selected: 

872 result.append(("[SetCursorPosition]", "")) 

873 

874 if checked: 

875 result.append((style, self.select_character)) 

876 else: 

877 result.append((style, " ")) 

878 

879 result.append((style, self.close_character)) 

880 result.append((f"{style} {self.default_style}", " ")) 

881 

882 if self.show_numbers: 

883 result.append((f"{style} {self.number_style}", f"{i + 1:2d}. ")) 

884 

885 result.extend( 

886 to_formatted_text(value[1], style=f"{style} {self.default_style}") 

887 ) 

888 result.append(("", "\n")) 

889 

890 # Add mouse handler to all fragments. 

891 for i in range(len(result)): 

892 result[i] = (result[i][0], result[i][1], mouse_handler) 

893 

894 result.pop() # Remove last newline. 

895 return result 

896 

897 def __pt_container__(self) -> Container: 

898 return self.window 

899 

900 

901class RadioList(_DialogList[_T]): 

902 """ 

903 List of radio buttons. Only one can be checked at the same time. 

904 

905 :param values: List of (value, label) tuples. 

906 """ 

907 

908 def __init__( 

909 self, 

910 values: Sequence[tuple[_T, AnyFormattedText]], 

911 default: _T | None = None, 

912 show_numbers: bool = False, 

913 select_on_focus: bool = False, 

914 open_character: str = "(", 

915 select_character: str = "*", 

916 close_character: str = ")", 

917 container_style: str = "class:radio-list", 

918 default_style: str = "class:radio", 

919 selected_style: str = "class:radio-selected", 

920 checked_style: str = "class:radio-checked", 

921 number_style: str = "class:radio-number", 

922 multiple_selection: bool = False, 

923 show_cursor: bool = True, 

924 show_scrollbar: bool = True, 

925 ) -> None: 

926 if default is None: 

927 default_values = None 

928 else: 

929 default_values = [default] 

930 

931 super().__init__( 

932 values, 

933 default_values=default_values, 

934 select_on_focus=select_on_focus, 

935 show_numbers=show_numbers, 

936 open_character=open_character, 

937 select_character=select_character, 

938 close_character=close_character, 

939 container_style=container_style, 

940 default_style=default_style, 

941 selected_style=selected_style, 

942 checked_style=checked_style, 

943 number_style=number_style, 

944 multiple_selection=False, 

945 show_cursor=show_cursor, 

946 show_scrollbar=show_scrollbar, 

947 ) 

948 

949 

950class CheckboxList(_DialogList[_T]): 

951 """ 

952 List of checkbox buttons. Several can be checked at the same time. 

953 

954 :param values: List of (value, label) tuples. 

955 """ 

956 

957 def __init__( 

958 self, 

959 values: Sequence[tuple[_T, AnyFormattedText]], 

960 default_values: Sequence[_T] | None = None, 

961 open_character: str = "[", 

962 select_character: str = "*", 

963 close_character: str = "]", 

964 container_style: str = "class:checkbox-list", 

965 default_style: str = "class:checkbox", 

966 selected_style: str = "class:checkbox-selected", 

967 checked_style: str = "class:checkbox-checked", 

968 ) -> None: 

969 super().__init__( 

970 values, 

971 default_values=default_values, 

972 open_character=open_character, 

973 select_character=select_character, 

974 close_character=close_character, 

975 container_style=container_style, 

976 default_style=default_style, 

977 selected_style=selected_style, 

978 checked_style=checked_style, 

979 multiple_selection=True, 

980 ) 

981 

982 

983class Checkbox(CheckboxList[str]): 

984 """Backward compatibility util: creates a 1-sized CheckboxList 

985 

986 :param text: the text 

987 """ 

988 

989 show_scrollbar = False 

990 

991 def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None: 

992 values = [("value", text)] 

993 super().__init__(values=values) 

994 self.checked = checked 

995 

996 @property 

997 def checked(self) -> bool: 

998 return "value" in self.current_values 

999 

1000 @checked.setter 

1001 def checked(self, value: bool) -> None: 

1002 if value: 

1003 self.current_values = ["value"] 

1004 else: 

1005 self.current_values = [] 

1006 

1007 

1008class VerticalLine: 

1009 """ 

1010 A simple vertical line with a width of 1. 

1011 """ 

1012 

1013 def __init__(self) -> None: 

1014 self.window = Window( 

1015 char=Border.VERTICAL, style="class:line,vertical-line", width=1 

1016 ) 

1017 

1018 def __pt_container__(self) -> Container: 

1019 return self.window 

1020 

1021 

1022class HorizontalLine: 

1023 """ 

1024 A simple horizontal line with a height of 1. 

1025 """ 

1026 

1027 def __init__(self) -> None: 

1028 self.window = Window( 

1029 char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1 

1030 ) 

1031 

1032 def __pt_container__(self) -> Container: 

1033 return self.window 

1034 

1035 

1036class ProgressBar: 

1037 def __init__(self) -> None: 

1038 self._percentage = 60 

1039 

1040 self.label = Label("60%") 

1041 self.container = FloatContainer( 

1042 content=Window(height=1), 

1043 floats=[ 

1044 # We first draw the label, then the actual progress bar. Right 

1045 # now, this is the only way to have the colors of the progress 

1046 # bar appear on top of the label. The problem is that our label 

1047 # can't be part of any `Window` below. 

1048 Float(content=self.label, top=0, bottom=0), 

1049 Float( 

1050 left=0, 

1051 top=0, 

1052 right=0, 

1053 bottom=0, 

1054 content=VSplit( 

1055 [ 

1056 Window( 

1057 style="class:progress-bar.used", 

1058 width=lambda: D(weight=int(self._percentage)), 

1059 ), 

1060 Window( 

1061 style="class:progress-bar", 

1062 width=lambda: D(weight=int(100 - self._percentage)), 

1063 ), 

1064 ] 

1065 ), 

1066 ), 

1067 ], 

1068 ) 

1069 

1070 @property 

1071 def percentage(self) -> int: 

1072 return self._percentage 

1073 

1074 @percentage.setter 

1075 def percentage(self, value: int) -> None: 

1076 self._percentage = value 

1077 self.label.text = f"{value}%" 

1078 

1079 def __pt_container__(self) -> Container: 

1080 return self.container