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 = self.width - (
440 get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)
441 ) + (
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(content=top_row_with_title, filter=has_title),
539 ConditionalContainer(content=top_row_without_title, filter=~has_title),
540 VSplit(
541 [
542 fill(width=1, char=Border.VERTICAL),
543 DynamicContainer(lambda: self.body),
544 fill(width=1, char=Border.VERTICAL),
545 # Padding is required to make sure that if the content is
546 # too small, the right frame border is still aligned.
547 ],
548 padding=0,
549 ),
550 VSplit(
551 [
552 fill(width=1, height=1, char=Border.BOTTOM_LEFT),
553 fill(char=Border.HORIZONTAL),
554 fill(width=1, height=1, char=Border.BOTTOM_RIGHT),
555 ],
556 # specifying height here will increase the rendering speed.
557 height=1,
558 ),
559 ],
560 width=width,
561 height=height,
562 style=style,
563 key_bindings=key_bindings,
564 modal=modal,
565 )
566
567 def __pt_container__(self) -> Container:
568 return self.container
569
570
571class Shadow:
572 """
573 Draw a shadow underneath/behind this container.
574 (This applies `class:shadow` the the cells under the shadow. The Style
575 should define the colors for the shadow.)
576
577 :param body: Another container object.
578 """
579
580 def __init__(self, body: AnyContainer) -> None:
581 self.container = FloatContainer(
582 content=body,
583 floats=[
584 Float(
585 bottom=-1,
586 height=1,
587 left=1,
588 right=-1,
589 transparent=True,
590 content=Window(style="class:shadow"),
591 ),
592 Float(
593 bottom=-1,
594 top=1,
595 width=1,
596 right=-1,
597 transparent=True,
598 content=Window(style="class:shadow"),
599 ),
600 ],
601 )
602
603 def __pt_container__(self) -> Container:
604 return self.container
605
606
607class Box:
608 """
609 Add padding around a container.
610
611 This also makes sure that the parent can provide more space than required by
612 the child. This is very useful when wrapping a small element with a fixed
613 size into a ``VSplit`` or ``HSplit`` object. The ``HSplit`` and ``VSplit``
614 try to make sure to adapt respectively the width and height, possibly
615 shrinking other elements. Wrapping something in a ``Box`` makes it flexible.
616
617 :param body: Another container object.
618 :param padding: The margin to be used around the body. This can be
619 overridden by `padding_left`, padding_right`, `padding_top` and
620 `padding_bottom`.
621 :param style: A style string.
622 :param char: Character to be used for filling the space around the body.
623 (This is supposed to be a character with a terminal width of 1.)
624 """
625
626 def __init__(
627 self,
628 body: AnyContainer,
629 padding: AnyDimension = None,
630 padding_left: AnyDimension = None,
631 padding_right: AnyDimension = None,
632 padding_top: AnyDimension = None,
633 padding_bottom: AnyDimension = None,
634 width: AnyDimension = None,
635 height: AnyDimension = None,
636 style: str = "",
637 char: None | str | Callable[[], str] = None,
638 modal: bool = False,
639 key_bindings: KeyBindings | None = None,
640 ) -> None:
641 self.padding = padding
642 self.padding_left = padding_left
643 self.padding_right = padding_right
644 self.padding_top = padding_top
645 self.padding_bottom = padding_bottom
646 self.body = body
647
648 def left() -> AnyDimension:
649 if self.padding_left is None:
650 return self.padding
651 return self.padding_left
652
653 def right() -> AnyDimension:
654 if self.padding_right is None:
655 return self.padding
656 return self.padding_right
657
658 def top() -> AnyDimension:
659 if self.padding_top is None:
660 return self.padding
661 return self.padding_top
662
663 def bottom() -> AnyDimension:
664 if self.padding_bottom is None:
665 return self.padding
666 return self.padding_bottom
667
668 self.container = HSplit(
669 [
670 Window(height=top, char=char),
671 VSplit(
672 [
673 Window(width=left, char=char),
674 body,
675 Window(width=right, char=char),
676 ]
677 ),
678 Window(height=bottom, char=char),
679 ],
680 width=width,
681 height=height,
682 style=style,
683 modal=modal,
684 key_bindings=None,
685 )
686
687 def __pt_container__(self) -> Container:
688 return self.container
689
690
691_T = TypeVar("_T")
692
693
694class _DialogList(Generic[_T]):
695 """
696 Common code for `RadioList` and `CheckboxList`.
697 """
698
699 open_character: str = ""
700 close_character: str = ""
701 container_style: str = ""
702 default_style: str = ""
703 selected_style: str = ""
704 checked_style: str = ""
705 multiple_selection: bool = False
706 show_scrollbar: bool = True
707
708 def __init__(
709 self,
710 values: Sequence[tuple[_T, AnyFormattedText]],
711 default_values: Sequence[_T] | None = None,
712 ) -> None:
713 assert len(values) > 0
714 default_values = default_values or []
715
716 self.values = values
717 # current_values will be used in multiple_selection,
718 # current_value will be used otherwise.
719 keys: list[_T] = [value for (value, _) in values]
720 self.current_values: list[_T] = [
721 value for value in default_values if value in keys
722 ]
723 self.current_value: _T = (
724 default_values[0]
725 if len(default_values) and default_values[0] in keys
726 else values[0][0]
727 )
728
729 # Cursor index: take first selected item or first item otherwise.
730 if len(self.current_values) > 0:
731 self._selected_index = keys.index(self.current_values[0])
732 else:
733 self._selected_index = 0
734
735 # Key bindings.
736 kb = KeyBindings()
737
738 @kb.add("up")
739 def _up(event: E) -> None:
740 self._selected_index = max(0, self._selected_index - 1)
741
742 @kb.add("down")
743 def _down(event: E) -> None:
744 self._selected_index = min(len(self.values) - 1, self._selected_index + 1)
745
746 @kb.add("pageup")
747 def _pageup(event: E) -> None:
748 w = event.app.layout.current_window
749 if w.render_info:
750 self._selected_index = max(
751 0, self._selected_index - len(w.render_info.displayed_lines)
752 )
753
754 @kb.add("pagedown")
755 def _pagedown(event: E) -> None:
756 w = event.app.layout.current_window
757 if w.render_info:
758 self._selected_index = min(
759 len(self.values) - 1,
760 self._selected_index + len(w.render_info.displayed_lines),
761 )
762
763 @kb.add("enter")
764 @kb.add(" ")
765 def _click(event: E) -> None:
766 self._handle_enter()
767
768 @kb.add(Keys.Any)
769 def _find(event: E) -> None:
770 # We first check values after the selected value, then all values.
771 values = list(self.values)
772 for value in values[self._selected_index + 1 :] + values:
773 text = fragment_list_to_text(to_formatted_text(value[1])).lower()
774
775 if text.startswith(event.data.lower()):
776 self._selected_index = self.values.index(value)
777 return
778
779 # Control and window.
780 self.control = FormattedTextControl(
781 self._get_text_fragments, key_bindings=kb, focusable=True
782 )
783
784 self.window = Window(
785 content=self.control,
786 style=self.container_style,
787 right_margins=[
788 ConditionalMargin(
789 margin=ScrollbarMargin(display_arrows=True),
790 filter=Condition(lambda: self.show_scrollbar),
791 ),
792 ],
793 dont_extend_height=True,
794 )
795
796 def _handle_enter(self) -> None:
797 if self.multiple_selection:
798 val = self.values[self._selected_index][0]
799 if val in self.current_values:
800 self.current_values.remove(val)
801 else:
802 self.current_values.append(val)
803 else:
804 self.current_value = self.values[self._selected_index][0]
805
806 def _get_text_fragments(self) -> StyleAndTextTuples:
807 def mouse_handler(mouse_event: MouseEvent) -> None:
808 """
809 Set `_selected_index` and `current_value` according to the y
810 position of the mouse click event.
811 """
812 if mouse_event.event_type == MouseEventType.MOUSE_UP:
813 self._selected_index = mouse_event.position.y
814 self._handle_enter()
815
816 result: StyleAndTextTuples = []
817 for i, value in enumerate(self.values):
818 if self.multiple_selection:
819 checked = value[0] in self.current_values
820 else:
821 checked = value[0] == self.current_value
822 selected = i == self._selected_index
823
824 style = ""
825 if checked:
826 style += " " + self.checked_style
827 if selected:
828 style += " " + self.selected_style
829
830 result.append((style, self.open_character))
831
832 if selected:
833 result.append(("[SetCursorPosition]", ""))
834
835 if checked:
836 result.append((style, "*"))
837 else:
838 result.append((style, " "))
839
840 result.append((style, self.close_character))
841 result.append((self.default_style, " "))
842 result.extend(to_formatted_text(value[1], style=self.default_style))
843 result.append(("", "\n"))
844
845 # Add mouse handler to all fragments.
846 for i in range(len(result)):
847 result[i] = (result[i][0], result[i][1], mouse_handler)
848
849 result.pop() # Remove last newline.
850 return result
851
852 def __pt_container__(self) -> Container:
853 return self.window
854
855
856class RadioList(_DialogList[_T]):
857 """
858 List of radio buttons. Only one can be checked at the same time.
859
860 :param values: List of (value, label) tuples.
861 """
862
863 open_character = "("
864 close_character = ")"
865 container_style = "class:radio-list"
866 default_style = "class:radio"
867 selected_style = "class:radio-selected"
868 checked_style = "class:radio-checked"
869 multiple_selection = False
870
871 def __init__(
872 self,
873 values: Sequence[tuple[_T, AnyFormattedText]],
874 default: _T | None = None,
875 ) -> None:
876 if default is None:
877 default_values = None
878 else:
879 default_values = [default]
880
881 super().__init__(values, default_values=default_values)
882
883
884class CheckboxList(_DialogList[_T]):
885 """
886 List of checkbox buttons. Several can be checked at the same time.
887
888 :param values: List of (value, label) tuples.
889 """
890
891 open_character = "["
892 close_character = "]"
893 container_style = "class:checkbox-list"
894 default_style = "class:checkbox"
895 selected_style = "class:checkbox-selected"
896 checked_style = "class:checkbox-checked"
897 multiple_selection = True
898
899
900class Checkbox(CheckboxList[str]):
901 """Backward compatibility util: creates a 1-sized CheckboxList
902
903 :param text: the text
904 """
905
906 show_scrollbar = False
907
908 def __init__(self, text: AnyFormattedText = "", checked: bool = False) -> None:
909 values = [("value", text)]
910 super().__init__(values=values)
911 self.checked = checked
912
913 @property
914 def checked(self) -> bool:
915 return "value" in self.current_values
916
917 @checked.setter
918 def checked(self, value: bool) -> None:
919 if value:
920 self.current_values = ["value"]
921 else:
922 self.current_values = []
923
924
925class VerticalLine:
926 """
927 A simple vertical line with a width of 1.
928 """
929
930 def __init__(self) -> None:
931 self.window = Window(
932 char=Border.VERTICAL, style="class:line,vertical-line", width=1
933 )
934
935 def __pt_container__(self) -> Container:
936 return self.window
937
938
939class HorizontalLine:
940 """
941 A simple horizontal line with a height of 1.
942 """
943
944 def __init__(self) -> None:
945 self.window = Window(
946 char=Border.HORIZONTAL, style="class:line,horizontal-line", height=1
947 )
948
949 def __pt_container__(self) -> Container:
950 return self.window
951
952
953class ProgressBar:
954 def __init__(self) -> None:
955 self._percentage = 60
956
957 self.label = Label("60%")
958 self.container = FloatContainer(
959 content=Window(height=1),
960 floats=[
961 # We first draw the label, then the actual progress bar. Right
962 # now, this is the only way to have the colors of the progress
963 # bar appear on top of the label. The problem is that our label
964 # can't be part of any `Window` below.
965 Float(content=self.label, top=0, bottom=0),
966 Float(
967 left=0,
968 top=0,
969 right=0,
970 bottom=0,
971 content=VSplit(
972 [
973 Window(
974 style="class:progress-bar.used",
975 width=lambda: D(weight=int(self._percentage)),
976 ),
977 Window(
978 style="class:progress-bar",
979 width=lambda: D(weight=int(100 - self._percentage)),
980 ),
981 ]
982 ),
983 ),
984 ],
985 )
986
987 @property
988 def percentage(self) -> int:
989 return self._percentage
990
991 @percentage.setter
992 def percentage(self, value: int) -> None:
993 self._percentage = value
994 self.label.text = f"{value}%"
995
996 def __pt_container__(self) -> Container:
997 return self.container