pytermgui.widgets.base

The basic building blocks making up the Widget system.

  1"""
  2The basic building blocks making up the Widget system.
  3"""
  4
  5# The classes defined here need more than 7 instance attributes,
  6# and there is no cyclic import during runtime.
  7# pylint: disable=too-many-instance-attributes, cyclic-import
  8
  9from __future__ import annotations
 10
 11from copy import deepcopy
 12from inspect import signature
 13from typing import Any, Callable, Generator, Iterator, Optional, Type, Union
 14
 15from ..ansi_interface import MouseEvent, reset
 16from ..enums import HorizontalAlignment, SizePolicy, WidgetChange
 17from ..helpers import break_line
 18from ..input import keys
 19from ..parser import markup
 20from ..regex import real_length
 21from ..terminal import Terminal, get_terminal
 22from . import styles as w_styles
 23
 24__all__ = ["Widget", "Label"]
 25
 26BoundCallback = Callable[..., Any]
 27WidgetType = Union["Widget", Type["Widget"]]
 28
 29
 30def _set_obj_or_cls_style(
 31    obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.StyleType
 32) -> Type[Widget] | Widget:
 33    """Sets a style for an object or class
 34
 35    Args:
 36        obj_or_cls: The Widget instance or type to update.
 37        key: The style key.
 38        value: The new style.
 39
 40    Returns:
 41        Type[Widget] | Widget: The updated class.
 42
 43    Raises:
 44        See `pytermgui.widgets.styles.StyleManager`.
 45    """
 46
 47    obj_or_cls.styles[key] = value
 48
 49    return obj_or_cls
 50
 51
 52def _set_obj_or_cls_char(
 53    obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.CharType
 54) -> Type[Widget] | Widget:
 55    """Sets a char for an object or class
 56
 57    Args:
 58        obj_or_cls: The Widget instance or type to update.
 59        key: The char key.
 60        value: The new char.
 61
 62    Returns:
 63        Type[Widget] | Widget: The updated class.
 64
 65    Raises:
 66        KeyError: The char key provided is invalid.
 67    """
 68
 69    if not key in obj_or_cls.chars.keys():
 70        raise KeyError(f"Char {key} is not valid for {obj_or_cls}!")
 71
 72    obj_or_cls.chars[key] = value
 73
 74    return obj_or_cls
 75
 76
 77class Widget:  # pylint: disable=too-many-public-methods
 78    """The base of the Widget system"""
 79
 80    set_style = classmethod(_set_obj_or_cls_style)
 81    set_char = classmethod(_set_obj_or_cls_char)
 82
 83    styles = w_styles.StyleManager()
 84    """Default styles for this class"""
 85
 86    chars: dict[str, w_styles.CharType] = {}
 87    """Default characters for this class"""
 88
 89    keys: dict[str, set[str]] = {}
 90    """Groups of keys that are used in `handle_key`"""
 91
 92    serialized: list[str] = [
 93        "id",
 94        "pos",
 95        "depth",
 96        "width",
 97        "height",
 98        "selected_index",
 99        "selectables_length",
100    ]
101    """Fields of widget that shall be serialized by `pytermgui.serializer.Serializer`"""
102
103    # This class is loaded after this module,
104    # and thus mypy doesn't see its existence.
105    _id_manager: Optional["_IDManager"] = None  # type: ignore
106
107    is_bindable = False
108    """Allow binding support"""
109
110    size_policy = SizePolicy.get_default()
111    """`pytermgui.enums.SizePolicy` to set widget's width according to"""
112
113    parent_align = HorizontalAlignment.get_default()
114    """`pytermgui.enums.HorizontalAlignment` to align widget by"""
115
116    from_data: Callable[..., Widget | list[Widget] | None]
117
118    # We cannot import boxes here due to cyclic imports.
119    box: Any
120
121    def __init__(self, **attrs: Any) -> None:
122        """Initialize object"""
123
124        self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value)
125        self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value)
126
127        self.width = 1
128        self.height = 1
129        self.pos = self.terminal.origin
130
131        self.depth = 0
132
133        self.styles = type(self).styles.branch(self)
134        self.chars = type(self).chars.copy()
135
136        self.parent: Widget | None = None
137        self.selected_index: int | None = None
138
139        self._selectables_length = 0
140        self._id: Optional[str] = None
141        self._serialized_fields = type(self).serialized
142        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
143        self._relative_width: float | None = None
144        self._previous_state: tuple[tuple[int, int], list[str]] | None = None
145
146        self.positioned_line_buffer: list[tuple[tuple[int, int], str]] = []
147
148        for attr, value in attrs.items():
149            setattr(self, attr, value)
150
151    def __repr__(self) -> str:
152        """Return repr string of this widget.
153
154        Returns:
155            Whatever this widget's `debug` method gives.
156        """
157
158        return self.debug()
159
160    def __fancy_repr__(self) -> Generator[str, None, None]:
161        """Yields the repr of this object, then a preview of it."""
162
163        yield self.debug()
164        yield "\n\n"
165        yield {
166            "text": "\n".join((line + reset() for line in self.get_lines())),
167            "highlight": False,
168        }
169
170    def __iter__(self) -> Iterator[Widget]:
171        """Return self for iteration"""
172
173        yield self
174
175    @property
176    def bindings(self) -> dict[str | Type[MouseEvent], tuple[BoundCallback, str]]:
177        """Gets a copy of the bindings internal dictionary.
178
179        Returns:
180            A copy of the internal bindings dictionary, such as:
181
182            ```
183            {
184                "*": (star_callback, "This is a callback activated when '*' is pressed.")
185            }
186            ```
187        """
188
189        return self._bindings.copy()
190
191    @property
192    def id(self) -> Optional[str]:  # pylint: disable=invalid-name
193        """Gets this widget's id property
194
195        Returns:
196            The id string if one is present, None otherwise.
197        """
198
199        return self._id
200
201    @id.setter
202    def id(self, value: str) -> None:  # pylint: disable=invalid-name
203        """Registers a widget to the Widget._id_manager.
204
205        If this widget already had an id, the old value is deregistered
206        before the new one is assigned.
207
208        Args:
209            value: The new id this widget will be registered as.
210        """
211
212        if self._id == value:
213            return
214
215        manager = Widget._id_manager
216        assert manager is not None
217
218        old = manager.get_id(self)
219        if old is not None:
220            manager.deregister(old)
221
222        self._id = value
223        manager.register(self)
224
225    @property
226    def selectables_length(self) -> int:
227        """Gets how many selectables this widget contains.
228
229        Returns:
230            An integer describing the amount of selectables in this widget.
231        """
232
233        return self._selectables_length
234
235    @property
236    def selectables(self) -> list[tuple[Widget, int]]:
237        """Gets a list of all selectables within this widget
238
239        Returns:
240            A list of tuples. In the default implementation this will be
241            a list of one tuple, containing a reference to `self`, as well
242            as the lowest index, 0.
243        """
244
245        return [(self, 0)]
246
247    @property
248    def is_selectable(self) -> bool:
249        """Determines whether this widget has any selectables.
250
251        Returns:
252            A boolean, representing `self.selectables_length != 0`.
253        """
254
255        return self.selectables_length != 0
256
257    @property
258    def static_width(self) -> int:
259        """Allows for a shorter way of setting a width, and SizePolicy.STATIC.
260
261        Args:
262            value: The new width integer.
263
264        Returns:
265            None, as this is setter only.
266        """
267
268        return None  # type: ignore
269
270    @static_width.setter
271    def static_width(self, value: int) -> None:
272        """See the static_width getter."""
273
274        self.width = value
275        self.size_policy = SizePolicy.STATIC
276
277    @property
278    def relative_width(self) -> float | None:
279        """Sets this widget's relative width, and changes size_policy to RELATIVE.
280
281        The value is clamped to 1.0.
282
283        If a Container holds a width of 30, and it has a subwidget with a relative
284        width of 0.5, it will be resized to 15.
285
286        Args:
287            value: The multiplier to apply to the parent's width.
288
289        Returns:
290            The current relative_width.
291        """
292
293        return self._relative_width
294
295    @relative_width.setter
296    def relative_width(self, value: float) -> None:
297        """See the relative_width getter."""
298
299        self.size_policy = SizePolicy.RELATIVE
300        self._relative_width = min(1.0, value)
301
302    @property
303    def terminal(self) -> Terminal:
304        """Returns the current global terminal instance."""
305
306        return get_terminal()
307
308    def get_change(self) -> WidgetChange | None:
309        """Determines whether widget lines changed since the last call to this function."""
310
311        lines = self.get_lines()
312
313        if self._previous_state is None:
314            self._previous_state = (self.width, self.height), lines
315            return WidgetChange.LINES
316
317        lines = self.get_lines()
318        (old_width, old_height), old_lines = self._previous_state
319
320        self._previous_state = (self.width, self.height), lines
321
322        if old_width != self.width and old_height != self.height:
323            return WidgetChange.SIZE
324
325        if old_width != self.width:
326            return WidgetChange.WIDTH
327
328        if old_height != self.height:
329            return WidgetChange.HEIGHT
330
331        if old_lines != lines:
332            return WidgetChange.LINES
333
334        return None
335
336    def contains(self, pos: tuple[int, int]) -> bool:
337        """Determines whether widget contains `pos`.
338
339        Args:
340            pos: Position to compare.
341
342        Returns:
343            Boolean describing whether the position is inside
344              this widget.
345        """
346
347        rect = self.pos, (
348            self.pos[0] + self.width,
349            self.pos[1] + self.height,
350        )
351
352        (left, top), (right, bottom) = rect
353
354        return left <= pos[0] < right and top <= pos[1] < bottom
355
356    def handle_mouse(self, event: MouseEvent) -> bool:
357        """Handles a mouse event, returning its success.
358
359        Args:
360            event: Object containing mouse event to handle.
361
362        Returns:
363            A boolean describing whether the mouse input was handled."""
364
365        return False and hasattr(self, event)
366
367    def handle_key(self, key: str) -> bool:
368        """Handles a mouse event, returning its success.
369
370        Args:
371            key: String representation of input string.
372              The `pytermgui.input.keys` object can be
373              used to retrieve special keys.
374
375        Returns:
376            A boolean describing whether the key was handled.
377        """
378
379        return False and hasattr(self, key)
380
381    def serialize(self) -> dict[str, Any]:
382        """Serializes a widget.
383
384        The fields looked at are defined `Widget.serialized`. Note that
385        this method is not very commonly used at the moment, so it might
386        not have full functionality in non-nuclear widgets.
387
388        Returns:
389            Dictionary of widget attributes. The dictionary will always
390            have a `type` field. Any styles are converted into markup
391            strings during serialization, so they can be loaded again in
392            their original form.
393
394            Example return:
395            ```
396                {
397                    "type": "Label",
398                    "value": "[210 bold]I am a title",
399                    "parent_align": 0,
400                    ...
401                }
402            ```
403        """
404
405        fields = self._serialized_fields
406
407        out: dict[str, Any] = {"type": type(self).__name__}
408        for key in fields:
409            # Detect styled values
410            if key.startswith("*"):
411                style = True
412                key = key[1:]
413            else:
414                style = False
415
416            value = getattr(self, key)
417
418            # Convert styled value into markup
419            if style:
420                style_call = self._get_style(key)
421                if isinstance(value, list):
422                    out[key] = [markup.get_markup(style_call(char)) for char in value]
423                else:
424                    out[key] = markup.get_markup(style_call(value))
425
426                continue
427
428            out[key] = value
429
430        # The chars need to be handled separately
431        out["chars"] = {}
432        for key, value in self.chars.items():
433            style_call = self._get_style(key)
434
435            if isinstance(value, list):
436                out["chars"][key] = [
437                    markup.get_markup(style_call(char)) for char in value
438                ]
439            else:
440                out["chars"][key] = markup.get_markup(style_call(value))
441
442        return out
443
444    def copy(self) -> Widget:
445        """Creates a deep copy of this widget"""
446
447        return deepcopy(self)
448
449    def _get_style(self, key: str) -> w_styles.DepthlessStyleType:
450        """Gets style call from its key.
451
452        This is analogous to using `self.styles.{key}`
453
454        Args:
455            key: A key into the widget's style manager.
456
457        Returns:
458            A `pytermgui.styles.StyleCall` object containing the referenced
459            style. StyleCall objects should only be used internally inside a
460            widget.
461
462        Raises:
463            KeyError: Style key is invalid.
464        """
465
466        return self.styles[key]
467
468    def _get_char(self, key: str) -> w_styles.CharType:
469        """Gets character from its key.
470
471        Args:
472            key: A key into the widget's chars dictionary.
473
474        Returns:
475            Either a `list[str]` or a simple `str`, depending on the character.
476
477        Raises:
478            KeyError: Style key is invalid.
479        """
480
481        chars = self.chars[key]
482        if isinstance(chars, str):
483            return chars
484
485        return chars.copy()
486
487    def get_lines(self) -> list[str]:
488        """Gets lines representing this widget.
489
490        These lines have to be equal to the widget in length. All
491        widgets must provide this method. Make sure to keep it performant,
492        as it will be called very often, often multiple times per WindowManager frame.
493
494        Any longer actions should be done outside of this method, and only their
495        result should be looked up here.
496
497        Returns:
498            Nothing by default.
499
500        Raises:
501            NotImplementedError: As this method is required for **all** widgets, not
502                having it defined will raise NotImplementedError.
503        """
504
505        raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.")
506
507    def bind(
508        self, key: str, action: BoundCallback, description: Optional[str] = None
509    ) -> None:
510        """Binds an action to a keypress.
511
512        This function is only called by implementations above this layer. To use this
513        functionality use `pytermgui.window_manager.WindowManager`, or write your own
514        custom layer.
515
516        Special keys:
517        - keys.ANY_KEY: Any and all keypresses execute this binding.
518        - keys.MouseAction: Any and all mouse inputs execute this binding.
519
520        Args:
521            key: The key that the action will be bound to.
522            action: The action executed when the key is pressed.
523            description: An optional description for this binding. It is not really
524                used anywhere, but you can provide a helper menu and display them.
525
526        Raises:
527            TypeError: This widget is not bindable, i.e. widget.is_bindable == False.
528        """
529
530        if not self.is_bindable:
531            raise TypeError(f"Widget of type {type(self)} does not accept bindings.")
532
533        if description is None:
534            description = f"Binding of {key} to {action}"
535
536        self._bindings[key] = (action, description)
537
538    def unbind(self, key: str) -> None:
539        """Unbinds the given key."""
540
541        del self._bindings[key]
542
543    def execute_binding(self, key: Any) -> bool:
544        """Executes a binding belonging to key, when present.
545
546        Use this method inside custom widget `handle_keys` methods, or to run a callback
547        without its corresponding key having been pressed.
548
549        Args:
550            key: Usually a string, indexing into the `_bindings` dictionary. These are the
551              same strings as defined in `Widget.bind`.
552
553        Returns:
554            True if the binding was found, False otherwise. Bindings will always be
555              executed if they are found.
556        """
557
558        # Execute special binding
559        if keys.ANY_KEY in self._bindings:
560            method, _ = self._bindings[keys.ANY_KEY]
561            method(self, key)
562
563        if key in self._bindings:
564            method, _ = self._bindings[key]
565            method(self, key)
566
567            return True
568
569        return False
570
571    def select(self, index: int | None = None) -> None:
572        """Selects a part of this Widget.
573
574        Args:
575            index: The index to select.
576
577        Raises:
578            TypeError: This widget has no selectables, i.e. widget.is_selectable == False.
579        """
580
581        if not self.is_selectable:
582            raise TypeError(f"Object of type {type(self)} has no selectables.")
583
584        if index is not None:
585            index = min(max(0, index), self.selectables_length - 1)
586        self.selected_index = index
587
588    def print(self) -> None:
589        """Prints this widget"""
590
591        for line in self.get_lines():
592            print(line)
593
594    def debug(self) -> str:
595        """Returns identifiable information about this widget.
596
597        This method is used to easily differentiate between widgets. By default, all widget's
598        __repr__ method is an alias to this. The signature of each widget is used to generate
599        the return value.
600
601        Returns:
602            A string almost exactly matching the line of code that could have defined the widget.
603
604            Example return:
605
606            ```
607            Container(Label(value="This is a label", padding=0),
608            Button(label="This is a button", padding=0), **attrs)
609            ```
610
611        """
612
613        constructor = "("
614        for name in signature(getattr(self, "__init__")).parameters:
615            current = ""
616            if name == "attrs":
617                current += "**attrs"
618                continue
619
620            if len(constructor) > 1:
621                current += ", "
622
623            current += name
624
625            attr = getattr(self, name, None)
626            if attr is None:
627                continue
628
629            current += "="
630
631            if isinstance(attr, str):
632                current += f'"{attr}"'
633            else:
634                current += str(attr)
635
636            constructor += current
637
638        constructor += ")"
639
640        return type(self).__name__ + constructor
641
642
643class Label(Widget):
644    """A Widget to display a string
645
646    By default, this widget uses `pytermgui.widgets.styles.MARKUP`. This
647    allows it to house markup text that is parsed before display, such as:
648
649    ```python3
650    import pytermgui as ptg
651
652    with ptg.alt_buffer():
653        root = ptg.Container(
654            ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!")
655        )
656        root.print()
657        ptg.getch()
658    ```
659
660    <p style="text-align: center">
661     <img
662      src="https://github.com/bczsalba/pytermgui/blob/master/assets/docs/widgets/label.png?raw=true"
663      width=100%>
664    </p>
665    """
666
667    serialized = Widget.serialized + ["*value", "align", "padding"]
668    styles = w_styles.StyleManager(value=w_styles.MARKUP)
669
670    def __init__(
671        self,
672        value: str = "",
673        style: str | w_styles.StyleValue = "",
674        padding: int = 0,
675        non_first_padding: int = 0,
676        **attrs: Any,
677    ) -> None:
678        """Initializes a Label.
679
680        Args:
681            value: The value of this string. Using the default value style
682                (`pytermgui.widgets.styles.MARKUP`),
683            style: A pre-set value for self.styles.value.
684            padding: The number of space (" ") characters to prepend to every line after
685                line breaking.
686            non_first_padding: The number of space characters to prepend to every
687                non-first line of `get_lines`. This is applied on top of `padding`.
688        """
689
690        super().__init__(**attrs)
691
692        self.value = value
693        self.padding = padding
694        self.non_first_padding = non_first_padding
695        self.width = real_length(value) + self.padding
696
697        if style != "":
698            self.styles.value = style
699
700    def get_lines(self) -> list[str]:
701        """Get lines representing this Label, breaking lines as necessary"""
702
703        lines = []
704        limit = self.width - self.padding
705        broken = break_line(
706            self.styles.value(self.value),
707            limit=limit,
708            non_first_limit=limit - self.non_first_padding,
709        )
710
711        for i, line in enumerate(broken):
712            if i == 0:
713                lines.append(self.padding * " " + line)
714                continue
715
716            lines.append(self.padding * " " + self.non_first_padding * " " + line)
717
718        return lines or [""]
719
720
721class ScrollableWidget(Widget):
722    """A widget with some scrolling helper methods.
723
724    This is not an implementation of the scrolling behaviour itself, just the
725    user-facing API for it.
726
727    It provides a `_scroll_offset` attribute, which is an integer describing the current
728    scroll state offset from the top, as well as some methods to modify the state."""
729
730    def __init__(self, **attrs: Any) -> None:
731        """Initializes the scrollable widget."""
732
733        super().__init__(**attrs)
734
735        self._max_scroll = 0
736        self._scroll_offset = 0
737
738    def scroll(self, offset: int) -> bool:
739        """Scrolls to given offset, returns the new scroll_offset.
740
741        Args:
742            offset: The amount to scroll by. Positive offsets scroll down,
743                negative up.
744
745        Returns:
746            True if the scroll offset changed, False otherwise.
747        """
748
749        base = self._scroll_offset
750
751        self._scroll_offset = min(
752            max(0, self._scroll_offset + offset), self._max_scroll
753        )
754
755        return base != self._scroll_offset
756
757    def scroll_end(self, end: int) -> int:
758        """Scrolls to either top or bottom end of this object.
759
760        Args:
761            end: The offset to scroll to. 0 goes to the very top, -1 to the
762                very bottom.
763
764        Returns:
765            True if the scroll offset changed, False otherwise.
766        """
767
768        base = self._scroll_offset
769
770        if end == 0:
771            self._scroll_offset = 0
772
773        elif end == -1:
774            self._scroll_offset = self._max_scroll
775
776        return base != self._scroll_offset
777
778    def get_lines(self) -> list[str]:
779        ...
class Widget:
 78class Widget:  # pylint: disable=too-many-public-methods
 79    """The base of the Widget system"""
 80
 81    set_style = classmethod(_set_obj_or_cls_style)
 82    set_char = classmethod(_set_obj_or_cls_char)
 83
 84    styles = w_styles.StyleManager()
 85    """Default styles for this class"""
 86
 87    chars: dict[str, w_styles.CharType] = {}
 88    """Default characters for this class"""
 89
 90    keys: dict[str, set[str]] = {}
 91    """Groups of keys that are used in `handle_key`"""
 92
 93    serialized: list[str] = [
 94        "id",
 95        "pos",
 96        "depth",
 97        "width",
 98        "height",
 99        "selected_index",
100        "selectables_length",
101    ]
102    """Fields of widget that shall be serialized by `pytermgui.serializer.Serializer`"""
103
104    # This class is loaded after this module,
105    # and thus mypy doesn't see its existence.
106    _id_manager: Optional["_IDManager"] = None  # type: ignore
107
108    is_bindable = False
109    """Allow binding support"""
110
111    size_policy = SizePolicy.get_default()
112    """`pytermgui.enums.SizePolicy` to set widget's width according to"""
113
114    parent_align = HorizontalAlignment.get_default()
115    """`pytermgui.enums.HorizontalAlignment` to align widget by"""
116
117    from_data: Callable[..., Widget | list[Widget] | None]
118
119    # We cannot import boxes here due to cyclic imports.
120    box: Any
121
122    def __init__(self, **attrs: Any) -> None:
123        """Initialize object"""
124
125        self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value)
126        self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value)
127
128        self.width = 1
129        self.height = 1
130        self.pos = self.terminal.origin
131
132        self.depth = 0
133
134        self.styles = type(self).styles.branch(self)
135        self.chars = type(self).chars.copy()
136
137        self.parent: Widget | None = None
138        self.selected_index: int | None = None
139
140        self._selectables_length = 0
141        self._id: Optional[str] = None
142        self._serialized_fields = type(self).serialized
143        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
144        self._relative_width: float | None = None
145        self._previous_state: tuple[tuple[int, int], list[str]] | None = None
146
147        self.positioned_line_buffer: list[tuple[tuple[int, int], str]] = []
148
149        for attr, value in attrs.items():
150            setattr(self, attr, value)
151
152    def __repr__(self) -> str:
153        """Return repr string of this widget.
154
155        Returns:
156            Whatever this widget's `debug` method gives.
157        """
158
159        return self.debug()
160
161    def __fancy_repr__(self) -> Generator[str, None, None]:
162        """Yields the repr of this object, then a preview of it."""
163
164        yield self.debug()
165        yield "\n\n"
166        yield {
167            "text": "\n".join((line + reset() for line in self.get_lines())),
168            "highlight": False,
169        }
170
171    def __iter__(self) -> Iterator[Widget]:
172        """Return self for iteration"""
173
174        yield self
175
176    @property
177    def bindings(self) -> dict[str | Type[MouseEvent], tuple[BoundCallback, str]]:
178        """Gets a copy of the bindings internal dictionary.
179
180        Returns:
181            A copy of the internal bindings dictionary, such as:
182
183            ```
184            {
185                "*": (star_callback, "This is a callback activated when '*' is pressed.")
186            }
187            ```
188        """
189
190        return self._bindings.copy()
191
192    @property
193    def id(self) -> Optional[str]:  # pylint: disable=invalid-name
194        """Gets this widget's id property
195
196        Returns:
197            The id string if one is present, None otherwise.
198        """
199
200        return self._id
201
202    @id.setter
203    def id(self, value: str) -> None:  # pylint: disable=invalid-name
204        """Registers a widget to the Widget._id_manager.
205
206        If this widget already had an id, the old value is deregistered
207        before the new one is assigned.
208
209        Args:
210            value: The new id this widget will be registered as.
211        """
212
213        if self._id == value:
214            return
215
216        manager = Widget._id_manager
217        assert manager is not None
218
219        old = manager.get_id(self)
220        if old is not None:
221            manager.deregister(old)
222
223        self._id = value
224        manager.register(self)
225
226    @property
227    def selectables_length(self) -> int:
228        """Gets how many selectables this widget contains.
229
230        Returns:
231            An integer describing the amount of selectables in this widget.
232        """
233
234        return self._selectables_length
235
236    @property
237    def selectables(self) -> list[tuple[Widget, int]]:
238        """Gets a list of all selectables within this widget
239
240        Returns:
241            A list of tuples. In the default implementation this will be
242            a list of one tuple, containing a reference to `self`, as well
243            as the lowest index, 0.
244        """
245
246        return [(self, 0)]
247
248    @property
249    def is_selectable(self) -> bool:
250        """Determines whether this widget has any selectables.
251
252        Returns:
253            A boolean, representing `self.selectables_length != 0`.
254        """
255
256        return self.selectables_length != 0
257
258    @property
259    def static_width(self) -> int:
260        """Allows for a shorter way of setting a width, and SizePolicy.STATIC.
261
262        Args:
263            value: The new width integer.
264
265        Returns:
266            None, as this is setter only.
267        """
268
269        return None  # type: ignore
270
271    @static_width.setter
272    def static_width(self, value: int) -> None:
273        """See the static_width getter."""
274
275        self.width = value
276        self.size_policy = SizePolicy.STATIC
277
278    @property
279    def relative_width(self) -> float | None:
280        """Sets this widget's relative width, and changes size_policy to RELATIVE.
281
282        The value is clamped to 1.0.
283
284        If a Container holds a width of 30, and it has a subwidget with a relative
285        width of 0.5, it will be resized to 15.
286
287        Args:
288            value: The multiplier to apply to the parent's width.
289
290        Returns:
291            The current relative_width.
292        """
293
294        return self._relative_width
295
296    @relative_width.setter
297    def relative_width(self, value: float) -> None:
298        """See the relative_width getter."""
299
300        self.size_policy = SizePolicy.RELATIVE
301        self._relative_width = min(1.0, value)
302
303    @property
304    def terminal(self) -> Terminal:
305        """Returns the current global terminal instance."""
306
307        return get_terminal()
308
309    def get_change(self) -> WidgetChange | None:
310        """Determines whether widget lines changed since the last call to this function."""
311
312        lines = self.get_lines()
313
314        if self._previous_state is None:
315            self._previous_state = (self.width, self.height), lines
316            return WidgetChange.LINES
317
318        lines = self.get_lines()
319        (old_width, old_height), old_lines = self._previous_state
320
321        self._previous_state = (self.width, self.height), lines
322
323        if old_width != self.width and old_height != self.height:
324            return WidgetChange.SIZE
325
326        if old_width != self.width:
327            return WidgetChange.WIDTH
328
329        if old_height != self.height:
330            return WidgetChange.HEIGHT
331
332        if old_lines != lines:
333            return WidgetChange.LINES
334
335        return None
336
337    def contains(self, pos: tuple[int, int]) -> bool:
338        """Determines whether widget contains `pos`.
339
340        Args:
341            pos: Position to compare.
342
343        Returns:
344            Boolean describing whether the position is inside
345              this widget.
346        """
347
348        rect = self.pos, (
349            self.pos[0] + self.width,
350            self.pos[1] + self.height,
351        )
352
353        (left, top), (right, bottom) = rect
354
355        return left <= pos[0] < right and top <= pos[1] < bottom
356
357    def handle_mouse(self, event: MouseEvent) -> bool:
358        """Handles a mouse event, returning its success.
359
360        Args:
361            event: Object containing mouse event to handle.
362
363        Returns:
364            A boolean describing whether the mouse input was handled."""
365
366        return False and hasattr(self, event)
367
368    def handle_key(self, key: str) -> bool:
369        """Handles a mouse event, returning its success.
370
371        Args:
372            key: String representation of input string.
373              The `pytermgui.input.keys` object can be
374              used to retrieve special keys.
375
376        Returns:
377            A boolean describing whether the key was handled.
378        """
379
380        return False and hasattr(self, key)
381
382    def serialize(self) -> dict[str, Any]:
383        """Serializes a widget.
384
385        The fields looked at are defined `Widget.serialized`. Note that
386        this method is not very commonly used at the moment, so it might
387        not have full functionality in non-nuclear widgets.
388
389        Returns:
390            Dictionary of widget attributes. The dictionary will always
391            have a `type` field. Any styles are converted into markup
392            strings during serialization, so they can be loaded again in
393            their original form.
394
395            Example return:
396            ```
397                {
398                    "type": "Label",
399                    "value": "[210 bold]I am a title",
400                    "parent_align": 0,
401                    ...
402                }
403            ```
404        """
405
406        fields = self._serialized_fields
407
408        out: dict[str, Any] = {"type": type(self).__name__}
409        for key in fields:
410            # Detect styled values
411            if key.startswith("*"):
412                style = True
413                key = key[1:]
414            else:
415                style = False
416
417            value = getattr(self, key)
418
419            # Convert styled value into markup
420            if style:
421                style_call = self._get_style(key)
422                if isinstance(value, list):
423                    out[key] = [markup.get_markup(style_call(char)) for char in value]
424                else:
425                    out[key] = markup.get_markup(style_call(value))
426
427                continue
428
429            out[key] = value
430
431        # The chars need to be handled separately
432        out["chars"] = {}
433        for key, value in self.chars.items():
434            style_call = self._get_style(key)
435
436            if isinstance(value, list):
437                out["chars"][key] = [
438                    markup.get_markup(style_call(char)) for char in value
439                ]
440            else:
441                out["chars"][key] = markup.get_markup(style_call(value))
442
443        return out
444
445    def copy(self) -> Widget:
446        """Creates a deep copy of this widget"""
447
448        return deepcopy(self)
449
450    def _get_style(self, key: str) -> w_styles.DepthlessStyleType:
451        """Gets style call from its key.
452
453        This is analogous to using `self.styles.{key}`
454
455        Args:
456            key: A key into the widget's style manager.
457
458        Returns:
459            A `pytermgui.styles.StyleCall` object containing the referenced
460            style. StyleCall objects should only be used internally inside a
461            widget.
462
463        Raises:
464            KeyError: Style key is invalid.
465        """
466
467        return self.styles[key]
468
469    def _get_char(self, key: str) -> w_styles.CharType:
470        """Gets character from its key.
471
472        Args:
473            key: A key into the widget's chars dictionary.
474
475        Returns:
476            Either a `list[str]` or a simple `str`, depending on the character.
477
478        Raises:
479            KeyError: Style key is invalid.
480        """
481
482        chars = self.chars[key]
483        if isinstance(chars, str):
484            return chars
485
486        return chars.copy()
487
488    def get_lines(self) -> list[str]:
489        """Gets lines representing this widget.
490
491        These lines have to be equal to the widget in length. All
492        widgets must provide this method. Make sure to keep it performant,
493        as it will be called very often, often multiple times per WindowManager frame.
494
495        Any longer actions should be done outside of this method, and only their
496        result should be looked up here.
497
498        Returns:
499            Nothing by default.
500
501        Raises:
502            NotImplementedError: As this method is required for **all** widgets, not
503                having it defined will raise NotImplementedError.
504        """
505
506        raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.")
507
508    def bind(
509        self, key: str, action: BoundCallback, description: Optional[str] = None
510    ) -> None:
511        """Binds an action to a keypress.
512
513        This function is only called by implementations above this layer. To use this
514        functionality use `pytermgui.window_manager.WindowManager`, or write your own
515        custom layer.
516
517        Special keys:
518        - keys.ANY_KEY: Any and all keypresses execute this binding.
519        - keys.MouseAction: Any and all mouse inputs execute this binding.
520
521        Args:
522            key: The key that the action will be bound to.
523            action: The action executed when the key is pressed.
524            description: An optional description for this binding. It is not really
525                used anywhere, but you can provide a helper menu and display them.
526
527        Raises:
528            TypeError: This widget is not bindable, i.e. widget.is_bindable == False.
529        """
530
531        if not self.is_bindable:
532            raise TypeError(f"Widget of type {type(self)} does not accept bindings.")
533
534        if description is None:
535            description = f"Binding of {key} to {action}"
536
537        self._bindings[key] = (action, description)
538
539    def unbind(self, key: str) -> None:
540        """Unbinds the given key."""
541
542        del self._bindings[key]
543
544    def execute_binding(self, key: Any) -> bool:
545        """Executes a binding belonging to key, when present.
546
547        Use this method inside custom widget `handle_keys` methods, or to run a callback
548        without its corresponding key having been pressed.
549
550        Args:
551            key: Usually a string, indexing into the `_bindings` dictionary. These are the
552              same strings as defined in `Widget.bind`.
553
554        Returns:
555            True if the binding was found, False otherwise. Bindings will always be
556              executed if they are found.
557        """
558
559        # Execute special binding
560        if keys.ANY_KEY in self._bindings:
561            method, _ = self._bindings[keys.ANY_KEY]
562            method(self, key)
563
564        if key in self._bindings:
565            method, _ = self._bindings[key]
566            method(self, key)
567
568            return True
569
570        return False
571
572    def select(self, index: int | None = None) -> None:
573        """Selects a part of this Widget.
574
575        Args:
576            index: The index to select.
577
578        Raises:
579            TypeError: This widget has no selectables, i.e. widget.is_selectable == False.
580        """
581
582        if not self.is_selectable:
583            raise TypeError(f"Object of type {type(self)} has no selectables.")
584
585        if index is not None:
586            index = min(max(0, index), self.selectables_length - 1)
587        self.selected_index = index
588
589    def print(self) -> None:
590        """Prints this widget"""
591
592        for line in self.get_lines():
593            print(line)
594
595    def debug(self) -> str:
596        """Returns identifiable information about this widget.
597
598        This method is used to easily differentiate between widgets. By default, all widget's
599        __repr__ method is an alias to this. The signature of each widget is used to generate
600        the return value.
601
602        Returns:
603            A string almost exactly matching the line of code that could have defined the widget.
604
605            Example return:
606
607            ```
608            Container(Label(value="This is a label", padding=0),
609            Button(label="This is a button", padding=0), **attrs)
610            ```
611
612        """
613
614        constructor = "("
615        for name in signature(getattr(self, "__init__")).parameters:
616            current = ""
617            if name == "attrs":
618                current += "**attrs"
619                continue
620
621            if len(constructor) > 1:
622                current += ", "
623
624            current += name
625
626            attr = getattr(self, name, None)
627            if attr is None:
628                continue
629
630            current += "="
631
632            if isinstance(attr, str):
633                current += f'"{attr}"'
634            else:
635                current += str(attr)
636
637            constructor += current
638
639        constructor += ")"
640
641        return type(self).__name__ + constructor

The base of the Widget system

Widget(**attrs: Any)
122    def __init__(self, **attrs: Any) -> None:
123        """Initialize object"""
124
125        self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value)
126        self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value)
127
128        self.width = 1
129        self.height = 1
130        self.pos = self.terminal.origin
131
132        self.depth = 0
133
134        self.styles = type(self).styles.branch(self)
135        self.chars = type(self).chars.copy()
136
137        self.parent: Widget | None = None
138        self.selected_index: int | None = None
139
140        self._selectables_length = 0
141        self._id: Optional[str] = None
142        self._serialized_fields = type(self).serialized
143        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
144        self._relative_width: float | None = None
145        self._previous_state: tuple[tuple[int, int], list[str]] | None = None
146
147        self.positioned_line_buffer: list[tuple[tuple[int, int], str]] = []
148
149        for attr, value in attrs.items():
150            setattr(self, attr, value)

Initialize object

def set_style( obj_or_cls: Union[Type[pytermgui.widgets.base.Widget], pytermgui.widgets.base.Widget], key: str, value: Callable[[int, str], str]) -> Union[Type[pytermgui.widgets.base.Widget], pytermgui.widgets.base.Widget]:
31def _set_obj_or_cls_style(
32    obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.StyleType
33) -> Type[Widget] | Widget:
34    """Sets a style for an object or class
35
36    Args:
37        obj_or_cls: The Widget instance or type to update.
38        key: The style key.
39        value: The new style.
40
41    Returns:
42        Type[Widget] | Widget: The updated class.
43
44    Raises:
45        See `pytermgui.widgets.styles.StyleManager`.
46    """
47
48    obj_or_cls.styles[key] = value
49
50    return obj_or_cls

Sets a style for an object or class

Args
  • obj_or_cls: The Widget instance or type to update.
  • key: The style key.
  • value: The new style.
Returns

Type[Widget] | Widget: The updated class.

Raises
def set_char( obj_or_cls: Union[Type[pytermgui.widgets.base.Widget], pytermgui.widgets.base.Widget], key: str, value: Union[List[str], str]) -> Union[Type[pytermgui.widgets.base.Widget], pytermgui.widgets.base.Widget]:
53def _set_obj_or_cls_char(
54    obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.CharType
55) -> Type[Widget] | Widget:
56    """Sets a char for an object or class
57
58    Args:
59        obj_or_cls: The Widget instance or type to update.
60        key: The char key.
61        value: The new char.
62
63    Returns:
64        Type[Widget] | Widget: The updated class.
65
66    Raises:
67        KeyError: The char key provided is invalid.
68    """
69
70    if not key in obj_or_cls.chars.keys():
71        raise KeyError(f"Char {key} is not valid for {obj_or_cls}!")
72
73    obj_or_cls.chars[key] = value
74
75    return obj_or_cls

Sets a char for an object or class

Args
  • obj_or_cls: The Widget instance or type to update.
  • key: The char key.
  • value: The new char.
Returns

Type[Widget] | Widget: The updated class.

Raises
  • KeyError: The char key provided is invalid.
styles = {}

Default styles for this class

chars: dict[str, typing.Union[typing.List[str], str]] = {}

Default characters for this class

keys: dict[str, set[str]] = {}

Groups of keys that are used in handle_key

serialized: list[str] = ['id', 'pos', 'depth', 'width', 'height', 'selected_index', 'selectables_length']

Fields of widget that shall be serialized by pytermgui.serializer.Serializer

is_bindable = False

Allow binding support

size_policy = <SizePolicy.FILL: 0>

pytermgui.enums.SizePolicy to set widget's width according to

parent_align = <HorizontalAlignment.CENTER: 1>
def from_data( data: Any, **widget_args: Any) -> Union[pytermgui.widgets.base.Widget, list[pytermgui.widgets.containers.Splitter], NoneType]:
 49def auto(data: Any, **widget_args: Any) -> Optional[Widget | list[Splitter]]:
 50    """Creates a widget from specific data structures.
 51
 52    This conversion includes various widget classes, as well as some shorthands for
 53    more complex objects.  This method is called implicitly whenever a non-widget is
 54    attempted to be added to a Widget.
 55
 56
 57    Args:
 58        data: The structure to convert. See below for formats.
 59        **widget_args: Arguments passed straight to the widget constructor.
 60
 61    Returns:
 62        The widget or list of widgets created, or None if the passed structure could
 63        not be converted.
 64
 65    <br>
 66    <details style="text-align: left">
 67        <summary style="all: revert; cursor: pointer">Data structures:</summary>
 68
 69    `pytermgui.widgets.base.Label`:
 70
 71    * Created from `str`
 72    * Syntax example: `"Label value"`
 73
 74    `pytermgui.widgets.extra.Splitter`:
 75
 76    * Created from `tuple[Any]`
 77    * Syntax example: `(YourWidget(), "auto_syntax", ...)`
 78
 79    `pytermgui.widgets.extra.Splitter` prompt:
 80
 81    * Created from `dict[Any, Any]`
 82    * Syntax example: `{YourWidget(): "auto_syntax"}`
 83
 84    `pytermgui.widgets.buttons.Button`:
 85
 86    * Created from `list[str, pytermgui.widgets.buttons.MouseCallback]`
 87    * Syntax example: `["Button label", lambda target, caller: ...]`
 88
 89    `pytermgui.widgets.buttons.Checkbox`:
 90
 91    * Created from `list[bool, Callable[[bool], Any]]`
 92    * Syntax example: `[True, lambda checked: ...]`
 93
 94    `pytermgui.widgets.buttons.Toggle`:
 95
 96    * Created from `list[tuple[str, str], Callable[[str], Any]]`
 97    * Syntax example: `[("On", "Off"), lambda new_value: ...]`
 98    </details>
 99
100    Example:
101
102    ```python3
103    from pytermgui import Container
104    form = (
105        Container(id="form")
106        + "[157 bold]This is a title"
107        + ""
108        + {"[72 italic]Label1": "[210]Button1"}
109        + {"[72 italic]Label2": "[210]Button2"}
110        + {"[72 italic]Label3": "[210]Button3"}
111        + ""
112        + ["Submit", lambda _, button, your_submit_handler(button.parent)]
113    )
114    ```
115    """
116    # In my opinion, returning immediately after construction is much more readable.
117    # pylint: disable=too-many-return-statements
118
119    # Nothing to do.
120    if isinstance(data, Widget):
121        # Set all **widget_args
122        for key, value in widget_args.items():
123            setattr(data, key, value)
124
125        return data
126
127    # Label
128    if isinstance(data, str):
129        return Label(data, **widget_args)
130
131    # Splitter
132    if isinstance(data, tuple):
133        return Splitter(*data, **widget_args)
134
135    # buttons
136    if isinstance(data, list):
137        label = data[0]
138        onclick = None
139        if len(data) > 1:
140            onclick = data[1]
141
142        # Checkbox
143        if isinstance(label, bool):
144            return Checkbox(onclick, checked=label, **widget_args)
145
146        # Toggle
147        if isinstance(label, tuple):
148            assert len(label) == 2
149            return Toggle(label, onclick, **widget_args)
150
151        return Button(label, onclick, **widget_args)
152
153    # prompt splitter
154    if isinstance(data, dict):
155        rows: list[Splitter] = []
156
157        for key, value in data.items():
158            left = auto(key, parent_align=HorizontalAlignment.LEFT)
159            right = auto(value, parent_align=HorizontalAlignment.RIGHT)
160
161            rows.append(Splitter(left, right, **widget_args))
162
163        if len(rows) == 1:
164            return rows[0]
165
166        return rows
167
168    return None

Creates a widget from specific data structures.

This conversion includes various widget classes, as well as some shorthands for more complex objects. This method is called implicitly whenever a non-widget is attempted to be added to a Widget.

Args
  • data: The structure to convert. See below for formats.
  • **widget_args: Arguments passed straight to the widget constructor.
Returns

The widget or list of widgets created, or None if the passed structure could not be converted.


Data structures:

pytermgui.widgets.base.Label:

  • Created from str
  • Syntax example: "Label value"

pytermgui.widgets.extra.Splitter:

  • Created from tuple[Any]
  • Syntax example: (YourWidget(), "auto_syntax", ...)

pytermgui.widgets.extra.Splitter prompt:

  • Created from dict[Any, Any]
  • Syntax example: {YourWidget(): "auto_syntax"}

pytermgui.widgets.buttons.Button:

  • Created from list[str, pytermgui.widgets.buttons.MouseCallback]
  • Syntax example: ["Button label", lambda target, caller: ...]

pytermgui.widgets.buttons.Checkbox:

  • Created from list[bool, Callable[[bool], Any]]
  • Syntax example: [True, lambda checked: ...]

pytermgui.widgets.buttons.Toggle:

  • Created from list[tuple[str, str], Callable[[str], Any]]
  • Syntax example: [("On", "Off"), lambda new_value: ...]

Example:

from pytermgui import Container
form = (
    Container(id="form")
    + "[157 bold]This is a title"
    + ""
    + {"[72 italic]Label1": "[210]Button1"}
    + {"[72 italic]Label2": "[210]Button2"}
    + {"[72 italic]Label3": "[210]Button3"}
    + ""
    + ["Submit", lambda _, button, your_submit_handler(button.parent)]
)
bindings: dict[typing.Union[str, typing.Type[pytermgui.ansi_interface.MouseEvent]], tuple[typing.Callable[..., typing.Any], str]]

Gets a copy of the bindings internal dictionary.

Returns

A copy of the internal bindings dictionary, such as:

{
    "*": (star_callback, "This is a callback activated when '*' is pressed.")
}
id: Optional[str]

Gets this widget's id property

Returns

The id string if one is present, None otherwise.

selectables_length: int

Gets how many selectables this widget contains.

Returns

An integer describing the amount of selectables in this widget.

selectables: list[tuple[pytermgui.widgets.base.Widget, int]]

Gets a list of all selectables within this widget

Returns

A list of tuples. In the default implementation this will be a list of one tuple, containing a reference to self, as well as the lowest index, 0.

is_selectable: bool

Determines whether this widget has any selectables.

Returns

A boolean, representing self.selectables_length != 0.

static_width: int

Allows for a shorter way of setting a width, and SizePolicy.STATIC.

Args
  • value: The new width integer.
Returns

None, as this is setter only.

relative_width: float | None

Sets this widget's relative width, and changes size_policy to RELATIVE.

The value is clamped to 1.0.

If a Container holds a width of 30, and it has a subwidget with a relative width of 0.5, it will be resized to 15.

Args
  • value: The multiplier to apply to the parent's width.
Returns

The current relative_width.

Returns the current global terminal instance.

def get_change(self) -> pytermgui.enums.WidgetChange | None:
309    def get_change(self) -> WidgetChange | None:
310        """Determines whether widget lines changed since the last call to this function."""
311
312        lines = self.get_lines()
313
314        if self._previous_state is None:
315            self._previous_state = (self.width, self.height), lines
316            return WidgetChange.LINES
317
318        lines = self.get_lines()
319        (old_width, old_height), old_lines = self._previous_state
320
321        self._previous_state = (self.width, self.height), lines
322
323        if old_width != self.width and old_height != self.height:
324            return WidgetChange.SIZE
325
326        if old_width != self.width:
327            return WidgetChange.WIDTH
328
329        if old_height != self.height:
330            return WidgetChange.HEIGHT
331
332        if old_lines != lines:
333            return WidgetChange.LINES
334
335        return None

Determines whether widget lines changed since the last call to this function.

def contains(self, pos: tuple[int, int]) -> bool:
337    def contains(self, pos: tuple[int, int]) -> bool:
338        """Determines whether widget contains `pos`.
339
340        Args:
341            pos: Position to compare.
342
343        Returns:
344            Boolean describing whether the position is inside
345              this widget.
346        """
347
348        rect = self.pos, (
349            self.pos[0] + self.width,
350            self.pos[1] + self.height,
351        )
352
353        (left, top), (right, bottom) = rect
354
355        return left <= pos[0] < right and top <= pos[1] < bottom

Determines whether widget contains pos.

Args
  • pos: Position to compare.
Returns

Boolean describing whether the position is inside this widget.

def handle_mouse(self, event: pytermgui.ansi_interface.MouseEvent) -> bool:
357    def handle_mouse(self, event: MouseEvent) -> bool:
358        """Handles a mouse event, returning its success.
359
360        Args:
361            event: Object containing mouse event to handle.
362
363        Returns:
364            A boolean describing whether the mouse input was handled."""
365
366        return False and hasattr(self, event)

Handles a mouse event, returning its success.

Args
  • event: Object containing mouse event to handle.
Returns

A boolean describing whether the mouse input was handled.

def handle_key(self, key: str) -> bool:
368    def handle_key(self, key: str) -> bool:
369        """Handles a mouse event, returning its success.
370
371        Args:
372            key: String representation of input string.
373              The `pytermgui.input.keys` object can be
374              used to retrieve special keys.
375
376        Returns:
377            A boolean describing whether the key was handled.
378        """
379
380        return False and hasattr(self, key)

Handles a mouse event, returning its success.

Args
  • key: String representation of input string. The pytermgui.input.keys object can be used to retrieve special keys.
Returns

A boolean describing whether the key was handled.

def serialize(self) -> dict[str, typing.Any]:
382    def serialize(self) -> dict[str, Any]:
383        """Serializes a widget.
384
385        The fields looked at are defined `Widget.serialized`. Note that
386        this method is not very commonly used at the moment, so it might
387        not have full functionality in non-nuclear widgets.
388
389        Returns:
390            Dictionary of widget attributes. The dictionary will always
391            have a `type` field. Any styles are converted into markup
392            strings during serialization, so they can be loaded again in
393            their original form.
394
395            Example return:
396            ```
397                {
398                    "type": "Label",
399                    "value": "[210 bold]I am a title",
400                    "parent_align": 0,
401                    ...
402                }
403            ```
404        """
405
406        fields = self._serialized_fields
407
408        out: dict[str, Any] = {"type": type(self).__name__}
409        for key in fields:
410            # Detect styled values
411            if key.startswith("*"):
412                style = True
413                key = key[1:]
414            else:
415                style = False
416
417            value = getattr(self, key)
418
419            # Convert styled value into markup
420            if style:
421                style_call = self._get_style(key)
422                if isinstance(value, list):
423                    out[key] = [markup.get_markup(style_call(char)) for char in value]
424                else:
425                    out[key] = markup.get_markup(style_call(value))
426
427                continue
428
429            out[key] = value
430
431        # The chars need to be handled separately
432        out["chars"] = {}
433        for key, value in self.chars.items():
434            style_call = self._get_style(key)
435
436            if isinstance(value, list):
437                out["chars"][key] = [
438                    markup.get_markup(style_call(char)) for char in value
439                ]
440            else:
441                out["chars"][key] = markup.get_markup(style_call(value))
442
443        return out

Serializes a widget.

The fields looked at are defined Widget.serialized. Note that this method is not very commonly used at the moment, so it might not have full functionality in non-nuclear widgets.

Returns

Dictionary of widget attributes. The dictionary will always have a type field. Any styles are converted into markup strings during serialization, so they can be loaded again in their original form.

Example return:

    {
        "type": "Label",
        "value": "[210 bold]I am a title",
        "parent_align": 0,
        ...
    }
def copy(self) -> pytermgui.widgets.base.Widget:
445    def copy(self) -> Widget:
446        """Creates a deep copy of this widget"""
447
448        return deepcopy(self)

Creates a deep copy of this widget

def get_lines(self) -> list[str]:
488    def get_lines(self) -> list[str]:
489        """Gets lines representing this widget.
490
491        These lines have to be equal to the widget in length. All
492        widgets must provide this method. Make sure to keep it performant,
493        as it will be called very often, often multiple times per WindowManager frame.
494
495        Any longer actions should be done outside of this method, and only their
496        result should be looked up here.
497
498        Returns:
499            Nothing by default.
500
501        Raises:
502            NotImplementedError: As this method is required for **all** widgets, not
503                having it defined will raise NotImplementedError.
504        """
505
506        raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.")

Gets lines representing this widget.

These lines have to be equal to the widget in length. All widgets must provide this method. Make sure to keep it performant, as it will be called very often, often multiple times per WindowManager frame.

Any longer actions should be done outside of this method, and only their result should be looked up here.

Returns

Nothing by default.

Raises
  • NotImplementedError: As this method is required for all widgets, not having it defined will raise NotImplementedError.
def bind( self, key: str, action: Callable[..., Any], description: Optional[str] = None) -> None:
508    def bind(
509        self, key: str, action: BoundCallback, description: Optional[str] = None
510    ) -> None:
511        """Binds an action to a keypress.
512
513        This function is only called by implementations above this layer. To use this
514        functionality use `pytermgui.window_manager.WindowManager`, or write your own
515        custom layer.
516
517        Special keys:
518        - keys.ANY_KEY: Any and all keypresses execute this binding.
519        - keys.MouseAction: Any and all mouse inputs execute this binding.
520
521        Args:
522            key: The key that the action will be bound to.
523            action: The action executed when the key is pressed.
524            description: An optional description for this binding. It is not really
525                used anywhere, but you can provide a helper menu and display them.
526
527        Raises:
528            TypeError: This widget is not bindable, i.e. widget.is_bindable == False.
529        """
530
531        if not self.is_bindable:
532            raise TypeError(f"Widget of type {type(self)} does not accept bindings.")
533
534        if description is None:
535            description = f"Binding of {key} to {action}"
536
537        self._bindings[key] = (action, description)

Binds an action to a keypress.

This function is only called by implementations above this layer. To use this functionality use pytermgui.window_manager.WindowManager, or write your own custom layer.

Special keys:

  • keys.ANY_KEY: Any and all keypresses execute this binding.
  • keys.MouseAction: Any and all mouse inputs execute this binding.
Args
  • key: The key that the action will be bound to.
  • action: The action executed when the key is pressed.
  • description: An optional description for this binding. It is not really used anywhere, but you can provide a helper menu and display them.
Raises
  • TypeError: This widget is not bindable, i.e. widget.is_bindable == False.
def unbind(self, key: str) -> None:
539    def unbind(self, key: str) -> None:
540        """Unbinds the given key."""
541
542        del self._bindings[key]

Unbinds the given key.

def execute_binding(self, key: Any) -> bool:
544    def execute_binding(self, key: Any) -> bool:
545        """Executes a binding belonging to key, when present.
546
547        Use this method inside custom widget `handle_keys` methods, or to run a callback
548        without its corresponding key having been pressed.
549
550        Args:
551            key: Usually a string, indexing into the `_bindings` dictionary. These are the
552              same strings as defined in `Widget.bind`.
553
554        Returns:
555            True if the binding was found, False otherwise. Bindings will always be
556              executed if they are found.
557        """
558
559        # Execute special binding
560        if keys.ANY_KEY in self._bindings:
561            method, _ = self._bindings[keys.ANY_KEY]
562            method(self, key)
563
564        if key in self._bindings:
565            method, _ = self._bindings[key]
566            method(self, key)
567
568            return True
569
570        return False

Executes a binding belonging to key, when present.

Use this method inside custom widget handle_keys methods, or to run a callback without its corresponding key having been pressed.

Args
  • key: Usually a string, indexing into the _bindings dictionary. These are the same strings as defined in Widget.bind.
Returns

True if the binding was found, False otherwise. Bindings will always be executed if they are found.

def select(self, index: int | None = None) -> None:
572    def select(self, index: int | None = None) -> None:
573        """Selects a part of this Widget.
574
575        Args:
576            index: The index to select.
577
578        Raises:
579            TypeError: This widget has no selectables, i.e. widget.is_selectable == False.
580        """
581
582        if not self.is_selectable:
583            raise TypeError(f"Object of type {type(self)} has no selectables.")
584
585        if index is not None:
586            index = min(max(0, index), self.selectables_length - 1)
587        self.selected_index = index

Selects a part of this Widget.

Args
  • index: The index to select.
Raises
  • TypeError: This widget has no selectables, i.e. widget.is_selectable == False.
def print(self) -> None:
589    def print(self) -> None:
590        """Prints this widget"""
591
592        for line in self.get_lines():
593            print(line)

Prints this widget

def debug(self) -> str:
595    def debug(self) -> str:
596        """Returns identifiable information about this widget.
597
598        This method is used to easily differentiate between widgets. By default, all widget's
599        __repr__ method is an alias to this. The signature of each widget is used to generate
600        the return value.
601
602        Returns:
603            A string almost exactly matching the line of code that could have defined the widget.
604
605            Example return:
606
607            ```
608            Container(Label(value="This is a label", padding=0),
609            Button(label="This is a button", padding=0), **attrs)
610            ```
611
612        """
613
614        constructor = "("
615        for name in signature(getattr(self, "__init__")).parameters:
616            current = ""
617            if name == "attrs":
618                current += "**attrs"
619                continue
620
621            if len(constructor) > 1:
622                current += ", "
623
624            current += name
625
626            attr = getattr(self, name, None)
627            if attr is None:
628                continue
629
630            current += "="
631
632            if isinstance(attr, str):
633                current += f'"{attr}"'
634            else:
635                current += str(attr)
636
637            constructor += current
638
639        constructor += ")"
640
641        return type(self).__name__ + constructor

Returns identifiable information about this widget.

This method is used to easily differentiate between widgets. By default, all widget's __repr__ method is an alias to this. The signature of each widget is used to generate the return value.

Returns

A string almost exactly matching the line of code that could have defined the widget.

Example return:

Container(Label(value="This is a label", padding=0),
Button(label="This is a button", padding=0), **attrs)
class Label(Widget):
644class Label(Widget):
645    """A Widget to display a string
646
647    By default, this widget uses `pytermgui.widgets.styles.MARKUP`. This
648    allows it to house markup text that is parsed before display, such as:
649
650    ```python3
651    import pytermgui as ptg
652
653    with ptg.alt_buffer():
654        root = ptg.Container(
655            ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!")
656        )
657        root.print()
658        ptg.getch()
659    ```
660
661    <p style="text-align: center">
662     <img
663      src="https://github.com/bczsalba/pytermgui/blob/master/assets/docs/widgets/label.png?raw=true"
664      width=100%>
665    </p>
666    """
667
668    serialized = Widget.serialized + ["*value", "align", "padding"]
669    styles = w_styles.StyleManager(value=w_styles.MARKUP)
670
671    def __init__(
672        self,
673        value: str = "",
674        style: str | w_styles.StyleValue = "",
675        padding: int = 0,
676        non_first_padding: int = 0,
677        **attrs: Any,
678    ) -> None:
679        """Initializes a Label.
680
681        Args:
682            value: The value of this string. Using the default value style
683                (`pytermgui.widgets.styles.MARKUP`),
684            style: A pre-set value for self.styles.value.
685            padding: The number of space (" ") characters to prepend to every line after
686                line breaking.
687            non_first_padding: The number of space characters to prepend to every
688                non-first line of `get_lines`. This is applied on top of `padding`.
689        """
690
691        super().__init__(**attrs)
692
693        self.value = value
694        self.padding = padding
695        self.non_first_padding = non_first_padding
696        self.width = real_length(value) + self.padding
697
698        if style != "":
699            self.styles.value = style
700
701    def get_lines(self) -> list[str]:
702        """Get lines representing this Label, breaking lines as necessary"""
703
704        lines = []
705        limit = self.width - self.padding
706        broken = break_line(
707            self.styles.value(self.value),
708            limit=limit,
709            non_first_limit=limit - self.non_first_padding,
710        )
711
712        for i, line in enumerate(broken):
713            if i == 0:
714                lines.append(self.padding * " " + line)
715                continue
716
717            lines.append(self.padding * " " + self.non_first_padding * " " + line)
718
719        return lines or [""]

A Widget to display a string

By default, this widget uses pytermgui.widgets.styles.MARKUP. This allows it to house markup text that is parsed before display, such as:

import pytermgui as ptg

with ptg.alt_buffer():
    root = ptg.Container(
        ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!")
    )
    root.print()
    ptg.getch()

Label( value: str = '', style: 'str | w_styles.StyleValue' = '', padding: int = 0, non_first_padding: int = 0, **attrs: Any)
671    def __init__(
672        self,
673        value: str = "",
674        style: str | w_styles.StyleValue = "",
675        padding: int = 0,
676        non_first_padding: int = 0,
677        **attrs: Any,
678    ) -> None:
679        """Initializes a Label.
680
681        Args:
682            value: The value of this string. Using the default value style
683                (`pytermgui.widgets.styles.MARKUP`),
684            style: A pre-set value for self.styles.value.
685            padding: The number of space (" ") characters to prepend to every line after
686                line breaking.
687            non_first_padding: The number of space characters to prepend to every
688                non-first line of `get_lines`. This is applied on top of `padding`.
689        """
690
691        super().__init__(**attrs)
692
693        self.value = value
694        self.padding = padding
695        self.non_first_padding = non_first_padding
696        self.width = real_length(value) + self.padding
697
698        if style != "":
699            self.styles.value = style

Initializes a Label.

Args
  • value: The value of this string. Using the default value style (pytermgui.widgets.styles.MARKUP),
  • style: A pre-set value for self.styles.value.
  • padding: The number of space (" ") characters to prepend to every line after line breaking.
  • non_first_padding: The number of space characters to prepend to every non-first line of get_lines. This is applied on top of padding.
serialized: list[str] = ['id', 'pos', 'depth', 'width', 'height', 'selected_index', 'selectables_length', '*value', 'align', 'padding']

Fields of widget that shall be serialized by pytermgui.serializer.Serializer

styles = {'value': StyleCall(obj=None, method=<function <lambda>>)}

Default styles for this class

def get_lines(self) -> list[str]:
701    def get_lines(self) -> list[str]:
702        """Get lines representing this Label, breaking lines as necessary"""
703
704        lines = []
705        limit = self.width - self.padding
706        broken = break_line(
707            self.styles.value(self.value),
708            limit=limit,
709            non_first_limit=limit - self.non_first_padding,
710        )
711
712        for i, line in enumerate(broken):
713            if i == 0:
714                lines.append(self.padding * " " + line)
715                continue
716
717            lines.append(self.padding * " " + self.non_first_padding * " " + line)
718
719        return lines or [""]

Get lines representing this Label, breaking lines as necessary