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

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

335 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 collections.abc import Callable, Sequence 

19from functools import partial 

20from typing import Generic, TypeVar 

21 

22from prompt_toolkit.application.current import get_app 

23from prompt_toolkit.auto_suggest import AutoSuggest, DynamicAutoSuggest 

24from prompt_toolkit.buffer import Buffer, BufferAcceptHandler 

25from prompt_toolkit.completion import Completer, DynamicCompleter 

26from prompt_toolkit.document import Document 

27from prompt_toolkit.filters import ( 

28 Condition, 

29 FilterOrBool, 

30 has_focus, 

31 is_done, 

32 is_true, 

33 to_filter, 

34) 

35from prompt_toolkit.formatted_text import ( 

36 AnyFormattedText, 

37 StyleAndTextTuples, 

38 Template, 

39 to_formatted_text, 

40) 

41from prompt_toolkit.formatted_text.utils import fragment_list_to_text 

42from prompt_toolkit.history import History 

43from prompt_toolkit.key_binding.key_bindings import KeyBindings 

44from prompt_toolkit.key_binding.key_processor import KeyPressEvent 

45from prompt_toolkit.keys import Keys 

46from prompt_toolkit.layout.containers import ( 

47 AnyContainer, 

48 ConditionalContainer, 

49 Container, 

50 DynamicContainer, 

51 Float, 

52 FloatContainer, 

53 HSplit, 

54 VSplit, 

55 Window, 

56 WindowAlign, 

57) 

58from prompt_toolkit.layout.controls import ( 

59 BufferControl, 

60 FormattedTextControl, 

61 GetLinePrefixCallable, 

62) 

63from prompt_toolkit.layout.dimension import AnyDimension 

64from prompt_toolkit.layout.dimension import Dimension as D 

65from prompt_toolkit.layout.margins import ( 

66 ConditionalMargin, 

67 NumberedMargin, 

68 ScrollbarMargin, 

69) 

70from prompt_toolkit.layout.processors import ( 

71 AppendAutoSuggestion, 

72 BeforeInput, 

73 ConditionalProcessor, 

74 PasswordProcessor, 

75 Processor, 

76) 

77from prompt_toolkit.lexers import DynamicLexer, Lexer 

78from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 

79from prompt_toolkit.utils import get_cwidth 

80from prompt_toolkit.validation import DynamicValidator, Validator 

81 

82from .toolbars import SearchToolbar 

83 

84__all__ = [ 

85 "TextArea", 

86 "Label", 

87 "Button", 

88 "Frame", 

89 "Shadow", 

90 "Box", 

91 "VerticalLine", 

92 "HorizontalLine", 

93 "RadioList", 

94 "CheckboxList", 

95 "Checkbox", # backward compatibility 

96 "ProgressBar", 

97] 

98 

99E = KeyPressEvent 

100 

101 

102class Border: 

103 "Box drawing characters. (Thin)" 

104 

105 HORIZONTAL = "\u2500" 

106 VERTICAL = "\u2502" 

107 TOP_LEFT = "\u250c" 

108 TOP_RIGHT = "\u2510" 

109 BOTTOM_LEFT = "\u2514" 

110 BOTTOM_RIGHT = "\u2518" 

111 

112 

113class TextArea: 

114 """ 

115 A simple input field. 

116 

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

118 sane defaults. 

119 

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

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

122 always build a text area manually, using a 

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

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

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

126 

127 Buffer attributes: 

128 

129 :param text: The initial text. 

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

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

132 for auto completion. 

133 :param complete_while_typing: Boolean. 

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

135 callable that takes a buffer as input). 

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

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

138 instance for input suggestions. 

139 

140 BufferControl attributes: 

141 

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

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

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

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

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

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

148 object. 

149 

150 Window attributes: 

151 

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

153 highlighting. 

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

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

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

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

158 :param style: A style string. 

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

160 preferred width reported by the control. 

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

162 preferred height reported by the control. 

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

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

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

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

167 so on. 

168 

169 Other attributes: 

170 

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

172 """ 

173 

174 def __init__( 

175 self, 

176 text: str = "", 

177 multiline: FilterOrBool = True, 

178 password: FilterOrBool = False, 

179 lexer: Lexer | None = None, 

180 auto_suggest: AutoSuggest | None = None, 

181 completer: Completer | None = None, 

182 complete_while_typing: FilterOrBool = True, 

183 validator: Validator | None = None, 

184 accept_handler: BufferAcceptHandler | None = None, 

185 history: History | None = None, 

186 focusable: FilterOrBool = True, 

187 focus_on_click: FilterOrBool = False, 

188 wrap_lines: FilterOrBool = True, 

189 read_only: FilterOrBool = False, 

190 width: AnyDimension = None, 

191 height: AnyDimension = None, 

192 dont_extend_height: FilterOrBool = False, 

193 dont_extend_width: FilterOrBool = False, 

194 line_numbers: bool = False, 

195 get_line_prefix: GetLinePrefixCallable | None = None, 

196 scrollbar: bool = False, 

197 style: str = "", 

198 search_field: SearchToolbar | None = None, 

199 preview_search: FilterOrBool = True, 

200 prompt: AnyFormattedText = "", 

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

202 name: str = "", 

203 ) -> None: 

204 if search_field is None: 

205 search_control = None 

206 elif isinstance(search_field, SearchToolbar): 

207 search_control = search_field.control 

208 

209 if input_processors is None: 

210 input_processors = [] 

211 

212 # Writeable attributes. 

213 self.completer = completer 

214 self.complete_while_typing = complete_while_typing 

215 self.lexer = lexer 

216 self.auto_suggest = auto_suggest 

217 self.read_only = read_only 

218 self.wrap_lines = wrap_lines 

219 self.validator = validator 

220 

221 self.buffer = Buffer( 

222 document=Document(text, 0), 

223 multiline=multiline, 

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

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

226 complete_while_typing=Condition( 

227 lambda: is_true(self.complete_while_typing) 

228 ), 

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

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

231 accept_handler=accept_handler, 

232 history=history, 

233 name=name, 

234 ) 

235 

236 self.control = BufferControl( 

237 buffer=self.buffer, 

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

239 input_processors=[ 

240 ConditionalProcessor( 

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

242 ), 

243 ConditionalProcessor( 

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

245 ), 

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

247 ] 

248 + input_processors, 

249 search_buffer_control=search_control, 

250 preview_search=preview_search, 

251 focusable=focusable, 

252 focus_on_click=focus_on_click, 

253 ) 

254 

255 if multiline: 

256 if scrollbar: 

257 right_margins = [ScrollbarMargin(display_arrows=True)] 

258 else: 

259 right_margins = [] 

260 if line_numbers: 

261 left_margins = [NumberedMargin()] 

262 else: 

263 left_margins = [] 

264 else: 

265 height = D.exact(1) 

266 left_margins = [] 

267 right_margins = [] 

268 

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

270 

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

272 if height is None: 

273 height = D(min=1) 

274 

275 self.window = Window( 

276 height=height, 

277 width=width, 

278 dont_extend_height=dont_extend_height, 

279 dont_extend_width=dont_extend_width, 

280 content=self.control, 

281 style=style, 

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

283 left_margins=left_margins, 

284 right_margins=right_margins, 

285 get_line_prefix=get_line_prefix, 

286 ) 

287 

288 @property 

289 def text(self) -> str: 

290 """ 

291 The `Buffer` text. 

292 """ 

293 return self.buffer.text 

294 

295 @text.setter 

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

297 self.document = Document(value, 0) 

298 

299 @property 

300 def document(self) -> Document: 

301 """ 

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

303 """ 

304 return self.buffer.document 

305 

306 @document.setter 

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

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

309 

310 @property 

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

312 """ 

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

314 """ 

315 return self.buffer.accept_handler 

316 

317 @accept_handler.setter 

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

319 self.buffer.accept_handler = value 

320 

321 def __pt_container__(self) -> Container: 

322 return self.window 

323 

324 

325class Label: 

326 """ 

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

328 

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

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

331 including a callable. 

332 :param style: A style string. 

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

334 the text size. 

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

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

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

338 given. `True` by default 

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

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

341 the text. `False` by default. 

342 """ 

343 

344 def __init__( 

345 self, 

346 text: AnyFormattedText, 

347 style: str = "", 

348 width: AnyDimension = None, 

349 dont_extend_height: bool = True, 

350 dont_extend_width: bool = False, 

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

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

353 # wrap lines by default. 

354 wrap_lines: FilterOrBool = True, 

355 ) -> None: 

356 self.text = text 

357 

358 def get_width() -> AnyDimension: 

359 if width is None: 

360 text_fragments = to_formatted_text(self.text) 

361 text = fragment_list_to_text(text_fragments) 

362 if text: 

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

364 else: 

365 return D(preferred=0) 

366 return D(preferred=longest_line) 

367 else: 

368 return width 

369 

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

371 

372 self.window = Window( 

373 content=self.formatted_text_control, 

374 width=get_width, 

375 height=D(min=1), 

376 style="class:label " + style, 

377 dont_extend_height=dont_extend_height, 

378 dont_extend_width=dont_extend_width, 

379 align=align, 

380 wrap_lines=wrap_lines, 

381 ) 

382 

383 def __pt_container__(self) -> Container: 

384 return self.window 

385 

386 

387class Button: 

388 """ 

389 Clickable button. 

390 

391 :param text: The caption for the button. 

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

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

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

395 :param width: Width of the button. 

396 """ 

397 

398 def __init__( 

399 self, 

400 text: str, 

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

402 width: int = 12, 

403 left_symbol: str = "<", 

404 right_symbol: str = ">", 

405 ) -> None: 

406 self.text = text 

407 self.left_symbol = left_symbol 

408 self.right_symbol = right_symbol 

409 self.handler = handler 

410 self.width = width 

411 self.control = FormattedTextControl( 

412 self._get_text_fragments, 

413 key_bindings=self._get_key_bindings(), 

414 focusable=True, 

415 ) 

416 

417 def get_style() -> str: 

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

419 return "class:button.focused" 

420 else: 

421 return "class:button" 

422 

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

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

425 # Otherwise, we will also truncate the text. 

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

427 # button to the text. 

428 

429 self.window = Window( 

430 self.control, 

431 align=WindowAlign.CENTER, 

432 height=1, 

433 width=width, 

434 style=get_style, 

435 dont_extend_width=False, 

436 dont_extend_height=True, 

437 ) 

438 

439 def _get_text_fragments(self) -> StyleAndTextTuples: 

440 width = ( 

441 self.width 

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

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

444 ) 

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

446 

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

448 if ( 

449 self.handler is not None 

450 and mouse_event.event_type == MouseEventType.MOUSE_UP 

451 ): 

452 self.handler() 

453 

454 return [ 

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

456 ("[SetCursorPosition]", ""), 

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

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

459 ] 

460 

461 def _get_key_bindings(self) -> KeyBindings: 

462 "Key bindings for the Button." 

463 kb = KeyBindings() 

464 

465 @kb.add(" ") 

466 @kb.add("enter") 

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

468 if self.handler is not None: 

469 self.handler() 

470 

471 return kb 

472 

473 def __pt_container__(self) -> Container: 

474 return self.window 

475 

476 

477class Frame: 

478 """ 

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

480 

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

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

483 

484 :param body: Another container object. 

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

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

487 """ 

488 

489 def __init__( 

490 self, 

491 body: AnyContainer, 

492 title: AnyFormattedText = "", 

493 style: str = "", 

494 width: AnyDimension = None, 

495 height: AnyDimension = None, 

496 key_bindings: KeyBindings | None = None, 

497 modal: bool = False, 

498 ) -> None: 

499 self.title = title 

500 self.body = body 

501 

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

503 style = "class:frame " + style 

504 

505 top_row_with_title = VSplit( 

506 [ 

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

508 fill(char=Border.HORIZONTAL), 

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

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

511 # `HTML` object for instance. 

512 Label( 

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

514 style="class:frame.label", 

515 dont_extend_width=True, 

516 ), 

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

518 fill(char=Border.HORIZONTAL), 

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

520 ], 

521 height=1, 

522 ) 

523 

524 top_row_without_title = VSplit( 

525 [ 

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

527 fill(char=Border.HORIZONTAL), 

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

529 ], 

530 height=1, 

531 ) 

532 

533 @Condition 

534 def has_title() -> bool: 

535 return bool(self.title) 

536 

537 self.container = HSplit( 

538 [ 

539 ConditionalContainer( 

540 content=top_row_with_title, 

541 filter=has_title, 

542 alternative_content=top_row_without_title, 

543 ), 

544 VSplit( 

545 [ 

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

547 DynamicContainer(lambda: self.body), 

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

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

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

551 ], 

552 padding=0, 

553 ), 

554 VSplit( 

555 [ 

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

557 fill(char=Border.HORIZONTAL), 

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

559 ], 

560 # specifying height here will increase the rendering speed. 

561 height=1, 

562 ), 

563 ], 

564 width=width, 

565 height=height, 

566 style=style, 

567 key_bindings=key_bindings, 

568 modal=modal, 

569 ) 

570 

571 def __pt_container__(self) -> Container: 

572 return self.container 

573 

574 

575class Shadow: 

576 """ 

577 Draw a shadow underneath/behind this container. 

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

579 should define the colors for the shadow.) 

580 

581 :param body: Another container object. 

582 """ 

583 

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

585 self.container = FloatContainer( 

586 content=body, 

587 floats=[ 

588 Float( 

589 bottom=-1, 

590 height=1, 

591 left=1, 

592 right=-1, 

593 transparent=True, 

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

595 ), 

596 Float( 

597 bottom=-1, 

598 top=1, 

599 width=1, 

600 right=-1, 

601 transparent=True, 

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

603 ), 

604 ], 

605 ) 

606 

607 def __pt_container__(self) -> Container: 

608 return self.container 

609 

610 

611class Box: 

612 """ 

613 Add padding around a container. 

614 

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

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

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

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

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

620 

621 :param body: Another container object. 

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

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

624 `padding_bottom`. 

625 :param style: A style string. 

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

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

628 """ 

629 

630 def __init__( 

631 self, 

632 body: AnyContainer, 

633 padding: AnyDimension = None, 

634 padding_left: AnyDimension = None, 

635 padding_right: AnyDimension = None, 

636 padding_top: AnyDimension = None, 

637 padding_bottom: AnyDimension = None, 

638 width: AnyDimension = None, 

639 height: AnyDimension = None, 

640 style: str = "", 

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

642 modal: bool = False, 

643 key_bindings: KeyBindings | None = None, 

644 ) -> None: 

645 self.padding = padding 

646 self.padding_left = padding_left 

647 self.padding_right = padding_right 

648 self.padding_top = padding_top 

649 self.padding_bottom = padding_bottom 

650 self.body = body 

651 

652 def left() -> AnyDimension: 

653 if self.padding_left is None: 

654 return self.padding 

655 return self.padding_left 

656 

657 def right() -> AnyDimension: 

658 if self.padding_right is None: 

659 return self.padding 

660 return self.padding_right 

661 

662 def top() -> AnyDimension: 

663 if self.padding_top is None: 

664 return self.padding 

665 return self.padding_top 

666 

667 def bottom() -> AnyDimension: 

668 if self.padding_bottom is None: 

669 return self.padding 

670 return self.padding_bottom 

671 

672 self.container = HSplit( 

673 [ 

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

675 VSplit( 

676 [ 

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

678 body, 

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

680 ] 

681 ), 

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

683 ], 

684 width=width, 

685 height=height, 

686 style=style, 

687 modal=modal, 

688 key_bindings=None, 

689 ) 

690 

691 def __pt_container__(self) -> Container: 

692 return self.container 

693 

694 

695_T = TypeVar("_T") 

696 

697 

698class _DialogList(Generic[_T]): 

699 """ 

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

701 """ 

702 

703 def __init__( 

704 self, 

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

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

707 select_on_focus: bool = False, 

708 open_character: str = "", 

709 select_character: str = "*", 

710 close_character: str = "", 

711 container_style: str = "", 

712 default_style: str = "", 

713 number_style: str = "", 

714 selected_style: str = "", 

715 checked_style: str = "", 

716 multiple_selection: bool = False, 

717 show_scrollbar: bool = True, 

718 show_cursor: bool = True, 

719 show_numbers: bool = False, 

720 ) -> None: 

721 assert len(values) > 0 

722 default_values = default_values or [] 

723 

724 self.values = values 

725 self.show_numbers = show_numbers 

726 

727 self.open_character = open_character 

728 self.select_character = select_character 

729 self.close_character = close_character 

730 self.container_style = container_style 

731 self.default_style = default_style 

732 self.number_style = number_style 

733 self.selected_style = selected_style 

734 self.checked_style = checked_style 

735 self.multiple_selection = multiple_selection 

736 self.show_scrollbar = show_scrollbar 

737 

738 # current_values will be used in multiple_selection, 

739 # current_value will be used otherwise. 

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

741 self.current_values: list[_T] = [ 

742 value for value in default_values if value in keys 

743 ] 

744 self.current_value: _T = ( 

745 default_values[0] 

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

747 else values[0][0] 

748 ) 

749 

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

751 if len(self.current_values) > 0: 

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

753 else: 

754 self._selected_index = 0 

755 

756 # Key bindings. 

757 kb = KeyBindings() 

758 

759 @kb.add("up") 

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

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

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

763 if select_on_focus: 

764 self._handle_enter() 

765 

766 @kb.add("down") 

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

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

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

770 if select_on_focus: 

771 self._handle_enter() 

772 

773 @kb.add("pageup") 

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

775 w = event.app.layout.current_window 

776 if w.render_info: 

777 self._selected_index = max( 

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

779 ) 

780 

781 @kb.add("pagedown") 

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

783 w = event.app.layout.current_window 

784 if w.render_info: 

785 self._selected_index = min( 

786 len(self.values) - 1, 

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

788 ) 

789 

790 @kb.add("enter") 

791 @kb.add(" ") 

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

793 self._handle_enter() 

794 

795 @kb.add(Keys.Any) 

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

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

798 values = list(self.values) 

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

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

801 

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

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

804 return 

805 

806 numbers_visible = Condition(lambda: self.show_numbers) 

807 

808 for i in range(1, 10): 

809 

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

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

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

813 if select_on_focus: 

814 self._handle_enter() 

815 

816 # Control and window. 

817 self.control = FormattedTextControl( 

818 self._get_text_fragments, 

819 key_bindings=kb, 

820 focusable=True, 

821 show_cursor=show_cursor, 

822 ) 

823 

824 self.window = Window( 

825 content=self.control, 

826 style=self.container_style, 

827 right_margins=[ 

828 ConditionalMargin( 

829 margin=ScrollbarMargin(display_arrows=True), 

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

831 ), 

832 ], 

833 dont_extend_height=True, 

834 ) 

835 

836 def _handle_enter(self) -> None: 

837 if self.multiple_selection: 

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

839 if val in self.current_values: 

840 self.current_values.remove(val) 

841 else: 

842 self.current_values.append(val) 

843 else: 

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

845 

846 def _get_text_fragments(self) -> StyleAndTextTuples: 

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

848 """ 

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

850 position of the mouse click event. 

851 """ 

852 if mouse_event.event_type == MouseEventType.MOUSE_UP: 

853 self._selected_index = mouse_event.position.y 

854 self._handle_enter() 

855 

856 result: StyleAndTextTuples = [] 

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

858 if self.multiple_selection: 

859 checked = value[0] in self.current_values 

860 else: 

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

862 selected = i == self._selected_index 

863 

864 style = "" 

865 if checked: 

866 style += " " + self.checked_style 

867 if selected: 

868 style += " " + self.selected_style 

869 

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

871 

872 if selected: 

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

874 

875 if checked: 

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

877 else: 

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

879 

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

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

882 

883 if self.show_numbers: 

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

885 

886 result.extend( 

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

888 ) 

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

890 

891 # Add mouse handler to all fragments. 

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

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

894 

895 result.pop() # Remove last newline. 

896 return result 

897 

898 def __pt_container__(self) -> Container: 

899 return self.window 

900 

901 

902class RadioList(_DialogList[_T]): 

903 """ 

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

905 

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

907 """ 

908 

909 def __init__( 

910 self, 

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

912 default: _T | None = None, 

913 show_numbers: bool = False, 

914 select_on_focus: bool = False, 

915 open_character: str = "(", 

916 select_character: str = "*", 

917 close_character: str = ")", 

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

919 default_style: str = "class:radio", 

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

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

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

923 multiple_selection: bool = False, 

924 show_cursor: bool = True, 

925 show_scrollbar: bool = True, 

926 ) -> None: 

927 if default is None: 

928 default_values = None 

929 else: 

930 default_values = [default] 

931 

932 super().__init__( 

933 values, 

934 default_values=default_values, 

935 select_on_focus=select_on_focus, 

936 show_numbers=show_numbers, 

937 open_character=open_character, 

938 select_character=select_character, 

939 close_character=close_character, 

940 container_style=container_style, 

941 default_style=default_style, 

942 selected_style=selected_style, 

943 checked_style=checked_style, 

944 number_style=number_style, 

945 multiple_selection=False, 

946 show_cursor=show_cursor, 

947 show_scrollbar=show_scrollbar, 

948 ) 

949 

950 

951class CheckboxList(_DialogList[_T]): 

952 """ 

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

954 

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

956 """ 

957 

958 def __init__( 

959 self, 

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

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

962 open_character: str = "[", 

963 select_character: str = "*", 

964 close_character: str = "]", 

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

966 default_style: str = "class:checkbox", 

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

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

969 ) -> None: 

970 super().__init__( 

971 values, 

972 default_values=default_values, 

973 open_character=open_character, 

974 select_character=select_character, 

975 close_character=close_character, 

976 container_style=container_style, 

977 default_style=default_style, 

978 selected_style=selected_style, 

979 checked_style=checked_style, 

980 multiple_selection=True, 

981 ) 

982 

983 

984class Checkbox(CheckboxList[str]): 

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

986 

987 :param text: the text 

988 """ 

989 

990 show_scrollbar = False 

991 

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

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

994 super().__init__(values=values) 

995 self.checked = checked 

996 

997 @property 

998 def checked(self) -> bool: 

999 return "value" in self.current_values 

1000 

1001 @checked.setter 

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

1003 if value: 

1004 self.current_values = ["value"] 

1005 else: 

1006 self.current_values = [] 

1007 

1008 

1009class VerticalLine: 

1010 """ 

1011 A simple vertical line with a width of 1. 

1012 """ 

1013 

1014 def __init__(self) -> None: 

1015 self.window = Window( 

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

1017 ) 

1018 

1019 def __pt_container__(self) -> Container: 

1020 return self.window 

1021 

1022 

1023class HorizontalLine: 

1024 """ 

1025 A simple horizontal line with a height of 1. 

1026 """ 

1027 

1028 def __init__(self) -> None: 

1029 self.window = Window( 

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

1031 ) 

1032 

1033 def __pt_container__(self) -> Container: 

1034 return self.window 

1035 

1036 

1037class ProgressBar: 

1038 def __init__(self) -> None: 

1039 self._percentage = 60 

1040 

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

1042 self.container = FloatContainer( 

1043 content=Window(height=1), 

1044 floats=[ 

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

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

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

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

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

1050 Float( 

1051 left=0, 

1052 top=0, 

1053 right=0, 

1054 bottom=0, 

1055 content=VSplit( 

1056 [ 

1057 Window( 

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

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

1060 ), 

1061 Window( 

1062 style="class:progress-bar", 

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

1064 ), 

1065 ] 

1066 ), 

1067 ), 

1068 ], 

1069 ) 

1070 

1071 @property 

1072 def percentage(self) -> int: 

1073 return self._percentage 

1074 

1075 @percentage.setter 

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

1077 self._percentage = value 

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

1079 

1080 def __pt_container__(self) -> Container: 

1081 return self.container