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