Skip to content

Index

The widget system.

keys = Keys(_platform_keys, 'nt') module-attribute

Instance storing platform specific key codes.

ASCII

Bases: Frame

A frame made up of only ASCII characters.

Preview:

-----
| x |
-----
Source code in pytermgui/widgets/frames.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
class ASCII(Frame):
    """A frame made up of only ASCII characters.

    Preview:

    ```
    -----
    | x |
    -----
    ```
    """

    descriptor = [
        "-----",
        "| x |",
        "-----",
    ]

ASCII_O

Bases: Frame

A frame made up of only ASCII characters, with X-s in the corners.

Preview:

o---o
| x |
o---o
Source code in pytermgui/widgets/frames.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
class ASCII_O(Frame):  # pylint: disable=invalid-name
    """A frame made up of only ASCII characters, with X-s in the corners.

    Preview:

    ```
    o---o
    | x |
    o---o
    ```
    """

    content_char = "x"

    descriptor = [
        "o---o",
        "| x |",
        "o---o",
    ]

ASCII_X

Bases: Frame

A frame made up of only ASCII characters, with X-s in the corners.

Preview:

x---x
| # |
x---x
Source code in pytermgui/widgets/frames.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
class ASCII_X(Frame):  # pylint: disable=invalid-name
    """A frame made up of only ASCII characters, with X-s in the corners.

    Preview:

    ```
    x---x
    | # |
    x---x
    ```
    """

    content_char = "#"

    descriptor = [
        "x---x",
        "| # |",
        "x---x",
    ]

CenteringPolicy

Bases: DefaultEnum

Policies to center Container according to.

Source code in pytermgui/enums.py
59
60
61
62
63
64
class CenteringPolicy(DefaultEnum):
    """Policies to center `Container` according to."""

    ALL = _auto()
    VERTICAL = _auto()
    HORIZONTAL = _auto()

Collapsible

Bases: Container

A collapsible section of UI.

Source code in pytermgui/widgets/collapsible.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
class Collapsible(Container):
    """A collapsible section of UI."""

    def __init__(
        self, label: str, *items: Any, keyboard: bool = False, **attrs: Any
    ) -> None:
        """Initializes the widget.

        Args:
            label: The label for the trigger toggle.
            *items: The items that will be hidden when the object is collapsed.
            keyboard: If set, the first character of the label will be used as
                a `CTRL_` binding to toggle the object.
        """

        if keyboard:
            bind = label[0]
            self.trigger = Toggle(
                (f"▶ ({bind}){label[1:]}", f"▼ ({bind}){label[1:]}"),
                lambda *_: self.toggle(),
            )
        else:
            self.trigger = Toggle(
                (f"▶ {label}", f"▼ {label}"), lambda *_: self.toggle()
            )

        super().__init__(self.trigger, *items, box="EMPTY", **attrs)

        if keyboard:
            self.bind(
                getattr(keys, f"CTRL_{bind}"),
                lambda *_: self.trigger.toggle(),
                "Open dropdown",
            )

        self.collapsed_height = 1
        self.overflow = Overflow.HIDE
        self.height = self.collapsed_height

        self._is_expanded = False

    @property
    def selectables(self) -> list[tuple[Widget, int]]:
        if self._is_expanded:
            return super().selectables

        return [(self.trigger, 0)]

    def toggle(self) -> Collapsible:
        """Toggles expanded state.

        Returns:
            This object.
        """

        if self.trigger.checked != self._is_expanded:
            self.trigger.toggle(run_callback=False)

        self._is_expanded = not self._is_expanded

        if self._is_expanded:
            self.overflow = Overflow.RESIZE
        else:
            self.overflow = Overflow.HIDE
            self.height = self.collapsed_height

        return self

    def collapse(self) -> Collapsible:
        """Collapses the dropdown.

        Does nothing if already collapsed.

        Returns:
            This object.
        """

        if self._is_expanded:
            self.toggle()

        return self

    def expand(self) -> Collapsible:
        """Expands the dropdown.

        Does nothing if already expanded.

        Returns:
            This object.
        """

        if not self._is_expanded:
            self.toggle()

        return self

__init__(label, *items, keyboard=False, **attrs)

Initializes the widget.

Parameters:

Name Type Description Default
label str

The label for the trigger toggle.

required
*items Any

The items that will be hidden when the object is collapsed.

()
keyboard bool

If set, the first character of the label will be used as a CTRL_ binding to toggle the object.

False
Source code in pytermgui/widgets/collapsible.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def __init__(
    self, label: str, *items: Any, keyboard: bool = False, **attrs: Any
) -> None:
    """Initializes the widget.

    Args:
        label: The label for the trigger toggle.
        *items: The items that will be hidden when the object is collapsed.
        keyboard: If set, the first character of the label will be used as
            a `CTRL_` binding to toggle the object.
    """

    if keyboard:
        bind = label[0]
        self.trigger = Toggle(
            (f"▶ ({bind}){label[1:]}", f"▼ ({bind}){label[1:]}"),
            lambda *_: self.toggle(),
        )
    else:
        self.trigger = Toggle(
            (f"▶ {label}", f"▼ {label}"), lambda *_: self.toggle()
        )

    super().__init__(self.trigger, *items, box="EMPTY", **attrs)

    if keyboard:
        self.bind(
            getattr(keys, f"CTRL_{bind}"),
            lambda *_: self.trigger.toggle(),
            "Open dropdown",
        )

    self.collapsed_height = 1
    self.overflow = Overflow.HIDE
    self.height = self.collapsed_height

    self._is_expanded = False

collapse()

Collapses the dropdown.

Does nothing if already collapsed.

Returns:

Type Description
Collapsible

This object.

Source code in pytermgui/widgets/collapsible.py
84
85
86
87
88
89
90
91
92
93
94
95
96
def collapse(self) -> Collapsible:
    """Collapses the dropdown.

    Does nothing if already collapsed.

    Returns:
        This object.
    """

    if self._is_expanded:
        self.toggle()

    return self

expand()

Expands the dropdown.

Does nothing if already expanded.

Returns:

Type Description
Collapsible

This object.

Source code in pytermgui/widgets/collapsible.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def expand(self) -> Collapsible:
    """Expands the dropdown.

    Does nothing if already expanded.

    Returns:
        This object.
    """

    if not self._is_expanded:
        self.toggle()

    return self

toggle()

Toggles expanded state.

Returns:

Type Description
Collapsible

This object.

Source code in pytermgui/widgets/collapsible.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def toggle(self) -> Collapsible:
    """Toggles expanded state.

    Returns:
        This object.
    """

    if self.trigger.checked != self._is_expanded:
        self.trigger.toggle(run_callback=False)

    self._is_expanded = not self._is_expanded

    if self._is_expanded:
        self.overflow = Overflow.RESIZE
    else:
        self.overflow = Overflow.HIDE
        self.height = self.collapsed_height

    return self

Container

Bases: ScrollableWidget

A widget that displays other widgets, stacked vertically.

Source code in pytermgui/widgets/containers.py
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
class Container(ScrollableWidget):
    """A widget that displays other widgets, stacked vertically."""

    styles = w_styles.StyleManager(
        border="surface",
        corner="surface",
        fill="background",
    )

    chars: dict[str, w_styles.CharType] = {
        "border": ["| ", "-", " |", "-"],
        "corner": [""] * 4,
    }

    keys = {
        "next": {keys.DOWN, keys.CTRL_N, "j"},
        "previous": {keys.UP, keys.CTRL_P, "k"},
        "scroll_down": {keys.SHIFT_DOWN, "J"},
        "scroll_up": {keys.SHIFT_UP, "K"},
    }

    serialized = Widget.serialized + ["centered_axis"]
    vertical_align = VerticalAlignment.CENTER
    allow_fullscreen = True

    overflow = Overflow.get_default()

    # TODO: Add `WidgetConvertible`? type instead of Any
    def __init__(self, *widgets: Any, **attrs: Any) -> None:
        """Initialize Container data"""

        super().__init__(**attrs)

        autosize = self.overflow is Overflow.SCROLL and "height" not in attrs
        if autosize:
            self.overflow = Overflow.RESIZE

        # TODO: This is just a band-aid.
        if not any("width" in attr for attr in attrs):
            self.width = 40

        self._widgets: list[Widget] = []
        self.dirty_widgets: list[Widget] = []
        self.centered_axis: CenteringPolicy | None = None

        self._prev_screen: tuple[int, int] = (0, 0)
        self._has_printed = False

        for widget in widgets:
            self._add_widget(widget)

        if autosize:
            self.overflow = Overflow.SCROLL

        if "box" not in attrs:
            attrs["box"] = "SINGLE"

        try:
            self.box = attrs["box"]
        # Splitter doesn't use boxes ATM.
        except KeyError:
            pass

        self._mouse_target: Widget | None = None

    @property
    def sidelength(self) -> int:
        """Gets the length of left and right borders combined.

        Returns:
            An integer equal to the `pytermgui.helpers.real_length` of the concatenation of
                the left and right borders of this widget, both with their respective styles
                applied.
        """

        return self.width - self.content_dimensions[0]

    @property
    def content_dimensions(self) -> tuple[int, int]:
        """Gets the size (width, height) of the available content area."""

        if "border" not in self.chars:
            return self.width, self.height

        chars = self._get_char("border")

        assert isinstance(chars, list)

        left, top, right, bottom = chars

        return (
            self.width - real_length(self.styles.border(left + right)),
            self.height - sum(1 if real_length(char) else 0 for char in [top, bottom]),
        )

    @property
    def selectables(self) -> list[tuple[Widget, int]]:
        """Gets all selectable widgets and their inner indices.

        This is used in order to have a constant reference to all selectable indices within this
        widget.

        Returns:
            A list of tuples containing a widget and an integer each. For each widget that is
            withing this one, it is added to this list as many times as it has selectables. Each
            of the integers correspond to a selectable_index within the widget.

            For example, a Container with a Button, InputField and an inner Container containing
            3 selectables might return something like this:

            ```
            [
                (Button(...), 0),
                (InputField(...), 0),
                (Container(...), 0),
                (Container(...), 1),
                (Container(...), 2),
            ]
            ```
        """

        _selectables: list[tuple[Widget, int]] = []
        for widget in self._widgets:
            if not widget.is_selectable:
                continue

            for i, (inner, _) in enumerate(widget.selectables):
                _selectables.append((inner, i))

        return _selectables

    @property
    def selectables_length(self) -> int:
        """Gets the length of the selectables list.

        Returns:
            An integer equal to the length of `self.selectables`.
        """

        return len(self.selectables)

    @property
    def selected(self) -> Widget | None:
        """Returns the currently selected object

        Returns:
            The currently selected widget if selected_index is not None,
            otherwise None.
        """

        # TODO: Add deeper selection

        if self.selected_index is None:
            return None

        if self.selected_index >= len(self.selectables):
            return None

        return self.selectables[self.selected_index][0]

    @property
    def box(self) -> boxes.Box:
        """Returns current box setting

        Returns:
            The currently set box instance.
        """

        return self._box

    @box.setter
    def box(self, new: str | boxes.Box) -> None:
        """Applies a new box.

        Args:
            new: Either a `pytermgui.boxes.Box` instance or a string
                analogous to one of the default box names.
        """

        if isinstance(new, str):
            from_module = vars(boxes).get(new)
            if from_module is None:
                raise ValueError(f"Unknown box type {new}.")

            new = from_module

        assert isinstance(new, boxes.Box)
        self._box = new
        new.set_chars_of(self)

    def get_change(self) -> WidgetChange | None:
        """Determines whether widget lines changed since the last call to this function."""

        change = super().get_change()

        if change is None:
            return None

        for widget in self._widgets:
            if widget.get_change() is not None:
                self.dirty_widgets.append(widget)

        return change

    def __iadd__(self, other: object) -> Container:
        """Adds a new widget, then returns self.

        Args:
            other: Any widget instance, or data structure that can be turned
                into a widget by `Widget.from_data`.

        Returns:
            A reference to self.
        """

        self._add_widget(other)
        return self

    def __add__(self, other: object) -> Container:
        """Adds a new widget, then returns self.

        This method is analogous to `Container.__iadd__`.

        Args:
            other: Any widget instance, or data structure that can be turned
                into a widget by `Widget.from_data`.

        Returns:
            A reference to self.
        """

        self.__iadd__(other)
        return self

    def __iter__(self) -> Iterator[Widget]:
        """Gets an iterator of self._widgets.

        Yields:
            The next widget.
        """

        yield from self._widgets

    def __len__(self) -> int:
        """Gets the length of the widgets list.

        Returns:
            An integer describing len(self._widgets).
        """

        return len(self._widgets)

    def __getitem__(self, sli: int | slice) -> Widget | list[Widget]:
        """Gets an item from self._widgets.

        Args:
            sli: Slice of the list.

        Returns:
            The slice in the list.
        """

        return self._widgets[sli]

    def __setitem__(self, index: int, value: Any) -> None:
        """Sets an item in self._widgets.

        Args:
            index: The index to be set.
            value: The new widget at this index.
        """

        self._widgets[index] = value

    def __contains__(self, other: object) -> bool:
        """Determines if self._widgets contains other widget.

        Args:
            other: Any widget-like.

        Returns:
            A boolean describing whether `other` is in `self.widgets`
        """

        if other in self._widgets:
            return True

        for widget in self._widgets:
            if isinstance(widget, Container) and other in widget:
                return True

        return False

    def _add_widget(self, other: object, run_get_lines: bool = True) -> Widget:
        """Adds other to this widget.

        Args:
            other: Any widget-like object.
            run_get_lines: Boolean controlling whether the self.get_lines is ran.

        Returns:
            The added widget. This is useful when data conversion took place in this
            function, e.g. a string was converted to a Label.
        """

        if not isinstance(other, Widget):
            to_widget = Widget.from_data(other)
            if to_widget is None:
                raise ValueError(
                    f"Could not convert {other} of type {type(other)} to a Widget!"
                )

            other = to_widget

        # This is safe to do, as it would've raised an exception above already
        assert isinstance(other, Widget)

        self._widgets.append(other)
        if isinstance(other, Container):
            other.set_recursive_depth(self.depth + 2)
        else:
            other.depth = self.depth + 1

        other.get_lines()
        other.parent = self

        if run_get_lines:
            self.get_lines()

        return other

    def _get_aligners(
        self, widget: Widget, borders: tuple[str, str]
    ) -> tuple[Callable[[str], str], int]:
        """Gets an aligning method and position offset.

        Args:
            widget: The widget to align.
            borders: The left and right borders to put the widget within.

        Returns:
            A tuple of a method that, when called with a line, will return that line
            centered using the passed in widget's parent_align and width, as well as
            the horizontal offset resulting from the widget being aligned.
        """

        left, right = self.styles.border(borders[0]), self.styles.border(borders[1])
        char = " "

        fill = self.styles.fill

        def _align_left(text: str) -> str:
            """Align line to the left"""

            padding = self.width - real_length(left + right) - real_length(text)
            return left + text + fill(padding * char) + right

        def _align_center(text: str) -> str:
            """Align line to the center"""

            total = self.width - real_length(left + right) - real_length(text)
            padding, offset = divmod(total, 2)
            return (
                left
                + fill((padding + offset) * char)
                + text
                + fill(padding * char)
                + right
            )

        def _align_right(text: str) -> str:
            """Align line to the right"""

            padding = self.width - real_length(left + right) - real_length(text)
            return left + fill(padding * char) + text + right

        if widget.parent_align == HorizontalAlignment.CENTER:
            total = self.width - real_length(left + right) - widget.width
            padding, offset = divmod(total, 2)
            return _align_center, real_length(left) + padding + offset

        if widget.parent_align == HorizontalAlignment.RIGHT:
            return _align_right, self.width - real_length(left) - widget.width

        # Default to left-aligned
        return _align_left, real_length(left)

    def _update_width(self, widget: Widget) -> None:
        """Updates the width of widget or self.

        This method respects widget.size_policy.

        Args:
            widget: The widget to update/base updates on.

        Raises:
            ValueError: Widget has SizePolicy.RELATIVE, but relative_width is None.
            WidthExceededError: Widget and self both have static widths, and widget's
                is larger than what is available.
        """

        available = self.width - self.sidelength

        if widget.size_policy == SizePolicy.FILL:
            widget.width = available
            return

        if widget.size_policy == SizePolicy.RELATIVE:
            if widget.relative_width is None:
                raise ValueError(f'Widget "{widget}"\'s relative width cannot be None.')

            widget.width = int(widget.relative_width * available)
            return

        if widget.width > available:
            if widget.size_policy == self.size_policy == SizePolicy.STATIC:
                raise WidthExceededError(
                    f"Widget {widget}'s static width of {widget.width}"
                    + f" exceeds its parent's available width {available}."
                    ""
                )

            if widget.size_policy == SizePolicy.STATIC:
                self.width = widget.width + self.sidelength

            else:
                widget.width = available

    def _apply_vertalign(
        self, lines: list[str], diff: int, padder: str
    ) -> tuple[int, list[str]]:
        """Insert padder line into lines diff times, depending on self.vertical_align.

        Args:
            lines: The list of lines to align.
            diff: The available height.
            padder: The line to use to pad.

        Returns:
            A tuple containing the vertical offset as well as the padded list of lines.

        Raises:
            NotImplementedError: The given vertical alignment is not implemented.
        """

        if self.vertical_align == VerticalAlignment.BOTTOM:
            for _ in range(diff):
                lines.insert(0, padder)

            return diff, lines

        if self.vertical_align == VerticalAlignment.TOP:
            for _ in range(diff):
                lines.append(padder)

            return 0, lines

        if self.vertical_align == VerticalAlignment.CENTER:
            top, extra = divmod(diff, 2)
            bottom = top + extra

            for _ in range(top):
                lines.insert(0, padder)

            for _ in range(bottom):
                lines.append(padder)

            return top, lines

        raise NotImplementedError(
            f"Vertical alignment {self.vertical_align} is not implemented for {type(self)}."
        )

    def lazy_add(self, other: object) -> None:
        """Adds `other` without running get_lines.

        This is analogous to `self._add_widget(other, run_get_lines=False).

        Args:
            other: The object to add.
        """

        self._add_widget(other, run_get_lines=False)

    def move(self, diff_x: int, diff_y: int) -> None:
        """Moves the widget and its children by the given x and y changes."""

        super().move(diff_x, diff_y)

        for child in self._widgets:
            child.move(diff_x, diff_y)

    def get_lines(self) -> list[str]:
        """Gets all lines by spacing out inner widgets.

        This method reflects & applies both width settings, as well as
        the `parent_align` field.

        Returns:
            A list of all lines that represent this Container.
        """

        def _get_border(left: str, char: str, right: str) -> str:
            """Gets a top or bottom border.

            Args:
                left: Left corner character.
                char: Border character filling between left & right.
                right: Right corner character.

            Returns:
                The border line.
            """

            offset = real_length(strip_markup(left + right))
            return (
                self.styles.corner(left)
                + self.styles.border(char * (self.width - offset))
                + self.styles.corner(right)
            )

        lines: list[str] = []

        borders = self._get_char("border")
        corners = self._get_char("corner")

        has_top_bottom = (real_length(borders[1]) > 0, real_length(borders[3]) > 0)

        align, offset = self._get_aligners(self, (borders[0], borders[2]))

        overflow = self.overflow

        for widget in self._widgets:
            align, offset = self._get_aligners(widget, (borders[0], borders[2]))

            self._update_width(widget)

            widget.pos = (
                self.pos[0] + offset,
                self.pos[1] + len(lines) + (1 if has_top_bottom[0] else 0),
            )

            widget_lines: list[str] = []
            for line in widget.get_lines():
                if len(lines) + len(widget_lines) >= self.height - sum(has_top_bottom):
                    if overflow is Overflow.HIDE:
                        break

                    if overflow == Overflow.AUTO:
                        overflow = Overflow.SCROLL

                widget_lines.append(align(line))

            lines.extend(widget_lines)

        if overflow == Overflow.SCROLL:
            self._max_scroll = len(lines) - self.height + sum(has_top_bottom)
            height = self.height - sum(has_top_bottom)

            self._scroll_offset = max(0, min(self._scroll_offset, len(lines) - height))
            lines = lines[self._scroll_offset : self._scroll_offset + height]

        elif overflow == Overflow.RESIZE:
            self.height = len(lines) + sum(has_top_bottom)

        vertical_offset, lines = self._apply_vertalign(
            lines, self.height - len(lines) - sum(has_top_bottom), align("")
        )

        for widget in self._widgets:
            widget.move(0, vertical_offset)

            self.positioned_line_buffer.extend(widget.positioned_line_buffer)
            widget.positioned_line_buffer = []

        if has_top_bottom[0]:
            lines.insert(0, _get_border(corners[0], borders[1], corners[1]))

        if has_top_bottom[1]:
            lines.append(_get_border(corners[3], borders[3], corners[2]))

        self.height = len(lines)
        return lines

    def set_widgets(self, new: list[Widget]) -> None:
        """Sets new list in place of self._widgets.

        Args:
            new: The new widget list.
        """

        self._widgets = []
        for widget in new:
            self._add_widget(widget)

    def serialize(self) -> dict[str, Any]:
        """Serializes this Container, adding in serializations of all widgets.

        See `pytermgui.widgets.base.Widget.serialize` for more info.

        Returns:
            The dictionary containing all serialized data.
        """

        out = super().serialize()
        out["_widgets"] = []

        for widget in self._widgets:
            out["_widgets"].append(widget.serialize())

        return out

    def pop(self, index: int = -1) -> Widget:
        """Pops widget from self._widgets.

        Analogous to self._widgets.pop(index).

        Args:
            index: The index to operate on.

        Returns:
            The widget that was popped off the list.
        """

        return self._widgets.pop(index)

    def remove(self, other: Widget) -> None:
        """Remove widget from self._widgets

        Analogous to self._widgets.remove(other).

        Args:
            other: The widget to remove.
        """

        return self._widgets.remove(other)

    def set_recursive_depth(self, value: int) -> None:
        """Set depth for this Container and all its children.

        All inner widgets will receive value+1 as their new depth.

        Args:
            value: The new depth to use as the base depth.
        """

        self.depth = value
        for widget in self._widgets:
            if isinstance(widget, Container):
                widget.set_recursive_depth(value + 1)
            else:
                widget.depth = value

    def select(self, index: int | None = None) -> None:
        """Selects inner subwidget.

        Args:
            index: The index to select.

        Raises:
            IndexError: The index provided was beyond len(self.selectables).
        """

        # Unselect all sub-elements
        for other in self._widgets:
            if other.selectables_length > 0:
                other.select(None)

        if index is not None:
            index = max(0, min(index, len(self.selectables) - 1))
            widget, inner_index = self.selectables[index]
            widget.select(inner_index)

        self.selected_index = index

        selected = self.selected
        if selected is None:
            return

        widget = selected
        parent = widget.parent

        while isinstance(parent, Container):
            if parent.overflow is Overflow.SCROLL:
                borders = parent._get_char("border")  # pylint: disable=protected-access
                assert isinstance(borders, list)

                viewport_top = (
                    parent.pos[1]
                    + (1 if real_length(borders[1]) else 0)
                    + parent._scroll_offset  # pylint: disable=protected-access
                )
                viewport_bottom = viewport_top + parent.content_dimensions[1]

                if widget.pos[1] < viewport_top:
                    parent.scroll(widget.pos[1] - viewport_top)
                elif widget.pos[1] + widget.height > viewport_bottom:
                    parent.scroll(widget.pos[1] + widget.height - viewport_bottom)

            widget = parent
            parent = parent.parent

    def center(
        self, where: CenteringPolicy | None = None, store: bool = True
    ) -> Container:
        """Centers this object to the given axis.

        Args:
            where: A CenteringPolicy describing the place to center to
            store: When set, this centering will be reapplied during every
                print, as well as when calling this method with no arguments.

        Returns:
            This Container.
        """

        # Refresh in case changes happened
        self.get_lines()

        if where is None:
            # See `enums.py` for explanation about this ignore.
            where = CenteringPolicy.get_default()  # type: ignore

        centerx = centery = where is CenteringPolicy.ALL
        centerx |= where is CenteringPolicy.HORIZONTAL
        centery |= where is CenteringPolicy.VERTICAL

        pos = list(self.pos)
        if centerx:
            pos[0] = (self.terminal.width - self.width + 2) // 2

        if centery:
            pos[1] = (self.terminal.height - self.height + 2) // 2

        self.pos = (pos[0], pos[1])

        if store:
            self.centered_axis = where

        self._prev_screen = self.terminal.size

        return self

    def handle_mouse(self, event: MouseEvent) -> bool:
        """Handles mouse events.

        This, like all mouse handlers should, calls super()'s implementation first,
        to allow usage of `on_{event}`-type callbacks. After that, it tries to find
        a target widget within itself to handle the event.

        Each handler will return a boolean. This boolean is then used to figure out
        whether the targeted widget should be "sticky", i.e. a slider. Returning
        True will set that widget as the current mouse target, and all mouse events will
        be sent to it as long as it returns True.

        Args:
            event: The event to handle.

        Returns:
            Whether the parent of this widget should treat it as one to "stick" events
            to, e.g. to keep sending mouse events to it. One can "unstick" a widget by
            returning False in the handler.
        """

        def _handle_scrolling() -> bool:
            """Scrolls the container."""

            if self.overflow != Overflow.SCROLL:
                return False

            if event.action is MouseAction.SCROLL_UP:
                return self.scroll(-1)

            if event.action is MouseAction.SCROLL_DOWN:
                return self.scroll(1)

            return False

        if super().handle_mouse(event):
            return True

        if event.action is MouseAction.RELEASE and self._mouse_target is not None:
            return self._mouse_target.handle_mouse(event)

        if (
            self._mouse_target is not None
            and (
                event.action.value.endswith("drag")
                or event.action.value.startswith("scroll")
            )
            and self._mouse_target.handle_mouse(event)
        ):
            return True

        release = MouseEvent(MouseAction.RELEASE, event.position)

        selectables_index = 0
        event.position = (event.position[0], event.position[1] + self._scroll_offset)

        handled = False
        for widget in self._widgets:
            if (
                widget.pos[1] - self.pos[1] - self._scroll_offset
                > self.content_dimensions[1]
            ):
                break

            if widget.contains(event.position):
                handled = widget.handle_mouse(event)
                selectables_index += widget.selected_index or 0

                # TODO: This really should be customizable somehow.
                if event.action is MouseAction.LEFT_CLICK:
                    if handled and selectables_index < len(self.selectables):
                        self.select(selectables_index)

                if self._mouse_target is not None and self._mouse_target is not widget:
                    self._mouse_target.handle_mouse(release)

                self._mouse_target = widget

                break

            if widget.is_selectable:
                selectables_index += widget.selectables_length

        handled = handled or _handle_scrolling()

        return handled

    def execute_binding(self, key: Any, ignore_any: bool = False) -> bool:
        """Executes a binding on self, and then on self._widgets.

        If a widget.execute_binding call returns True this function will too. Note
        that on success the function returns immediately; no further widgets are
        checked.

        Args:
            key: The binding key.
            ignore_any: If set, `keys.ANY_KEY` bindings will not be executed.

        Returns:
            True if any widget returned True, False otherwise.
        """

        if super().execute_binding(key, ignore_any=ignore_any):
            return True

        selectables_index = 0
        for widget in self._widgets:
            if widget.execute_binding(key):
                selectables_index += widget.selected_index or 0
                self.select(selectables_index)
                return True

            if widget.is_selectable:
                selectables_index += widget.selectables_length

        return False

    def handle_key(  # pylint: disable=too-many-return-statements, too-many-branches
        self, key: str
    ) -> bool:
        """Handles a keypress, returns its success.

        Args:
            key: A key str.

        Returns:
            A boolean showing whether the key was handled.
        """

        def _is_nav(key: str) -> bool:
            """Determine if a key is in the navigation sets"""

            return key in self.keys["next"] | self.keys["previous"]

        if self.selected is not None and self.selected.handle_key(key):
            return True

        scroll_actions = {
            **{key: 1 for key in self.keys["scroll_down"]},
            **{key: -1 for key in self.keys["scroll_up"]},
        }

        if key in self.keys["scroll_down"] | self.keys["scroll_up"]:
            for widget in self._widgets:
                if isinstance(widget, Container) and self.selected in widget:
                    widget.handle_key(key)

            self.scroll(scroll_actions[key])
            return True

        # Only use navigation when there is more than one selectable
        if self.selectables_length >= 1 and _is_nav(key):
            if self.selected_index is None:
                self.select(0)
                return True

            handled = False

            assert isinstance(self.selected_index, int)

            if key in self.keys["previous"]:
                # No more selectables left, user wants to exit Container
                # upwards.
                if self.selected_index == 0:
                    return False

                self.select(self.selected_index - 1)
                handled = True

            elif key in self.keys["next"]:
                # Stop selection at last element, return as unhandled
                new = self.selected_index + 1
                if new == len(self.selectables):
                    return False

                self.select(new)
                handled = True

            if handled:
                return True

        if key == keys.ENTER:
            if self.selected_index is None and self.selectables_length > 0:
                self.select(0)

            if self.selected is not None:
                self.selected.handle_key(key)
                return True

        for widget in self._widgets:
            if widget.execute_binding(key):
                return True

        return False

    def wipe(self) -> None:
        """Wipes the characters occupied by the object"""

        with cursor_at(self.pos) as print_here:
            for line in self.get_lines():
                print_here(real_length(line) * " ")

    def print(self) -> None:
        """Prints this Container.

        If the screen size has changed since last `print` call, the object
        will be centered based on its `centered_axis`.
        """

        if not self.terminal.size == self._prev_screen:
            clear()
            self.center(self.centered_axis)

        self._prev_screen = self.terminal.size

        if self.allow_fullscreen:
            self.pos = self.terminal.origin

        with cursor_at(self.pos) as print_here:
            for line in self.get_lines():
                print_here(line)

        self._has_printed = True

    def debug(self) -> str:
        """Returns a string with identifiable information on this widget.

        Returns:
            A str in the form of a class construction. This string is in a form that
            __could have been__ used to create this Container.
        """

        return (
            f"{type(self).__name__}(width={self.width}, height={self.height}"
            + (f", id={self.id}" if self.id is not None else "")
            + ")"
        )

box property writable

Returns current box setting

Returns:

Type Description
Box

The currently set box instance.

content_dimensions property

Gets the size (width, height) of the available content area.

selectables property

Gets all selectable widgets and their inner indices.

This is used in order to have a constant reference to all selectable indices within this widget.

Returns:

Type Description
list[tuple[Widget, int]]

A list of tuples containing a widget and an integer each. For each widget that is

list[tuple[Widget, int]]

withing this one, it is added to this list as many times as it has selectables. Each

list[tuple[Widget, int]]

of the integers correspond to a selectable_index within the widget.

list[tuple[Widget, int]]

For example, a Container with a Button, InputField and an inner Container containing

list[tuple[Widget, int]]

3 selectables might return something like this:

list[tuple[Widget, int]]

```

list[tuple[Widget, int]]

[ (Button(...), 0), (InputField(...), 0), (Container(...), 0), (Container(...), 1), (Container(...), 2),

list[tuple[Widget, int]]

]

list[tuple[Widget, int]]

```

selectables_length property

Gets the length of the selectables list.

Returns:

Type Description
int

An integer equal to the length of self.selectables.

selected property

Returns the currently selected object

Returns:

Type Description
Widget | None

The currently selected widget if selected_index is not None,

Widget | None

otherwise None.

sidelength property

Gets the length of left and right borders combined.

Returns:

Type Description
int

An integer equal to the pytermgui.helpers.real_length of the concatenation of the left and right borders of this widget, both with their respective styles applied.

__add__(other)

Adds a new widget, then returns self.

This method is analogous to Container.__iadd__.

Parameters:

Name Type Description Default
other object

Any widget instance, or data structure that can be turned into a widget by Widget.from_data.

required

Returns:

Type Description
Container

A reference to self.

Source code in pytermgui/widgets/containers.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def __add__(self, other: object) -> Container:
    """Adds a new widget, then returns self.

    This method is analogous to `Container.__iadd__`.

    Args:
        other: Any widget instance, or data structure that can be turned
            into a widget by `Widget.from_data`.

    Returns:
        A reference to self.
    """

    self.__iadd__(other)
    return self

__contains__(other)

Determines if self._widgets contains other widget.

Parameters:

Name Type Description Default
other object

Any widget-like.

required

Returns:

Type Description
bool

A boolean describing whether other is in self.widgets

Source code in pytermgui/widgets/containers.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
def __contains__(self, other: object) -> bool:
    """Determines if self._widgets contains other widget.

    Args:
        other: Any widget-like.

    Returns:
        A boolean describing whether `other` is in `self.widgets`
    """

    if other in self._widgets:
        return True

    for widget in self._widgets:
        if isinstance(widget, Container) and other in widget:
            return True

    return False

__getitem__(sli)

Gets an item from self._widgets.

Parameters:

Name Type Description Default
sli int | slice

Slice of the list.

required

Returns:

Type Description
Widget | list[Widget]

The slice in the list.

Source code in pytermgui/widgets/containers.py
281
282
283
284
285
286
287
288
289
290
291
def __getitem__(self, sli: int | slice) -> Widget | list[Widget]:
    """Gets an item from self._widgets.

    Args:
        sli: Slice of the list.

    Returns:
        The slice in the list.
    """

    return self._widgets[sli]

__iadd__(other)

Adds a new widget, then returns self.

Parameters:

Name Type Description Default
other object

Any widget instance, or data structure that can be turned into a widget by Widget.from_data.

required

Returns:

Type Description
Container

A reference to self.

Source code in pytermgui/widgets/containers.py
233
234
235
236
237
238
239
240
241
242
243
244
245
def __iadd__(self, other: object) -> Container:
    """Adds a new widget, then returns self.

    Args:
        other: Any widget instance, or data structure that can be turned
            into a widget by `Widget.from_data`.

    Returns:
        A reference to self.
    """

    self._add_widget(other)
    return self

__init__(*widgets, **attrs)

Initialize Container data

Source code in pytermgui/widgets/containers.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def __init__(self, *widgets: Any, **attrs: Any) -> None:
    """Initialize Container data"""

    super().__init__(**attrs)

    autosize = self.overflow is Overflow.SCROLL and "height" not in attrs
    if autosize:
        self.overflow = Overflow.RESIZE

    # TODO: This is just a band-aid.
    if not any("width" in attr for attr in attrs):
        self.width = 40

    self._widgets: list[Widget] = []
    self.dirty_widgets: list[Widget] = []
    self.centered_axis: CenteringPolicy | None = None

    self._prev_screen: tuple[int, int] = (0, 0)
    self._has_printed = False

    for widget in widgets:
        self._add_widget(widget)

    if autosize:
        self.overflow = Overflow.SCROLL

    if "box" not in attrs:
        attrs["box"] = "SINGLE"

    try:
        self.box = attrs["box"]
    # Splitter doesn't use boxes ATM.
    except KeyError:
        pass

    self._mouse_target: Widget | None = None

__iter__()

Gets an iterator of self._widgets.

Yields:

Type Description
Widget

The next widget.

Source code in pytermgui/widgets/containers.py
263
264
265
266
267
268
269
270
def __iter__(self) -> Iterator[Widget]:
    """Gets an iterator of self._widgets.

    Yields:
        The next widget.
    """

    yield from self._widgets

__len__()

Gets the length of the widgets list.

Returns:

Type Description
int

An integer describing len(self._widgets).

Source code in pytermgui/widgets/containers.py
272
273
274
275
276
277
278
279
def __len__(self) -> int:
    """Gets the length of the widgets list.

    Returns:
        An integer describing len(self._widgets).
    """

    return len(self._widgets)

__setitem__(index, value)

Sets an item in self._widgets.

Parameters:

Name Type Description Default
index int

The index to be set.

required
value Any

The new widget at this index.

required
Source code in pytermgui/widgets/containers.py
293
294
295
296
297
298
299
300
301
def __setitem__(self, index: int, value: Any) -> None:
    """Sets an item in self._widgets.

    Args:
        index: The index to be set.
        value: The new widget at this index.
    """

    self._widgets[index] = value

center(where=None, store=True)

Centers this object to the given axis.

Parameters:

Name Type Description Default
where CenteringPolicy | None

A CenteringPolicy describing the place to center to

None
store bool

When set, this centering will be reapplied during every print, as well as when calling this method with no arguments.

True

Returns:

Type Description
Container

This Container.

Source code in pytermgui/widgets/containers.py
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
def center(
    self, where: CenteringPolicy | None = None, store: bool = True
) -> Container:
    """Centers this object to the given axis.

    Args:
        where: A CenteringPolicy describing the place to center to
        store: When set, this centering will be reapplied during every
            print, as well as when calling this method with no arguments.

    Returns:
        This Container.
    """

    # Refresh in case changes happened
    self.get_lines()

    if where is None:
        # See `enums.py` for explanation about this ignore.
        where = CenteringPolicy.get_default()  # type: ignore

    centerx = centery = where is CenteringPolicy.ALL
    centerx |= where is CenteringPolicy.HORIZONTAL
    centery |= where is CenteringPolicy.VERTICAL

    pos = list(self.pos)
    if centerx:
        pos[0] = (self.terminal.width - self.width + 2) // 2

    if centery:
        pos[1] = (self.terminal.height - self.height + 2) // 2

    self.pos = (pos[0], pos[1])

    if store:
        self.centered_axis = where

    self._prev_screen = self.terminal.size

    return self

debug()

Returns a string with identifiable information on this widget.

Returns:

Type Description
str

A str in the form of a class construction. This string is in a form that

str

could have been used to create this Container.

Source code in pytermgui/widgets/containers.py
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
def debug(self) -> str:
    """Returns a string with identifiable information on this widget.

    Returns:
        A str in the form of a class construction. This string is in a form that
        __could have been__ used to create this Container.
    """

    return (
        f"{type(self).__name__}(width={self.width}, height={self.height}"
        + (f", id={self.id}" if self.id is not None else "")
        + ")"
    )

execute_binding(key, ignore_any=False)

Executes a binding on self, and then on self._widgets.

If a widget.execute_binding call returns True this function will too. Note that on success the function returns immediately; no further widgets are checked.

Parameters:

Name Type Description Default
key Any

The binding key.

required
ignore_any bool

If set, keys.ANY_KEY bindings will not be executed.

False

Returns:

Type Description
bool

True if any widget returned True, False otherwise.

Source code in pytermgui/widgets/containers.py
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
def execute_binding(self, key: Any, ignore_any: bool = False) -> bool:
    """Executes a binding on self, and then on self._widgets.

    If a widget.execute_binding call returns True this function will too. Note
    that on success the function returns immediately; no further widgets are
    checked.

    Args:
        key: The binding key.
        ignore_any: If set, `keys.ANY_KEY` bindings will not be executed.

    Returns:
        True if any widget returned True, False otherwise.
    """

    if super().execute_binding(key, ignore_any=ignore_any):
        return True

    selectables_index = 0
    for widget in self._widgets:
        if widget.execute_binding(key):
            selectables_index += widget.selected_index or 0
            self.select(selectables_index)
            return True

        if widget.is_selectable:
            selectables_index += widget.selectables_length

    return False

get_change()

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

Source code in pytermgui/widgets/containers.py
219
220
221
222
223
224
225
226
227
228
229
230
231
def get_change(self) -> WidgetChange | None:
    """Determines whether widget lines changed since the last call to this function."""

    change = super().get_change()

    if change is None:
        return None

    for widget in self._widgets:
        if widget.get_change() is not None:
            self.dirty_widgets.append(widget)

    return change

get_lines()

Gets all lines by spacing out inner widgets.

This method reflects & applies both width settings, as well as the parent_align field.

Returns:

Type Description
list[str]

A list of all lines that represent this Container.

Source code in pytermgui/widgets/containers.py
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
def get_lines(self) -> list[str]:
    """Gets all lines by spacing out inner widgets.

    This method reflects & applies both width settings, as well as
    the `parent_align` field.

    Returns:
        A list of all lines that represent this Container.
    """

    def _get_border(left: str, char: str, right: str) -> str:
        """Gets a top or bottom border.

        Args:
            left: Left corner character.
            char: Border character filling between left & right.
            right: Right corner character.

        Returns:
            The border line.
        """

        offset = real_length(strip_markup(left + right))
        return (
            self.styles.corner(left)
            + self.styles.border(char * (self.width - offset))
            + self.styles.corner(right)
        )

    lines: list[str] = []

    borders = self._get_char("border")
    corners = self._get_char("corner")

    has_top_bottom = (real_length(borders[1]) > 0, real_length(borders[3]) > 0)

    align, offset = self._get_aligners(self, (borders[0], borders[2]))

    overflow = self.overflow

    for widget in self._widgets:
        align, offset = self._get_aligners(widget, (borders[0], borders[2]))

        self._update_width(widget)

        widget.pos = (
            self.pos[0] + offset,
            self.pos[1] + len(lines) + (1 if has_top_bottom[0] else 0),
        )

        widget_lines: list[str] = []
        for line in widget.get_lines():
            if len(lines) + len(widget_lines) >= self.height - sum(has_top_bottom):
                if overflow is Overflow.HIDE:
                    break

                if overflow == Overflow.AUTO:
                    overflow = Overflow.SCROLL

            widget_lines.append(align(line))

        lines.extend(widget_lines)

    if overflow == Overflow.SCROLL:
        self._max_scroll = len(lines) - self.height + sum(has_top_bottom)
        height = self.height - sum(has_top_bottom)

        self._scroll_offset = max(0, min(self._scroll_offset, len(lines) - height))
        lines = lines[self._scroll_offset : self._scroll_offset + height]

    elif overflow == Overflow.RESIZE:
        self.height = len(lines) + sum(has_top_bottom)

    vertical_offset, lines = self._apply_vertalign(
        lines, self.height - len(lines) - sum(has_top_bottom), align("")
    )

    for widget in self._widgets:
        widget.move(0, vertical_offset)

        self.positioned_line_buffer.extend(widget.positioned_line_buffer)
        widget.positioned_line_buffer = []

    if has_top_bottom[0]:
        lines.insert(0, _get_border(corners[0], borders[1], corners[1]))

    if has_top_bottom[1]:
        lines.append(_get_border(corners[3], borders[3], corners[2]))

    self.height = len(lines)
    return lines

handle_key(key)

Handles a keypress, returns its success.

Parameters:

Name Type Description Default
key str

A key str.

required

Returns:

Type Description
bool

A boolean showing whether the key was handled.

Source code in pytermgui/widgets/containers.py
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
def handle_key(  # pylint: disable=too-many-return-statements, too-many-branches
    self, key: str
) -> bool:
    """Handles a keypress, returns its success.

    Args:
        key: A key str.

    Returns:
        A boolean showing whether the key was handled.
    """

    def _is_nav(key: str) -> bool:
        """Determine if a key is in the navigation sets"""

        return key in self.keys["next"] | self.keys["previous"]

    if self.selected is not None and self.selected.handle_key(key):
        return True

    scroll_actions = {
        **{key: 1 for key in self.keys["scroll_down"]},
        **{key: -1 for key in self.keys["scroll_up"]},
    }

    if key in self.keys["scroll_down"] | self.keys["scroll_up"]:
        for widget in self._widgets:
            if isinstance(widget, Container) and self.selected in widget:
                widget.handle_key(key)

        self.scroll(scroll_actions[key])
        return True

    # Only use navigation when there is more than one selectable
    if self.selectables_length >= 1 and _is_nav(key):
        if self.selected_index is None:
            self.select(0)
            return True

        handled = False

        assert isinstance(self.selected_index, int)

        if key in self.keys["previous"]:
            # No more selectables left, user wants to exit Container
            # upwards.
            if self.selected_index == 0:
                return False

            self.select(self.selected_index - 1)
            handled = True

        elif key in self.keys["next"]:
            # Stop selection at last element, return as unhandled
            new = self.selected_index + 1
            if new == len(self.selectables):
                return False

            self.select(new)
            handled = True

        if handled:
            return True

    if key == keys.ENTER:
        if self.selected_index is None and self.selectables_length > 0:
            self.select(0)

        if self.selected is not None:
            self.selected.handle_key(key)
            return True

    for widget in self._widgets:
        if widget.execute_binding(key):
            return True

    return False

handle_mouse(event)

Handles mouse events.

This, like all mouse handlers should, calls super()'s implementation first, to allow usage of on_{event}-type callbacks. After that, it tries to find a target widget within itself to handle the event.

Each handler will return a boolean. This boolean is then used to figure out whether the targeted widget should be "sticky", i.e. a slider. Returning True will set that widget as the current mouse target, and all mouse events will be sent to it as long as it returns True.

Parameters:

Name Type Description Default
event MouseEvent

The event to handle.

required

Returns:

Type Description
bool

Whether the parent of this widget should treat it as one to "stick" events

bool

to, e.g. to keep sending mouse events to it. One can "unstick" a widget by

bool

returning False in the handler.

Source code in pytermgui/widgets/containers.py
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
def handle_mouse(self, event: MouseEvent) -> bool:
    """Handles mouse events.

    This, like all mouse handlers should, calls super()'s implementation first,
    to allow usage of `on_{event}`-type callbacks. After that, it tries to find
    a target widget within itself to handle the event.

    Each handler will return a boolean. This boolean is then used to figure out
    whether the targeted widget should be "sticky", i.e. a slider. Returning
    True will set that widget as the current mouse target, and all mouse events will
    be sent to it as long as it returns True.

    Args:
        event: The event to handle.

    Returns:
        Whether the parent of this widget should treat it as one to "stick" events
        to, e.g. to keep sending mouse events to it. One can "unstick" a widget by
        returning False in the handler.
    """

    def _handle_scrolling() -> bool:
        """Scrolls the container."""

        if self.overflow != Overflow.SCROLL:
            return False

        if event.action is MouseAction.SCROLL_UP:
            return self.scroll(-1)

        if event.action is MouseAction.SCROLL_DOWN:
            return self.scroll(1)

        return False

    if super().handle_mouse(event):
        return True

    if event.action is MouseAction.RELEASE and self._mouse_target is not None:
        return self._mouse_target.handle_mouse(event)

    if (
        self._mouse_target is not None
        and (
            event.action.value.endswith("drag")
            or event.action.value.startswith("scroll")
        )
        and self._mouse_target.handle_mouse(event)
    ):
        return True

    release = MouseEvent(MouseAction.RELEASE, event.position)

    selectables_index = 0
    event.position = (event.position[0], event.position[1] + self._scroll_offset)

    handled = False
    for widget in self._widgets:
        if (
            widget.pos[1] - self.pos[1] - self._scroll_offset
            > self.content_dimensions[1]
        ):
            break

        if widget.contains(event.position):
            handled = widget.handle_mouse(event)
            selectables_index += widget.selected_index or 0

            # TODO: This really should be customizable somehow.
            if event.action is MouseAction.LEFT_CLICK:
                if handled and selectables_index < len(self.selectables):
                    self.select(selectables_index)

            if self._mouse_target is not None and self._mouse_target is not widget:
                self._mouse_target.handle_mouse(release)

            self._mouse_target = widget

            break

        if widget.is_selectable:
            selectables_index += widget.selectables_length

    handled = handled or _handle_scrolling()

    return handled

lazy_add(other)

Adds other without running get_lines.

This is analogous to `self._add_widget(other, run_get_lines=False).

Parameters:

Name Type Description Default
other object

The object to add.

required
Source code in pytermgui/widgets/containers.py
502
503
504
505
506
507
508
509
510
511
def lazy_add(self, other: object) -> None:
    """Adds `other` without running get_lines.

    This is analogous to `self._add_widget(other, run_get_lines=False).

    Args:
        other: The object to add.
    """

    self._add_widget(other, run_get_lines=False)

move(diff_x, diff_y)

Moves the widget and its children by the given x and y changes.

Source code in pytermgui/widgets/containers.py
513
514
515
516
517
518
519
def move(self, diff_x: int, diff_y: int) -> None:
    """Moves the widget and its children by the given x and y changes."""

    super().move(diff_x, diff_y)

    for child in self._widgets:
        child.move(diff_x, diff_y)

pop(index=-1)

Pops widget from self._widgets.

Analogous to self._widgets.pop(index).

Parameters:

Name Type Description Default
index int

The index to operate on.

-1

Returns:

Type Description
Widget

The widget that was popped off the list.

Source code in pytermgui/widgets/containers.py
641
642
643
644
645
646
647
648
649
650
651
652
653
def pop(self, index: int = -1) -> Widget:
    """Pops widget from self._widgets.

    Analogous to self._widgets.pop(index).

    Args:
        index: The index to operate on.

    Returns:
        The widget that was popped off the list.
    """

    return self._widgets.pop(index)

print()

Prints this Container.

If the screen size has changed since last print call, the object will be centered based on its centered_axis.

Source code in pytermgui/widgets/containers.py
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
def print(self) -> None:
    """Prints this Container.

    If the screen size has changed since last `print` call, the object
    will be centered based on its `centered_axis`.
    """

    if not self.terminal.size == self._prev_screen:
        clear()
        self.center(self.centered_axis)

    self._prev_screen = self.terminal.size

    if self.allow_fullscreen:
        self.pos = self.terminal.origin

    with cursor_at(self.pos) as print_here:
        for line in self.get_lines():
            print_here(line)

    self._has_printed = True

remove(other)

Remove widget from self._widgets

Analogous to self._widgets.remove(other).

Parameters:

Name Type Description Default
other Widget

The widget to remove.

required
Source code in pytermgui/widgets/containers.py
655
656
657
658
659
660
661
662
663
664
def remove(self, other: Widget) -> None:
    """Remove widget from self._widgets

    Analogous to self._widgets.remove(other).

    Args:
        other: The widget to remove.
    """

    return self._widgets.remove(other)

select(index=None)

Selects inner subwidget.

Parameters:

Name Type Description Default
index int | None

The index to select.

None

Raises:

Type Description
IndexError

The index provided was beyond len(self.selectables).

Source code in pytermgui/widgets/containers.py
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
def select(self, index: int | None = None) -> None:
    """Selects inner subwidget.

    Args:
        index: The index to select.

    Raises:
        IndexError: The index provided was beyond len(self.selectables).
    """

    # Unselect all sub-elements
    for other in self._widgets:
        if other.selectables_length > 0:
            other.select(None)

    if index is not None:
        index = max(0, min(index, len(self.selectables) - 1))
        widget, inner_index = self.selectables[index]
        widget.select(inner_index)

    self.selected_index = index

    selected = self.selected
    if selected is None:
        return

    widget = selected
    parent = widget.parent

    while isinstance(parent, Container):
        if parent.overflow is Overflow.SCROLL:
            borders = parent._get_char("border")  # pylint: disable=protected-access
            assert isinstance(borders, list)

            viewport_top = (
                parent.pos[1]
                + (1 if real_length(borders[1]) else 0)
                + parent._scroll_offset  # pylint: disable=protected-access
            )
            viewport_bottom = viewport_top + parent.content_dimensions[1]

            if widget.pos[1] < viewport_top:
                parent.scroll(widget.pos[1] - viewport_top)
            elif widget.pos[1] + widget.height > viewport_bottom:
                parent.scroll(widget.pos[1] + widget.height - viewport_bottom)

        widget = parent
        parent = parent.parent

serialize()

Serializes this Container, adding in serializations of all widgets.

See pytermgui.widgets.base.Widget.serialize for more info.

Returns:

Type Description
dict[str, Any]

The dictionary containing all serialized data.

Source code in pytermgui/widgets/containers.py
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
def serialize(self) -> dict[str, Any]:
    """Serializes this Container, adding in serializations of all widgets.

    See `pytermgui.widgets.base.Widget.serialize` for more info.

    Returns:
        The dictionary containing all serialized data.
    """

    out = super().serialize()
    out["_widgets"] = []

    for widget in self._widgets:
        out["_widgets"].append(widget.serialize())

    return out

set_recursive_depth(value)

Set depth for this Container and all its children.

All inner widgets will receive value+1 as their new depth.

Parameters:

Name Type Description Default
value int

The new depth to use as the base depth.

required
Source code in pytermgui/widgets/containers.py
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
def set_recursive_depth(self, value: int) -> None:
    """Set depth for this Container and all its children.

    All inner widgets will receive value+1 as their new depth.

    Args:
        value: The new depth to use as the base depth.
    """

    self.depth = value
    for widget in self._widgets:
        if isinstance(widget, Container):
            widget.set_recursive_depth(value + 1)
        else:
            widget.depth = value

set_widgets(new)

Sets new list in place of self._widgets.

Parameters:

Name Type Description Default
new list[Widget]

The new widget list.

required
Source code in pytermgui/widgets/containers.py
613
614
615
616
617
618
619
620
621
622
def set_widgets(self, new: list[Widget]) -> None:
    """Sets new list in place of self._widgets.

    Args:
        new: The new widget list.
    """

    self._widgets = []
    for widget in new:
        self._add_widget(widget)

wipe()

Wipes the characters occupied by the object

Source code in pytermgui/widgets/containers.py
967
968
969
970
971
972
def wipe(self) -> None:
    """Wipes the characters occupied by the object"""

    with cursor_at(self.pos) as print_here:
        for line in self.get_lines():
            print_here(real_length(line) * " ")

DensePixelMatrix

Bases: PixelMatrix

A more dense (2x) PixelMatrix.

Due to each pixel only occupying 1/2 characters in height, accurately determining selected_pixel is impossible, thus the functionality does not exist here.

Source code in pytermgui/widgets/pixel_matrix.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
class DensePixelMatrix(PixelMatrix):
    """A more dense (2x) PixelMatrix.

    Due to each pixel only occupying 1/2 characters in height, accurately
    determining selected_pixel is impossible, thus the functionality does
    not exist here.
    """

    def __init__(self, width: int, height: int, default: str = "", **attrs) -> None:
        """Initializes DensePixelMatrix.

        Args:
            width: The width of the matrix.
            height: The height of the matrix.
            default: The default color to use to initialize the matrix with.
        """

        super().__init__(width, height, default, **attrs)

        self.width = width // 2

    def handle_mouse(self, event: MouseEvent) -> bool:
        """As mentioned in the class documentation, mouse handling is disabled here."""

        return False

    def build(self) -> list[str]:
        """Builds the image pixels, using half-block characters.

        Returns:
            The lines that this object will return, until a subsequent `build` call.
            These lines are stored in the `self._lines` variable.
        """

        lines = []
        lines_to_zip: list[list[str]] = []
        for row in self._matrix:
            lines_to_zip.append(row)
            if len(lines_to_zip) != 2:
                continue

            line = ""
            top_row, bottom_row = lines_to_zip[0], lines_to_zip[1]
            for bottom, top in zip(bottom_row, top_row):
                if len(top) + len(bottom) == 0:
                    line += " "
                    continue

                if bottom == "":
                    line += tim.parse(f"[{top}]▀")
                    continue

                markup_str = "@" + top + " " if len(top) > 0 else ""

                markup_str += bottom
                line += tim.parse(f"[{markup_str}]▄")

            lines.append(line)
            lines_to_zip = []

        self._lines = lines
        self._update_dimensions(lines)

        return lines

__init__(width, height, default='', **attrs)

Initializes DensePixelMatrix.

Parameters:

Name Type Description Default
width int

The width of the matrix.

required
height int

The height of the matrix.

required
default str

The default color to use to initialize the matrix with.

''
Source code in pytermgui/widgets/pixel_matrix.py
162
163
164
165
166
167
168
169
170
171
172
173
def __init__(self, width: int, height: int, default: str = "", **attrs) -> None:
    """Initializes DensePixelMatrix.

    Args:
        width: The width of the matrix.
        height: The height of the matrix.
        default: The default color to use to initialize the matrix with.
    """

    super().__init__(width, height, default, **attrs)

    self.width = width // 2

build()

Builds the image pixels, using half-block characters.

Returns:

Type Description
list[str]

The lines that this object will return, until a subsequent build call.

list[str]

These lines are stored in the self._lines variable.

Source code in pytermgui/widgets/pixel_matrix.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def build(self) -> list[str]:
    """Builds the image pixels, using half-block characters.

    Returns:
        The lines that this object will return, until a subsequent `build` call.
        These lines are stored in the `self._lines` variable.
    """

    lines = []
    lines_to_zip: list[list[str]] = []
    for row in self._matrix:
        lines_to_zip.append(row)
        if len(lines_to_zip) != 2:
            continue

        line = ""
        top_row, bottom_row = lines_to_zip[0], lines_to_zip[1]
        for bottom, top in zip(bottom_row, top_row):
            if len(top) + len(bottom) == 0:
                line += " "
                continue

            if bottom == "":
                line += tim.parse(f"[{top}]▀")
                continue

            markup_str = "@" + top + " " if len(top) > 0 else ""

            markup_str += bottom
            line += tim.parse(f"[{markup_str}]▄")

        lines.append(line)
        lines_to_zip = []

    self._lines = lines
    self._update_dimensions(lines)

    return lines

handle_mouse(event)

As mentioned in the class documentation, mouse handling is disabled here.

Source code in pytermgui/widgets/pixel_matrix.py
175
176
177
178
def handle_mouse(self, event: MouseEvent) -> bool:
    """As mentioned in the class documentation, mouse handling is disabled here."""

    return False

Double

Bases: Frame

A frame with a double outline.

Preview:

╔═══╗
║ x ║
╚═══╝
Source code in pytermgui/widgets/frames.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
class Double(Frame):
    """A frame with a double outline.

    Preview:

    ```
    ╔═══╗
    ║ x ║
    ╚═══╝
    ```
    """

    descriptor = [
        "╔═══╗",
        "║ x ║",
        "╚═══╝",
    ]

Frame

An object that wraps a frame around its parent.

It can be used by any widget in order to draw a 'box' around itself. It implements scrolling as well.

Source code in pytermgui/widgets/frames.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
class Frame:
    """An object that wraps a frame around its parent.

    It can be used by any widget in order to draw a 'box' around itself. It
    implements scrolling as well.
    """

    descriptor: str
    content_char: str = "x"

    # left, top, right, bottom
    borders: tuple[str, str, str, str]

    # left_top, right_top, right_bottom, left_bottom
    corners: tuple[str, str, str, str]

    styles = StyleManager(
        border="surface",
        corner="surface2+2",
    )

    def __init__(self, parent: Widget) -> None:
        """Initializes Frame."""

        self._parent = parent
        self.styles = self.styles.branch(self._parent)

        if self.descriptor is not None:
            self._init_from_descriptor()

    def _init_from_descriptor(self) -> None:
        """Initializes the Frame's border & corner chars from the content property."""

        top, _, bottom = self.descriptor
        top_left, top_right = self._get_corners(top)
        bottom_left, bottom_right = self._get_corners(bottom)

        self.borders = list(self._get_borders(self.descriptor))
        self.corners = [
            top_left,
            top_right,
            bottom_right,
            bottom_left,
        ]

    @staticmethod
    def _find_mode_char(line: str) -> str:
        """Finds the most often consecutively occuring character."""

        instances = 0
        current_char = ""

        results: list[tuple[str, int]] = []
        for char in line:
            if current_char == char:
                instances += 1
            else:
                if len(current_char) > 0:
                    results.append((current_char, instances))

                instances = 1
                current_char = char

        results.append((current_char, instances))

        results.sort(key=lambda item: item[1])
        if len(results) == 0:
            print(line, instances, current_char)

        return results[-1][0]

    def _get_corners(self, line: str) -> tuple[str, str]:
        """Gets corners from a line."""

        mode_char = self._find_mode_char(line)
        left = line[: line.index(mode_char)]
        right = line[real_length(line) - (line[::-1].index(mode_char)) :]

        return left, right

    def _get_borders(self, lines: list[str]) -> tuple[str, str, str, str]:
        """Gets borders from all lines."""

        top, middle, bottom = lines
        middle_reversed = middle[::-1]

        top_border = self._find_mode_char(top)
        left_border = middle[: middle.index(self.content_char)]

        right_border = middle[
            real_length(middle) - middle_reversed.index(self.content_char) :
        ]
        bottom_border = self._find_mode_char(bottom)

        return left_border, top_border, right_border, bottom_border

    @staticmethod
    def from_name(name: str) -> Type[Frame]:
        """Gets a builtin Frame type from its name."""

        if frame := globals().get(name):
            return frame

        raise ValueError(f"No frame defined with name {name!r}.")

    @cached_property
    def left_size(self) -> int:
        """Returns the length of the left border character."""

        return real_length(self.borders[0])

    @cached_property
    def top_size(self) -> int:
        """Returns the height of the top border."""

        return 1

    @cached_property
    def right_size(self) -> int:
        """Returns the length of the right border character."""

        return real_length(self.borders[2])

    @cached_property
    def bottom_size(self) -> int:
        """Returns the height of the bottom border."""

        return 1

    def __call__(self, lines: list[str]) -> list[str]:
        """Frames the given lines, handles scrolling when necessary.

        Args:
            lines: A list of lines to 'frame'. If there are too many
                lines, they are clipped according to the parent's
                `scroll` field.

        Returns:
            Framed lines, clipped to the current scrolling settings.
        """

        if len(self.borders) != 4 or len(self.corners) != 4:
            raise ValueError("Cannot frame with no border or corner values.")

        scroll = self._parent.scroll

        # TODO: Widget.size should substract frame size, once
        #       it is aware of the frame.
        # width, height = self._parent.size
        width, height = self._parent.width, self._parent.height

        lines = lines[scroll.vertical : scroll.vertical + height]

        left_top, right_top, right_bottom, left_bottom = [
            self.styles.corner(corner) for corner in self.corners
        ]

        borders = [self.styles.border(char) for char in self.borders]

        top = (
            left_top
            + (width - real_length(left_top + right_top)) * borders[1]
            + right_top
        )

        bottom = (
            left_bottom
            + (width - real_length(left_bottom + right_bottom)) * borders[3]
            + right_bottom
        )

        framed = []

        if top != "":
            framed.append(top)

        for line in lines:
            # TODO: Implement horizontal scrolling
            framed.append(borders[0] + line + borders[2])

        if bottom != "":
            framed.append(bottom)

        return framed

bottom_size cached property

Returns the height of the bottom border.

left_size cached property

Returns the length of the left border character.

right_size cached property

Returns the length of the right border character.

top_size cached property

Returns the height of the top border.

__call__(lines)

Frames the given lines, handles scrolling when necessary.

Parameters:

Name Type Description Default
lines list[str]

A list of lines to 'frame'. If there are too many lines, they are clipped according to the parent's scroll field.

required

Returns:

Type Description
list[str]

Framed lines, clipped to the current scrolling settings.

Source code in pytermgui/widgets/frames.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def __call__(self, lines: list[str]) -> list[str]:
    """Frames the given lines, handles scrolling when necessary.

    Args:
        lines: A list of lines to 'frame'. If there are too many
            lines, they are clipped according to the parent's
            `scroll` field.

    Returns:
        Framed lines, clipped to the current scrolling settings.
    """

    if len(self.borders) != 4 or len(self.corners) != 4:
        raise ValueError("Cannot frame with no border or corner values.")

    scroll = self._parent.scroll

    # TODO: Widget.size should substract frame size, once
    #       it is aware of the frame.
    # width, height = self._parent.size
    width, height = self._parent.width, self._parent.height

    lines = lines[scroll.vertical : scroll.vertical + height]

    left_top, right_top, right_bottom, left_bottom = [
        self.styles.corner(corner) for corner in self.corners
    ]

    borders = [self.styles.border(char) for char in self.borders]

    top = (
        left_top
        + (width - real_length(left_top + right_top)) * borders[1]
        + right_top
    )

    bottom = (
        left_bottom
        + (width - real_length(left_bottom + right_bottom)) * borders[3]
        + right_bottom
    )

    framed = []

    if top != "":
        framed.append(top)

    for line in lines:
        # TODO: Implement horizontal scrolling
        framed.append(borders[0] + line + borders[2])

    if bottom != "":
        framed.append(bottom)

    return framed

__init__(parent)

Initializes Frame.

Source code in pytermgui/widgets/frames.py
47
48
49
50
51
52
53
54
def __init__(self, parent: Widget) -> None:
    """Initializes Frame."""

    self._parent = parent
    self.styles = self.styles.branch(self._parent)

    if self.descriptor is not None:
        self._init_from_descriptor()

from_name(name) staticmethod

Gets a builtin Frame type from its name.

Source code in pytermgui/widgets/frames.py
122
123
124
125
126
127
128
129
@staticmethod
def from_name(name: str) -> Type[Frame]:
    """Gets a builtin Frame type from its name."""

    if frame := globals().get(name):
        return frame

    raise ValueError(f"No frame defined with name {name!r}.")

Frameless

Bases: Frame

A frame that is not. No frame will be drawn around the object.

Preview:

x
Source code in pytermgui/widgets/frames.py
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
class Frameless(Frame):
    """A frame that is not. No frame will be drawn around the object.

    Preview:

    ```

    x

    ```
    """

    descriptor = [
        "",
        "x",
        "",
    ]

Heavy

Bases: Frame

A frame with a heavy outline.

Preview:

┏━━━┓
┃ x ┃
┗━━━┛
Source code in pytermgui/widgets/frames.py
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
class Heavy(Frame):
    """A frame with a heavy outline.

    Preview:

    ```
    ┏━━━┓
    ┃ x ┃
    ┗━━━┛
    ```
    """

    descriptor = [
        "┏━━━┓",
        "┃ x ┃",
        "┗━━━┛",
    ]

HighlighterStyle dataclass

A style that highlights the items given to it.

See pytermgui.highlighters for more information.

Source code in pytermgui/widgets/styles.py
121
122
123
124
125
126
127
128
129
130
131
132
133
@dataclass
class HighlighterStyle:
    """A style that highlights the items given to it.

    See `pytermgui.highlighters` for more information.
    """

    highlighter: Highlighter

    def __call__(self, _: int, item: str) -> str:
        """Highlights the given string."""

        return tim.parse(self.highlighter(item))

__call__(_, item)

Highlights the given string.

Source code in pytermgui/widgets/styles.py
130
131
132
133
def __call__(self, _: int, item: str) -> str:
    """Highlights the given string."""

    return tim.parse(self.highlighter(item))

HorizontalAlignment

Bases: DefaultEnum

Policies to align widgets by.

These are applied by the parent object, and are relative to them.

Source code in pytermgui/enums.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class HorizontalAlignment(DefaultEnum):
    """Policies to align widgets by.

    These are applied by the parent object, and are
    relative to them."""

    LEFT = 0
    """Align widget to the left edge."""

    CENTER = 1
    """Center widget in the available width."""

    RIGHT = 2
    """Align widget to the right edge."""

CENTER = 1 class-attribute instance-attribute

Center widget in the available width.

LEFT = 0 class-attribute instance-attribute

Align widget to the left edge.

RIGHT = 2 class-attribute instance-attribute

Align widget to the right edge.

Label

Bases: Widget

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:

print("hello world")
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()
Source code in pytermgui/widgets/base.py
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
class Label(Widget):
    """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:

    ```termage-svg
    print("hello world")
    ```

    ```python3
    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()
    ```
    """

    serialized = Widget.serialized + ["*value", "align", "padding"]
    styles = w_styles.StyleManager(value="")

    def __init__(
        self,
        value: str = "",
        style: str | w_styles.StyleValue = "",
        padding: int = 0,
        non_first_padding: int = 0,
        **attrs: Any,
    ) -> None:
        """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`.
        """

        super().__init__(**attrs)

        self.value = value
        self.padding = padding
        self.non_first_padding = non_first_padding
        self.width = real_length(value) + self.padding

        if style != "":
            self.styles.value = style

    def get_lines(self) -> list[str]:
        """Get lines representing this Label, breaking lines as necessary"""

        lines = []
        limit = self.width - self.padding
        broken = break_line(
            self.styles.value(self.value),
            limit=limit,
            non_first_limit=limit - self.non_first_padding,
        )

        for i, line in enumerate(broken):
            if i == 0:
                lines.append(self.padding * " " + line)
                continue

            lines.append(self.padding * " " + self.non_first_padding * " " + line)

        return lines or [""]

__init__(value='', style='', padding=0, non_first_padding=0, **attrs)

Initializes a Label.

Parameters:

Name Type Description Default
value str

The value of this string. Using the default value style (pytermgui.widgets.styles.MARKUP),

''
style str | StyleValue

A pre-set value for self.styles.value.

''
padding int

The number of space (" ") characters to prepend to every line after line breaking.

0
non_first_padding int

The number of space characters to prepend to every non-first line of get_lines. This is applied on top of padding.

0
Source code in pytermgui/widgets/base.py
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
def __init__(
    self,
    value: str = "",
    style: str | w_styles.StyleValue = "",
    padding: int = 0,
    non_first_padding: int = 0,
    **attrs: Any,
) -> None:
    """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`.
    """

    super().__init__(**attrs)

    self.value = value
    self.padding = padding
    self.non_first_padding = non_first_padding
    self.width = real_length(value) + self.padding

    if style != "":
        self.styles.value = style

get_lines()

Get lines representing this Label, breaking lines as necessary

Source code in pytermgui/widgets/base.py
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
def get_lines(self) -> list[str]:
    """Get lines representing this Label, breaking lines as necessary"""

    lines = []
    limit = self.width - self.padding
    broken = break_line(
        self.styles.value(self.value),
        limit=limit,
        non_first_limit=limit - self.non_first_padding,
    )

    for i, line in enumerate(broken):
        if i == 0:
            lines.append(self.padding * " " + line)
            continue

        lines.append(self.padding * " " + self.non_first_padding * " " + line)

    return lines or [""]

Light

Bases: Frame

A frame with a light outline.

Preview:

┌───┐
│ x │
└───┘
Source code in pytermgui/widgets/frames.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
class Light(Frame):
    """A frame with a light outline.

    Preview:

    ```
    ┌───┐
    │ x │
    └───┘
    ```
    """

    descriptor = [
        "┌───┐",
        "│ x │",
        "└───┘",
    ]

MarkupFormatter dataclass

A style that formats depth & item into the given markup on call.

Useful in Widget styles, such as:

import pytermgui as ptg

root = ptg.Container()

# Set border style to be reactive to the widget's depth
root.set_style("border", ptg.MarkupFactory("[35 @{depth}]{item}]")
Source code in pytermgui/widgets/styles.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
@dataclass
class MarkupFormatter:
    """A style that formats depth & item into the given markup on call.

    Useful in Widget styles, such as:

    ```python3
    import pytermgui as ptg

    root = ptg.Container()

    # Set border style to be reactive to the widget's depth
    root.set_style("border", ptg.MarkupFactory("[35 @{depth}]{item}]")
    ```
    """

    markup: str
    ensure_strip: bool = False

    _markup_cache: dict[str, str] = field(init=False, default_factory=dict)

    def __call__(self, depth: int, item: str) -> str:
        """StyleType: Format depth & item into given markup template"""

        if self.ensure_strip:
            item = strip_ansi(item)

        if item in self._markup_cache:
            item = self._markup_cache[item]

        else:
            original = item
            item = get_markup(item)
            self._markup_cache[original] = item

        return tim.parse(self.markup.format(depth=depth, item=item))

    def __str__(self) -> str:
        """Returns __repr__, but with markup escaped."""

        return self.__repr__().replace("[", r"\[")

__call__(depth, item)

StyleType: Format depth & item into given markup template

Source code in pytermgui/widgets/styles.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def __call__(self, depth: int, item: str) -> str:
    """StyleType: Format depth & item into given markup template"""

    if self.ensure_strip:
        item = strip_ansi(item)

    if item in self._markup_cache:
        item = self._markup_cache[item]

    else:
        original = item
        item = get_markup(item)
        self._markup_cache[original] = item

    return tim.parse(self.markup.format(depth=depth, item=item))

__str__()

Returns repr, but with markup escaped.

Source code in pytermgui/widgets/styles.py
115
116
117
118
def __str__(self) -> str:
    """Returns __repr__, but with markup escaped."""

    return self.__repr__().replace("[", r"\[")

MouseAction

Bases: Enum

An enumeration of all the polled mouse actions

Source code in pytermgui/ansi_interface.py
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
class MouseAction(Enum):
    """An enumeration of all the polled mouse actions"""

    LEFT_CLICK = "left_click"
    """Start of a left button action sequence."""

    LEFT_DRAG = "left_drag"
    """Mouse moved while left button was held down."""

    RIGHT_CLICK = "right_click"
    """Start of a right button action sequence."""

    RIGHT_DRAG = "right_drag"
    """Mouse moved while right button was held down."""

    SCROLL_UP = "scroll_up"
    """Mouse wheel or touchpad scroll upwards."""

    SCROLL_DOWN = "scroll_down"
    """Mouse wheel or touchpad scroll downwards."""

    SHIFT_SCROLL_UP = "shift_scroll_up"
    """Mouse wheel or touchpad scroll upwards."""

    SHIFT_SCROLL_DOWN = "shift_scroll_down"
    """Mouse wheel or touchpad scroll downwards."""

    HOVER = "hover"
    """Mouse moved without clicking."""

    # TODO: Support left & right mouse release separately, without breaking
    #       current API.
    RELEASE = "release"
    """Mouse button released; end of any and all mouse action sequences."""

HOVER = 'hover' class-attribute instance-attribute

Mouse moved without clicking.

LEFT_CLICK = 'left_click' class-attribute instance-attribute

Start of a left button action sequence.

LEFT_DRAG = 'left_drag' class-attribute instance-attribute

Mouse moved while left button was held down.

RELEASE = 'release' class-attribute instance-attribute

Mouse button released; end of any and all mouse action sequences.

RIGHT_CLICK = 'right_click' class-attribute instance-attribute

Start of a right button action sequence.

RIGHT_DRAG = 'right_drag' class-attribute instance-attribute

Mouse moved while right button was held down.

SCROLL_DOWN = 'scroll_down' class-attribute instance-attribute

Mouse wheel or touchpad scroll downwards.

SCROLL_UP = 'scroll_up' class-attribute instance-attribute

Mouse wheel or touchpad scroll upwards.

SHIFT_SCROLL_DOWN = 'shift_scroll_down' class-attribute instance-attribute

Mouse wheel or touchpad scroll downwards.

SHIFT_SCROLL_UP = 'shift_scroll_up' class-attribute instance-attribute

Mouse wheel or touchpad scroll upwards.

MouseEvent dataclass

A class to represent events created by mouse actions.

Its first argument is a MouseAction describing what happened, and its second argument is a tuple[int, int] describing where it happened.

This class mostly exists for readability & typing reasons. It also implements the iterable protocol, so you can use the unpacking syntax, such as:

action, position = MouseEvent(...)
Source code in pytermgui/ansi_interface.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
@dataclass
class MouseEvent:
    """A class to represent events created by mouse actions.

    Its first argument is a `MouseAction` describing what happened,
    and its second argument is a `tuple[int, int]` describing where
    it happened.

    This class mostly exists for readability & typing reasons. It also
    implements the iterable protocol, so you can use the unpacking syntax,
    such as:

    ```python3
    action, position = MouseEvent(...)
    ```
    """

    action: MouseAction
    position: tuple[int, int]

    def __post_init__(self) -> None:
        """Initialize iteration counter"""

        self._iter_index = 0

    def __next__(self) -> MouseAction | tuple[int, int]:
        """Get next iteration item"""

        data = fields(self)

        if self._iter_index >= len(data):
            self._iter_index = 0
            raise StopIteration

        self._iter_index += 1
        return getattr(self, data[self._iter_index - 1].name)

    def __iter__(self) -> MouseEvent:
        """Start iteration"""

        return self

    def is_scroll(self) -> bool:
        """Returns True if event.action is one of the scrolling actions."""

        return self.action in {MouseAction.SCROLL_DOWN, MouseAction.SCROLL_UP}

    def is_primary(self) -> bool:
        """Returns True if event.action is one of the primary (left-button) actions."""

        return self.action in {MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG}

    def is_secondary(self) -> bool:
        """Returns True if event.action is one of the secondary (secondary-button) actions."""

        return self.action in {MouseAction.RIGHT_CLICK, MouseAction.RIGHT_DRAG}

__iter__()

Start iteration

Source code in pytermgui/ansi_interface.py
450
451
452
453
def __iter__(self) -> MouseEvent:
    """Start iteration"""

    return self

__next__()

Get next iteration item

Source code in pytermgui/ansi_interface.py
438
439
440
441
442
443
444
445
446
447
448
def __next__(self) -> MouseAction | tuple[int, int]:
    """Get next iteration item"""

    data = fields(self)

    if self._iter_index >= len(data):
        self._iter_index = 0
        raise StopIteration

    self._iter_index += 1
    return getattr(self, data[self._iter_index - 1].name)

__post_init__()

Initialize iteration counter

Source code in pytermgui/ansi_interface.py
433
434
435
436
def __post_init__(self) -> None:
    """Initialize iteration counter"""

    self._iter_index = 0

is_primary()

Returns True if event.action is one of the primary (left-button) actions.

Source code in pytermgui/ansi_interface.py
460
461
462
463
def is_primary(self) -> bool:
    """Returns True if event.action is one of the primary (left-button) actions."""

    return self.action in {MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG}

is_scroll()

Returns True if event.action is one of the scrolling actions.

Source code in pytermgui/ansi_interface.py
455
456
457
458
def is_scroll(self) -> bool:
    """Returns True if event.action is one of the scrolling actions."""

    return self.action in {MouseAction.SCROLL_DOWN, MouseAction.SCROLL_UP}

is_secondary()

Returns True if event.action is one of the secondary (secondary-button) actions.

Source code in pytermgui/ansi_interface.py
465
466
467
468
def is_secondary(self) -> bool:
    """Returns True if event.action is one of the secondary (secondary-button) actions."""

    return self.action in {MouseAction.RIGHT_CLICK, MouseAction.RIGHT_DRAG}

Overflow

Bases: DefaultEnum

Overflow policies implemented by Container.

Source code in pytermgui/enums.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class Overflow(DefaultEnum):
    """Overflow policies implemented by Container."""

    HIDE = 0
    """Stop gathering lines once there is no room left."""

    SCROLL = 1
    """Allow scrolling when there is too many lines."""

    RESIZE = 2
    """Resize parent to fit with the new lines.

    Note:
        When applied to a window, this prevents resizing its height
        using the bottom border.
    """

    # TODO: Implement Overflow.AUTO
    AUTO = 9999
    """NotImplemented"""

AUTO = 9999 class-attribute instance-attribute

NotImplemented

HIDE = 0 class-attribute instance-attribute

Stop gathering lines once there is no room left.

RESIZE = 2 class-attribute instance-attribute

Resize parent to fit with the new lines.

Note

When applied to a window, this prevents resizing its height using the bottom border.

SCROLL = 1 class-attribute instance-attribute

Allow scrolling when there is too many lines.

Padded

Bases: Frame

A frame that pads its content by a single space on all sides.

Preview:

x
Source code in pytermgui/widgets/frames.py
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
class Padded(Frame):
    """A frame that pads its content by a single space on all sides.

    Preview:

    ```
    x
    ```
    """

    descriptor = [
        "   ",
        " x ",
        "   ",
    ]

PixelMatrix

Bases: Widget

A matrix of pixels.

The way this object should be used is by accessing & modifying the underlying matrix. This can be done using the set & getitem syntacies:

from pytermgui import PixelMatrix

matrix = PixelMatrix(10, 10, default="white")
for y in matrix.rows:
    for x in matrix.columns:
        matrix[y, x] = "black"

The above snippet draws a black diagonal going from the top left to bottom right.

Each item of the rows should be a single PyTermGUI-parsable color string. For more information about this, see pytermgui.ansi_interface.Color.

Source code in pytermgui/widgets/pixel_matrix.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
class PixelMatrix(Widget):
    """A matrix of pixels.

    The way this object should be used is by accessing & modifying
    the underlying matrix. This can be done using the set & getitem
    syntacies:

    ```python3
    from pytermgui import PixelMatrix

    matrix = PixelMatrix(10, 10, default="white")
    for y in matrix.rows:
        for x in matrix.columns:
            matrix[y, x] = "black"
    ```

    The above snippet draws a black diagonal going from the top left
    to bottom right.

    Each item of the rows should be a single PyTermGUI-parsable color
    string. For more information about this, see
    `pytermgui.ansi_interface.Color`.
    """

    selected_pixel: tuple[tuple[int, int], str] | None
    """A tuple of the position & value (color) of the currently hovered pixel."""

    def __init__(
        self, width: int, height: int, default: str = "background", **attrs
    ) -> None:
        """Initializes a PixelMatrix.

        Args:
            width: The amount of columns the matrix will have.
            height: The amount of rows the matrix will have.
            default: The default color to use to initialize the matrix with.
        """

        super().__init__(**attrs)

        self.rows = height
        self.columns = width

        self._matrix = []

        for _ in range(self.rows):
            self._matrix.append([default] * self.columns)

        self.selected_pixel = None
        self.build()

    @classmethod
    def from_matrix(cls, matrix: list[list[str]]) -> PixelMatrix:
        """Creates a PixelMatrix from the given matrix.

        The given matrix should be a list of rows, each containing a number
        of cells. It is optimal for all rows to share the same amount of cells.

        Args:
            matrix: The matrix to use. This is a list of lists of strings
                with each element representing a PyTermGUI-parseable color.

        Returns:
            A new type(self).
        """

        obj = cls(max(len(row) for row in matrix), len(matrix))
        setattr(obj, "_matrix", matrix)
        obj.build()

        return obj

    def _update_dimensions(self, lines: list[str]):
        """Updates the dimensions of this matrix.

        Args:
            lines: A list of lines that the calculations will be based upon.
        """

        self.static_width = max(real_length(line) for line in lines)
        self.height = len(lines)

    def on_hover(self, event: MouseEvent) -> bool:
        """Sets `selected_pixel` to the current pixel."""

        xoffset = event.position[0] - self.pos[0]
        yoffset = event.position[1] - self.pos[1]

        color = self._matrix[yoffset][xoffset // 2]

        self.selected_pixel = ((xoffset // 2, yoffset), color)
        return True

    def get_lines(self) -> list[str]:
        """Returns lines built by the `build` method."""

        return self._lines

    def build(self) -> list[str]:
        """Builds the image pixels.

        Returns:
            The lines that this object will return, until a subsequent `build` call.
            These lines are stored in the `self._lines` variable.
        """

        lines: list[str] = []
        for row in self._matrix:
            line = ""
            for pixel in row:
                if len(pixel) > 0 and pixel != "background":
                    line += f"[@{pixel}]  "
                else:
                    line += "[/ background]  "

            lines.append(tim.parse(line))

        self._lines = lines
        self._update_dimensions(lines)

        return lines

    def __getitem__(self, indices: tuple[int, int]) -> str:
        """Gets a matrix item."""

        posy, posx = indices
        return self._matrix[posy][posx]

    def __setitem__(self, indices: tuple[int, int], value: str) -> None:
        """Sets a matrix item."""

        posy, posx = indices
        self._matrix[posy][posx] = value

selected_pixel = None instance-attribute

A tuple of the position & value (color) of the currently hovered pixel.

__getitem__(indices)

Gets a matrix item.

Source code in pytermgui/widgets/pixel_matrix.py
141
142
143
144
145
def __getitem__(self, indices: tuple[int, int]) -> str:
    """Gets a matrix item."""

    posy, posx = indices
    return self._matrix[posy][posx]

__init__(width, height, default='background', **attrs)

Initializes a PixelMatrix.

Parameters:

Name Type Description Default
width int

The amount of columns the matrix will have.

required
height int

The amount of rows the matrix will have.

required
default str

The default color to use to initialize the matrix with.

'background'
Source code in pytermgui/widgets/pixel_matrix.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def __init__(
    self, width: int, height: int, default: str = "background", **attrs
) -> None:
    """Initializes a PixelMatrix.

    Args:
        width: The amount of columns the matrix will have.
        height: The amount of rows the matrix will have.
        default: The default color to use to initialize the matrix with.
    """

    super().__init__(**attrs)

    self.rows = height
    self.columns = width

    self._matrix = []

    for _ in range(self.rows):
        self._matrix.append([default] * self.columns)

    self.selected_pixel = None
    self.build()

__setitem__(indices, value)

Sets a matrix item.

Source code in pytermgui/widgets/pixel_matrix.py
147
148
149
150
151
def __setitem__(self, indices: tuple[int, int], value: str) -> None:
    """Sets a matrix item."""

    posy, posx = indices
    self._matrix[posy][posx] = value

build()

Builds the image pixels.

Returns:

Type Description
list[str]

The lines that this object will return, until a subsequent build call.

list[str]

These lines are stored in the self._lines variable.

Source code in pytermgui/widgets/pixel_matrix.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def build(self) -> list[str]:
    """Builds the image pixels.

    Returns:
        The lines that this object will return, until a subsequent `build` call.
        These lines are stored in the `self._lines` variable.
    """

    lines: list[str] = []
    for row in self._matrix:
        line = ""
        for pixel in row:
            if len(pixel) > 0 and pixel != "background":
                line += f"[@{pixel}]  "
            else:
                line += "[/ background]  "

        lines.append(tim.parse(line))

    self._lines = lines
    self._update_dimensions(lines)

    return lines

from_matrix(matrix) classmethod

Creates a PixelMatrix from the given matrix.

The given matrix should be a list of rows, each containing a number of cells. It is optimal for all rows to share the same amount of cells.

Parameters:

Name Type Description Default
matrix list[list[str]]

The matrix to use. This is a list of lists of strings with each element representing a PyTermGUI-parseable color.

required

Returns:

Type Description
PixelMatrix

A new type(self).

Source code in pytermgui/widgets/pixel_matrix.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@classmethod
def from_matrix(cls, matrix: list[list[str]]) -> PixelMatrix:
    """Creates a PixelMatrix from the given matrix.

    The given matrix should be a list of rows, each containing a number
    of cells. It is optimal for all rows to share the same amount of cells.

    Args:
        matrix: The matrix to use. This is a list of lists of strings
            with each element representing a PyTermGUI-parseable color.

    Returns:
        A new type(self).
    """

    obj = cls(max(len(row) for row in matrix), len(matrix))
    setattr(obj, "_matrix", matrix)
    obj.build()

    return obj

get_lines()

Returns lines built by the build method.

Source code in pytermgui/widgets/pixel_matrix.py
112
113
114
115
def get_lines(self) -> list[str]:
    """Returns lines built by the `build` method."""

    return self._lines

on_hover(event)

Sets selected_pixel to the current pixel.

Source code in pytermgui/widgets/pixel_matrix.py
101
102
103
104
105
106
107
108
109
110
def on_hover(self, event: MouseEvent) -> bool:
    """Sets `selected_pixel` to the current pixel."""

    xoffset = event.position[0] - self.pos[0]
    yoffset = event.position[1] - self.pos[1]

    color = self._matrix[yoffset][xoffset // 2]

    self.selected_pixel = ((xoffset // 2, yoffset), color)
    return True

Rounded

Bases: Frame

A frame with a light outline and rounded corners.

Preview:

╭───╮
│ x │
╰───╯
Source code in pytermgui/widgets/frames.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
class Rounded(Frame):
    """A frame with a light outline and rounded corners.

    Preview:

    ```
    ╭───╮
    │ x │
    ╰───╯
    ```
    """

    descriptor = [
        "╭───╮",
        "│ x │",
        "╰───╯",
    ]

ScrollableWidget

Bases: Widget

A widget with some scrolling helper methods.

This is not an implementation of the scrolling behaviour itself, just the user-facing API for it.

It provides a _scroll_offset attribute, which is an integer describing the current scroll state offset from the top, as well as some methods to modify the state.

Source code in pytermgui/widgets/base.py
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
class ScrollableWidget(Widget):
    """A widget with some scrolling helper methods.

    This is not an implementation of the scrolling behaviour itself, just the
    user-facing API for it.

    It provides a `_scroll_offset` attribute, which is an integer describing the current
    scroll state offset from the top, as well as some methods to modify the state."""

    def __init__(self, **attrs: Any) -> None:
        """Initializes the scrollable widget."""

        super().__init__(**attrs)

        self._max_scroll = 0
        self._scroll_offset = 0

    def scroll(self, offset: int) -> bool:
        """Scrolls to given offset, returns the new scroll_offset.

        Args:
            offset: The amount to scroll by. Positive offsets scroll down,
                negative up.

        Returns:
            True if the scroll offset changed, False otherwise.
        """

        base = self._scroll_offset

        self._scroll_offset = min(
            max(0, self._scroll_offset + offset), self._max_scroll
        )

        return base != self._scroll_offset

    def scroll_end(self, end: int) -> int:
        """Scrolls to either top or bottom end of this object.

        Args:
            end: The offset to scroll to. 0 goes to the very top, -1 to the
                very bottom.

        Returns:
            True if the scroll offset changed, False otherwise.
        """

        base = self._scroll_offset

        if end == 0:
            self._scroll_offset = 0

        elif end == -1:
            self._scroll_offset = self._max_scroll

        return base != self._scroll_offset

    def get_lines(self) -> list[str]:
        ...

__init__(**attrs)

Initializes the scrollable widget.

Source code in pytermgui/widgets/base.py
804
805
806
807
808
809
810
def __init__(self, **attrs: Any) -> None:
    """Initializes the scrollable widget."""

    super().__init__(**attrs)

    self._max_scroll = 0
    self._scroll_offset = 0

scroll(offset)

Scrolls to given offset, returns the new scroll_offset.

Parameters:

Name Type Description Default
offset int

The amount to scroll by. Positive offsets scroll down, negative up.

required

Returns:

Type Description
bool

True if the scroll offset changed, False otherwise.

Source code in pytermgui/widgets/base.py
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
def scroll(self, offset: int) -> bool:
    """Scrolls to given offset, returns the new scroll_offset.

    Args:
        offset: The amount to scroll by. Positive offsets scroll down,
            negative up.

    Returns:
        True if the scroll offset changed, False otherwise.
    """

    base = self._scroll_offset

    self._scroll_offset = min(
        max(0, self._scroll_offset + offset), self._max_scroll
    )

    return base != self._scroll_offset

scroll_end(end)

Scrolls to either top or bottom end of this object.

Parameters:

Name Type Description Default
end int

The offset to scroll to. 0 goes to the very top, -1 to the very bottom.

required

Returns:

Type Description
int

True if the scroll offset changed, False otherwise.

Source code in pytermgui/widgets/base.py
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
def scroll_end(self, end: int) -> int:
    """Scrolls to either top or bottom end of this object.

    Args:
        end: The offset to scroll to. 0 goes to the very top, -1 to the
            very bottom.

    Returns:
        True if the scroll offset changed, False otherwise.
    """

    base = self._scroll_offset

    if end == 0:
        self._scroll_offset = 0

    elif end == -1:
        self._scroll_offset = self._max_scroll

    return base != self._scroll_offset

SizePolicy

Bases: DefaultEnum

Values according to which Widget sizes are assigned.

Source code in pytermgui/enums.py
45
46
47
48
49
50
51
52
53
54
55
56
class SizePolicy(DefaultEnum):
    """Values according to which Widget sizes are assigned."""

    FILL = 0
    """Inner widget will take up as much width as possible."""

    STATIC = 1
    """Inner widget will take up an exact amount of width."""

    RELATIVE = 2
    """Inner widget will take up widget.relative_width * available
    space."""

FILL = 0 class-attribute instance-attribute

Inner widget will take up as much width as possible.

RELATIVE = 2 class-attribute instance-attribute

Inner widget will take up widget.relative_width * available space.

STATIC = 1 class-attribute instance-attribute

Inner widget will take up an exact amount of width.

Splitter

Bases: Container

A widget that displays other widgets, stacked horizontally.

Source code in pytermgui/widgets/containers.py
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
class Splitter(Container):
    """A widget that displays other widgets, stacked horizontally."""

    styles = w_styles.StyleManager(separator="surface", fill="background")

    chars: dict[str, list[str] | str] = {"separator": " | "}
    keys = {
        "previous": {keys.LEFT, "h", keys.CTRL_B},
        "next": {keys.RIGHT, "l", keys.CTRL_F},
    }

    parent_align = HorizontalAlignment.RIGHT

    def _align_line(
        self, alignment: HorizontalAlignment, target_width: int, line: str
    ) -> tuple[int, str]:
        """Align a line

        r/wordavalanches"""

        available = target_width - real_length(line)
        fill_style = self._get_style("fill")

        char = fill_style(" ")
        line = fill_style(line)

        if alignment == HorizontalAlignment.CENTER:
            padding, offset = divmod(available, 2)
            return padding, padding * char + line + (padding + offset) * char

        if alignment == HorizontalAlignment.RIGHT:
            return available, available * char + line

        return 0, line + available * char

    @property
    def content_dimensions(self) -> tuple[int, int]:
        """Returns the available area for widgets."""

        return self.height, self.width

    def get_lines(self) -> list[str]:  # pylint: disable=too-many-locals
        """Join all widgets horizontally."""

        # An error will be raised if `separator` is not the correct type (str).
        separator = self._get_style("separator")(self._get_char("separator"))  # type: ignore
        separator_length = real_length(separator)

        target_width, error = divmod(
            self.width - (len(self._widgets) - 1) * separator_length, len(self._widgets)
        )

        self.positioned_line_buffer = []
        vertical_lines = []
        column_widths = []
        total_offset = 0

        for widget in self._widgets:
            inner = []

            if widget.size_policy is SizePolicy.STATIC:
                target_width += target_width - widget.width
                width = widget.width
            else:
                widget.width = target_width + error
                width = widget.width
                error = 0

            column_widths.append(width)

            aligned: str | None = None
            for line in widget.get_lines():
                # See `enums.py` for information about this ignore
                padding, aligned = self._align_line(
                    cast(HorizontalAlignment, widget.parent_align), width, line
                )
                inner.append(aligned)

            new_pos = (
                self.pos[0] + padding + total_offset,
                self.pos[1] + (1 if type(widget).__name__ == "Container" else 0),
            )

            diff_x = new_pos[0] - widget.pos[0]
            diff_y = new_pos[1] - widget.pos[1]

            widget.pos = new_pos

            for pos, line in widget.positioned_line_buffer:
                self.positioned_line_buffer.append(
                    ((pos[0] + diff_x, pos[1] + diff_y), line)
                )

            widget.positioned_line_buffer = []

            if aligned is not None:
                total_offset += real_length(inner[-1]) + separator_length

            vertical_lines.append(inner)

        # Pad columns to max height using each column's actual width.
        # (target_width is mutated above, so we can't use it as a fillvalue)
        max_height = max(len(col) for col in vertical_lines) if vertical_lines else 0
        for i, col in enumerate(vertical_lines):
            fill = " " * column_widths[i]
            while len(col) < max_height:
                col.append(fill)

        lines = []
        for horizontal in zip(*vertical_lines):
            lines.append((reset() + separator).join(horizontal))

        self.height = max(widget.height for widget in self)
        return lines

content_dimensions property

Returns the available area for widgets.

get_lines()

Join all widgets horizontally.

Source code in pytermgui/widgets/containers.py
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
def get_lines(self) -> list[str]:  # pylint: disable=too-many-locals
    """Join all widgets horizontally."""

    # An error will be raised if `separator` is not the correct type (str).
    separator = self._get_style("separator")(self._get_char("separator"))  # type: ignore
    separator_length = real_length(separator)

    target_width, error = divmod(
        self.width - (len(self._widgets) - 1) * separator_length, len(self._widgets)
    )

    self.positioned_line_buffer = []
    vertical_lines = []
    column_widths = []
    total_offset = 0

    for widget in self._widgets:
        inner = []

        if widget.size_policy is SizePolicy.STATIC:
            target_width += target_width - widget.width
            width = widget.width
        else:
            widget.width = target_width + error
            width = widget.width
            error = 0

        column_widths.append(width)

        aligned: str | None = None
        for line in widget.get_lines():
            # See `enums.py` for information about this ignore
            padding, aligned = self._align_line(
                cast(HorizontalAlignment, widget.parent_align), width, line
            )
            inner.append(aligned)

        new_pos = (
            self.pos[0] + padding + total_offset,
            self.pos[1] + (1 if type(widget).__name__ == "Container" else 0),
        )

        diff_x = new_pos[0] - widget.pos[0]
        diff_y = new_pos[1] - widget.pos[1]

        widget.pos = new_pos

        for pos, line in widget.positioned_line_buffer:
            self.positioned_line_buffer.append(
                ((pos[0] + diff_x, pos[1] + diff_y), line)
            )

        widget.positioned_line_buffer = []

        if aligned is not None:
            total_offset += real_length(inner[-1]) + separator_length

        vertical_lines.append(inner)

    # Pad columns to max height using each column's actual width.
    # (target_width is mutated above, so we can't use it as a fillvalue)
    max_height = max(len(col) for col in vertical_lines) if vertical_lines else 0
    for i, col in enumerate(vertical_lines):
        fill = " " * column_widths[i]
        while len(col) < max_height:
            col.append(fill)

    lines = []
    for horizontal in zip(*vertical_lines):
        lines.append((reset() + separator).join(horizontal))

    self.height = max(widget.height for widget in self)
    return lines

StyleCall dataclass

A callable object that simplifies calling style methods.

Instances of this class are created within the Widget._get_style method, and this class should not be used outside of that context.

Source code in pytermgui/widgets/styles.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@dataclass
class StyleCall:
    """A callable object that simplifies calling style methods.

    Instances of this class are created within the `Widget._get_style`
    method, and this class should not be used outside of that context."""

    obj: Widget | Type[Widget] | None
    method: StyleType

    def __call__(self, item: str) -> str:
        """DepthlessStyleType: Apply style method to item, using depth"""

        if self.obj is None:
            raise ValueError(
                f"Can not call {self.method!r}, as no object is assigned to this StyleCall."
            )

        try:
            # mypy fails on one machine with this, but not on the other.
            return self.method(self.obj.depth, item)  # type: ignore

        # this is purposefully broad, as anything can happen during these calls.
        except Exception as error:
            raise RuntimeError(
                f"Could not apply style {self.method} to {item!r}: {error}"  # type: ignore
            ) from error

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, type(self)):
            return False

        return other.method == self.method

__call__(item)

DepthlessStyleType: Apply style method to item, using depth

Source code in pytermgui/widgets/styles.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def __call__(self, item: str) -> str:
    """DepthlessStyleType: Apply style method to item, using depth"""

    if self.obj is None:
        raise ValueError(
            f"Can not call {self.method!r}, as no object is assigned to this StyleCall."
        )

    try:
        # mypy fails on one machine with this, but not on the other.
        return self.method(self.obj.depth, item)  # type: ignore

    # this is purposefully broad, as anything can happen during these calls.
    except Exception as error:
        raise RuntimeError(
            f"Could not apply style {self.method} to {item!r}: {error}"  # type: ignore
        ) from error

StyleManager

Bases: UserDict

An fancy dictionary to manage a Widget's styles.

Individual styles can be accessed two ways:

manager.styles.style_name == manager._get_style("style_name")

Same with setting:

widget.styles.style_name = ...
widget.set_style("style_name", ...)

The set and get methods remain for backwards compatibility reasons, but all newly written code should use the dot syntax.

It is also possible to set styles as markup shorthands. For example:

widget.styles.border = "60 bold"

...is equivalent to:

widget.styles.border = "[60 bold]{item}"
Source code in pytermgui/widgets/styles.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
class StyleManager(UserDict):  # pylint: disable=too-many-ancestors
    """An fancy dictionary to manage a Widget's styles.

    Individual styles can be accessed two ways:

    ```python3
    manager.styles.style_name == manager._get_style("style_name")
    ```

    Same with setting:

    ```python3
    widget.styles.style_name = ...
    widget.set_style("style_name", ...)
    ```

    The `set` and `get` methods remain for backwards compatibility reasons, but all
    newly written code should use the dot syntax.

    It is also possible to set styles as markup shorthands. For example:

    ```python3
    widget.styles.border = "60 bold"
    ```

    ...is equivalent to:

    ```python3
    widget.styles.border = "[60 bold]{item}"
    ```
    """

    def __init__(
        self,
        parent: Widget | Type[Widget] | None = None,
        **base,
    ) -> None:

        """Initializes a `StyleManager`.

        Args:
            parent: The parent of this instance. It will be assigned in all
                `StyleCall`-s created by it.
        """

        self.__dict__["_is_setup"] = False

        self.parent = parent

        super().__init__()

        for key, value in base.items():
            self._set_as_stylecall(key, value)

        self.__dict__["_is_setup"] = self.parent is not None

    @staticmethod
    def expand_shorthand(shorthand: str) -> MarkupFormatter:
        """Expands a shorthand string into a `MarkupFormatter` instance.

        For example, all of these will expand into `MarkupFormatter([60]{item}')`:
        - '60'
        - '[60]'
        - '[60]{item}'

        Args:
            shorthand: The short version of markup to expand.

        Returns:
            A `MarkupFormatter` with the expanded markup.
        """

        if len(shorthand) == 0:
            return MarkupFormatter("{item}")

        if RE_MARKUP.match(shorthand) is not None:
            return MarkupFormatter(shorthand)

        tokens = _sub_aliases(list(tokenize_markup(f"[{shorthand}]")), tim.context)

        colors = [tkn for tkn in tokens if Token.is_color(tkn)]

        if any(tkn.color.background for tkn in colors) and not any(
            not tkn.color.background for tkn in colors
        ):
            shorthand += " #auto"

        markup = f"[{shorthand}]"

        if not "{item}" in shorthand:
            markup += "{item}"

        return MarkupFormatter(markup)

    @classmethod
    def merge(cls, other: StyleManager, **styles: str) -> StyleManager:
        """Creates a new manager that merges `other` with the passed in styles.

        Args:
            other: The style manager to base the new one from.
            **styles: The additional styles the new instance should have.

        Returns:
            A new `StyleManager`. This instance will only gather its data when
            `branch` is called on it. This is done so any changes made to the original
            data between the `merge` call and the actual usage of the instance will be
            reflected.
        """

        return cls(**{**other, **styles})

    def branch(self, parent: Widget | Type[Widget]) -> StyleManager:
        """Branch off from the `base` style dictionary.

        This method should be called during widget construction. It creates a new
        `StyleManager` based on self, but with its data detached from the original.

        Args:
            parent: The parent of the new instance.

        Returns:
            A new `StyleManager`, with detached instances of data. This can then be
            modified without touching the original instance.
        """

        return type(self)(parent, **self.data)

    def _set_as_stylecall(self, key: str, item: StyleValue) -> None:
        """Sets `self.data[key]` as a `StyleCall` of the given item.

        If the item is a string, it will be expanded into a `MarkupFormatter` before
        being converted into the `StyleCall`, using `expand_shorthand`.
        """

        if isinstance(item, StyleCall):
            self.data[key] = StyleCall(self.parent, item.method)
            return

        if isinstance(item, str):
            item = self.expand_shorthand(item)

        self.data[key] = StyleCall(self.parent, item)

    def __setitem__(self, key: str, value: StyleValue) -> None:
        """Sets an item in `self.data`.

        If the item is a string, it will be expanded into a `MarkupFormatter` before
        being converted into the `StyleCall`, using `expand_shorthand`.
        """

        self._set_as_stylecall(key, value)

    def __setattr__(self, key: str, value: StyleValue) -> None:
        """Sets an attribute.

        It first looks if it can set inside self.data, and defaults back to
        self.__dict__.

        Raises:
            KeyError: The given key is not a defined attribute, and is not part of this
                object's style set.
        """

        found = False
        if "data" in self.__dict__:
            for part in key.split("__"):
                if part in self.data:
                    self._set_as_stylecall(part, value)
                    found = True

        if found:
            return

        if self.__dict__.get("_is_setup") and key not in self.__dict__:
            raise KeyError(f"Style {key!r} was not defined during construction.")

        self.__dict__[key] = value

    def __getattr__(self, key: str) -> StyleCall:
        """Allows styles.dot_syntax."""

        if key in self.__dict__:
            return self.__dict__[key]

        if key in self.__dict__["data"]:
            return self.__dict__["data"][key]

        raise AttributeError(key, self.data)

    def __call__(self, **styles: StyleValue) -> Any:
        """Allows calling the manager and setting its styles.

        For example:
        ```
        >>> Button("Hello").styles(label="@60")
        ```
        """

        for key, value in styles.items():
            self._set_as_stylecall(key, value)

        return self.parent

__call__(**styles)

Allows calling the manager and setting its styles.

For example:

>>> Button("Hello").styles(label="@60")

Source code in pytermgui/widgets/styles.py
326
327
328
329
330
331
332
333
334
335
336
337
338
def __call__(self, **styles: StyleValue) -> Any:
    """Allows calling the manager and setting its styles.

    For example:
    ```
    >>> Button("Hello").styles(label="@60")
    ```
    """

    for key, value in styles.items():
        self._set_as_stylecall(key, value)

    return self.parent

__getattr__(key)

Allows styles.dot_syntax.

Source code in pytermgui/widgets/styles.py
315
316
317
318
319
320
321
322
323
324
def __getattr__(self, key: str) -> StyleCall:
    """Allows styles.dot_syntax."""

    if key in self.__dict__:
        return self.__dict__[key]

    if key in self.__dict__["data"]:
        return self.__dict__["data"][key]

    raise AttributeError(key, self.data)

__init__(parent=None, **base)

Initializes a StyleManager.

Parameters:

Name Type Description Default
parent Widget | Type[Widget] | None

The parent of this instance. It will be assigned in all StyleCall-s created by it.

None
Source code in pytermgui/widgets/styles.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def __init__(
    self,
    parent: Widget | Type[Widget] | None = None,
    **base,
) -> None:

    """Initializes a `StyleManager`.

    Args:
        parent: The parent of this instance. It will be assigned in all
            `StyleCall`-s created by it.
    """

    self.__dict__["_is_setup"] = False

    self.parent = parent

    super().__init__()

    for key, value in base.items():
        self._set_as_stylecall(key, value)

    self.__dict__["_is_setup"] = self.parent is not None

__setattr__(key, value)

Sets an attribute.

It first looks if it can set inside self.data, and defaults back to self.dict.

Raises:

Type Description
KeyError

The given key is not a defined attribute, and is not part of this object's style set.

Source code in pytermgui/widgets/styles.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def __setattr__(self, key: str, value: StyleValue) -> None:
    """Sets an attribute.

    It first looks if it can set inside self.data, and defaults back to
    self.__dict__.

    Raises:
        KeyError: The given key is not a defined attribute, and is not part of this
            object's style set.
    """

    found = False
    if "data" in self.__dict__:
        for part in key.split("__"):
            if part in self.data:
                self._set_as_stylecall(part, value)
                found = True

    if found:
        return

    if self.__dict__.get("_is_setup") and key not in self.__dict__:
        raise KeyError(f"Style {key!r} was not defined during construction.")

    self.__dict__[key] = value

__setitem__(key, value)

Sets an item in self.data.

If the item is a string, it will be expanded into a MarkupFormatter before being converted into the StyleCall, using expand_shorthand.

Source code in pytermgui/widgets/styles.py
280
281
282
283
284
285
286
287
def __setitem__(self, key: str, value: StyleValue) -> None:
    """Sets an item in `self.data`.

    If the item is a string, it will be expanded into a `MarkupFormatter` before
    being converted into the `StyleCall`, using `expand_shorthand`.
    """

    self._set_as_stylecall(key, value)

branch(parent)

Branch off from the base style dictionary.

This method should be called during widget construction. It creates a new StyleManager based on self, but with its data detached from the original.

Parameters:

Name Type Description Default
parent Widget | Type[Widget]

The parent of the new instance.

required

Returns:

Type Description
StyleManager

A new StyleManager, with detached instances of data. This can then be

StyleManager

modified without touching the original instance.

Source code in pytermgui/widgets/styles.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
def branch(self, parent: Widget | Type[Widget]) -> StyleManager:
    """Branch off from the `base` style dictionary.

    This method should be called during widget construction. It creates a new
    `StyleManager` based on self, but with its data detached from the original.

    Args:
        parent: The parent of the new instance.

    Returns:
        A new `StyleManager`, with detached instances of data. This can then be
        modified without touching the original instance.
    """

    return type(self)(parent, **self.data)

expand_shorthand(shorthand) staticmethod

Expands a shorthand string into a MarkupFormatter instance.

For example, all of these will expand into MarkupFormatter([60]{item}'): - '60' - '[60]' - '[60]{item}'

Parameters:

Name Type Description Default
shorthand str

The short version of markup to expand.

required

Returns:

Type Description
MarkupFormatter

A MarkupFormatter with the expanded markup.

Source code in pytermgui/widgets/styles.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
@staticmethod
def expand_shorthand(shorthand: str) -> MarkupFormatter:
    """Expands a shorthand string into a `MarkupFormatter` instance.

    For example, all of these will expand into `MarkupFormatter([60]{item}')`:
    - '60'
    - '[60]'
    - '[60]{item}'

    Args:
        shorthand: The short version of markup to expand.

    Returns:
        A `MarkupFormatter` with the expanded markup.
    """

    if len(shorthand) == 0:
        return MarkupFormatter("{item}")

    if RE_MARKUP.match(shorthand) is not None:
        return MarkupFormatter(shorthand)

    tokens = _sub_aliases(list(tokenize_markup(f"[{shorthand}]")), tim.context)

    colors = [tkn for tkn in tokens if Token.is_color(tkn)]

    if any(tkn.color.background for tkn in colors) and not any(
        not tkn.color.background for tkn in colors
    ):
        shorthand += " #auto"

    markup = f"[{shorthand}]"

    if not "{item}" in shorthand:
        markup += "{item}"

    return MarkupFormatter(markup)

merge(other, **styles) classmethod

Creates a new manager that merges other with the passed in styles.

Parameters:

Name Type Description Default
other StyleManager

The style manager to base the new one from.

required
**styles str

The additional styles the new instance should have.

{}

Returns:

Type Description
StyleManager

A new StyleManager. This instance will only gather its data when

StyleManager

branch is called on it. This is done so any changes made to the original

StyleManager

data between the merge call and the actual usage of the instance will be

StyleManager

reflected.

Source code in pytermgui/widgets/styles.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
@classmethod
def merge(cls, other: StyleManager, **styles: str) -> StyleManager:
    """Creates a new manager that merges `other` with the passed in styles.

    Args:
        other: The style manager to base the new one from.
        **styles: The additional styles the new instance should have.

    Returns:
        A new `StyleManager`. This instance will only gather its data when
        `branch` is called on it. This is done so any changes made to the original
        data between the `merge` call and the actual usage of the instance will be
        reflected.
    """

    return cls(**{**other, **styles})

VerticalAlignment

Bases: DefaultEnum

Vertical alignment options for widgets.

Source code in pytermgui/enums.py
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class VerticalAlignment(DefaultEnum):
    """Vertical alignment options for widgets."""

    TOP = 0
    """Align widgets to the top"""

    CENTER = 1
    """Align widgets in the center, with equal* padding on the top and bottom.

    Note:
        When the available height is not divisible by 2, the extra line of padding
        is added to the bottom.
    """

    BOTTOM = 2
    """Align widgets to the bottom."""

BOTTOM = 2 class-attribute instance-attribute

Align widgets to the bottom.

CENTER = 1 class-attribute instance-attribute

Align widgets in the center, with equal* padding on the top and bottom.

Note

When the available height is not divisible by 2, the extra line of padding is added to the bottom.

TOP = 0 class-attribute instance-attribute

Align widgets to the top

Widget

The base of the Widget system

Source code in pytermgui/widgets/base.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
class Widget:  # pylint: disable=too-many-public-methods
    """The base of the Widget system"""

    set_style = classmethod(_set_obj_or_cls_style)
    set_char = classmethod(_set_obj_or_cls_char)

    styles = w_styles.StyleManager()
    """Default styles for this class"""

    chars: dict[str, w_styles.CharType] = {}
    """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`"""

    # This class is loaded after this module,
    # and thus mypy doesn't see its existence.
    _id_manager: Optional["_IDManager"] = None  # type: ignore

    size_policy = SizePolicy.get_default()
    """`pytermgui.enums.SizePolicy` to set widget's width according to"""

    parent_align = HorizontalAlignment.get_default()
    """`pytermgui.enums.HorizontalAlignment` to align widget by"""

    from_data: Callable[..., Widget | list[Widget] | None]

    # We cannot import boxes here due to cyclic imports.
    box: Any

    def __init__(self, **attrs: Any) -> None:
        """Initialize object"""

        self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value)
        self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value)

        self.width = 1
        self.height = 1
        self.pos = self.terminal.origin

        self.depth = 0

        self.styles = type(self).styles.branch(self)
        self.chars = type(self).chars.copy()

        self.parent: Widget | None = None
        self.selected_index: int | None = None

        self._selectables_length = 0
        self._id: Optional[str] = None
        self._serialized_fields = type(self).serialized
        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
        self._relative_width: float | None = None
        self._previous_state: tuple[tuple[int, int], list[str]] | None = None

        self.positioned_line_buffer: list[tuple[tuple[int, int], str]] = []

        for attr, value in attrs.items():
            setattr(self, attr, value)

    def __repr__(self) -> str:
        """Return repr string of this widget.

        Returns:
            Whatever this widget's `debug` method gives.
        """

        return self.debug()

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Yields the repr of this object, then a preview of it."""

        yield self.debug()
        yield "\n\n"
        yield {
            "text": "\n".join((line + reset() for line in self.get_lines())),
            "highlight": False,
        }

    def __iter__(self) -> Iterator[Widget]:
        """Return self for iteration"""

        yield self

    @property
    def bindings(self) -> dict[str | Type[MouseEvent], tuple[BoundCallback, 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.")
            }
            ```
        """

        return self._bindings.copy()

    @property
    def id(self) -> Optional[str]:  # pylint: disable=invalid-name
        """Gets this widget's id property

        Returns:
            The id string if one is present, None otherwise.
        """

        return self._id

    @id.setter
    def id(self, value: str) -> None:  # pylint: disable=invalid-name
        """Registers a widget to the Widget._id_manager.

        If this widget already had an id, the old value is deregistered
        before the new one is assigned.

        Args:
            value: The new id this widget will be registered as.
        """

        if self._id == value:
            return

        manager = Widget._id_manager
        assert manager is not None

        old = manager.get_id(self)
        if old is not None:
            manager.deregister(old)

        self._id = value
        manager.register(self)

    @property
    def selectables_length(self) -> int:
        """Gets how many selectables this widget contains.

        Returns:
            An integer describing the amount of selectables in this widget.
        """

        return self._selectables_length

    @property
    def selectables(self) -> list[tuple[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.
        """

        return [(self, 0)]

    @property
    def is_selectable(self) -> bool:
        """Determines whether this widget has any selectables.

        Returns:
            A boolean, representing `self.selectables_length != 0`.
        """

        return self.selectables_length != 0

    @property
    def static_width(self) -> int:
        """Allows for a shorter way of setting a width, and SizePolicy.STATIC.

        Returns:
            None, as this is setter only.
        """

        return None  # type: ignore

    @static_width.setter
    def static_width(self, value: int) -> None:
        """See the static_width getter."""

        self.width = value
        self.size_policy = SizePolicy.STATIC

    @property
    def relative_width(self) -> 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.

        Returns:
            The current relative_width.
        """

        return self._relative_width

    @relative_width.setter
    def relative_width(self, value: float) -> None:
        """See the relative_width getter."""

        self.size_policy = SizePolicy.RELATIVE
        self._relative_width = min(1.0, value)

    @property
    def terminal(self) -> Terminal:
        """Returns the current global terminal instance."""

        return get_terminal()

    def _align(self, lines: list[str]) -> list[str]:
        """Aligns the given lines based on this widget's `parent_align` attribute."""

        width = self.width

        def _align_left(line: str) -> str:
            return line + (width - real_length(line)) * " "

        def _align_center(line: str) -> str:
            right, extra = divmod(width - real_length(line), 2)
            left = right + extra

            return left * " " + line + right * " "

        def _align_right(line: str) -> str:
            return (width - real_length(line)) * " " + line

        if self.parent_align is None:
            raise TypeError("Horizontal alignment cannot be None.")

        assert isinstance(self.parent_align, HorizontalAlignment)

        aligner = {
            HorizontalAlignment.LEFT: _align_left,
            HorizontalAlignment.CENTER: _align_center,
            HorizontalAlignment.RIGHT: _align_right,
        }[self.parent_align]

        aligned = []

        for line in lines:
            aligned.append(aligner(line))

        return aligned

    def get_change(self) -> WidgetChange | None:
        """Determines whether widget lines changed since the last call to this function."""

        lines = self.get_lines()

        if self._previous_state is None:
            self._previous_state = (self.width, self.height), lines
            return WidgetChange.LINES

        lines = self.get_lines()
        (old_width, old_height), old_lines = self._previous_state

        self._previous_state = (self.width, self.height), lines

        if old_width != self.width and old_height != self.height:
            return WidgetChange.SIZE

        if old_width != self.width:
            return WidgetChange.WIDTH

        if old_height != self.height:
            return WidgetChange.HEIGHT

        if old_lines != lines:
            return WidgetChange.LINES

        return None

    def contains(self, pos: tuple[int, int]) -> bool:
        """Determines whether widget contains `pos`.

        Args:
            pos: Position to compare.

        Returns:
            Boolean describing whether the position is inside
                this widget.
        """

        rect = self.pos, (
            self.pos[0] + self.width,
            self.pos[1] + self.height,
        )

        (left, top), (right, bottom) = rect

        return left <= pos[0] < right and top <= pos[1] < bottom

    def handle_mouse(self, event: MouseEvent) -> bool:
        """Tries to call the most specific mouse handler function available.

        This function looks for a set of mouse action handlers. Each handler follows
        the format

            on_{event_name}

        For example, the handler triggered on MouseAction.LEFT_CLICK would be
        `on_left_click`. If no handler is found nothing is done.

        You can also define more general handlers, for example to group left & right
        clicks you can use `on_click`, and to catch both up and down scroll you can use
        `on_scroll`. General handlers are only used if they are the most specific ones,
        i.e. there is no "specific" handler.

        Args:
            event: The event to handle.

        Returns:
            Whether the parent of this widget should treat it as one to "stick" events
            to, e.g. to keep sending mouse events to it. One can "unstick" a widget by
            returning False in the handler.
        """

        def _get_names(action: MouseAction) -> tuple[str, ...]:
            if action.value in ["hover", "release"]:
                return (action.value,)

            parts = action.value.split("_")

            # left click & right click
            if parts[0] in ["left", "right"]:
                return (action.value, parts[1])

            if parts[0] == "shift":
                return (action.value, f"shift_{parts[1]}", parts[1])

            # scroll up & down
            return (action.value, parts[0])

        possible_names = _get_names(event.action)
        for name in possible_names:
            if hasattr(self, f"on_{name}"):
                handle = getattr(self, f"on_{name}")

                return handle(event)

        return False

    def handle_key(self, key: str) -> bool:
        """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.
        """

        return False and hasattr(self, key)

    def serialize(self) -> dict[str, Any]:
        """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,
                    ...
                }
            ```
        """

        fields = self._serialized_fields

        out: dict[str, Any] = {"type": type(self).__name__}
        for key in fields:
            # Detect styled values
            if key.startswith("*"):
                style = True
                key = key[1:]
            else:
                style = False

            value = getattr(self, key)

            # Convert styled value into markup
            if style:
                style_call = self._get_style(key)
                if isinstance(value, list):
                    out[key] = [get_markup(style_call(char)) for char in value]
                else:
                    out[key] = get_markup(style_call(value))

                continue

            out[key] = value

        # The chars need to be handled separately
        out["chars"] = {}
        for key, value in self.chars.items():
            style_call = self._get_style(key)

            if isinstance(value, list):
                out["chars"][key] = [get_markup(style_call(char)) for char in value]
            else:
                out["chars"][key] = get_markup(style_call(value))

        return out

    def copy(self) -> Widget:
        """Creates a deep copy of this widget"""

        return deepcopy(self)

    def _get_style(self, key: str) -> w_styles.DepthlessStyleType:
        """Gets style call from its key.

        This is analogous to using `self.styles.{key}`

        Args:
            key: A key into the widget's style manager.

        Returns:
            A `pytermgui.styles.StyleCall` object containing the referenced
            style. StyleCall objects should only be used internally inside a
            widget.

        Raises:
            KeyError: Style key is invalid.
        """

        return self.styles[key]

    def _get_char(self, key: str) -> w_styles.CharType:
        """Gets character from its key.

        Args:
            key: A key into the widget's chars dictionary.

        Returns:
            Either a `list[str]` or a simple `str`, depending on the character.

        Raises:
            KeyError: Style key is invalid.
        """

        chars = self.chars[key]

        if isinstance(chars, str):
            if chars.startswith("u:"):
                identifier = " ".join(chars[2:].split("_"))
                chars = u_lookup(identifier)

            return chars

        return chars.copy()

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

        raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.")

    def move(self, diff_x: int, diff_y: int) -> None:
        """Moves the widget by the given x and y changes."""

        self.pos = (self.pos[0] + diff_x, self.pos[1] + diff_y)

        adjusted = []
        for pos, line in self.positioned_line_buffer:
            adjusted.append(((pos[0] + diff_x, pos[1] + diff_y), line))

        self.positioned_line_buffer = adjusted

    def bind(
        self, key: str, action: BoundCallback, description: Optional[str] = None
    ) -> None:
        """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.
        """

        if description is None:
            description = f"Binding of {key} to {action}"

        self._bindings[key] = (action, description)

    def unbind(self, key: str) -> None:
        """Unbinds the given key."""

        del self._bindings[key]

    def execute_binding(self, key: Any, ignore_any: bool = False) -> bool:
        """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`.
            ignore_any: If set, `keys.ANY_KEY` bindings will not be executed.

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

        # Execute special binding
        if not ignore_any and keys.ANY_KEY in self._bindings:
            method, _ = self._bindings[keys.ANY_KEY]
            method(self, key)

        if key in self._bindings:
            method, _ = self._bindings[key]
            method(self, key)

            return True

        return False

    def select(self, index: int | None = None) -> None:
        """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.
        """

        if not self.is_selectable:
            raise TypeError(f"Object of type {type(self)} has no selectables.")

        if index is not None:
            index = min(max(0, index), self.selectables_length - 1)
        self.selected_index = index

    def print(self) -> None:
        """Prints this widget"""

        for line in self.get_lines():
            print(line)

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

        """

        constructor = "("
        for name in signature(getattr(self, "__init__")).parameters:
            current = ""
            if name == "attrs":
                current += "**attrs"
                continue

            if len(constructor) > 1:
                current += ", "

            current += name

            attr = getattr(self, name, None)
            if attr is None:
                continue

            current += "="

            if isinstance(attr, str):
                current += f'"{attr}"'
            else:
                current += str(attr)

            constructor += current

        constructor += ")"

        return type(self).__name__ + constructor

bindings property

Gets a copy of the bindings internal dictionary.

Returns:

Type Description
dict[str | Type[MouseEvent], tuple[BoundCallback, str]]

A copy of the internal bindings dictionary, such as:

dict[str | Type[MouseEvent], tuple[BoundCallback, str]]

```

dict[str | Type[MouseEvent], tuple[BoundCallback, str]]

{ "": (star_callback, "This is a callback activated when '' is pressed.")

dict[str | Type[MouseEvent], tuple[BoundCallback, str]]

}

dict[str | Type[MouseEvent], tuple[BoundCallback, str]]

```

chars = type(self).chars.copy() class-attribute instance-attribute

Default characters for this class

id property writable

Gets this widget's id property

Returns:

Type Description
Optional[str]

The id string if one is present, None otherwise.

is_selectable property

Determines whether this widget has any selectables.

Returns:

Type Description
bool

A boolean, representing self.selectables_length != 0.

keys = {} class-attribute instance-attribute

Groups of keys that are used in handle_key

parent_align = HorizontalAlignment.get_default() class-attribute instance-attribute

pytermgui.enums.HorizontalAlignment to align widget by

relative_width property writable

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.

Returns:

Type Description
float | None

The current relative_width.

selectables property

Gets a list of all selectables within this widget

Returns:

Type Description
list[tuple[Widget, int]]

A list of tuples. In the default implementation this will be

list[tuple[Widget, int]]

a list of one tuple, containing a reference to self, as well

list[tuple[Widget, int]]

as the lowest index, 0.

selectables_length property

Gets how many selectables this widget contains.

Returns:

Type Description
int

An integer describing the amount of selectables in this widget.

serialized = ['id', 'pos', 'depth', 'width', 'height', 'selected_index', 'selectables_length'] class-attribute instance-attribute

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

size_policy = SizePolicy.get_default() class-attribute instance-attribute

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

static_width property writable

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

Returns:

Type Description
int

None, as this is setter only.

styles = type(self).styles.branch(self) class-attribute instance-attribute

Default styles for this class

terminal property

Returns the current global terminal instance.

__fancy_repr__()

Yields the repr of this object, then a preview of it.

Source code in pytermgui/widgets/base.py
159
160
161
162
163
164
165
166
167
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Yields the repr of this object, then a preview of it."""

    yield self.debug()
    yield "\n\n"
    yield {
        "text": "\n".join((line + reset() for line in self.get_lines())),
        "highlight": False,
    }

__init__(**attrs)

Initialize object

Source code in pytermgui/widgets/base.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def __init__(self, **attrs: Any) -> None:
    """Initialize object"""

    self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value)
    self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value)

    self.width = 1
    self.height = 1
    self.pos = self.terminal.origin

    self.depth = 0

    self.styles = type(self).styles.branch(self)
    self.chars = type(self).chars.copy()

    self.parent: Widget | None = None
    self.selected_index: int | None = None

    self._selectables_length = 0
    self._id: Optional[str] = None
    self._serialized_fields = type(self).serialized
    self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}
    self._relative_width: float | None = None
    self._previous_state: tuple[tuple[int, int], list[str]] | None = None

    self.positioned_line_buffer: list[tuple[tuple[int, int], str]] = []

    for attr, value in attrs.items():
        setattr(self, attr, value)

__iter__()

Return self for iteration

Source code in pytermgui/widgets/base.py
169
170
171
172
def __iter__(self) -> Iterator[Widget]:
    """Return self for iteration"""

    yield self

__repr__()

Return repr string of this widget.

Returns:

Type Description
str

Whatever this widget's debug method gives.

Source code in pytermgui/widgets/base.py
150
151
152
153
154
155
156
157
def __repr__(self) -> str:
    """Return repr string of this widget.

    Returns:
        Whatever this widget's `debug` method gives.
    """

    return self.debug()

bind(key, action, description=None)

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.

Parameters:

Name Type Description Default
key str

The key that the action will be bound to.

required
action BoundCallback

The action executed when the key is pressed.

required
description Optional[str]

An optional description for this binding. It is not really used anywhere, but you can provide a helper menu and display them.

None
Source code in pytermgui/widgets/base.py
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
def bind(
    self, key: str, action: BoundCallback, description: Optional[str] = None
) -> None:
    """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.
    """

    if description is None:
        description = f"Binding of {key} to {action}"

    self._bindings[key] = (action, description)

contains(pos)

Determines whether widget contains pos.

Parameters:

Name Type Description Default
pos tuple[int, int]

Position to compare.

required

Returns:

Type Description
bool

Boolean describing whether the position is inside this widget.

Source code in pytermgui/widgets/base.py
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
def contains(self, pos: tuple[int, int]) -> bool:
    """Determines whether widget contains `pos`.

    Args:
        pos: Position to compare.

    Returns:
        Boolean describing whether the position is inside
            this widget.
    """

    rect = self.pos, (
        self.pos[0] + self.width,
        self.pos[1] + self.height,
    )

    (left, top), (right, bottom) = rect

    return left <= pos[0] < right and top <= pos[1] < bottom

copy()

Creates a deep copy of this widget

Source code in pytermgui/widgets/base.py
509
510
511
512
def copy(self) -> Widget:
    """Creates a deep copy of this widget"""

    return deepcopy(self)

debug()

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:

Type Description
str

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

str

Example return:

str

```

str

Container(Label(value="This is a label", padding=0),

str

Button(label="This is a button", padding=0), **attrs)

str

```

Source code in pytermgui/widgets/base.py
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
def debug(self) -> str:
    """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)
        ```

    """

    constructor = "("
    for name in signature(getattr(self, "__init__")).parameters:
        current = ""
        if name == "attrs":
            current += "**attrs"
            continue

        if len(constructor) > 1:
            current += ", "

        current += name

        attr = getattr(self, name, None)
        if attr is None:
            continue

        current += "="

        if isinstance(attr, str):
            current += f'"{attr}"'
        else:
            current += str(attr)

        constructor += current

    constructor += ")"

    return type(self).__name__ + constructor

execute_binding(key, ignore_any=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.

Parameters:

Name Type Description Default
key Any

Usually a string, indexing into the _bindings dictionary. These are the same strings as defined in Widget.bind.

required
ignore_any bool

If set, keys.ANY_KEY bindings will not be executed.

False

Returns:

Type Description
bool

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

Source code in pytermgui/widgets/base.py
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
def execute_binding(self, key: Any, ignore_any: bool = False) -> bool:
    """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`.
        ignore_any: If set, `keys.ANY_KEY` bindings will not be executed.

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

    # Execute special binding
    if not ignore_any and keys.ANY_KEY in self._bindings:
        method, _ = self._bindings[keys.ANY_KEY]
        method(self, key)

    if key in self._bindings:
        method, _ = self._bindings[key]
        method(self, key)

        return True

    return False

get_change()

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

Source code in pytermgui/widgets/base.py
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def get_change(self) -> WidgetChange | None:
    """Determines whether widget lines changed since the last call to this function."""

    lines = self.get_lines()

    if self._previous_state is None:
        self._previous_state = (self.width, self.height), lines
        return WidgetChange.LINES

    lines = self.get_lines()
    (old_width, old_height), old_lines = self._previous_state

    self._previous_state = (self.width, self.height), lines

    if old_width != self.width and old_height != self.height:
        return WidgetChange.SIZE

    if old_width != self.width:
        return WidgetChange.WIDTH

    if old_height != self.height:
        return WidgetChange.HEIGHT

    if old_lines != lines:
        return WidgetChange.LINES

    return None

get_lines()

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:

Type Description
list[str]

Nothing by default.

Raises:

Type Description
NotImplementedError

As this method is required for all widgets, not having it defined will raise NotImplementedError.

Source code in pytermgui/widgets/base.py
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
def get_lines(self) -> list[str]:
    """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.
    """

    raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.")

handle_key(key)

Handles a mouse event, returning its success.

Parameters:

Name Type Description Default
key str

String representation of input string. The pytermgui.input.keys object can be used to retrieve special keys.

required

Returns:

Type Description
bool

A boolean describing whether the key was handled.

Source code in pytermgui/widgets/base.py
434
435
436
437
438
439
440
441
442
443
444
445
446
def handle_key(self, key: str) -> bool:
    """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.
    """

    return False and hasattr(self, key)

handle_mouse(event)

Tries to call the most specific mouse handler function available.

This function looks for a set of mouse action handlers. Each handler follows the format

on_{event_name}

For example, the handler triggered on MouseAction.LEFT_CLICK would be on_left_click. If no handler is found nothing is done.

You can also define more general handlers, for example to group left & right clicks you can use on_click, and to catch both up and down scroll you can use on_scroll. General handlers are only used if they are the most specific ones, i.e. there is no "specific" handler.

Parameters:

Name Type Description Default
event MouseEvent

The event to handle.

required

Returns:

Type Description
bool

Whether the parent of this widget should treat it as one to "stick" events

bool

to, e.g. to keep sending mouse events to it. One can "unstick" a widget by

bool

returning False in the handler.

Source code in pytermgui/widgets/base.py
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
def handle_mouse(self, event: MouseEvent) -> bool:
    """Tries to call the most specific mouse handler function available.

    This function looks for a set of mouse action handlers. Each handler follows
    the format

        on_{event_name}

    For example, the handler triggered on MouseAction.LEFT_CLICK would be
    `on_left_click`. If no handler is found nothing is done.

    You can also define more general handlers, for example to group left & right
    clicks you can use `on_click`, and to catch both up and down scroll you can use
    `on_scroll`. General handlers are only used if they are the most specific ones,
    i.e. there is no "specific" handler.

    Args:
        event: The event to handle.

    Returns:
        Whether the parent of this widget should treat it as one to "stick" events
        to, e.g. to keep sending mouse events to it. One can "unstick" a widget by
        returning False in the handler.
    """

    def _get_names(action: MouseAction) -> tuple[str, ...]:
        if action.value in ["hover", "release"]:
            return (action.value,)

        parts = action.value.split("_")

        # left click & right click
        if parts[0] in ["left", "right"]:
            return (action.value, parts[1])

        if parts[0] == "shift":
            return (action.value, f"shift_{parts[1]}", parts[1])

        # scroll up & down
        return (action.value, parts[0])

    possible_names = _get_names(event.action)
    for name in possible_names:
        if hasattr(self, f"on_{name}"):
            handle = getattr(self, f"on_{name}")

            return handle(event)

    return False

move(diff_x, diff_y)

Moves the widget by the given x and y changes.

Source code in pytermgui/widgets/base.py
577
578
579
580
581
582
583
584
585
586
def move(self, diff_x: int, diff_y: int) -> None:
    """Moves the widget by the given x and y changes."""

    self.pos = (self.pos[0] + diff_x, self.pos[1] + diff_y)

    adjusted = []
    for pos, line in self.positioned_line_buffer:
        adjusted.append(((pos[0] + diff_x, pos[1] + diff_y), line))

    self.positioned_line_buffer = adjusted

print()

Prints this widget

Source code in pytermgui/widgets/base.py
664
665
666
667
668
def print(self) -> None:
    """Prints this widget"""

    for line in self.get_lines():
        print(line)

select(index=None)

Selects a part of this Widget.

Parameters:

Name Type Description Default
index int | None

The index to select.

None

Raises:

Type Description
TypeError

This widget has no selectables, i.e. widget.is_selectable == False.

Source code in pytermgui/widgets/base.py
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
def select(self, index: int | None = None) -> None:
    """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.
    """

    if not self.is_selectable:
        raise TypeError(f"Object of type {type(self)} has no selectables.")

    if index is not None:
        index = min(max(0, index), self.selectables_length - 1)
    self.selected_index = index

serialize()

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:

Type Description
dict[str, Any]

Dictionary of widget attributes. The dictionary will always

dict[str, Any]

have a type field. Any styles are converted into markup

dict[str, Any]

strings during serialization, so they can be loaded again in

dict[str, Any]

their original form.

dict[str, Any]

Example return:

dict[str, Any]

``` { "type": "Label", "value": "[210 bold]I am a title", "parent_align": 0, ... }

dict[str, Any]

```

Source code in pytermgui/widgets/base.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
def serialize(self) -> dict[str, Any]:
    """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,
                ...
            }
        ```
    """

    fields = self._serialized_fields

    out: dict[str, Any] = {"type": type(self).__name__}
    for key in fields:
        # Detect styled values
        if key.startswith("*"):
            style = True
            key = key[1:]
        else:
            style = False

        value = getattr(self, key)

        # Convert styled value into markup
        if style:
            style_call = self._get_style(key)
            if isinstance(value, list):
                out[key] = [get_markup(style_call(char)) for char in value]
            else:
                out[key] = get_markup(style_call(value))

            continue

        out[key] = value

    # The chars need to be handled separately
    out["chars"] = {}
    for key, value in self.chars.items():
        style_call = self._get_style(key)

        if isinstance(value, list):
            out["chars"][key] = [get_markup(style_call(char)) for char in value]
        else:
            out["chars"][key] = get_markup(style_call(value))

    return out

unbind(key)

Unbinds the given key.

Source code in pytermgui/widgets/base.py
613
614
615
616
def unbind(self, key: str) -> None:
    """Unbinds the given key."""

    del self._bindings[key]

WidgetChange

Bases: Enum

The type of change that happened within a widget.

Source code in pytermgui/enums.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class WidgetChange(Enum):
    """The type of change that happened within a widget."""

    LINES = _auto()
    """The result of `get_lines` has changed, but size changes didn't happen."""

    SIZE = _auto()
    """Both WIDTH and HEIGHT has changed."""

    WIDTH = _auto()
    """The width of the widget changed, possibly involving LINES type changes."""

    HEIGHT = _auto()
    """The height of the widget changed, possibly involving LINES type changes."""

HEIGHT = _auto() class-attribute instance-attribute

The height of the widget changed, possibly involving LINES type changes.

LINES = _auto() class-attribute instance-attribute

The result of get_lines has changed, but size changes didn't happen.

SIZE = _auto() class-attribute instance-attribute

Both WIDTH and HEIGHT has changed.

WIDTH = _auto() class-attribute instance-attribute

The width of the widget changed, possibly involving LINES type changes.

WidthExceededError

Bases: Exception

Raised when an element's width is larger than the screen.

Source code in pytermgui/exceptions.py
21
22
class WidthExceededError(Exception):
    """Raised when an element's width is larger than the screen."""

clear(what='screen')

Clears the specified screen region.

Parameters:

Name Type Description Default
what str

The specifier defining the screen area.

'screen'

Available options: * screen: clear whole screen and go to origin * bos: clear screen from cursor backwards * eos: clear screen from cursor forwards * line: clear line and go to beginning * bol: clear line from cursor backwards * eol: clear line from cursor forwards

Source code in pytermgui/ansi_interface.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def clear(what: str = "screen") -> None:
    """Clears the specified screen region.

    Args:
        what: The specifier defining the screen area.

    Available options:
    * screen: clear whole screen and go to origin
    * bos: clear screen from cursor backwards
    * eos: clear screen from cursor forwards
    * line: clear line and go to beginning
    * bol: clear line from cursor backwards
    * eol: clear line from cursor forwards
    """

    commands = {
        "eos": "\x1b[0J",
        "bos": "\x1b[1J",
        "screen": "\x1b[H\x1b[2J",
        "eol": "\x1b[0K",
        "bol": "\x1b[1K",
        "line": "\x1b[2K",
    }

    get_terminal().write(commands[what])

cursor_at(pos)

Gets callable to print at pos, incrementing y on every print.

Parameters:

Name Type Description Default
pos tuple[int, int]

The position to start printing at. Follows the order (columns, rows).

required

Yields:

Type Description
Callable[..., None]

A callable printing function. This function forwards all arguments to print,

Callable[..., None]

but positions the cursor before doing so. After every call, the y position is

Callable[..., None]

incremented.

Source code in pytermgui/context_managers.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@contextmanager
def cursor_at(pos: tuple[int, int]) -> Generator[Callable[..., None], None, None]:
    """Gets callable to print at `pos`, incrementing `y` on every print.

    Args:
        pos: The position to start printing at. Follows the order (columns, rows).

    Yields:
        A callable printing function. This function forwards all arguments to `print`,
        but positions the cursor before doing so. After every call, the y position is
        incremented.
    """

    offset = 0
    posx, posy = pos

    def printer(*args: Any, **kwargs: Any) -> None:
        """Print to posx, current y"""

        nonlocal offset

        print_to((posx, posy + offset), *args, **kwargs)
        offset += 1

    try:
        save_cursor()
        yield printer

    finally:
        restore_cursor()

real_length(text) cached

Gets the display-length of text.

This length means no ANSI sequences are counted. This method is a convenience wrapper for len(strip_ansi(text)).

Parameters:

Name Type Description Default
text str

The text to calculate the length of.

required

Returns:

Type Description
int

The display-length of text.

Source code in pytermgui/regex.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@lru_cache(maxsize=None)
def real_length(text: str) -> int:
    """Gets the display-length of text.

    This length means no ANSI sequences are counted. This method is a convenience wrapper
    for `len(strip_ansi(text))`.

    Args:
        text: The text to calculate the length of.

    Returns:
        The display-length of text.
    """

    return max(wcswidth(strip_ansi(text)), 0)

reset()

Resets printing mode.

Source code in pytermgui/ansi_interface.py
632
633
634
635
def reset() -> str:
    """Resets printing mode."""

    return set_mode("reset", False)

strip_markup(text) cached

Removes markup tags from text.

Parameters:

Name Type Description Default
text str

A string or bytes object containing 0 or more markup tags.

required

Returns:

Type Description
str

The text without any markup tags.

Source code in pytermgui/regex.py
48
49
50
51
52
53
54
55
56
57
58
59
@lru_cache()
def strip_markup(text: str) -> str:
    """Removes markup tags from text.

    Args:
        text: A string or bytes object containing 0 or more markup tags.

    Returns:
        The text without any markup tags.
    """

    return RE_MARKUP.sub("", text)