pytermgui.widgets.base

The basic building blocks making up the Widget system.

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

The base of the Widget system

#   Widget(**attrs: Any)
View Source
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)

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

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

#   def contains(self, pos: tuple[int, int]) -> bool:
View Source
325    def contains(self, pos: tuple[int, int]) -> bool:
326        """Determines whether widget contains `pos`.
327
328        Args:
329            pos: Position to compare.
330
331        Returns:
332            Boolean describing whether the position is inside
333              this widget.
334        """
335
336        rect = self.pos, (
337            self.pos[0] + self.width,
338            self.pos[1] + self.height,
339        )
340
341        (left, top), (right, bottom) = rect
342
343        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:
View Source
345    def handle_mouse(self, event: MouseEvent) -> bool:
346        """Handles a mouse event, returning its success.
347
348        Args:
349            event: Object containing mouse event to handle.
350
351        Returns:
352            A boolean describing whether the mouse input was handled."""
353
354        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:
View Source
356    def handle_key(self, key: str) -> bool:
357        """Handles a mouse event, returning its success.
358
359        Args:
360            key: String representation of input string.
361              The `pytermgui.input.keys` object can be
362              used to retrieve special keys.
363
364        Returns:
365            A boolean describing whether the key was handled.
366        """
367
368        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]:
View Source
370    def serialize(self) -> dict[str, Any]:
371        """Serializes a widget.
372
373        The fields looked at are defined `Widget.serialized`. Note that
374        this method is not very commonly used at the moment, so it might
375        not have full functionality in non-nuclear widgets.
376
377        Returns:
378            Dictionary of widget attributes. The dictionary will always
379            have a `type` field. Any styles are converted into markup
380            strings during serialization, so they can be loaded again in
381            their original form.
382
383            Example return:
384            ```
385                {
386                    "type": "Label",
387                    "value": "[210 bold]I am a title",
388                    "parent_align": 0,
389                    ...
390                }
391            ```
392        """
393
394        fields = self._serialized_fields
395
396        out: dict[str, Any] = {"type": type(self).__name__}
397        for key in fields:
398            # Detect styled values
399            if key.startswith("*"):
400                style = True
401                key = key[1:]
402            else:
403                style = False
404
405            value = getattr(self, key)
406
407            # Convert styled value into markup
408            if style:
409                style_call = self._get_style(key)
410                if isinstance(value, list):
411                    out[key] = [markup.get_markup(style_call(char)) for char in value]
412                else:
413                    out[key] = markup.get_markup(style_call(value))
414
415                continue
416
417            out[key] = value
418
419        # The chars need to be handled separately
420        out["chars"] = {}
421        for key, value in self.chars.items():
422            style_call = self._get_style(key)
423
424            if isinstance(value, list):
425                out["chars"][key] = [
426                    markup.get_markup(style_call(char)) for char in value
427                ]
428            else:
429                out["chars"][key] = markup.get_markup(style_call(value))
430
431        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,
        ...
    }
View Source
433    def copy(self) -> Widget:
434        """Creates a deep copy of this widget"""
435
436        return deepcopy(self)

Creates a deep copy of this widget

#   def get_lines(self) -> list[str]:
View Source
476    def get_lines(self) -> list[str]:
477        """Gets lines representing this widget.
478
479        These lines have to be equal to the widget in length. All
480        widgets must provide this method. Make sure to keep it performant,
481        as it will be called very often, often multiple times per WindowManager frame.
482
483        Any longer actions should be done outside of this method, and only their
484        result should be looked up here.
485
486        Returns:
487            Nothing by default.
488
489        Raises:
490            NotImplementedError: As this method is required for **all** widgets, not
491                having it defined will raise NotImplementedError.
492        """
493
494        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:
View Source
496    def bind(
497        self, key: str, action: BoundCallback, description: Optional[str] = None
498    ) -> None:
499        """Binds an action to a keypress.
500
501        This function is only called by implementations above this layer. To use this
502        functionality use `pytermgui.window_manager.WindowManager`, or write your own
503        custom layer.
504
505        Special keys:
506        - keys.ANY_KEY: Any and all keypresses execute this binding.
507        - keys.MouseAction: Any and all mouse inputs execute this binding.
508
509        Args:
510            key: The key that the action will be bound to.
511            action: The action executed when the key is pressed.
512            description: An optional description for this binding. It is not really
513                used anywhere, but you can provide a helper menu and display them.
514
515        Raises:
516            TypeError: This widget is not bindable, i.e. widget.is_bindable == False.
517        """
518
519        if not self.is_bindable:
520            raise TypeError(f"Widget of type {type(self)} does not accept bindings.")
521
522        if description is None:
523            description = f"Binding of {key} to {action}"
524
525        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:
View Source
527    def unbind(self, key: str) -> None:
528        """Unbinds the given key."""
529
530        del self._bindings[key]

Unbinds the given key.

#   def execute_binding(self, key: Any) -> bool:
View Source
532    def execute_binding(self, key: Any) -> bool:
533        """Executes a binding belonging to key, when present.
534
535        Use this method inside custom widget `handle_keys` methods, or to run a callback
536        without its corresponding key having been pressed.
537
538        Args:
539            key: Usually a string, indexing into the `_bindings` dictionary. These are the
540              same strings as defined in `Widget.bind`.
541
542        Returns:
543            True if the binding was found, False otherwise. Bindings will always be
544              executed if they are found.
545        """
546
547        # Execute special binding
548        if keys.ANY_KEY in self._bindings:
549            method, _ = self._bindings[keys.ANY_KEY]
550            method(self, key)
551
552        if key in self._bindings:
553            method, _ = self._bindings[key]
554            method(self, key)
555
556            return True
557
558        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:
View Source
560    def select(self, index: int | None = None) -> None:
561        """Selects a part of this Widget.
562
563        Args:
564            index: The index to select.
565
566        Raises:
567            TypeError: This widget has no selectables, i.e. widget.is_selectable == False.
568        """
569
570        if not self.is_selectable:
571            raise TypeError(f"Object of type {type(self)} has no selectables.")
572
573        if index is not None:
574            index = min(max(0, index), self.selectables_length - 1)
575        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:
View Source
577    def print(self) -> None:
578        """Prints this widget"""
579
580        for line in self.get_lines():
581            print(line)

Prints this widget

#   def debug(self) -> str:
View Source
583    def debug(self) -> str:
584        """Returns identifiable information about this widget.
585
586        This method is used to easily differentiate between widgets. By default, all widget's
587        __repr__ method is an alias to this. The signature of each widget is used to generate
588        the return value.
589
590        Returns:
591            A string almost exactly matching the line of code that could have defined the widget.
592
593            Example return:
594
595            ```
596            Container(Label(value="This is a label", padding=0),
597            Button(label="This is a button", padding=0), **attrs)
598            ```
599
600        """
601
602        constructor = "("
603        for name in signature(getattr(self, "__init__")).parameters:
604            current = ""
605            if name == "attrs":
606                current += "**attrs"
607                continue
608
609            if len(constructor) > 1:
610                current += ", "
611
612            current += name
613
614            attr = getattr(self, name, None)
615            if attr is None:
616                continue
617
618            current += "="
619
620            if isinstance(attr, str):
621                current += f'"{attr}"'
622            else:
623                current += str(attr)
624
625            constructor += current
626
627        constructor += ")"
628
629        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):
View Source
632class Label(Widget):
633    """A Widget to display a string
634
635    By default, this widget uses `pytermgui.widgets.styles.MARKUP`. This
636    allows it to house markup text that is parsed before display, such as:
637
638    ```python3
639    import pytermgui as ptg
640
641    with ptg.alt_buffer():
642        root = ptg.Container(
643            ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!")
644        )
645        root.print()
646        ptg.getch()
647    ```
648
649    <p style="text-align: center">
650     <img
651      src="https://github.com/bczsalba/pytermgui/blob/master/assets/docs/widgets/label.png?raw=true"
652      width=100%>
653    </p>
654    """
655
656    serialized = Widget.serialized + ["*value", "align", "padding"]
657    styles = w_styles.StyleManager(value=w_styles.MARKUP)
658
659    def __init__(
660        self,
661        value: str = "",
662        style: str | w_styles.StyleValue = "",
663        padding: int = 0,
664        non_first_padding: int = 0,
665        **attrs: Any,
666    ) -> None:
667        """Initializes a Label.
668
669        Args:
670            value: The value of this string. Using the default value style
671                (`pytermgui.widgets.styles.MARKUP`),
672            style: A pre-set value for self.styles.value.
673            padding: The number of space (" ") characters to prepend to every line after
674                line breaking.
675            non_first_padding: The number of space characters to prepend to every
676                non-first line of `get_lines`. This is applied on top of `padding`.
677        """
678
679        super().__init__(**attrs)
680
681        self.value = value
682        self.padding = padding
683        self.non_first_padding = non_first_padding
684        self.width = real_length(value) + self.padding
685
686        if style != "":
687            self.styles.value = style
688
689    def get_lines(self) -> list[str]:
690        """Get lines representing this Label, breaking lines as necessary"""
691
692        lines = []
693        limit = self.width - self.padding
694        broken = break_line(
695            self.styles.value(self.value),
696            limit=limit,
697            non_first_limit=limit - self.non_first_padding,
698        )
699
700        for i, line in enumerate(broken):
701            if i == 0:
702                lines.append(self.padding * " " + line)
703                continue
704
705            lines.append(self.padding * " " + self.non_first_padding * " " + line)
706
707        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 )
View Source
659    def __init__(
660        self,
661        value: str = "",
662        style: str | w_styles.StyleValue = "",
663        padding: int = 0,
664        non_first_padding: int = 0,
665        **attrs: Any,
666    ) -> None:
667        """Initializes a Label.
668
669        Args:
670            value: The value of this string. Using the default value style
671                (`pytermgui.widgets.styles.MARKUP`),
672            style: A pre-set value for self.styles.value.
673            padding: The number of space (" ") characters to prepend to every line after
674                line breaking.
675            non_first_padding: The number of space characters to prepend to every
676                non-first line of `get_lines`. This is applied on top of `padding`.
677        """
678
679        super().__init__(**attrs)
680
681        self.value = value
682        self.padding = padding
683        self.non_first_padding = non_first_padding
684        self.width = real_length(value) + self.padding
685
686        if style != "":
687            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]:
View Source
689    def get_lines(self) -> list[str]:
690        """Get lines representing this Label, breaking lines as necessary"""
691
692        lines = []
693        limit = self.width - self.padding
694        broken = break_line(
695            self.styles.value(self.value),
696            limit=limit,
697            non_first_limit=limit - self.non_first_padding,
698        )
699
700        for i, line in enumerate(broken):
701            if i == 0:
702                lines.append(self.padding * " " + line)
703                continue
704
705            lines.append(self.padding * " " + self.non_first_padding * " " + line)
706
707        return lines or [""]

Get lines representing this Label, breaking lines as necessary