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

The base of the Widget system

Widget(**attrs: Any)
123    def __init__(self, **attrs: Any) -> None:
124        """Initialize object"""
125
126        self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value)
127        self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value)
128
129        self.width = 1
130        self.height = 1
131        self.pos = self.terminal.origin
132
133        self.depth = 0
134
135        self.styles = type(self).styles.branch(self)
136        self.chars = type(self).chars.copy()
137
138        self.parent: Widget | None = None
139        self.selected_index: int | None = None
140
141        self._selectables_length = 0
142        self._id: Optional[str] = None
143        self._serialized_fields = type(self).serialized
144        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
145        self._relative_width: float | None = None
146        self._previous_state: tuple[tuple[int, int], list[str]] | None = None
147
148        for attr, value in attrs.items():
149            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]:
32def _set_obj_or_cls_style(
33    obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.StyleType
34) -> Type[Widget] | Widget:
35    """Sets a style for an object or class
36
37    Args:
38        obj_or_cls: The Widget instance or type to update.
39        key: The style key.
40        value: The new style.
41
42    Returns:
43        Type[Widget] | Widget: The updated class.
44
45    Raises:
46        See `pytermgui.widgets.styles.StyleManager`.
47    """
48
49    obj_or_cls.styles[key] = value
50
51    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]:
54def _set_obj_or_cls_char(
55    obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.CharType
56) -> Type[Widget] | Widget:
57    """Sets a char for an object or class
58
59    Args:
60        obj_or_cls: The Widget instance or type to update.
61        key: The char key.
62        value: The new char.
63
64    Returns:
65        Type[Widget] | Widget: The updated class.
66
67    Raises:
68        KeyError: The char key provided is invalid.
69    """
70
71    if not key in obj_or_cls.chars.keys():
72        raise KeyError(f"Char {key} is not valid for {obj_or_cls}!")
73
74    obj_or_cls.chars[key] = value
75
76    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:
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

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

def contains(self, pos: tuple[int, int]) -> bool:
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

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:
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)

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:
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)

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]:
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

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:
444    def copy(self) -> Widget:
445        """Creates a deep copy of this widget"""
446
447        return deepcopy(self)

Creates a deep copy of this widget

def get_lines(self) -> list[str]:
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)}.")

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:
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)

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:
538    def unbind(self, key: str) -> None:
539        """Unbinds the given key."""
540
541        del self._bindings[key]

Unbinds the given key.

def execute_binding(self, key: Any) -> bool:
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

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:
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

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:
588    def print(self) -> None:
589        """Prints this widget"""
590
591        for line in self.get_lines():
592            print(line)

Prints this widget

def debug(self) -> str:
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

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):
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 [""]

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)
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

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]:
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 [""]

Get lines representing this Label, breaking lines as necessary