Skip to content

Index

Welcome to the API reference for PyTermGUI, a Python TUI framework with mouse support, modular widget system, customizable and rapid terminal markup language and more!

animator = Animator() module-attribute

The global Animator instance used by all of the library.

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

Instance storing platform specific key codes.

terminal = Terminal() module-attribute

Terminal instance that should be used pretty much always.

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",
    ]

AliasToken dataclass

Bases: Token

A way to reference a set of tags from one central name.

Source code in pytermgui/markup/tokens.py
260
261
262
263
264
265
266
@dataclass(frozen=True, repr=False)
class AliasToken(Token):
    """A way to reference a set of tags from one central name."""

    __slots__ = ("value",)

    value: str

Animator

The Animator class

This class maintains a list of animations (self._animations), stepping each of them forward as long as they return False. When they return False, the animation is removed from the tracked animations.

This stepping is done when step is called.

Source code in pytermgui/animations.py
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
class Animator:
    """The Animator class

    This class maintains a list of animations (self._animations), stepping
    each of them forward as long as they return False. When they return
    False, the animation is removed from the tracked animations.

    This stepping is done when `step` is called.
    """

    def __init__(self) -> None:
        """Initializes an animator."""

        self._animations: list[Animation] = []

    def __contains__(self, item: object) -> bool:
        """Returns whether the item is inside _animations."""

        return item in self._animations

    @property
    def is_active(self) -> bool:
        """Determines whether there are any active animations."""

        return len(self._animations) > 0

    def step(self, elapsed: float) -> None:
        """Steps the animation forward by the given elapsed time."""

        for animation in self._animations.copy():
            if animation.step(elapsed):
                self._animations.remove(animation)
                animation.finish()

    def schedule(self, animation: Animation) -> None:
        """Starts an animation on the next step."""

        self._animations.append(animation)

    def animate_attr(self, **animation_args: Any) -> AttrAnimation:
        """Creates and schedules an AttrAnimation.

        All arguments are passed to the `AttrAnimation` constructor. `direction`, if
        given as an integer, will be converted to a `Direction` before being passed.

        Returns:
            The created animation.
        """

        if "direction" in animation_args:
            animation_args["direction"] = Direction(animation_args["direction"])

        anim = AttrAnimation(**animation_args)
        self.schedule(anim)

        return anim

    def animate_float(self, **animation_args: Any) -> FloatAnimation:
        """Creates and schedules an Animation.

        All arguments are passed to the `Animation` constructor. `direction`, if
        given as an integer, will be converted to a `Direction` before being passed.

        Returns:
            The created animation.
        """

        if "direction" in animation_args:
            animation_args["direction"] = Direction(animation_args["direction"])

        anim = FloatAnimation(**animation_args)
        self.schedule(anim)

        return anim

is_active property

Determines whether there are any active animations.

__contains__(item)

Returns whether the item is inside _animations.

Source code in pytermgui/animations.py
264
265
266
267
def __contains__(self, item: object) -> bool:
    """Returns whether the item is inside _animations."""

    return item in self._animations

__init__()

Initializes an animator.

Source code in pytermgui/animations.py
259
260
261
262
def __init__(self) -> None:
    """Initializes an animator."""

    self._animations: list[Animation] = []

animate_attr(**animation_args)

Creates and schedules an AttrAnimation.

All arguments are passed to the AttrAnimation constructor. direction, if given as an integer, will be converted to a Direction before being passed.

Returns:

Type Description
AttrAnimation

The created animation.

Source code in pytermgui/animations.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
def animate_attr(self, **animation_args: Any) -> AttrAnimation:
    """Creates and schedules an AttrAnimation.

    All arguments are passed to the `AttrAnimation` constructor. `direction`, if
    given as an integer, will be converted to a `Direction` before being passed.

    Returns:
        The created animation.
    """

    if "direction" in animation_args:
        animation_args["direction"] = Direction(animation_args["direction"])

    anim = AttrAnimation(**animation_args)
    self.schedule(anim)

    return anim

animate_float(**animation_args)

Creates and schedules an Animation.

All arguments are passed to the Animation constructor. direction, if given as an integer, will be converted to a Direction before being passed.

Returns:

Type Description
FloatAnimation

The created animation.

Source code in pytermgui/animations.py
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
def animate_float(self, **animation_args: Any) -> FloatAnimation:
    """Creates and schedules an Animation.

    All arguments are passed to the `Animation` constructor. `direction`, if
    given as an integer, will be converted to a `Direction` before being passed.

    Returns:
        The created animation.
    """

    if "direction" in animation_args:
        animation_args["direction"] = Direction(animation_args["direction"])

    anim = FloatAnimation(**animation_args)
    self.schedule(anim)

    return anim

schedule(animation)

Starts an animation on the next step.

Source code in pytermgui/animations.py
283
284
285
286
def schedule(self, animation: Animation) -> None:
    """Starts an animation on the next step."""

    self._animations.append(animation)

step(elapsed)

Steps the animation forward by the given elapsed time.

Source code in pytermgui/animations.py
275
276
277
278
279
280
281
def step(self, elapsed: float) -> None:
    """Steps the animation forward by the given elapsed time."""

    for animation in self._animations.copy():
        if animation.step(elapsed):
            self._animations.remove(animation)
            animation.finish()

AnsiSyntaxError dataclass

Bases: ParserSyntaxError

Raised when parsed ANSI text contains an error.

Source code in pytermgui/exceptions.py
90
91
92
93
class AnsiSyntaxError(ParserSyntaxError):
    """Raised when parsed ANSI text contains an error."""

    _delimiters = ("\\x1b[", "m")

AttrAnimation dataclass

Bases: Animation

Animates an attribute going from one value to another.

Source code in pytermgui/animations.py
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
@dataclass
class AttrAnimation(Animation):
    """Animates an attribute going from one value to another."""

    target: object = None
    attr: str = ""
    value_type: type = int
    end: int | float = 0
    start: int | float | None = None

    on_step: Callable[[Animation], bool] | None = None
    on_finish: Callable[[Animation], None] | None = None

    direction: Direction = Direction.FORWARD
    loop: bool = False

    state: float = field(init=False)
    _remaining: int = field(init=False)

    def __post_init__(self) -> None:
        super().__post_init__()

        if self.start is None:
            self.start = getattr(self.target, self.attr)

        if self.end < self.start:
            self.start, self.end = self.end, self.start
            self.direction = Direction.BACKWARD

        self.end -= self.start

        _add_flag(self.target, self.attr)

    def step(self, elapsed: float) -> bool:
        """Steps forward in the attribute animation."""

        state_finished = self._update_state(elapsed)

        step_finished = False

        assert self.start is not None

        updated = self.start + (self.end * self.state)
        setattr(self.target, self.attr, self.value_type(updated))

        if self.on_step is not None:
            step_finished = self.on_step(self)

        if step_finished or state_finished:
            return True

        return False

    def finish(self) -> None:
        """Deletes `__ptg_animated__` flag, calls `on_finish`."""

        _remove_flag(self.target, self.attr)
        super().finish()

finish()

Deletes __ptg_animated__ flag, calls on_finish.

Source code in pytermgui/animations.py
242
243
244
245
246
def finish(self) -> None:
    """Deletes `__ptg_animated__` flag, calls `on_finish`."""

    _remove_flag(self.target, self.attr)
    super().finish()

step(elapsed)

Steps forward in the attribute animation.

Source code in pytermgui/animations.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def step(self, elapsed: float) -> bool:
    """Steps forward in the attribute animation."""

    state_finished = self._update_state(elapsed)

    step_finished = False

    assert self.start is not None

    updated = self.start + (self.end * self.state)
    setattr(self.target, self.attr, self.value_type(updated))

    if self.on_step is not None:
        step_finished = self.on_step(self)

    if step_finished or state_finished:
        return True

    return False

Button

Bases: Widget

A simple Widget representing a mouse-clickable button

Source code in pytermgui/widgets/button.py
 14
 15
 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
class Button(Widget):
    """A simple Widget representing a mouse-clickable button"""

    styles = w_styles.StyleManager(
        label="@surface dim #auto",
        highlight="@surface+1 dim #auto",
        _current=None,
    )

    chars: dict[str, w_styles.CharType] = {"delimiter": ["  ", "  "]}

    def __init__(
        self,
        label: str = "Button",
        onclick: Optional[Callable[[Button], Any]] = None,
        padding: int = 0,
        centered: bool = False,
        **attrs: Any,
    ) -> None:
        """Initialize object"""

        super().__init__(**attrs)
        self._selectables_length = 1

        if not any("width" in attr for attr in attrs):
            self.width = len(label)

        self.label = label
        self.onclick = onclick
        self.padding = padding
        self.centered = centered

        self.styles["_current"] = self.styles.label

    def on_hover(self, _) -> bool:
        """Sets highlight style when hovering."""

        self.styles["_current"] = self.styles.highlight
        return False

    def on_release(self, _) -> bool:
        """Sets normal style when no longer hovering."""

        self.styles["_current"] = self.styles.label
        return False

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

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

        if event.action == MouseAction.LEFT_CLICK:
            self.selected_index = 0
            if self.onclick is not None:
                self.onclick(self)

            return True

        if event.action == MouseAction.RELEASE:
            self.selected_index = None
            return True

        return False

    def handle_key(self, key: str) -> bool:
        """Handles a keypress"""

        if key in (keys.RETURN, keys.CARRIAGE_RETURN) and self.onclick is not None:
            self.onclick(self)
            return True

        return False

    def get_lines(self) -> list[str]:
        """Get object lines"""

        delimiters = self._get_char("delimiter")
        assert isinstance(delimiters, list) and len(delimiters) == 2

        left, right = delimiters
        left = left.replace("[", r"\[")

        if self.selected_index is None:
            style = self.styles["_current"]
        else:
            style = self.styles.highlight

        line = style(left + self.label + right + self.padding * " ")

        return [line]

__init__(label='Button', onclick=None, padding=0, centered=False, **attrs)

Initialize object

Source code in pytermgui/widgets/button.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def __init__(
    self,
    label: str = "Button",
    onclick: Optional[Callable[[Button], Any]] = None,
    padding: int = 0,
    centered: bool = False,
    **attrs: Any,
) -> None:
    """Initialize object"""

    super().__init__(**attrs)
    self._selectables_length = 1

    if not any("width" in attr for attr in attrs):
        self.width = len(label)

    self.label = label
    self.onclick = onclick
    self.padding = padding
    self.centered = centered

    self.styles["_current"] = self.styles.label

get_lines()

Get object lines

Source code in pytermgui/widgets/button.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def get_lines(self) -> list[str]:
    """Get object lines"""

    delimiters = self._get_char("delimiter")
    assert isinstance(delimiters, list) and len(delimiters) == 2

    left, right = delimiters
    left = left.replace("[", r"\[")

    if self.selected_index is None:
        style = self.styles["_current"]
    else:
        style = self.styles.highlight

    line = style(left + self.label + right + self.padding * " ")

    return [line]

handle_key(key)

Handles a keypress

Source code in pytermgui/widgets/button.py
79
80
81
82
83
84
85
86
def handle_key(self, key: str) -> bool:
    """Handles a keypress"""

    if key in (keys.RETURN, keys.CARRIAGE_RETURN) and self.onclick is not None:
        self.onclick(self)
        return True

    return False

handle_mouse(event)

Handles a mouse event

Source code in pytermgui/widgets/button.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def handle_mouse(self, event: MouseEvent) -> bool:
    """Handles a mouse event"""

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

    if event.action == MouseAction.LEFT_CLICK:
        self.selected_index = 0
        if self.onclick is not None:
            self.onclick(self)

        return True

    if event.action == MouseAction.RELEASE:
        self.selected_index = None
        return True

    return False

on_hover(_)

Sets highlight style when hovering.

Source code in pytermgui/widgets/button.py
48
49
50
51
52
def on_hover(self, _) -> bool:
    """Sets highlight style when hovering."""

    self.styles["_current"] = self.styles.highlight
    return False

on_release(_)

Sets normal style when no longer hovering.

Source code in pytermgui/widgets/button.py
54
55
56
57
58
def on_release(self, _) -> bool:
    """Sets normal style when no longer hovering."""

    self.styles["_current"] = self.styles.label
    return False

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

Checkbox

Bases: Button

A simple checkbox

Source code in pytermgui/widgets/checkbox.py
11
12
13
14
15
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
class Checkbox(Button):
    """A simple checkbox"""

    chars = {
        **Button.chars,
        **{"delimiter": [" ", " "], "checked": "▣", "unchecked": "□"},
    }

    def __init__(
        self,
        callback: Callable[[Any], Any] | None = None,
        checked: bool = False,
        **attrs: Any,
    ) -> None:
        """Initialize object"""

        unchecked = self._get_char("unchecked")
        assert isinstance(unchecked, str)

        super().__init__(unchecked, onclick=self.toggle, **attrs)

        self.callback = None
        self.checked = False
        if self.checked != checked:
            self.toggle(run_callback=False)

        self.callback = callback

    def _run_callback(self) -> None:
        """Run the checkbox callback with the new checked flag as its argument"""

        if self.callback is not None:
            self.callback(self.checked)

    def toggle(self, *_: Any, run_callback: bool = True) -> None:
        """Toggle state"""

        chars = self._get_char("checked"), self._get_char("unchecked")
        assert isinstance(chars[0], str) and isinstance(chars[1], str)

        self.checked ^= True
        if self.checked:
            self.label = chars[0]
        else:
            self.label = chars[1]

        self.get_lines()

        if run_callback:
            self._run_callback()

__init__(callback=None, checked=False, **attrs)

Initialize object

Source code in pytermgui/widgets/checkbox.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def __init__(
    self,
    callback: Callable[[Any], Any] | None = None,
    checked: bool = False,
    **attrs: Any,
) -> None:
    """Initialize object"""

    unchecked = self._get_char("unchecked")
    assert isinstance(unchecked, str)

    super().__init__(unchecked, onclick=self.toggle, **attrs)

    self.callback = None
    self.checked = False
    if self.checked != checked:
        self.toggle(run_callback=False)

    self.callback = callback

toggle(*_, run_callback=True)

Toggle state

Source code in pytermgui/widgets/checkbox.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def toggle(self, *_: Any, run_callback: bool = True) -> None:
    """Toggle state"""

    chars = self._get_char("checked"), self._get_char("unchecked")
    assert isinstance(chars[0], str) and isinstance(chars[1], str)

    self.checked ^= True
    if self.checked:
        self.label = chars[0]
    else:
        self.label = chars[1]

    self.get_lines()

    if run_callback:
        self._run_callback()

ClearToken dataclass

Bases: Token

A tag-clearer.

These tokens are prefixed by /, and followed by the name of the tag they target.

To reset color information in the current text, use the /fg and /bg special tags. We cannot unset a specific color due to how the terminal works; all these do is "reset" the current stroke color to the default of the terminal.

Additionally, there are some other special identifiers:

  • /: Clears all tags, including styles, colors, macros, links and more.
  • /!: Clears all currently applied macros.
  • /~: Clears all currently applied links.
Source code in pytermgui/markup/tokens.py
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
@dataclass(frozen=True, repr=False)
class ClearToken(Token):
    """A tag-clearer.

    These tokens are prefixed by `/`, and followed by the name of the tag they target.

    To reset color information in the current text, use the `/fg` and `/bg` special
    tags. We cannot unset a specific color due to how the terminal works; all these do
    is "reset" the current stroke color to the default of the terminal.

    Additionally, there are some other special identifiers:

    - `/`:  Clears all tags, including styles, colors, macros, links and more.
    - `/!`: Clears all currently applied macros.
    - `/~`: Clears all currently applied links.
    """

    __slots__ = ("value",)

    value: str

    @cached_property
    def prettified_markup(self) -> str:
        target = self.markup[1:]

        return f"[210 strikethrough]/[/fg]{target}[/]"

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

        return super().__eq__(other) or all(
            obj.markup in ["/dim", "/bold"] for obj in [self, other]
        )

    def targets(  # pylint: disable=too-many-return-statements
        self, token: Token
    ) -> bool:
        """Returns True if this token targets the one given as an argument."""

        if token.is_clear() or token.is_cursor():
            return False

        if self.value in ("/", f"/{token.value}"):
            return True

        if token.is_hyperlink() and self.value == "/~":
            return True

        if token.is_macro() and self.value == "/!":
            return True

        if not Token.is_color(token):
            return False

        if self.value == "/fg" and not token.color.background:
            return True

        return self.value == "/bg" and token.color.background

targets(token)

Returns True if this token targets the one given as an argument.

Source code in pytermgui/markup/tokens.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def targets(  # pylint: disable=too-many-return-statements
    self, token: Token
) -> bool:
    """Returns True if this token targets the one given as an argument."""

    if token.is_clear() or token.is_cursor():
        return False

    if self.value in ("/", f"/{token.value}"):
        return True

    if token.is_hyperlink() and self.value == "/~":
        return True

    if token.is_macro() and self.value == "/!":
        return True

    if not Token.is_color(token):
        return False

    if self.value == "/fg" and not token.color.background:
        return True

    return self.value == "/bg" and token.color.background

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

Color dataclass

A terminal color.

Parameters:

Name Type Description Default
value str

The data contained within this color.

required
background bool

Whether this color will represent a color.

False

These colors are all formattable. There are currently 2 'spec' strings: - f"{my_color:tim}" -> Returns self.markup - f"{my_color:seq}" -> Returns self.sequence

They can thus be used in TIM strings:

>>> ptg.tim.parse("[{my_color:tim}]Hello")
'[<my_color.markup>]Hello'

And in normal, ANSI coded strings:

>>> "{my_color:seq}Hello"
'<my_color.sequence>Hello'
Source code in pytermgui/colors.py
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
@dataclass
class Color:  # pylint: disable=too-many-public-methods
    """A terminal color.

    Args:
        value: The data contained within this color.
        background: Whether this color will represent a color.

    These colors are all formattable. There are currently 2 'spec' strings:
    - f"{my_color:tim}" -> Returns self.markup
    - f"{my_color:seq}" -> Returns self.sequence

    They can thus be used in TIM strings:

        >>> ptg.tim.parse("[{my_color:tim}]Hello")
        '[<my_color.markup>]Hello'

    And in normal, ANSI coded strings:

        >>> "{my_color:seq}Hello"
        '<my_color.sequence>Hello'
    """

    value: str
    background: bool = False

    system: ColorSystem = field(init=False)

    default_foreground: Color | None = field(default=None, repr=False)
    default_background: Color | None = field(default=None, repr=False)

    _rgb: tuple[int, int, int] | None = field(init=False, default=None, repr=False)

    def __format__(self, spec: str) -> str:
        """Formats the color by the given specification."""

        if spec == "tim":
            return self.markup

        if spec == "seq":
            return self.sequence

        return repr(self)

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> Color:
        """Creates a color from the given RGB.

        Args:
            rgb: The RGB value to base the new color off of.
        """

        return RGBColor.from_rgb(rgb)

    @classmethod
    def from_hls(cls, hsl: RGBTriplet) -> Color:
        """Creates a color from the given HLS.

        HLS stands for Hue, Lightness & Saturation. It is more commonly known as HSL,
        but the `colorsys` library uses HLS instead so that's what we use too.

        Args:
            hsl: The HLS value to base the new color off of.
        """

        rgb = cast(
            RGBTriplet,
            map(lambda n: int(256 * n), colorsys.hls_to_rgb(*hsl)),
        )

        return RGBColor.from_rgb(rgb)

    @property
    def sequence(self) -> str:
        """Returns the ANSI sequence representation of the color."""

        raise NotImplementedError

    @cached_property
    def markup(self) -> str:
        """Returns the TIM representation of this color."""

        return ("@" if self.background else "") + self.value

    @cached_property
    def rgb(self) -> RGBTriplet:
        """Returns this color as a tuple of (red, green, blue) values."""

        if self._rgb is None:
            raise NotImplementedError

        return self._rgb

    @cached_property
    def red(self) -> Number:
        """Returns the red component of this color."""

        return self.rgb[0]

    @cached_property
    def green(self) -> Number:
        """Returns the red component of this color."""

        return self.rgb[1]

    @cached_property
    def blue(self) -> Number:
        """Returns the red component of this color."""

        return self.rgb[2]

    @cached_property
    def hls(self) -> RGBTriplet:
        """Returns the HLS (Hue, Lightness, Saturation) representation of this color."""

        return colorsys.rgb_to_hls(self.red / 256, self.green / 256, self.blue / 256)

    @cached_property
    def hue(self) -> float:
        """Returns the hue component of this color."""

        return self.hls[0]

    @cached_property
    def lightness(self) -> float:
        """Returns the lightness component of this color."""

        return self.hls[1]

    @cached_property
    def saturation(self) -> float:
        """Returns the saturation component of this color."""

        return self.hls[2]

    @cached_property
    def hex(self) -> str:
        """Returns CSS-like HEX representation of this color."""

        buff = "#"
        for color in self.rgb:
            buff += f"{format(color, 'x'):0>2}"

        return buff

    @classmethod
    def get_default_foreground(cls) -> Color:
        """Gets the terminal emulator's default foreground color."""

        if cls.default_foreground is not None:
            return cls.default_foreground

        return _get_palette_color("10")

    @classmethod
    def get_default_background(cls) -> Color:
        """Gets the terminal emulator's default foreground color."""

        if cls.default_background is not None:
            return cls.default_background

        return _get_palette_color("11")

    @property
    def name(self) -> str:
        """Returns the reverse-parseable name of this color."""

        return ("@" if self.background else "") + self.value

    @cached_property
    def luminance(self) -> float:
        """Returns this color's perceived luminance (brightness).

        From https://stackoverflow.com/a/596243
        """

        def _linearize(color: float) -> float:
            """Converts sRGB color to linear value."""

            if color <= 0.04045:
                return color / 12.92

            return ((color + 0.055) / 1.055) ** 2.4

        red, green, blue = float(self.rgb[0]), float(self.rgb[1]), float(self.rgb[2])

        red /= 255
        green /= 255
        blue /= 255

        red = _linearize(red)
        blue = _linearize(blue)
        green = _linearize(green)

        return 0.2126 * red + 0.7152 * green + 0.0722 * blue

    def hue_offset(self, offset: float) -> Color:
        """Returns the color offset by the given hue."""

        hue, lightness, saturation = colorsys.rgb_to_hls(
            self.red / 256, self.green / 256, self.blue / 256
        )

        hue = (hue + offset) % 1

        return Color.parse(
            ";".join(
                map(
                    lambda n: str(int(256 * n)),
                    colorsys.hls_to_rgb(hue, lightness, saturation),
                )
            ),
            background=self.background,
            localize=False,
        )

    @cached_property
    def brightness(self) -> float:
        """Returns the perceived "brightness" of a color.

        From https://stackoverflow.com/a/56678483
        """

        if self.luminance <= (216 / 24389):
            brightness = self.luminance * (24389 / 27)

        else:
            brightness = self.luminance ** (1 / 3) * 116 - 16

        return brightness / 100

    @cached_property
    def complement(self) -> Color:
        """Returns the complement of this color."""

        if self.hue == 0.0:
            return (
                Color.parse("#FFFFFF")
                if self.lightness == 0.0
                else Color.parse("#000000")
            )

        return self.hue_offset(0.5)

    @cached_property
    def triadic(self) -> tuple[Color, Color, Color]:
        """Computes the triadic group this color is in.

        Triadic colors are 3-way complements of eachother.

        Returns:
            This color, the first triadic element and the second one.
        """

        return self, self.hue_offset(1 / 3), self.hue_offset(2 / 3)

    @cached_property
    def tetradic(self) -> tuple[Color, Color, Color, Color]:
        """Computes the tetradic group this color is in.

        Tetradic colors are 4-way complements of eachother.

        Returns:
            This color, the first tetradic element and the second one.
        """

        return self, self.hue_offset(1 / 4), self.complement, self.hue_offset(3 / 4)

    @cached_property
    def analogous(self) -> tuple[Color, Color, Color]:
        """Computes the analogous group this colors is in.

        Analogous colors are located next to eachother on the color wheel.

        Returns:
            The color to the left, this color and the color to the right.
        """

        return self.hue_offset(-1 / 12), self, self.hue_offset(1 / 12)

    @cached_property
    def contrast(self) -> Color:
        """Returns a color (black or white) that complies with the W3C contrast ratio guidelines."""

        if self.luminance > 0.179:
            return Color.parse("#000000").blend_complement(0.05)

        return Color.parse("#FFFFFF").blend_complement(0.05)

    def blend(self, other: Color, alpha: float = 0.5, localize: bool = False) -> Color:
        """Blends a color into another one.

        Args:
            other: The color to blend with.
            alpha: How much the other color should influence the outcome.
            localize: If set, the returned color will returned its localized version by running
                `get_localized` on it before returning.

        Returns:
            A `Color` that is the result of the blending.
        """

        red1, green1, blue1 = self.rgb
        red2, green2, blue2 = other.rgb

        blended: Color = RGBColor.from_rgb(
            (
                int(red1 + (red2 - red1) * alpha),
                int(green1 + (green2 - green1) * alpha),
                int(blue1 + (blue2 - blue1) * alpha),
            )
        )

        if localize:
            blended = blended.get_localized()

        return blended

    def blend_complement(self, alpha: float = 0.5) -> Color:
        """Blends this color with its complement.

        See `Color.blend`.
        """

        return self.blend(self.complement, alpha)

    def blend_contrast(self, alpha: float = 0.5) -> Color:
        """Blends this color with its contrast pair.

        See `Color.blend`.
        """

        return self.blend(self.contrast, alpha)

    def darken(self, alpha: float = 0.5) -> Color:
        """Darkens the color by blending it with black, using the alpha provided."""

        return self.blend(Color.parse("#000000"), alpha)

    def lighten(self, alpha: float = 0.5) -> Color:
        """Lightens the color by blending it with white, using the alpha provided."""

        return self.blend(Color.parse("#FFFFFF"), alpha)

    @classmethod
    def parse(
        cls,
        text: str,
        background: bool = False,  # pylint: disable=redefined-outer-name
        localize: bool = True,
        use_cache: bool = False,
    ) -> Color:
        """Uses `str_to_color` to parse some text into a `Color`."""

        return str_to_color(
            text=text,
            is_background=background,
            localize=localize,
            use_cache=use_cache,
        )

    def __call__(self, text: str, reset: bool = True) -> str:
        """Colors the given string."""

        buff = self.sequence + text
        if reset:
            buff += reset_style()

        return buff

    def get_localized(self) -> Color:
        """Creates a terminal-capability local Color instance.

        This method essentially allows for graceful degradation of colors in the
        terminal.
        """

        system = terminal.colorsystem
        if self.system <= system:
            return self

        colortype = SYSTEM_TO_TYPE[system]

        local = colortype.from_rgb(self.rgb)
        local.background = self.background

        return local

analogous cached property

Computes the analogous group this colors is in.

Analogous colors are located next to eachother on the color wheel.

Returns:

Type Description
tuple[Color, Color, Color]

The color to the left, this color and the color to the right.

blue cached property

Returns the red component of this color.

brightness cached property

Returns the perceived "brightness" of a color.

From https://stackoverflow.com/a/56678483

complement cached property

Returns the complement of this color.

contrast cached property

Returns a color (black or white) that complies with the W3C contrast ratio guidelines.

green cached property

Returns the red component of this color.

hex cached property

Returns CSS-like HEX representation of this color.

hls cached property

Returns the HLS (Hue, Lightness, Saturation) representation of this color.

hue cached property

Returns the hue component of this color.

lightness cached property

Returns the lightness component of this color.

luminance cached property

Returns this color's perceived luminance (brightness).

From https://stackoverflow.com/a/596243

markup cached property

Returns the TIM representation of this color.

name property

Returns the reverse-parseable name of this color.

red cached property

Returns the red component of this color.

rgb cached property

Returns this color as a tuple of (red, green, blue) values.

saturation cached property

Returns the saturation component of this color.

sequence property

Returns the ANSI sequence representation of the color.

tetradic cached property

Computes the tetradic group this color is in.

Tetradic colors are 4-way complements of eachother.

Returns:

Type Description
tuple[Color, Color, Color, Color]

This color, the first tetradic element and the second one.

triadic cached property

Computes the triadic group this color is in.

Triadic colors are 3-way complements of eachother.

Returns:

Type Description
tuple[Color, Color, Color]

This color, the first triadic element and the second one.

__call__(text, reset=True)

Colors the given string.

Source code in pytermgui/colors.py
493
494
495
496
497
498
499
500
def __call__(self, text: str, reset: bool = True) -> str:
    """Colors the given string."""

    buff = self.sequence + text
    if reset:
        buff += reset_style()

    return buff

__format__(spec)

Formats the color by the given specification.

Source code in pytermgui/colors.py
165
166
167
168
169
170
171
172
173
174
def __format__(self, spec: str) -> str:
    """Formats the color by the given specification."""

    if spec == "tim":
        return self.markup

    if spec == "seq":
        return self.sequence

    return repr(self)

blend(other, alpha=0.5, localize=False)

Blends a color into another one.

Parameters:

Name Type Description Default
other Color

The color to blend with.

required
alpha float

How much the other color should influence the outcome.

0.5
localize bool

If set, the returned color will returned its localized version by running get_localized on it before returning.

False

Returns:

Type Description
Color

A Color that is the result of the blending.

Source code in pytermgui/colors.py
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
def blend(self, other: Color, alpha: float = 0.5, localize: bool = False) -> Color:
    """Blends a color into another one.

    Args:
        other: The color to blend with.
        alpha: How much the other color should influence the outcome.
        localize: If set, the returned color will returned its localized version by running
            `get_localized` on it before returning.

    Returns:
        A `Color` that is the result of the blending.
    """

    red1, green1, blue1 = self.rgb
    red2, green2, blue2 = other.rgb

    blended: Color = RGBColor.from_rgb(
        (
            int(red1 + (red2 - red1) * alpha),
            int(green1 + (green2 - green1) * alpha),
            int(blue1 + (blue2 - blue1) * alpha),
        )
    )

    if localize:
        blended = blended.get_localized()

    return blended

blend_complement(alpha=0.5)

Blends this color with its complement.

See Color.blend.

Source code in pytermgui/colors.py
450
451
452
453
454
455
456
def blend_complement(self, alpha: float = 0.5) -> Color:
    """Blends this color with its complement.

    See `Color.blend`.
    """

    return self.blend(self.complement, alpha)

blend_contrast(alpha=0.5)

Blends this color with its contrast pair.

See Color.blend.

Source code in pytermgui/colors.py
458
459
460
461
462
463
464
def blend_contrast(self, alpha: float = 0.5) -> Color:
    """Blends this color with its contrast pair.

    See `Color.blend`.
    """

    return self.blend(self.contrast, alpha)

darken(alpha=0.5)

Darkens the color by blending it with black, using the alpha provided.

Source code in pytermgui/colors.py
466
467
468
469
def darken(self, alpha: float = 0.5) -> Color:
    """Darkens the color by blending it with black, using the alpha provided."""

    return self.blend(Color.parse("#000000"), alpha)

from_hls(hsl) classmethod

Creates a color from the given HLS.

HLS stands for Hue, Lightness & Saturation. It is more commonly known as HSL, but the colorsys library uses HLS instead so that's what we use too.

Parameters:

Name Type Description Default
hsl RGBTriplet

The HLS value to base the new color off of.

required
Source code in pytermgui/colors.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
@classmethod
def from_hls(cls, hsl: RGBTriplet) -> Color:
    """Creates a color from the given HLS.

    HLS stands for Hue, Lightness & Saturation. It is more commonly known as HSL,
    but the `colorsys` library uses HLS instead so that's what we use too.

    Args:
        hsl: The HLS value to base the new color off of.
    """

    rgb = cast(
        RGBTriplet,
        map(lambda n: int(256 * n), colorsys.hls_to_rgb(*hsl)),
    )

    return RGBColor.from_rgb(rgb)

from_rgb(rgb) classmethod

Creates a color from the given RGB.

Parameters:

Name Type Description Default
rgb RGBTriplet

The RGB value to base the new color off of.

required
Source code in pytermgui/colors.py
176
177
178
179
180
181
182
183
184
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> Color:
    """Creates a color from the given RGB.

    Args:
        rgb: The RGB value to base the new color off of.
    """

    return RGBColor.from_rgb(rgb)

get_default_background() classmethod

Gets the terminal emulator's default foreground color.

Source code in pytermgui/colors.py
286
287
288
289
290
291
292
293
@classmethod
def get_default_background(cls) -> Color:
    """Gets the terminal emulator's default foreground color."""

    if cls.default_background is not None:
        return cls.default_background

    return _get_palette_color("11")

get_default_foreground() classmethod

Gets the terminal emulator's default foreground color.

Source code in pytermgui/colors.py
277
278
279
280
281
282
283
284
@classmethod
def get_default_foreground(cls) -> Color:
    """Gets the terminal emulator's default foreground color."""

    if cls.default_foreground is not None:
        return cls.default_foreground

    return _get_palette_color("10")

get_localized()

Creates a terminal-capability local Color instance.

This method essentially allows for graceful degradation of colors in the terminal.

Source code in pytermgui/colors.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
def get_localized(self) -> Color:
    """Creates a terminal-capability local Color instance.

    This method essentially allows for graceful degradation of colors in the
    terminal.
    """

    system = terminal.colorsystem
    if self.system <= system:
        return self

    colortype = SYSTEM_TO_TYPE[system]

    local = colortype.from_rgb(self.rgb)
    local.background = self.background

    return local

hue_offset(offset)

Returns the color offset by the given hue.

Source code in pytermgui/colors.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
def hue_offset(self, offset: float) -> Color:
    """Returns the color offset by the given hue."""

    hue, lightness, saturation = colorsys.rgb_to_hls(
        self.red / 256, self.green / 256, self.blue / 256
    )

    hue = (hue + offset) % 1

    return Color.parse(
        ";".join(
            map(
                lambda n: str(int(256 * n)),
                colorsys.hls_to_rgb(hue, lightness, saturation),
            )
        ),
        background=self.background,
        localize=False,
    )

lighten(alpha=0.5)

Lightens the color by blending it with white, using the alpha provided.

Source code in pytermgui/colors.py
471
472
473
474
def lighten(self, alpha: float = 0.5) -> Color:
    """Lightens the color by blending it with white, using the alpha provided."""

    return self.blend(Color.parse("#FFFFFF"), alpha)

parse(text, background=False, localize=True, use_cache=False) classmethod

Uses str_to_color to parse some text into a Color.

Source code in pytermgui/colors.py
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
@classmethod
def parse(
    cls,
    text: str,
    background: bool = False,  # pylint: disable=redefined-outer-name
    localize: bool = True,
    use_cache: bool = False,
) -> Color:
    """Uses `str_to_color` to parse some text into a `Color`."""

    return str_to_color(
        text=text,
        is_background=background,
        localize=localize,
        use_cache=use_cache,
    )

ColorPicker

Bases: Container

A simple ColorPicker widget.

This is used to visualize xterm-255 colors. RGB colors are not included here, as it is probably easier to use a web-based picker for those anyways.

Source code in pytermgui/widgets/color_picker.py
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
class ColorPicker(Container):
    """A simple ColorPicker widget.

    This is used to visualize xterm-255 colors. RGB colors are not
    included here, as it is probably easier to use a web-based picker
    for those anyways.
    """

    size_policy = SizePolicy.STATIC

    def __init__(self, show_output: bool = True, **attrs: Any) -> None:
        """Initializes a ColorPicker.

        Attrs:
            show_output: Decides whether the output Container should be
                added. If not set, the widget will only display the
                PixelMatrix of colors.
        """

        super().__init__(**attrs)
        self.show_output = show_output

        self._matrix = PixelMatrix.from_matrix(_get_xterm_matrix())

        self.width = 72
        self.box = boxes.EMPTY

        self._add_widget(self._matrix, run_get_lines=False)

        self.chosen = Joiner()
        self._output = Container(self.chosen, "", "", "")

        if self.show_output:
            self._add_widget(self._output)

    @property
    def selectables_length(self) -> int:
        """Returns either the button count or 1."""

        return max(super().selectables_length, 1)

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

        On hover, the widget will display the currently hovered
        color and some testing text.

        On click, it will add a _FadeInButton for the currently
        hovered color.

        Args:
            event: The event to handle.
        """

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

        if not self.show_output or not self._matrix.contains(event.position):
            return False

        if event.action is MouseAction.LEFT_CLICK:
            if self._matrix.selected_pixel is None:
                return True

            _, color = self._matrix.selected_pixel
            if len(color) == 0:
                return False

            button = _FadeInButton(f"{color:^5}", width=5)
            button.styles.label = f"@{color}"
            self.chosen.lazy_add(button)

            return True

        return False

    def get_lines(self) -> list[str]:
        """Updates self._output and gets widget lines."""

        if self.show_output and self._matrix.selected_pixel is not None:
            _, color = self._matrix.selected_pixel
            if len(color) == 0:
                return super().get_lines()

            color_obj = str_to_color(color)
            rgb = color_obj.rgb
            hex_ = color_obj.hex
            lines: list[Widget] = [
                Label(f"[@{color} #auto] {color} [/ {color}] {color}"),
                Label(
                    f"[{color} bold]Here[/bold italic] is "
                    + "[/italic underline]some[/underline dim] example[/dim] text"
                ),
                Label(),
                Label(
                    f"RGB: [{';'.join(map(str, rgb))}]"
                    + f"rgb({rgb[0]:>3}, {rgb[1]:>3}, {rgb[2]:>3})"
                ),
                Label(f"HEX: [{hex_}]{hex_}"),
            ]
            self._output.set_widgets(lines + [Label(), self.chosen])

            return super().get_lines()

        return super().get_lines()

selectables_length property

Returns either the button count or 1.

__init__(show_output=True, **attrs)

Initializes a ColorPicker.

Attrs

show_output: Decides whether the output Container should be added. If not set, the widget will only display the PixelMatrix of colors.

Source code in pytermgui/widgets/color_picker.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def __init__(self, show_output: bool = True, **attrs: Any) -> None:
    """Initializes a ColorPicker.

    Attrs:
        show_output: Decides whether the output Container should be
            added. If not set, the widget will only display the
            PixelMatrix of colors.
    """

    super().__init__(**attrs)
    self.show_output = show_output

    self._matrix = PixelMatrix.from_matrix(_get_xterm_matrix())

    self.width = 72
    self.box = boxes.EMPTY

    self._add_widget(self._matrix, run_get_lines=False)

    self.chosen = Joiner()
    self._output = Container(self.chosen, "", "", "")

    if self.show_output:
        self._add_widget(self._output)

get_lines()

Updates self._output and gets widget lines.

Source code in pytermgui/widgets/color_picker.py
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
def get_lines(self) -> list[str]:
    """Updates self._output and gets widget lines."""

    if self.show_output and self._matrix.selected_pixel is not None:
        _, color = self._matrix.selected_pixel
        if len(color) == 0:
            return super().get_lines()

        color_obj = str_to_color(color)
        rgb = color_obj.rgb
        hex_ = color_obj.hex
        lines: list[Widget] = [
            Label(f"[@{color} #auto] {color} [/ {color}] {color}"),
            Label(
                f"[{color} bold]Here[/bold italic] is "
                + "[/italic underline]some[/underline dim] example[/dim] text"
            ),
            Label(),
            Label(
                f"RGB: [{';'.join(map(str, rgb))}]"
                + f"rgb({rgb[0]:>3}, {rgb[1]:>3}, {rgb[2]:>3})"
            ),
            Label(f"HEX: [{hex_}]{hex_}"),
        ]
        self._output.set_widgets(lines + [Label(), self.chosen])

        return super().get_lines()

    return super().get_lines()

handle_mouse(event)

Handles mouse events.

On hover, the widget will display the currently hovered color and some testing text.

On click, it will add a _FadeInButton for the currently hovered color.

Parameters:

Name Type Description Default
event MouseEvent

The event to handle.

required
Source code in pytermgui/widgets/color_picker.py
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
def handle_mouse(self, event: MouseEvent) -> bool:
    """Handles mouse events.

    On hover, the widget will display the currently hovered
    color and some testing text.

    On click, it will add a _FadeInButton for the currently
    hovered color.

    Args:
        event: The event to handle.
    """

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

    if not self.show_output or not self._matrix.contains(event.position):
        return False

    if event.action is MouseAction.LEFT_CLICK:
        if self._matrix.selected_pixel is None:
            return True

        _, color = self._matrix.selected_pixel
        if len(color) == 0:
            return False

        button = _FadeInButton(f"{color:^5}", width=5)
        button.styles.label = f"@{color}"
        self.chosen.lazy_add(button)

        return True

    return False

ColorSystem

Bases: Enum

An enumeration of various terminal-supported colorsystems.

Source code in pytermgui/term.py
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
class ColorSystem(Enum):
    """An enumeration of various terminal-supported colorsystems."""

    NO_COLOR = -1
    """No-color terminal. See https://no-color.org/."""

    STANDARD = 0
    """Standard 3-bit colorsystem of the basic 16 colors."""

    EIGHT_BIT = 1
    """xterm 8-bit colors, 0-256."""

    TRUE = 2
    """'True' color, a.k.a. 24-bit RGB colors."""

    def __ge__(self, other):
        """Comparison: self >= other."""

        if self.__class__ is other.__class__:
            return self.value >= other.value

        return NotImplemented

    def __gt__(self, other):
        """Comparison: self > other."""

        if self.__class__ is other.__class__:
            return self.value > other.value

        return NotImplemented

    def __le__(self, other):
        """Comparison: self <= other."""

        if self.__class__ is other.__class__:
            return self.value <= other.value

        return NotImplemented

    def __lt__(self, other):
        """Comparison: self < other."""

        if self.__class__ is other.__class__:
            return self.value < other.value

        return NotImplemented

EIGHT_BIT = 1 class-attribute instance-attribute

xterm 8-bit colors, 0-256.

NO_COLOR = -1 class-attribute instance-attribute

No-color terminal. See https://no-color.org/.

STANDARD = 0 class-attribute instance-attribute

Standard 3-bit colorsystem of the basic 16 colors.

TRUE = 2 class-attribute instance-attribute

'True' color, a.k.a. 24-bit RGB colors.

__ge__(other)

Comparison: self >= other.

Source code in pytermgui/term.py
180
181
182
183
184
185
186
def __ge__(self, other):
    """Comparison: self >= other."""

    if self.__class__ is other.__class__:
        return self.value >= other.value

    return NotImplemented

__gt__(other)

Comparison: self > other.

Source code in pytermgui/term.py
188
189
190
191
192
193
194
def __gt__(self, other):
    """Comparison: self > other."""

    if self.__class__ is other.__class__:
        return self.value > other.value

    return NotImplemented

__le__(other)

Comparison: self <= other.

Source code in pytermgui/term.py
196
197
198
199
200
201
202
def __le__(self, other):
    """Comparison: self <= other."""

    if self.__class__ is other.__class__:
        return self.value <= other.value

    return NotImplemented

__lt__(other)

Comparison: self < other.

Source code in pytermgui/term.py
204
205
206
207
208
209
210
def __lt__(self, other):
    """Comparison: self < other."""

    if self.__class__ is other.__class__:
        return self.value < other.value

    return NotImplemented

ColorToken dataclass

Bases: Token

A color identifier.

It stores the markup that created it, as well as the pytermgui.colors.Color object that it represents.

Source code in pytermgui/markup/tokens.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
@dataclass(frozen=True, repr=False)
class ColorToken(Token):
    """A color identifier.

    It stores the markup that created it, as well as the `pytermgui.colors.Color` object
    that it represents.
    """

    __slots__ = ("value",)

    value: str
    color: Color

    @cached_property
    def markup(self) -> str:
        return self.color.markup

    @cached_property
    def prettified_markup(self) -> str:
        clearer = "bg" if self.color.background else "fg"

        return f"[{self.markup}]{self.markup}[/{clearer}]"

Compositor

The class used to draw pytermgui.window_managers.manager.WindowManager state.

This class handles turning a list of windows into a drawable buffer (composite), and then drawing it onto the screen.

Calling its run method will start the drawing thread, which will draw the current window states onto the screen. This routine targets framerate, though will likely not match it perfectly.

Source code in pytermgui/window_manager/compositor.py
 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
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
class Compositor:
    """The class used to draw `pytermgui.window_managers.manager.WindowManager` state.

    This class handles turning a list of windows into a drawable buffer (composite),
    and then drawing it onto the screen.

    Calling its `run` method will start the drawing thread, which will draw the current
    window states onto the screen. This routine targets `framerate`, though will likely
    not match it perfectly.
    """

    def __init__(self, windows: list[Window], framerate: int) -> None:
        """Initializes the Compositor.

        Args:
            windows: A list of the windows to be drawn.
        """

        self._windows = windows
        self._is_running = False

        self._previous: PositionedLineList = []
        self._frametime = 0.0
        self._should_redraw: bool = True
        self._cache: dict[int, list[str]] = {}

        self.fps = 0
        self.framerate = framerate

    @property
    def terminal(self) -> Terminal:
        """Returns the current global terminal."""

        return get_terminal()

    def _draw_loop(self) -> None:
        """A loop that draws at regular intervals."""

        framecount = 0
        last_frame = fps_start_time = time.perf_counter()

        while self._is_running:
            elapsed = time.perf_counter() - last_frame

            if elapsed < self._frametime:
                time.sleep(self._frametime - elapsed)
                continue

            self.terminal.process_pending_resize()

            animator.step(elapsed)

            last_frame = time.perf_counter()
            self.draw()

            framecount += 1

            if last_frame - fps_start_time >= 1:
                self.fps = framecount
                fps_start_time = last_frame
                framecount = 0

    # NOTE: This is not needed at the moment, but might be at some point soon.
    # def _get_lines(self, window: Window) -> list[str]:
    #     """Gets lines from the window, caching when possible.

    #     This also applies the blurred style of the window, if it has no focus.
    #     """

    #     if window.allow_fullscreen:
    #         window.pos = self.terminal.origin
    #         window.width = self.terminal.width
    #         window.height = self.terminal.height

    #     return window.get_lines()

    #     if window.has_focus or window.is_noblur:
    #         return window.get_lines()

    #     _id = id(window)
    #     if not window.is_dirty and _id in self._cache:
    #         return self._cache[_id]

    #     lines: list[str] = []
    #     for line in window.get_lines():
    #         if not window.has_focus:
    #             line = tim.parse("[239]" + strip_ansi(line).replace("[", r"\["))

    #         lines.append(line)

    #     self._cache[_id] = lines
    #     return lines

    def _iter_positioned(
        self, widget: Widget, until: int | None = None
    ) -> Iterator[tuple[tuple[int, int], str]]:
        """Iterates through (pos, line) tuples from widget.get_lines()."""

        # get_lines = widget.get_lines
        # if isinstance(widget, Window):
        #     get_lines = lambda *_: self._get_lines(widget)  # type: ignore
        width, height = self.terminal.size

        if until is None:
            until = widget.height

        for i, line in enumerate(widget.get_lines()[:until]):
            if i >= until:
                break

            pos = (widget.pos[0], widget.pos[1] + i)

            yield (pos, line)

        for item in widget.positioned_line_buffer.copy():
            pos, line = item

            if 0 <= pos[0] <= width and 0 <= pos[1] <= height:
                yield item

            widget.positioned_line_buffer.remove(item)

    @property
    def framerate(self) -> int:
        """The framerate the draw loop runs at.

        Note:
            This will likely not be matched very accurately, mostly undershooting
            the given target.
        """

        return self._framerate

    @framerate.setter
    def framerate(self, new: int) -> None:
        """Updates the framerate."""

        self._frametime = 1 / new
        self._framerate = new

    def clear_cache(self, window: Window) -> None:
        """Clears the compositor's cache related to the given window."""

        if id(window) in self._cache:
            del self._cache[id(window)]

    def run(self) -> None:
        """Runs the compositor draw loop as a thread."""

        self._is_running = True
        Thread(name="CompositorDrawLoop", target=self._draw_loop, daemon=True).start()

    def stop(self) -> None:
        """Stops the compositor."""

        self._is_running = False

    def composite(self) -> PositionedLineList:
        """Creates a composited buffer from the assigned windows.

        Note that this is currently not used."""

        lines = []
        windows = self._windows

        # Don't unnecessarily print under full screen windows
        if any(window.allow_fullscreen for window in self._windows):
            for window in reversed(self._windows):
                if window.allow_fullscreen:
                    windows = [window]
                    break

        size_changes = {WidgetChange.WIDTH, WidgetChange.HEIGHT, WidgetChange.SIZE}
        for window in reversed(windows):
            if not window.has_focus:
                continue

            change = window.get_change()

            if change is None:
                continue

            if window.is_dirty or change in size_changes:
                for pos, line in self._iter_positioned(window):
                    lines.append((pos, line))

                window.is_dirty = False
                continue

            if change is not None:
                remaining = window.content_dimensions[1]

                for widget in window.dirty_widgets:
                    for pos, line in self._iter_positioned(widget, until=remaining):
                        lines.append((pos, line))

                    remaining -= widget.height

                window.dirty_widgets = []
                continue

            if window.allow_fullscreen:
                break

        return lines

    def set_redraw(self) -> None:
        """Flags compositor for full redraw.

        Note:
            At the moment the compositor will always redraw the entire screen.
        """

        self._should_redraw = True

    def draw(self, force: bool = False) -> None:
        """Writes composited screen to the terminal.

        At the moment this uses full-screen rewrites. There is a compositing
        implementation in `composite`, but it is currently not performant enough to use.

        Args:
            force: When set, new composited lines will not be checked against the
                previous ones, and everything will be redrawn.
        """

        # if self._should_redraw or force:
        lines: PositionedLineList = []

        for window in reversed(self._windows):
            lines.extend(self._iter_positioned(window))

        self._should_redraw = False

        # else:
        # lines = self.composite()

        if not force and self._previous == lines:
            return

        with self.terminal.frame() as frame:
            frame_write = frame.write

            for pos, line in lines:
                frame_write(f"\x1b[{pos[1]};{pos[0]}H{line}")

        self._previous = lines

    def redraw(self) -> None:
        """Force-redraws the buffer."""

        self.draw(force=True)

    def capture(self, title: str, filename: str | None = None) -> None:
        """Captures the most-recently drawn buffer as `filename`.

        See `pytermgui.exporters.to_svg` for more information.
        """

        with self.terminal.record() as recording:
            self.redraw()

        recording.save_svg(title=title, filename=filename)

framerate property writable

The framerate the draw loop runs at.

Note

This will likely not be matched very accurately, mostly undershooting the given target.

terminal property

Returns the current global terminal.

__init__(windows, framerate)

Initializes the Compositor.

Parameters:

Name Type Description Default
windows list[Window]

A list of the windows to be drawn.

required
Source code in pytermgui/window_manager/compositor.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def __init__(self, windows: list[Window], framerate: int) -> None:
    """Initializes the Compositor.

    Args:
        windows: A list of the windows to be drawn.
    """

    self._windows = windows
    self._is_running = False

    self._previous: PositionedLineList = []
    self._frametime = 0.0
    self._should_redraw: bool = True
    self._cache: dict[int, list[str]] = {}

    self.fps = 0
    self.framerate = framerate

capture(title, filename=None)

Captures the most-recently drawn buffer as filename.

See pytermgui.exporters.to_svg for more information.

Source code in pytermgui/window_manager/compositor.py
273
274
275
276
277
278
279
280
281
282
def capture(self, title: str, filename: str | None = None) -> None:
    """Captures the most-recently drawn buffer as `filename`.

    See `pytermgui.exporters.to_svg` for more information.
    """

    with self.terminal.record() as recording:
        self.redraw()

    recording.save_svg(title=title, filename=filename)

clear_cache(window)

Clears the compositor's cache related to the given window.

Source code in pytermgui/window_manager/compositor.py
160
161
162
163
164
def clear_cache(self, window: Window) -> None:
    """Clears the compositor's cache related to the given window."""

    if id(window) in self._cache:
        del self._cache[id(window)]

composite()

Creates a composited buffer from the assigned windows.

Note that this is currently not used.

Source code in pytermgui/window_manager/compositor.py
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
def composite(self) -> PositionedLineList:
    """Creates a composited buffer from the assigned windows.

    Note that this is currently not used."""

    lines = []
    windows = self._windows

    # Don't unnecessarily print under full screen windows
    if any(window.allow_fullscreen for window in self._windows):
        for window in reversed(self._windows):
            if window.allow_fullscreen:
                windows = [window]
                break

    size_changes = {WidgetChange.WIDTH, WidgetChange.HEIGHT, WidgetChange.SIZE}
    for window in reversed(windows):
        if not window.has_focus:
            continue

        change = window.get_change()

        if change is None:
            continue

        if window.is_dirty or change in size_changes:
            for pos, line in self._iter_positioned(window):
                lines.append((pos, line))

            window.is_dirty = False
            continue

        if change is not None:
            remaining = window.content_dimensions[1]

            for widget in window.dirty_widgets:
                for pos, line in self._iter_positioned(widget, until=remaining):
                    lines.append((pos, line))

                remaining -= widget.height

            window.dirty_widgets = []
            continue

        if window.allow_fullscreen:
            break

    return lines

draw(force=False)

Writes composited screen to the terminal.

At the moment this uses full-screen rewrites. There is a compositing implementation in composite, but it is currently not performant enough to use.

Parameters:

Name Type Description Default
force bool

When set, new composited lines will not be checked against the previous ones, and everything will be redrawn.

False
Source code in pytermgui/window_manager/compositor.py
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
def draw(self, force: bool = False) -> None:
    """Writes composited screen to the terminal.

    At the moment this uses full-screen rewrites. There is a compositing
    implementation in `composite`, but it is currently not performant enough to use.

    Args:
        force: When set, new composited lines will not be checked against the
            previous ones, and everything will be redrawn.
    """

    # if self._should_redraw or force:
    lines: PositionedLineList = []

    for window in reversed(self._windows):
        lines.extend(self._iter_positioned(window))

    self._should_redraw = False

    # else:
    # lines = self.composite()

    if not force and self._previous == lines:
        return

    with self.terminal.frame() as frame:
        frame_write = frame.write

        for pos, line in lines:
            frame_write(f"\x1b[{pos[1]};{pos[0]}H{line}")

    self._previous = lines

redraw()

Force-redraws the buffer.

Source code in pytermgui/window_manager/compositor.py
268
269
270
271
def redraw(self) -> None:
    """Force-redraws the buffer."""

    self.draw(force=True)

run()

Runs the compositor draw loop as a thread.

Source code in pytermgui/window_manager/compositor.py
166
167
168
169
170
def run(self) -> None:
    """Runs the compositor draw loop as a thread."""

    self._is_running = True
    Thread(name="CompositorDrawLoop", target=self._draw_loop, daemon=True).start()

set_redraw()

Flags compositor for full redraw.

Note

At the moment the compositor will always redraw the entire screen.

Source code in pytermgui/window_manager/compositor.py
226
227
228
229
230
231
232
233
def set_redraw(self) -> None:
    """Flags compositor for full redraw.

    Note:
        At the moment the compositor will always redraw the entire screen.
    """

    self._should_redraw = True

stop()

Stops the compositor.

Source code in pytermgui/window_manager/compositor.py
172
173
174
175
def stop(self) -> None:
    """Stops the compositor."""

    self._is_running = False

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

ContextDict

Bases: TypedDict

A dictionary to hold context about a markup language's environment.

It has two sub-dicts:

  • aliases
  • macros

For information about what they do and contain, see the MarkupLanguage docs.

Source code in pytermgui/markup/parsing.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class ContextDict(TypedDict):
    """A dictionary to hold context about a markup language's environment.

    It has two sub-dicts:

    - aliases
    - macros

    For information about what they do and contain, see the
    [MarkupLanguage docs](/reference/pytermgui/markup/
    language#pytermgui.markup.language.MarkupLanguage).
    """

    aliases: dict[str, str]
    macros: dict[str, MacroType]

CursorToken dataclass

Bases: Token

A cursor location.

These can be used to move the terminal's cursor.

Source code in pytermgui/markup/tokens.py
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
@dataclass(frozen=True, repr=False)
class CursorToken(Token):
    """A cursor location.

    These can be used to move the terminal's cursor.
    """

    __slots__ = ("value", "y", "x")

    value: str
    y: int | None
    x: int | None

    def __iter__(self) -> Iterator[int | None]:
        return iter((self.y, self.x))

    def __repr__(self) -> str:
        return f"<{type(self).__name__} position: {(';'.join(map(str, self)))}>"

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        yield self.__repr__()

    @cached_property
    def markup(self) -> str:
        return f"({self.value})"

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 ║",
        "╚═══╝",
    ]

FancyReprWidget

Bases: Widget

A widget that wraps objects supporting the fancy_repr protocol.

Source code in pytermgui/widgets/fancy_repr.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class FancyReprWidget(Widget):
    """A widget that wraps objects supporting the `fancy_repr` protocol."""

    def __init__(
        self, target: SupportsFancyRepr, starts_at: int = 0, **attrs: Any
    ) -> None:
        self.target = target
        self.starts_at = starts_at

        super().__init__(**attrs)

    def get_lines(self) -> list[str]:
        """Builds fancy repr of target and returns it."""

        start = self.starts_at
        lines = [
            tim.parse(line)
            for line in build_fancy_repr(self.target).splitlines()[start:]
        ]

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

        return lines

get_lines()

Builds fancy repr of target and returns it.

Source code in pytermgui/widgets/fancy_repr.py
25
26
27
28
29
30
31
32
33
34
35
36
37
def get_lines(self) -> list[str]:
    """Builds fancy repr of target and returns it."""

    start = self.starts_at
    lines = [
        tim.parse(line)
        for line in build_fancy_repr(self.target).splitlines()[start:]
    ]

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

    return lines

FileLoader

Bases: ABC

Base class for file loader objects.

These allow users to load pytermgui content from a specific filetype, with each filetype having their own loaders.

To use custom widgets with children of this class, you need to call FileLoader.register.

Source code in pytermgui/file_loaders.py
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
class FileLoader(ABC):
    """Base class for file loader objects.

    These allow users to load pytermgui content from a specific filetype,
    with each filetype having their own loaders.

    To use custom widgets with children of this class, you need to call `FileLoader.register`."""

    serializer: Serializer
    """Object-specific serializer instance. In order to use a specific, already created
    instance you need to pass it on `FileLoader` construction."""

    @abstractmethod
    def parse(self, data: str) -> dict[Any, Any]:
        """Parses string into a dictionary used by `pytermgui.serializer.Serializer`.

        This dictionary follows the structure defined above.
        """

    def __init__(self, serializer: Serializer | None = None) -> None:
        """Initialize FileLoader.

        Args:
            serializer: An optional `pytermgui.serializer.Serializer` instance. If not provided, one
                is instantiated for every FileLoader instance.
        """

        if serializer is None:
            serializer = Serializer()

        self.serializer = serializer

    def __enter__(self) -> FileLoader:
        """Starts context manager."""

        return self

    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
        """Ends context manager."""

        if exception is not None:
            raise exception

    def register(self, cls: Type[widgets_m.Widget]) -> None:
        """Registers a widget to the serializer.

        Args:
            cls: The widget type to register.
        """

        self.serializer.register(cls)

    def bind(self, name: str, method: Callable[..., Any]) -> None:
        """Binds a name to a method.

        Args:
            name: The name of the method, as referenced in the loaded
                files.
            method: The callable to bind.
        """

        self.serializer.bind(name, method)

    def load_str(self, data: str) -> WidgetNamespace:
        """Creates a `WidgetNamespace` from string data.

        To parse the data, we use `FileLoader.parse`. To implement custom formats,
        subclass `FileLoader` with your own `parse` implementation.

        Args:
            data: The data to parse.

        Returns:
            A WidgetNamespace created from the provided data.
        """

        parsed = self.parse(data)

        # Get & load config data
        config_data = parsed.get("config")
        if config_data is not None:
            namespace = WidgetNamespace.from_config(config_data, loader=self)
        else:
            namespace = WidgetNamespace.from_config({}, loader=self)

        # Create aliases
        for key, value in (parsed.get("markup") or {}).items():
            tim.alias(key, value)

        # Create boxes
        for name, inner in (parsed.get("boxes") or {}).items():
            self.serializer.register_box(name, widgets_m.boxes.Box(inner))

        # Create widgets
        for name, inner in (parsed.get("widgets") or {}).items():
            widget_type = inner.get("type") or name

            box_name = inner.get("box")

            box = None
            if box_name is not None and box_name in namespace.boxes:
                box = namespace.boxes[box_name]
                del inner["box"]

            try:
                namespace.widgets[name] = self.serializer.from_dict(
                    inner, widget_type=widget_type
                )
            except AttributeError as error:
                raise ValueError(
                    f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
                ) from error

            if box is not None:
                namespace.widgets[name].box = box

        return namespace

    def load(self, data: str | IO) -> WidgetNamespace:
        """Loads data from a string or a file.

        When an IO object is passed, its data is extracted as a string.
        This string can then be passed to `load_str`.

        Args:
            data: Either a string or file stream to load data from.

        Returns:
            A WidgetNamespace with the data loaded.
        """

        if not isinstance(data, str):
            data = data.read()

        assert isinstance(data, str)
        return self.load_str(data)

serializer = serializer instance-attribute

Object-specific serializer instance. In order to use a specific, already created instance you need to pass it on FileLoader construction.

__enter__()

Starts context manager.

Source code in pytermgui/file_loaders.py
287
288
289
290
def __enter__(self) -> FileLoader:
    """Starts context manager."""

    return self

__exit__(_, exception, __)

Ends context manager.

Source code in pytermgui/file_loaders.py
292
293
294
295
296
def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
    """Ends context manager."""

    if exception is not None:
        raise exception

__init__(serializer=None)

Initialize FileLoader.

Parameters:

Name Type Description Default
serializer Serializer | None

An optional pytermgui.serializer.Serializer instance. If not provided, one is instantiated for every FileLoader instance.

None
Source code in pytermgui/file_loaders.py
274
275
276
277
278
279
280
281
282
283
284
285
def __init__(self, serializer: Serializer | None = None) -> None:
    """Initialize FileLoader.

    Args:
        serializer: An optional `pytermgui.serializer.Serializer` instance. If not provided, one
            is instantiated for every FileLoader instance.
    """

    if serializer is None:
        serializer = Serializer()

    self.serializer = serializer

bind(name, method)

Binds a name to a method.

Parameters:

Name Type Description Default
name str

The name of the method, as referenced in the loaded files.

required
method Callable[..., Any]

The callable to bind.

required
Source code in pytermgui/file_loaders.py
307
308
309
310
311
312
313
314
315
316
def bind(self, name: str, method: Callable[..., Any]) -> None:
    """Binds a name to a method.

    Args:
        name: The name of the method, as referenced in the loaded
            files.
        method: The callable to bind.
    """

    self.serializer.bind(name, method)

load(data)

Loads data from a string or a file.

When an IO object is passed, its data is extracted as a string. This string can then be passed to load_str.

Parameters:

Name Type Description Default
data str | IO

Either a string or file stream to load data from.

required

Returns:

Type Description
WidgetNamespace

A WidgetNamespace with the data loaded.

Source code in pytermgui/file_loaders.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def load(self, data: str | IO) -> WidgetNamespace:
    """Loads data from a string or a file.

    When an IO object is passed, its data is extracted as a string.
    This string can then be passed to `load_str`.

    Args:
        data: Either a string or file stream to load data from.

    Returns:
        A WidgetNamespace with the data loaded.
    """

    if not isinstance(data, str):
        data = data.read()

    assert isinstance(data, str)
    return self.load_str(data)

load_str(data)

Creates a WidgetNamespace from string data.

To parse the data, we use FileLoader.parse. To implement custom formats, subclass FileLoader with your own parse implementation.

Parameters:

Name Type Description Default
data str

The data to parse.

required

Returns:

Type Description
WidgetNamespace

A WidgetNamespace created from the provided data.

Source code in pytermgui/file_loaders.py
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
def load_str(self, data: str) -> WidgetNamespace:
    """Creates a `WidgetNamespace` from string data.

    To parse the data, we use `FileLoader.parse`. To implement custom formats,
    subclass `FileLoader` with your own `parse` implementation.

    Args:
        data: The data to parse.

    Returns:
        A WidgetNamespace created from the provided data.
    """

    parsed = self.parse(data)

    # Get & load config data
    config_data = parsed.get("config")
    if config_data is not None:
        namespace = WidgetNamespace.from_config(config_data, loader=self)
    else:
        namespace = WidgetNamespace.from_config({}, loader=self)

    # Create aliases
    for key, value in (parsed.get("markup") or {}).items():
        tim.alias(key, value)

    # Create boxes
    for name, inner in (parsed.get("boxes") or {}).items():
        self.serializer.register_box(name, widgets_m.boxes.Box(inner))

    # Create widgets
    for name, inner in (parsed.get("widgets") or {}).items():
        widget_type = inner.get("type") or name

        box_name = inner.get("box")

        box = None
        if box_name is not None and box_name in namespace.boxes:
            box = namespace.boxes[box_name]
            del inner["box"]

        try:
            namespace.widgets[name] = self.serializer.from_dict(
                inner, widget_type=widget_type
            )
        except AttributeError as error:
            raise ValueError(
                f'Could not load "{name}" from data:\n{json.dumps(inner, indent=2)}'
            ) from error

        if box is not None:
            namespace.widgets[name].box = box

    return namespace

parse(data) abstractmethod

Parses string into a dictionary used by pytermgui.serializer.Serializer.

This dictionary follows the structure defined above.

Source code in pytermgui/file_loaders.py
267
268
269
270
271
272
@abstractmethod
def parse(self, data: str) -> dict[Any, Any]:
    """Parses string into a dictionary used by `pytermgui.serializer.Serializer`.

    This dictionary follows the structure defined above.
    """

register(cls)

Registers a widget to the serializer.

Parameters:

Name Type Description Default
cls Type[Widget]

The widget type to register.

required
Source code in pytermgui/file_loaders.py
298
299
300
301
302
303
304
305
def register(self, cls: Type[widgets_m.Widget]) -> None:
    """Registers a widget to the serializer.

    Args:
        cls: The widget type to register.
    """

    self.serializer.register(cls)

FloatAnimation dataclass

Bases: Animation

Transitions a floating point number from 0.0 to 1.0.

Note that this is just a wrapper over the base class, and provides no extra functionality.

Source code in pytermgui/animations.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
@dataclass
class FloatAnimation(Animation):
    """Transitions a floating point number from 0.0 to 1.0.

    Note that this is just a wrapper over the base class, and provides no extra
    functionality.
    """

    duration: int

    on_step: Callable[[Animation], bool] | None = None
    on_finish: Callable[[Animation], None] | None = None

    direction: Direction = Direction.FORWARD
    loop: bool = False

    state: float = field(init=False)
    _remaining: int = field(init=False)

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",
        "",
    ]

HEXColor dataclass

Bases: RGBColor

An arbitrary, CSS-like HEX color.

Source code in pytermgui/colors.py
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
@dataclass
class HEXColor(RGBColor):
    """An arbitrary, CSS-like HEX color."""

    system = ColorSystem.TRUE

    def __post_init__(self) -> None:
        """Ensures data validity."""

        data = self.value
        if data.startswith("#"):
            data = data[1:]

        indices = (0, 2), (2, 4), (4, 6)
        rgb = []
        for start, end in indices:
            value = data[start:end]
            rgb.append(int(value, base=16))

        self._rgb = rgb[0], rgb[1], rgb[2]

        assert len(self._rgb) == 3

__post_init__()

Ensures data validity.

Source code in pytermgui/colors.py
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
def __post_init__(self) -> None:
    """Ensures data validity."""

    data = self.value
    if data.startswith("#"):
        data = data[1:]

    indices = (0, 2), (2, 4), (4, 6)
    rgb = []
    for start, end in indices:
        value = data[start:end]
        rgb.append(int(value, base=16))

    self._rgb = rgb[0], rgb[1], rgb[2]

    assert len(self._rgb) == 3

HLinkToken dataclass

Bases: Token

A terminal hyperlink.

See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda.

Source code in pytermgui/markup/tokens.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
@dataclass(frozen=True, repr=False)
class HLinkToken(Token):
    """A terminal hyperlink.

    See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda.
    """

    __slots__ = ("value",)

    value: str

    @cached_property
    def markup(self) -> str:
        return f"~{self.value}"

    @cached_property
    def prettified_markup(self) -> str:
        return f"[{self.markup}]~[blue underline]{self.value}[/fg /underline /~]"

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 ┃",
        "┗━━━┛",
    ]

Highlighter

Bases: Protocol

The protocol for highlighters.

Source code in pytermgui/highlighters.py
26
27
28
29
30
31
32
33
34
35
36
class Highlighter(Protocol):  # pylint: disable=too-few-public-methods
    """The protocol for highlighters."""

    def __call__(self, text: str, cache: bool = True) -> str:
        """Highlights the given text.

        Args:
            text: The text to highlight.
            cache: If set (default), results will be stored, keyed by their respective
                inputs, and retrieved the next time the same key is given.
        """

__call__(text, cache=True)

Highlights the given text.

Parameters:

Name Type Description Default
text str

The text to highlight.

required
cache bool

If set (default), results will be stored, keyed by their respective inputs, and retrieved the next time the same key is given.

True
Source code in pytermgui/highlighters.py
29
30
31
32
33
34
35
36
def __call__(self, text: str, cache: bool = True) -> str:
    """Highlights the given text.

    Args:
        text: The text to highlight.
        cache: If set (default), results will be stored, keyed by their respective
            inputs, and retrieved the next time the same key is given.
    """

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.

IndexedColor dataclass

Bases: Color

A color representing an index into the xterm-256 color palette.

Source code in pytermgui/colors.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
@dataclass(repr=False)
class IndexedColor(Color):
    """A color representing an index into the xterm-256 color palette."""

    system = ColorSystem.EIGHT_BIT

    def __post_init__(self) -> None:
        """Ensures data validity."""

        if not self.value.isdigit():
            raise ValueError(
                f"IndexedColor value has to be numerical, got {self.value!r}."
            )

        if not 0 <= int(self.value) < 256:
            raise ValueError(
                f"IndexedColor value has to fit in range 0-255, got {self.value!r}."
            )

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Yields a fancy looking string."""

        yield f"<{type(self).__name__} value: {self.value}, preview: "

        yield {"text": f"{self:seq}{PREVIEW_CHAR}\x1b[0m", "highlight": False}

        yield ">"

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> IndexedColor:
        """Constructs an `IndexedColor` from the closest matching option."""

        if rgb in _COLOR_MATCH_CACHE:
            color = _COLOR_MATCH_CACHE[rgb]

            assert isinstance(color, IndexedColor)
            return color

        if terminal.colorsystem == ColorSystem.STANDARD:
            return StandardColor.from_rgb(rgb)

        # Normalize the color values
        red, green, blue = (x / 255 for x in rgb)

        # Calculate the eight-bit color index
        color_num = 16
        color_num += 36 * round(red * 5.0)
        color_num += 6 * round(green * 5.0)
        color_num += round(blue * 5.0)

        color = cls(str(color_num))
        _COLOR_MATCH_CACHE[rgb] = color

        return color

    @property
    def sequence(self) -> str:
        r"""Returns an ANSI sequence representing this color."""

        index = int(self.value)

        return "\x1b[" + ("48" if self.background else "38") + f";5;{index}m"

    @cached_property
    def rgb(self) -> RGBTriplet:
        """Returns an RGB representation of this color."""

        if self._rgb is not None:
            return self._rgb

        index = int(self.value)
        rgb = COLOR_TABLE[index]

        return (rgb[0], rgb[1], rgb[2])

rgb cached property

Returns an RGB representation of this color.

sequence property

Returns an ANSI sequence representing this color.

__fancy_repr__()

Yields a fancy looking string.

Source code in pytermgui/colors.py
540
541
542
543
544
545
546
547
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Yields a fancy looking string."""

    yield f"<{type(self).__name__} value: {self.value}, preview: "

    yield {"text": f"{self:seq}{PREVIEW_CHAR}\x1b[0m", "highlight": False}

    yield ">"

__post_init__()

Ensures data validity.

Source code in pytermgui/colors.py
527
528
529
530
531
532
533
534
535
536
537
538
def __post_init__(self) -> None:
    """Ensures data validity."""

    if not self.value.isdigit():
        raise ValueError(
            f"IndexedColor value has to be numerical, got {self.value!r}."
        )

    if not 0 <= int(self.value) < 256:
        raise ValueError(
            f"IndexedColor value has to fit in range 0-255, got {self.value!r}."
        )

from_rgb(rgb) classmethod

Constructs an IndexedColor from the closest matching option.

Source code in pytermgui/colors.py
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
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> IndexedColor:
    """Constructs an `IndexedColor` from the closest matching option."""

    if rgb in _COLOR_MATCH_CACHE:
        color = _COLOR_MATCH_CACHE[rgb]

        assert isinstance(color, IndexedColor)
        return color

    if terminal.colorsystem == ColorSystem.STANDARD:
        return StandardColor.from_rgb(rgb)

    # Normalize the color values
    red, green, blue = (x / 255 for x in rgb)

    # Calculate the eight-bit color index
    color_num = 16
    color_num += 36 * round(red * 5.0)
    color_num += 6 * round(green * 5.0)
    color_num += round(blue * 5.0)

    color = cls(str(color_num))
    _COLOR_MATCH_CACHE[rgb] = color

    return color

InputField

Bases: Widget

An element to display user input

Source code in pytermgui/widgets/input_field.py
 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
class InputField(Widget):  # pylint: disable=too-many-instance-attributes
    """An element to display user input"""

    styles = w_styles.StyleManager(
        value="",
        prompt="surface+2",
        cursor="@primary dim #auto",
    )

    keys = {
        "move_left": {keys.LEFT},
        "move_right": {keys.RIGHT},
        "move_word_left": {keys.ALT_LEFT, keys.CTRL_LEFT},
        "move_word_right": {keys.ALT_RIGHT, keys.CTRL_RIGHT},
        "move_up": {keys.UP},
        "move_down": {keys.DOWN},
        "move_end": {keys.END},
        "move_home": {keys.HOME},
        "select_left": {keys.SHIFT_LEFT},
        "select_right": {keys.SHIFT_RIGHT},
        "select_up": {keys.SHIFT_UP},
        "select_down": {keys.SHIFT_DOWN},
        "word_remove": {keys.ALT_BACKSPACE, keys.CTRL_BACKSPACE},
    }

    parent_align = HorizontalAlignment.LEFT

    def __init__( # pylint: disable=too-many-arguments
        self,
        value: str = "",
        *,
        prompt: str = "",
        tablength: int = 4,
        multiline: bool = False,
        cursor: Cursor | None = None,
        **attrs: Any,
    ) -> None:
        """Initialize object"""

        super().__init__(**attrs)

        if "width" not in attrs:
            self.width = len(value)

        if any(wcwidth(char) > 1 for char in value):
            raise ValueError("InputField doesn't support wide unicode characters.")

        self.prompt = prompt
        self.height = 1
        self.tablength = tablength
        self.multiline = multiline

        self._lines = value.splitlines() or [""]
        self.cursor = cursor or Cursor(len(self._lines) - 1, len(self._lines[-1]))
        self._selection_length = 1

        self._styled_cache: list[str] | None = self._style_and_break_lines()

        self._cached_state: int = self.width
        self._drag_start: tuple[int, int] | None = None

    @property
    def selectables_length(self) -> int:
        """Get length of selectables in object"""

        return 1

    @property
    def value(self) -> str:
        """Returns the internal value of this field."""

        return "\n".join(self._lines)

    @property
    def selection(self) -> str:
        """Returns the currently selected span of text."""

        start, end = sorted([self.cursor.col, self.cursor.col + self._selection_length])
        return self._lines[self.cursor.row][start:end]

    def _cache_is_valid(self) -> bool:
        """Determines if the styled line cache is still usable."""

        return self.width == self._cached_state

    def _style_and_break_lines(self) -> list[str]:
        """Styles and breaks self._lines."""

        document = (
            self.styles.prompt(self.prompt) + self.styles.value(self.value)
        ).splitlines()

        lines: list[str] = []
        width = self.width
        extend = lines.extend

        for line in document:
            extend(break_line(line.replace("\n", "\\n"), width, fill=" "))
            extend("")

        return lines

    def update_selection(self, count: int, correct_zero_length: bool = True) -> None:
        """Updates the selection state.

        Args:
            count: How many characters the cursor should change by. Negative for
                selecting leftward, positive for right.
            correct_zero_length: If set, when the selection length is 0 both the cursor
                and the selection length are manipulated to keep the original selection
                start while moving the selection in more of the way the user might
                expect.
        """

        self._selection_length += count

        if correct_zero_length and abs(self._selection_length) == 0:
            self._selection_length += 2 if count > 0 else -2
            self.move_cursor((0, (-1 if count > 0 else 1)))

    def delete_back(self, count: int = 1) -> str:
        """Deletes `count` characters from the cursor, backwards.

        Args:
            count: How many characters should be deleted.

        Returns:
            The deleted string.
        """

        row, col = self.cursor

        if len(self._lines) <= row:
            return ""

        line = self._lines[row]

        start, end = sorted([col, col - count])
        start = max(0, start)
        self._lines[row] = line[:start] + line[end:]

        self._styled_cache = None

        if self._lines[row] == "":
            self.move_cursor((0, -2))

            return self._lines.pop(row)

        if count > 0:
            self.move_cursor((0, -count))

        return line[col - count : col]

    def insert_text(self, text: str) -> None:
        """Inserts text at the cursor location."""

        row, col = self.cursor

        if len(self._lines) <= row:
            self._lines.insert(row, "")

        line = self._lines[row]

        self._lines[row] = line[:col] + text + line[col:]
        self.move_cursor((0, len(text)))

        self._styled_cache = None

    def get_word_pos(self, direction: Literal[-1, 1]) -> int:
        """Gets the column offset to the next word in the given direction.

        Args:
            direction: Which direction we need to look for.

        Returns:
            The column offset.
        """

        row, col = self.cursor
        if len(self._lines) <= row:
            return direction

        # Consistent with unix shell behaviour:
        # * Always delete first char, then remove any non-punctuation
        # Note that the exact behaviour isn't standardized:
        # * Python repl: until change in letter+digit & punctionation
        # * Unix shells: only removes letter+digit
        word_chars = string.ascii_letters + string.digits

        if direction == -1:
            line = self._lines[row][: col - 1]
            strip_line = line.rstrip(word_chars)

        else:
            line = self._lines[row][col:]
            strip_line = line.lstrip(word_chars)

        return -direction * (len(strip_line) - len(line)) + direction

    def handle_action(self, action: str) -> bool:
        """Handles some action.

        This will be expanded in the future to allow using all behaviours with
        just their actions.
        """

        cursors = {
            "move_left": (0, -1),
            "move_right": (0, 1),
            "move_up": (-1, 0),
            "move_down": (1, 0),
        }

        if action.startswith("move_"):
            if action.endswith(("word_left", "word_right")):
                col = self.get_word_pos(-1 if action == "move_word_left" else 1)
                self.move_cursor((0, col))
                return True

            if action.endswith(("end", "home")):
                crow, ccol = self.cursor
                if action == "move_end":
                    ccol = len(self._lines[crow])
                else:
                    ccol = 0
                self.move_cursor((crow, ccol), absolute=True)
                return True

            row, col = cursors[action]

            if self.cursor.row + row > len(self._lines):
                self._lines.append("")

            col += self._selection_length
            if self._selection_length > 0:
                col -= 1

            self._selection_length = 1
            self.move_cursor((row, col))
            return True

        if action.startswith("select_"):
            if action == "select_right":
                self.update_selection(1)

            elif action == "select_left":
                self.update_selection(-1)

            return True

        if action == "word_remove":
            row, col = self.cursor
            self.delete_back(-self.get_word_pos(-1))
            return True

        return False

    # TODO: This could probably be simplified by a wider adoption of the action pattern.
    def handle_key(  # pylint: disable=too-many-return-statements, too-many-branches
        self, key: str
    ) -> bool:
        """Adds text to the field, or moves the cursor."""

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

        for name, options in self.keys.items():
            if (
                name.rsplit("_", maxsplit=1)[-1] in ("up", "down")
                and not self.multiline
            ):
                continue

            if key in options:
                return self.handle_action(name)

        if key == keys.TAB:
            if not self.multiline:
                return False

            for _ in range(self.tablength):
                self.handle_key(" ")

            return True

        if key in string.printable and key not in "\x0c\x0b":
            if key == keys.ENTER:
                if not self.multiline:
                    return False

                if len(self._lines) <= self.cursor.row:
                    self._lines.append("")

                line = self._lines[self.cursor.row]
                left, right = line[: self.cursor.col], line[self.cursor.col :]

                self._lines[self.cursor.row] = left
                self._lines.insert(self.cursor.row + 1, right)

                self.move_cursor((1, -self.cursor.col))
                self._styled_cache = None

            else:
                self.insert_text(key)

            if keys.ANY_KEY in self._bindings:
                method, _ = self._bindings[keys.ANY_KEY]
                method(self, key)

            return True

        if key == keys.BACKSPACE:
            if self._selection_length == 1:
                self.delete_back(1)
            else:
                self.delete_back(-self._selection_length)

            self._selection_length = 1
            self._styled_cache = None

            return True

        if len(key) > 1 and not key.startswith("\x1b["):
            for char in key:
                self.handle_key(char)

            return True

        return False

    def handle_mouse(self, event: MouseEvent) -> bool:
        """Allows point-and-click selection."""

        x_offset = event.position[0] - self.pos[0]
        y_offset = event.position[1] - self.pos[1]

        if y_offset == 0:
            x_offset -= len(self.prompt)

            if x_offset < 0:
                return False

        # Set cursor to mouse location
        if event.action is MouseAction.LEFT_CLICK:
            if not y_offset < len(self._lines):
                return False

            line = self._lines[y_offset]

            if y_offset == 0:
                line = self.prompt + line

            self.move_cursor((y_offset, min(len(line), x_offset)), absolute=True)

            self._drag_start = (x_offset, y_offset)
            self._selection_length = 1

            return True

        # Select text using dragging the mouse
        if event.action is MouseAction.LEFT_DRAG and self._drag_start is not None:
            change = x_offset - self._drag_start[0]
            self.update_selection(
                change - self._selection_length + 1, correct_zero_length=False
            )

            return True

        return super().handle_mouse(event)

    def move_cursor(self, new: tuple[int, int], *, absolute: bool = False) -> None:
        """Moves the cursor, then possible re-positions it to a valid location.

        Args:
            new: The new set of (y, x) positions to use.
            absolute: If set, `new` will be interpreted as absolute coordinates,
                instead of being added on top of the current ones.
        """

        if len(self._lines) == 0:
            return

        if absolute:
            new_y, new_x = new
            self.cursor.row = new_y
            self.cursor.col = new_x

        else:
            self.cursor += new

        self.cursor.row = max(0, min(self.cursor.row, len(self._lines) - 1))
        row, col = self.cursor

        line = self._lines[row]
        width = len(line)

        # Going left, possibly upwards
        if col < 0:
            if row <= 0:
                self.cursor.col = 0

            else:
                self.cursor.row -= 1
                line = self._lines[self.cursor.row]
                self.cursor.col = width

        # Going right, possibly downwards
        elif col > width and line != "":
            if len(self._lines) > row + 1:
                self.cursor.row += 1
                self.cursor.col = 0

            line = self._lines[self.cursor.row]

        self.cursor.col = max(0, min(self.cursor.col, width))

    def get_lines(self) -> list[str]:
        """Builds the input field's lines."""

        if not self._cache_is_valid() or self._styled_cache is None:
            self._styled_cache = self._style_and_break_lines()

        lines = self._styled_cache

        row, col = self.cursor

        if len(self._lines) == 0:
            line = " "
        else:
            line = self._lines[row]

        start = col
        cursor_char = " "
        if len(line) > col:
            start = col
            end = col + self._selection_length
            start, end = sorted([start, end])

            try:
                cursor_char = line[start:end]
            except IndexError as error:
                raise ValueError(f"Invalid index in {line!r}: {col}") from error

        style_cursor = (
            self.styles.value if self.selected_index is None else self.styles.cursor
        )

        # TODO: This is horribly hackish, but is the only way to "get around" the
        #       limits of the current scrolling techniques. Should be refactored
        #       once a better solution is available
        if self.parent is not None and self.selected_index is not None:
            offset = 0
            parent = self.parent
            while hasattr(parent, "parent"):
                offset += getattr(parent, "_scroll_offset")

                parent = parent.parent  # type: ignore

            offset_row = -offset + row
            offset_col = start + (len(self.prompt) if row == 0 else 0)

            if offset_col > self.width - 1:
                offset_col -= self.width
                offset_row += 1
                row += 1

                if row >= len(lines):
                    lines.append(self.styles.value(""))

            position = (
                self.pos[0] + offset_col,
                self.pos[1] + offset_row,
            )

            self.positioned_line_buffer.append(
                (position, style_cursor(cursor_char))  # type: ignore
            )

        lines = lines or [""]
        self.height = len(lines)

        return lines

selectables_length property

Get length of selectables in object

selection property

Returns the currently selected span of text.

value property

Returns the internal value of this field.

__init__(value='', *, prompt='', tablength=4, multiline=False, cursor=None, **attrs)

Initialize object

Source code in pytermgui/widgets/input_field.py
 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
def __init__( # pylint: disable=too-many-arguments
    self,
    value: str = "",
    *,
    prompt: str = "",
    tablength: int = 4,
    multiline: bool = False,
    cursor: Cursor | None = None,
    **attrs: Any,
) -> None:
    """Initialize object"""

    super().__init__(**attrs)

    if "width" not in attrs:
        self.width = len(value)

    if any(wcwidth(char) > 1 for char in value):
        raise ValueError("InputField doesn't support wide unicode characters.")

    self.prompt = prompt
    self.height = 1
    self.tablength = tablength
    self.multiline = multiline

    self._lines = value.splitlines() or [""]
    self.cursor = cursor or Cursor(len(self._lines) - 1, len(self._lines[-1]))
    self._selection_length = 1

    self._styled_cache: list[str] | None = self._style_and_break_lines()

    self._cached_state: int = self.width
    self._drag_start: tuple[int, int] | None = None

delete_back(count=1)

Deletes count characters from the cursor, backwards.

Parameters:

Name Type Description Default
count int

How many characters should be deleted.

1

Returns:

Type Description
str

The deleted string.

Source code in pytermgui/widgets/input_field.py
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
def delete_back(self, count: int = 1) -> str:
    """Deletes `count` characters from the cursor, backwards.

    Args:
        count: How many characters should be deleted.

    Returns:
        The deleted string.
    """

    row, col = self.cursor

    if len(self._lines) <= row:
        return ""

    line = self._lines[row]

    start, end = sorted([col, col - count])
    start = max(0, start)
    self._lines[row] = line[:start] + line[end:]

    self._styled_cache = None

    if self._lines[row] == "":
        self.move_cursor((0, -2))

        return self._lines.pop(row)

    if count > 0:
        self.move_cursor((0, -count))

    return line[col - count : col]

get_lines()

Builds the input field's lines.

Source code in pytermgui/widgets/input_field.py
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
def get_lines(self) -> list[str]:
    """Builds the input field's lines."""

    if not self._cache_is_valid() or self._styled_cache is None:
        self._styled_cache = self._style_and_break_lines()

    lines = self._styled_cache

    row, col = self.cursor

    if len(self._lines) == 0:
        line = " "
    else:
        line = self._lines[row]

    start = col
    cursor_char = " "
    if len(line) > col:
        start = col
        end = col + self._selection_length
        start, end = sorted([start, end])

        try:
            cursor_char = line[start:end]
        except IndexError as error:
            raise ValueError(f"Invalid index in {line!r}: {col}") from error

    style_cursor = (
        self.styles.value if self.selected_index is None else self.styles.cursor
    )

    # TODO: This is horribly hackish, but is the only way to "get around" the
    #       limits of the current scrolling techniques. Should be refactored
    #       once a better solution is available
    if self.parent is not None and self.selected_index is not None:
        offset = 0
        parent = self.parent
        while hasattr(parent, "parent"):
            offset += getattr(parent, "_scroll_offset")

            parent = parent.parent  # type: ignore

        offset_row = -offset + row
        offset_col = start + (len(self.prompt) if row == 0 else 0)

        if offset_col > self.width - 1:
            offset_col -= self.width
            offset_row += 1
            row += 1

            if row >= len(lines):
                lines.append(self.styles.value(""))

        position = (
            self.pos[0] + offset_col,
            self.pos[1] + offset_row,
        )

        self.positioned_line_buffer.append(
            (position, style_cursor(cursor_char))  # type: ignore
        )

    lines = lines or [""]
    self.height = len(lines)

    return lines

get_word_pos(direction)

Gets the column offset to the next word in the given direction.

Parameters:

Name Type Description Default
direction Literal[-1, 1]

Which direction we need to look for.

required

Returns:

Type Description
int

The column offset.

Source code in pytermgui/widgets/input_field.py
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
def get_word_pos(self, direction: Literal[-1, 1]) -> int:
    """Gets the column offset to the next word in the given direction.

    Args:
        direction: Which direction we need to look for.

    Returns:
        The column offset.
    """

    row, col = self.cursor
    if len(self._lines) <= row:
        return direction

    # Consistent with unix shell behaviour:
    # * Always delete first char, then remove any non-punctuation
    # Note that the exact behaviour isn't standardized:
    # * Python repl: until change in letter+digit & punctionation
    # * Unix shells: only removes letter+digit
    word_chars = string.ascii_letters + string.digits

    if direction == -1:
        line = self._lines[row][: col - 1]
        strip_line = line.rstrip(word_chars)

    else:
        line = self._lines[row][col:]
        strip_line = line.lstrip(word_chars)

    return -direction * (len(strip_line) - len(line)) + direction

handle_action(action)

Handles some action.

This will be expanded in the future to allow using all behaviours with just their actions.

Source code in pytermgui/widgets/input_field.py
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
def handle_action(self, action: str) -> bool:
    """Handles some action.

    This will be expanded in the future to allow using all behaviours with
    just their actions.
    """

    cursors = {
        "move_left": (0, -1),
        "move_right": (0, 1),
        "move_up": (-1, 0),
        "move_down": (1, 0),
    }

    if action.startswith("move_"):
        if action.endswith(("word_left", "word_right")):
            col = self.get_word_pos(-1 if action == "move_word_left" else 1)
            self.move_cursor((0, col))
            return True

        if action.endswith(("end", "home")):
            crow, ccol = self.cursor
            if action == "move_end":
                ccol = len(self._lines[crow])
            else:
                ccol = 0
            self.move_cursor((crow, ccol), absolute=True)
            return True

        row, col = cursors[action]

        if self.cursor.row + row > len(self._lines):
            self._lines.append("")

        col += self._selection_length
        if self._selection_length > 0:
            col -= 1

        self._selection_length = 1
        self.move_cursor((row, col))
        return True

    if action.startswith("select_"):
        if action == "select_right":
            self.update_selection(1)

        elif action == "select_left":
            self.update_selection(-1)

        return True

    if action == "word_remove":
        row, col = self.cursor
        self.delete_back(-self.get_word_pos(-1))
        return True

    return False

handle_key(key)

Adds text to the field, or moves the cursor.

Source code in pytermgui/widgets/input_field.py
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
def handle_key(  # pylint: disable=too-many-return-statements, too-many-branches
    self, key: str
) -> bool:
    """Adds text to the field, or moves the cursor."""

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

    for name, options in self.keys.items():
        if (
            name.rsplit("_", maxsplit=1)[-1] in ("up", "down")
            and not self.multiline
        ):
            continue

        if key in options:
            return self.handle_action(name)

    if key == keys.TAB:
        if not self.multiline:
            return False

        for _ in range(self.tablength):
            self.handle_key(" ")

        return True

    if key in string.printable and key not in "\x0c\x0b":
        if key == keys.ENTER:
            if not self.multiline:
                return False

            if len(self._lines) <= self.cursor.row:
                self._lines.append("")

            line = self._lines[self.cursor.row]
            left, right = line[: self.cursor.col], line[self.cursor.col :]

            self._lines[self.cursor.row] = left
            self._lines.insert(self.cursor.row + 1, right)

            self.move_cursor((1, -self.cursor.col))
            self._styled_cache = None

        else:
            self.insert_text(key)

        if keys.ANY_KEY in self._bindings:
            method, _ = self._bindings[keys.ANY_KEY]
            method(self, key)

        return True

    if key == keys.BACKSPACE:
        if self._selection_length == 1:
            self.delete_back(1)
        else:
            self.delete_back(-self._selection_length)

        self._selection_length = 1
        self._styled_cache = None

        return True

    if len(key) > 1 and not key.startswith("\x1b["):
        for char in key:
            self.handle_key(char)

        return True

    return False

handle_mouse(event)

Allows point-and-click selection.

Source code in pytermgui/widgets/input_field.py
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
def handle_mouse(self, event: MouseEvent) -> bool:
    """Allows point-and-click selection."""

    x_offset = event.position[0] - self.pos[0]
    y_offset = event.position[1] - self.pos[1]

    if y_offset == 0:
        x_offset -= len(self.prompt)

        if x_offset < 0:
            return False

    # Set cursor to mouse location
    if event.action is MouseAction.LEFT_CLICK:
        if not y_offset < len(self._lines):
            return False

        line = self._lines[y_offset]

        if y_offset == 0:
            line = self.prompt + line

        self.move_cursor((y_offset, min(len(line), x_offset)), absolute=True)

        self._drag_start = (x_offset, y_offset)
        self._selection_length = 1

        return True

    # Select text using dragging the mouse
    if event.action is MouseAction.LEFT_DRAG and self._drag_start is not None:
        change = x_offset - self._drag_start[0]
        self.update_selection(
            change - self._selection_length + 1, correct_zero_length=False
        )

        return True

    return super().handle_mouse(event)

insert_text(text)

Inserts text at the cursor location.

Source code in pytermgui/widgets/input_field.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def insert_text(self, text: str) -> None:
    """Inserts text at the cursor location."""

    row, col = self.cursor

    if len(self._lines) <= row:
        self._lines.insert(row, "")

    line = self._lines[row]

    self._lines[row] = line[:col] + text + line[col:]
    self.move_cursor((0, len(text)))

    self._styled_cache = None

move_cursor(new, *, absolute=False)

Moves the cursor, then possible re-positions it to a valid location.

Parameters:

Name Type Description Default
new tuple[int, int]

The new set of (y, x) positions to use.

required
absolute bool

If set, new will be interpreted as absolute coordinates, instead of being added on top of the current ones.

False
Source code in pytermgui/widgets/input_field.py
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
def move_cursor(self, new: tuple[int, int], *, absolute: bool = False) -> None:
    """Moves the cursor, then possible re-positions it to a valid location.

    Args:
        new: The new set of (y, x) positions to use.
        absolute: If set, `new` will be interpreted as absolute coordinates,
            instead of being added on top of the current ones.
    """

    if len(self._lines) == 0:
        return

    if absolute:
        new_y, new_x = new
        self.cursor.row = new_y
        self.cursor.col = new_x

    else:
        self.cursor += new

    self.cursor.row = max(0, min(self.cursor.row, len(self._lines) - 1))
    row, col = self.cursor

    line = self._lines[row]
    width = len(line)

    # Going left, possibly upwards
    if col < 0:
        if row <= 0:
            self.cursor.col = 0

        else:
            self.cursor.row -= 1
            line = self._lines[self.cursor.row]
            self.cursor.col = width

    # Going right, possibly downwards
    elif col > width and line != "":
        if len(self._lines) > row + 1:
            self.cursor.row += 1
            self.cursor.col = 0

        line = self._lines[self.cursor.row]

    self.cursor.col = max(0, min(self.cursor.col, width))

update_selection(count, correct_zero_length=True)

Updates the selection state.

Parameters:

Name Type Description Default
count int

How many characters the cursor should change by. Negative for selecting leftward, positive for right.

required
correct_zero_length bool

If set, when the selection length is 0 both the cursor and the selection length are manipulated to keep the original selection start while moving the selection in more of the way the user might expect.

True
Source code in pytermgui/widgets/input_field.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def update_selection(self, count: int, correct_zero_length: bool = True) -> None:
    """Updates the selection state.

    Args:
        count: How many characters the cursor should change by. Negative for
            selecting leftward, positive for right.
        correct_zero_length: If set, when the selection length is 0 both the cursor
            and the selection length are manipulated to keep the original selection
            start while moving the selection in more of the way the user might
            expect.
    """

    self._selection_length += count

    if correct_zero_length and abs(self._selection_length) == 0:
        self._selection_length += 2 if count > 0 else -2
        self.move_cursor((0, (-1 if count > 0 else 1)))

Inspector

Bases: Container

A widget to inspect any Python object.

Source code in pytermgui/inspector.py
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
class Inspector(Container):
    """A widget to inspect any Python object."""

    def __init__(  # pylint: disable=too-many-arguments, R0917
        self,
        target: object = None,
        show_private: bool = False,
        show_dunder: bool = False,
        show_methods: bool = False,
        show_full_doc: bool = False,
        show_qualname: bool = True,
        show_header: bool = True,
        **attrs: Any,
    ):
        """Initializes an inspector.

        Note that most of the time, using `inspect` to do this is going to be more
        useful.

        Some styles of the inspector can be changed using the `code.name`,
        `code.file` and `code.keyword` markup aliases. The rest of the
        highlighting is done using `pprint`, with all of its respective colors.

        Args:
            show_private: Whether `_private` attributes should be shown.
            show_dunder: Whether `__dunder__` attributes should be shown.
            show_methods: Whether methods should be shown when encountering a class.
            show_full_doc: If not set, docstrings are cut to only include their first
                line.
            show_qualname: Show fully-qualified name, e.g. `module.submodule.name`
                instead of `name`.
            show_header: If not set, the header containing the path to the object and
                its qualname will not be added.
        """

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

        super().__init__(**attrs)

        self.width = self.terminal.width

        self.show_private = show_private
        self.show_dunder = show_dunder
        self.show_methods = show_methods
        self.show_full_doc = show_full_doc
        self.show_qualname = show_qualname
        self.show_header = show_header

        # TODO: Fix attr-showing
        self.show_attrs = False

        self.target: object
        if target is not None:
            self.inspect(target)
            self.target = target

    def _get_header(self) -> Container:
        """Creates a header containing the name and location of the object."""

        header = Container(box="SINGLE")

        line = "[code.name]"
        if self.target_type is ObjectType.MODULE:
            line += self.target.__name__  # type: ignore

        else:
            cls = (
                self.target
                if isclass(self.target) or isfunction(self.target)
                else self.target.__class__
            )
            line += cls.__module__ + "." + cls.__qualname__  # type: ignore

        header += line

        try:
            file = getfile(self.target)  # type: ignore
        except TypeError:
            return header

        header += f"Located in [code.file ~file://{file}]{file}[/]"

        return header

    def _get_definition(self) -> Label:
        """Returns the definition str of self.target."""

        target = self.target

        if self.show_qualname:
            name = getattr(target, "__qualname__", type(target).__name__)
        else:
            name = getattr(target, "__name__", type(target).__name__)

        if self.target_type == ObjectType.LIVE:
            target = type(target)

        otype = _determine_type(target)

        keyword = ""
        if otype == ObjectType.CLASS:
            keyword = "class "

        elif otype == ObjectType.FUNCTION:
            keyword = "def "

        try:
            assert callable(target)
            definition = self.highlight(keyword + name + str(signature(target)) + ":")

        except (TypeError, ValueError, AssertionError):
            definition = self.highlight(keyword + name + "(...)")

        return Label(definition, parent_align=0, non_first_padding=4)

    def _get_docs(self, padding: int) -> Label:
        """Returns a list of Labels of the object's documentation."""

        default = Label("...", style="102")
        if self.target.__doc__ is None:
            return default

        doc = getdoc(self.target)

        if doc is None:
            return default

        lines = doc.splitlines()
        if not self.show_full_doc and len(lines) > 0:
            lines = [lines[0]]

        trimmed = "\n".join(lines)

        return Label(
            trimmed.replace("[", r"\["),
            style="102",
            parent_align=0,
            padding=padding,
        )

    def _get_keys(self) -> list[str]:
        """Gets all inspectable keys of an object.

        It first checks for an `__all__` attribute, and substitutes `dir` if not found.
        Then, if there are too many keys and the given target is a module it tries to
        list all of the present submodules.
        """

        keys = getattr(self.target, "__all__", dir(self.target))

        if not self.show_dunder:
            keys = [key for key in keys if not key.startswith("__")]

        if not self.show_private:
            keys = [key for key in keys if not (key.startswith("_") and key[1] != "_")]

        if not self.show_methods:
            keys = [
                key for key in keys if not callable(getattr(self.target, key, None))
            ]

        keys.sort(key=lambda item: callable(getattr(self.target, item, None)))

        return keys

    def _get_preview(self) -> Container:
        """Gets a Container with self.target inside."""

        preview = Container(static_width=self.width // 2, parent_align=0, box="SINGLE")

        if isinstance(self.target, str) and RE_MARKUP.match(self.target) is not None:
            preview += Label(prettify(self.target, parse=False), parent_align=0)
            return preview

        for line in prettify(self.target).splitlines():

            if real_length(line) > preview.width - preview.sidelength:
                preview.width = real_length(line) + preview.sidelength

            preview += Label(tim.get_markup(line), parent_align=0)

        preview.width = min(preview.width, self.terminal.width - preview.sidelength)
        return preview

    @staticmethod
    def highlight(text: str) -> str:
        """Applies highlighting to a given string.

        This highlight includes keywords, builtin types and more.

        Args:
            text: The string to highlight.

        Returns:
            Unparsed markup.
        """

        def _split(text: str, chars: str = " ,:|()[]{}") -> list[tuple[str, str]]:
            """Splits given text by the given chars.

            Args:
                text: The text to split.
                chars: A string of characters we will split by.

            Returns:
                A tuple of (delimiter, word) tuples. Delimiter is one of the characters
                of `chars`.
            """

            last_delim = ""
            output = []
            word = ""
            for char in text:
                if char in chars:
                    output.append((last_delim, word))
                    last_delim = char
                    word = ""
                    continue

                word += char

            output.append((last_delim, word))
            return output

        buff = ""
        for (delim, word) in _split(text):
            stripped = word.strip("'")
            highlighted = highlight_python(stripped)

            if highlighted != stripped:
                buff += delim + stripped
                continue

            buff += delim + stripped

        return highlight_python(buff)

    def inspect(self, target: object) -> Inspector:
        """Inspects a given object, and sets self.target to it.

        Returns:
            Self, with the new content based on the inspection.
        """

        self.target = target
        self.target_type = _determine_type(target)

        # Header
        if self.show_header and self.box is not INDENTED_EMPTY_BOX:
            self.lazy_add(self._get_header())

        # Body
        if self.target_type is not ObjectType.MODULE:
            self.lazy_add(self._get_definition())

        padding = 0 if self.target_type is ObjectType.MODULE else 4

        self.lazy_add(self._get_docs(padding))

        keys = self._get_keys()

        for key in keys:
            attr = getattr(target, key, None)

            # Don't show type aliases
            if _is_type_alias(attr):
                continue

            # Only show functions if they are not lambdas
            if (isfunction(attr) or callable(attr)) and (
                hasattr(attr, "__name__") and not attr.__name__ == "<lambda>"
            ):
                self.lazy_add(
                    Inspector(
                        box=INDENTED_EMPTY_BOX,
                        show_dunder=self.show_dunder,
                        show_private=self.show_private,
                        show_full_doc=False,
                        show_qualname=self.show_qualname,
                    ).inspect(attr)
                )
                continue

            if not self.show_attrs:
                continue

            for i, line in enumerate(prettify(attr, parse=False).splitlines()):
                if i == 0:
                    line = f"- {key}: {line}"

                self.lazy_add(Label(line, parent_align=0))

        # Footer
        if self.target_type in [ObjectType.LIVE, ObjectType.BUILTIN]:
            self.lazy_add(self._get_preview())

        return self

    def debug(self) -> str:
        """Returns identifiable information used in repr."""

        if self.terminal.is_interactive and not self.terminal.displayhook_installed:
            return "\n".join(self.get_lines())

        return Widget.debug(self)

__init__(target=None, show_private=False, show_dunder=False, show_methods=False, show_full_doc=False, show_qualname=True, show_header=True, **attrs)

Initializes an inspector.

Note that most of the time, using inspect to do this is going to be more useful.

Some styles of the inspector can be changed using the code.name, code.file and code.keyword markup aliases. The rest of the highlighting is done using pprint, with all of its respective colors.

Parameters:

Name Type Description Default
show_private bool

Whether _private attributes should be shown.

False
show_dunder bool

Whether __dunder__ attributes should be shown.

False
show_methods bool

Whether methods should be shown when encountering a class.

False
show_full_doc bool

If not set, docstrings are cut to only include their first line.

False
show_qualname bool

Show fully-qualified name, e.g. module.submodule.name instead of name.

True
show_header bool

If not set, the header containing the path to the object and its qualname will not be added.

True
Source code in pytermgui/inspector.py
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
def __init__(  # pylint: disable=too-many-arguments, R0917
    self,
    target: object = None,
    show_private: bool = False,
    show_dunder: bool = False,
    show_methods: bool = False,
    show_full_doc: bool = False,
    show_qualname: bool = True,
    show_header: bool = True,
    **attrs: Any,
):
    """Initializes an inspector.

    Note that most of the time, using `inspect` to do this is going to be more
    useful.

    Some styles of the inspector can be changed using the `code.name`,
    `code.file` and `code.keyword` markup aliases. The rest of the
    highlighting is done using `pprint`, with all of its respective colors.

    Args:
        show_private: Whether `_private` attributes should be shown.
        show_dunder: Whether `__dunder__` attributes should be shown.
        show_methods: Whether methods should be shown when encountering a class.
        show_full_doc: If not set, docstrings are cut to only include their first
            line.
        show_qualname: Show fully-qualified name, e.g. `module.submodule.name`
            instead of `name`.
        show_header: If not set, the header containing the path to the object and
            its qualname will not be added.
    """

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

    super().__init__(**attrs)

    self.width = self.terminal.width

    self.show_private = show_private
    self.show_dunder = show_dunder
    self.show_methods = show_methods
    self.show_full_doc = show_full_doc
    self.show_qualname = show_qualname
    self.show_header = show_header

    # TODO: Fix attr-showing
    self.show_attrs = False

    self.target: object
    if target is not None:
        self.inspect(target)
        self.target = target

debug()

Returns identifiable information used in repr.

Source code in pytermgui/inspector.py
474
475
476
477
478
479
480
def debug(self) -> str:
    """Returns identifiable information used in repr."""

    if self.terminal.is_interactive and not self.terminal.displayhook_installed:
        return "\n".join(self.get_lines())

    return Widget.debug(self)

highlight(text) staticmethod

Applies highlighting to a given string.

This highlight includes keywords, builtin types and more.

Parameters:

Name Type Description Default
text str

The string to highlight.

required

Returns:

Type Description
str

Unparsed markup.

Source code in pytermgui/inspector.py
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
@staticmethod
def highlight(text: str) -> str:
    """Applies highlighting to a given string.

    This highlight includes keywords, builtin types and more.

    Args:
        text: The string to highlight.

    Returns:
        Unparsed markup.
    """

    def _split(text: str, chars: str = " ,:|()[]{}") -> list[tuple[str, str]]:
        """Splits given text by the given chars.

        Args:
            text: The text to split.
            chars: A string of characters we will split by.

        Returns:
            A tuple of (delimiter, word) tuples. Delimiter is one of the characters
            of `chars`.
        """

        last_delim = ""
        output = []
        word = ""
        for char in text:
            if char in chars:
                output.append((last_delim, word))
                last_delim = char
                word = ""
                continue

            word += char

        output.append((last_delim, word))
        return output

    buff = ""
    for (delim, word) in _split(text):
        stripped = word.strip("'")
        highlighted = highlight_python(stripped)

        if highlighted != stripped:
            buff += delim + stripped
            continue

        buff += delim + stripped

    return highlight_python(buff)

inspect(target)

Inspects a given object, and sets self.target to it.

Returns:

Type Description
Inspector

Self, with the new content based on the inspection.

Source code in pytermgui/inspector.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
469
470
471
472
def inspect(self, target: object) -> Inspector:
    """Inspects a given object, and sets self.target to it.

    Returns:
        Self, with the new content based on the inspection.
    """

    self.target = target
    self.target_type = _determine_type(target)

    # Header
    if self.show_header and self.box is not INDENTED_EMPTY_BOX:
        self.lazy_add(self._get_header())

    # Body
    if self.target_type is not ObjectType.MODULE:
        self.lazy_add(self._get_definition())

    padding = 0 if self.target_type is ObjectType.MODULE else 4

    self.lazy_add(self._get_docs(padding))

    keys = self._get_keys()

    for key in keys:
        attr = getattr(target, key, None)

        # Don't show type aliases
        if _is_type_alias(attr):
            continue

        # Only show functions if they are not lambdas
        if (isfunction(attr) or callable(attr)) and (
            hasattr(attr, "__name__") and not attr.__name__ == "<lambda>"
        ):
            self.lazy_add(
                Inspector(
                    box=INDENTED_EMPTY_BOX,
                    show_dunder=self.show_dunder,
                    show_private=self.show_private,
                    show_full_doc=False,
                    show_qualname=self.show_qualname,
                ).inspect(attr)
            )
            continue

        if not self.show_attrs:
            continue

        for i, line in enumerate(prettify(attr, parse=False).splitlines()):
            if i == 0:
                line = f"- {key}: {line}"

            self.lazy_add(Label(line, parent_align=0))

    # Footer
    if self.target_type in [ObjectType.LIVE, ObjectType.BUILTIN]:
        self.lazy_add(self._get_preview())

    return self

JsonLoader

Bases: FileLoader

JSON specific loader subclass.

Source code in pytermgui/file_loaders.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
class JsonLoader(FileLoader):
    """JSON specific loader subclass."""

    def parse(self, data: str) -> dict[Any, Any]:
        """Parse JSON str.

        Args:
            data: JSON formatted string.

        Returns:
            Loadable dictionary.
        """

        return json.loads(data)

parse(data)

Parse JSON str.

Parameters:

Name Type Description Default
data str

JSON formatted string.

required

Returns:

Type Description
dict[Any, Any]

Loadable dictionary.

Source code in pytermgui/file_loaders.py
396
397
398
399
400
401
402
403
404
405
406
def parse(self, data: str) -> dict[Any, Any]:
    """Parse JSON str.

    Args:
        data: JSON formatted string.

    Returns:
        Loadable dictionary.
    """

    return json.loads(data)

KeyboardButton

Bases: Button

A button with keyboard mnemonics in mind.

Shoutout to the HackerNews thread where this was originally suggested

https://news.ycombinator.com/item?id=30517299#30533444

Source code in pytermgui/widgets/keyboard_button.py
11
12
13
14
15
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
class KeyboardButton(Button):
    """A button with keyboard mnemonics in mind.

    Shoutout to the HackerNews thread where this was originally suggested:
        https://news.ycombinator.com/item?id=30517299#30533444
    """

    chars = {**Button.chars, **{"bracket": ["(", ")"]}}

    is_bindable = True

    def __init__(
        self,
        label: str,
        onclick: Callable[[Button], Any],
        index: int = 0,
        bound: str | None = None,
    ) -> None:
        """Initializes a KeyboardButton.

        For example, `KeyboardButton("Help")` will look like: "[ (H)elp ]", and
        `KeyboardButton("Test", index=1)` will give "[ T(e)st ]"

        Args:
            label: The label of the button.
            onclick: The callback to be executed when the button is activated.
            index: The index of the label to use as the binding character.
            bound: The keybind that activates this button. Defaults to `keys.CTRL_{char}`
                is used as the default binding.
        """

        if bound is None:
            bound = getattr(keys, "CTRL_" + label[index].upper())

        brackets = "{}".join(self._get_char("bracket"))
        original = label
        label = label[:index] + brackets.format(label[index])

        if index > -1:
            label += original[index + 1 :]

        super().__init__(label, onclick)
        self.bind(bound, lambda btn, _: onclick(btn))

__init__(label, onclick, index=0, bound=None)

Initializes a KeyboardButton.

For example, KeyboardButton("Help") will look like: "[ (H)elp ]", and KeyboardButton("Test", index=1) will give "[ T(e)st ]"

Parameters:

Name Type Description Default
label str

The label of the button.

required
onclick Callable[[Button], Any]

The callback to be executed when the button is activated.

required
index int

The index of the label to use as the binding character.

0
bound str | None

The keybind that activates this button. Defaults to keys.CTRL_{char} is used as the default binding.

None
Source code in pytermgui/widgets/keyboard_button.py
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
def __init__(
    self,
    label: str,
    onclick: Callable[[Button], Any],
    index: int = 0,
    bound: str | None = None,
) -> None:
    """Initializes a KeyboardButton.

    For example, `KeyboardButton("Help")` will look like: "[ (H)elp ]", and
    `KeyboardButton("Test", index=1)` will give "[ T(e)st ]"

    Args:
        label: The label of the button.
        onclick: The callback to be executed when the button is activated.
        index: The index of the label to use as the binding character.
        bound: The keybind that activates this button. Defaults to `keys.CTRL_{char}`
            is used as the default binding.
    """

    if bound is None:
        bound = getattr(keys, "CTRL_" + label[index].upper())

    brackets = "{}".join(self._get_char("bracket"))
    original = label
    label = label[:index] + brackets.format(label[index])

    if index > -1:
        label += original[index + 1 :]

    super().__init__(label, onclick)
    self.bind(bound, lambda btn, _: onclick(btn))

Keys

Class for easy access to key-codes.

The keys for CTRL_{ascii_letter}-s can be generated with the following code:

for i, letter in enumerate(ascii_lowercase):
    key = f"CTRL_{letter.upper()}"
    code = chr(i+1).encode('unicode_escape').decode('utf-8')

    print(key, code)
Source code in pytermgui/input.py
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
class Keys:
    """Class for easy access to key-codes.

    The keys for CTRL_{ascii_letter}-s can be generated with
    the following code:

    ```python3
    for i, letter in enumerate(ascii_lowercase):
        key = f"CTRL_{letter.upper()}"
        code = chr(i+1).encode('unicode_escape').decode('utf-8')

        print(key, code)
    ```
    """

    def __init__(self, platform_keys: dict[str, str], platform: str) -> None:
        """Initialize Keys object.

        Args:
            platform_keys: A dictionary of platform-specific keys.
            platform: The platform the program is running on.
        """

        self._keys = {
            "SPACE": " ",
            "ESC": "\x1b",
            # The ALT character in key combinations is the same as ESC
            "ALT": "\x1b",
            "TAB": "\t",
            "ENTER": "\n",
            "RETURN": "\n",
            "CARRIAGE_RETURN": "\r",
            "CTRL_SPACE": "\x00",
            "CTRL_A": "\x01",
            "CTRL_B": "\x02",
            "CTRL_C": "\x03",
            "CTRL_D": "\x04",
            "CTRL_E": "\x05",
            "CTRL_F": "\x06",
            "CTRL_G": "\x07",
            "CTRL_H": "\x08",
            "CTRL_I": "\t",
            "CTRL_J": "\n",
            "CTRL_K": "\x0b",
            "CTRL_L": "\x0c",
            "CTRL_M": "\r",
            "CTRL_N": "\x0e",
            "CTRL_O": "\x0f",
            "CTRL_P": "\x10",
            "CTRL_Q": "\x11",
            "CTRL_R": "\x12",
            "CTRL_S": "\x13",
            "CTRL_T": "\x14",
            "CTRL_U": "\x15",
            "CTRL_V": "\x16",
            "CTRL_W": "\x17",
            "CTRL_X": "\x18",
            "CTRL_Y": "\x19",
            "CTRL_Z": "\x1a",
        }

        self.platform = platform

        if platform_keys is not None:
            for key, code in platform_keys.items():
                if key == "name":
                    self.name = code
                    continue

                self._keys[key] = code

    def __getattr__(self, attr: str) -> str:
        """Gets attr from self._keys."""

        if attr == "ANY_KEY":
            return attr

        return self._keys.get(attr, "")

    def get_name(self, key: str, default: Optional[str] = None) -> Optional[str]:
        """Gets canonical name of a key code.

        Args:
            key: The key to get the name of.
            default: The return value to substitute if no canonical name could be
                found. Defaults to None.

        Returns:
            The canonical name if one can be found, default otherwise.
        """

        for name, value in self._keys.items():
            if key == value:
                return name

        return default

    def values(self) -> ValuesView[str]:
        """Returns values() of self._keys."""

        return self._keys.values()

    def keys(self) -> KeysView[str]:
        """Returns keys() of self._keys."""

        return self._keys.keys()

    def items(self) -> ItemsView[str, str]:
        """Returns items() of self._keys."""

        return self._keys.items()

__getattr__(attr)

Gets attr from self._keys.

Source code in pytermgui/input.py
277
278
279
280
281
282
283
def __getattr__(self, attr: str) -> str:
    """Gets attr from self._keys."""

    if attr == "ANY_KEY":
        return attr

    return self._keys.get(attr, "")

__init__(platform_keys, platform)

Initialize Keys object.

Parameters:

Name Type Description Default
platform_keys dict[str, str]

A dictionary of platform-specific keys.

required
platform str

The platform the program is running on.

required
Source code in pytermgui/input.py
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
def __init__(self, platform_keys: dict[str, str], platform: str) -> None:
    """Initialize Keys object.

    Args:
        platform_keys: A dictionary of platform-specific keys.
        platform: The platform the program is running on.
    """

    self._keys = {
        "SPACE": " ",
        "ESC": "\x1b",
        # The ALT character in key combinations is the same as ESC
        "ALT": "\x1b",
        "TAB": "\t",
        "ENTER": "\n",
        "RETURN": "\n",
        "CARRIAGE_RETURN": "\r",
        "CTRL_SPACE": "\x00",
        "CTRL_A": "\x01",
        "CTRL_B": "\x02",
        "CTRL_C": "\x03",
        "CTRL_D": "\x04",
        "CTRL_E": "\x05",
        "CTRL_F": "\x06",
        "CTRL_G": "\x07",
        "CTRL_H": "\x08",
        "CTRL_I": "\t",
        "CTRL_J": "\n",
        "CTRL_K": "\x0b",
        "CTRL_L": "\x0c",
        "CTRL_M": "\r",
        "CTRL_N": "\x0e",
        "CTRL_O": "\x0f",
        "CTRL_P": "\x10",
        "CTRL_Q": "\x11",
        "CTRL_R": "\x12",
        "CTRL_S": "\x13",
        "CTRL_T": "\x14",
        "CTRL_U": "\x15",
        "CTRL_V": "\x16",
        "CTRL_W": "\x17",
        "CTRL_X": "\x18",
        "CTRL_Y": "\x19",
        "CTRL_Z": "\x1a",
    }

    self.platform = platform

    if platform_keys is not None:
        for key, code in platform_keys.items():
            if key == "name":
                self.name = code
                continue

            self._keys[key] = code

get_name(key, default=None)

Gets canonical name of a key code.

Parameters:

Name Type Description Default
key str

The key to get the name of.

required
default Optional[str]

The return value to substitute if no canonical name could be found. Defaults to None.

None

Returns:

Type Description
Optional[str]

The canonical name if one can be found, default otherwise.

Source code in pytermgui/input.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
def get_name(self, key: str, default: Optional[str] = None) -> Optional[str]:
    """Gets canonical name of a key code.

    Args:
        key: The key to get the name of.
        default: The return value to substitute if no canonical name could be
            found. Defaults to None.

    Returns:
        The canonical name if one can be found, default otherwise.
    """

    for name, value in self._keys.items():
        if key == value:
            return name

    return default

items()

Returns items() of self._keys.

Source code in pytermgui/input.py
313
314
315
316
def items(self) -> ItemsView[str, str]:
    """Returns items() of self._keys."""

    return self._keys.items()

keys()

Returns keys() of self._keys.

Source code in pytermgui/input.py
308
309
310
311
def keys(self) -> KeysView[str]:
    """Returns keys() of self._keys."""

    return self._keys.keys()

values()

Returns values() of self._keys.

Source code in pytermgui/input.py
303
304
305
306
def values(self) -> ValuesView[str]:
    """Returns values() of self._keys."""

    return self._keys.values()

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

Layout

Defines a layout of Widgets, used by WindowManager.

Internally, it keeps track of a list of Slot. This list is then turned into a list of rows, all containing slots. This is done either when the current row has run out of the terminal's width, or ROW_BREAK is encountered.

Source code in pytermgui/window_manager/layouts.py
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
class Layout:
    """Defines a layout of Widgets, used by WindowManager.

    Internally, it keeps track of a list of `Slot`. This list is then turned into a list
    of rows, all containing slots. This is done either when the current row has run out
    of the terminal's width, or `ROW_BREAK` is encountered.
    """

    name: str

    def __init__(self, name: str = "Layout") -> None:
        self.name = name
        self.slots: list[Slot] = []

    @property
    def terminal(self) -> Terminal:
        """Returns the current global terminal instance."""

        return get_terminal()

    def __len__(self) -> int:
        """Gets the slot count of this layout."""

        return len(self.slots)

    def _to_rows(self) -> list[list[Slot]]:
        """Breaks `self.slots` into a list of list of slots.

        The terminal's remaining width is kept track of, and when a slot doesn't have enough
        space left it is pushed to a new row. Additionally, `ROW_BREAK` will force a new
        row to be created, starting with the next slot.
        """

        rows: list[list[Slot]] = []
        available = self.terminal.width

        row: list[Slot] = []
        for slot in self.slots:
            if available <= 0 or slot is ROW_BREAK:
                rows.append(row)

                row = []
                available = self.terminal.width - slot.width.value

            if slot is ROW_BREAK:
                continue

            available -= slot.width.value
            row.append(slot)

        if len(row) > 0:
            rows.append(row)

        return rows

    def build_rows(self) -> list[list[Slot]]:
        """Builds a list of slot rows, breaking them & applying automatic dimensions.

        Returns:
            A list[list[Slot]], aka. a list of slot-rows.
        """

        def _get_height(row: list[Slot]) -> int:
            defined = list(filter(lambda slot: not isinstance(slot.height, Auto), row))

            if len(defined) > 0:
                return max(slot.height.value for slot in defined)

            return 0

        def _calculate_widths(row: list[Slot]) -> tuple[int, int]:
            defined: list[Slot] = list(
                filter(lambda slt: not isinstance(slt.width, Auto), row)
            )
            undefined = list(filter(lambda slt: slt not in defined, row))

            available = self.terminal.width - sum(slot.width.value for slot in defined)

            return divmod(available, len(undefined) or 1)

        rows = self._to_rows()
        heights = [_get_height(row) for row in rows]

        occupied = sum(heights)
        auto_height, extra_height = divmod(
            self.terminal.height - occupied, heights.count(0) or 1
        )

        for row, height in zip(rows, heights):
            height = height or auto_height

            auto_width, extra_width = _calculate_widths(row)
            for slot in row:
                width = auto_width if isinstance(slot.width, Auto) else slot.width.value

                if isinstance(slot.height, Auto):
                    slot.height.value = height + extra_height
                    extra_height = 0

                if isinstance(slot.width, Auto):
                    slot.width.value = width + extra_width
                    extra_width = 0

        return rows

    def add_slot(  # pylint: disable=too-many-arguments
        self,
        name: str = "Slot",
        *,
        slot: Slot | None = None,
        width: Dimension | int | float | None = None,
        height: Dimension | int | float | None = None,
        index: int = -1,
    ) -> Slot:
        """Adds a new slot to the layout.

        Args:
            name: The **snakeified** name of the slot. A non-snake case name
                will cause issues when trying to retrieve the slot (see GH#147).
            slot: An already instantiated `Slot` instance. If this is given,
                the additional width & height arguments will be ignored.
            width: The width for the new slot. See below for special types.
            height: The height for the new slot. See below for special types.
            index: The index to add the new slot to.

        Returns:
            The just-added slot.

        When defining dimensions, either width or height, some special value
        types can be given:
        - `Dimension`: Passed directly to the new slot.
        - `None`: An `Auto` dimension is created with no value.
        - `int`: A `Static` dimension is created with the given value.
        - `float`: A `Relative` dimension is created with the given value as its
            scale. Its `bound` attribute will default to the relevant part of the
            terminal's size.
        """

        if slot is None:
            if width is None:
                width = Auto()

            elif isinstance(width, int):
                width = Static(width)

            elif isinstance(width, float):
                width = Relative(width, bound=lambda: self.terminal.width)

            if height is None:
                height = Auto()

            elif isinstance(height, int):
                height = Static(height)

            elif isinstance(height, float):
                height = Relative(height, bound=lambda: self.terminal.height)

            slot = Slot(name, width=width, height=height)

        if index == -1:
            self.slots.append(slot)
            return slot

        self.slots.insert(index, slot)

        return slot

    def add_break(self, *, index: int = -1) -> None:
        """Adds `ROW_BREAK` to the given index.

        This special slot is ignored for all intents and purposes, other than when
        breaking the slots into rows. In that context, when encountered, the current
        row is deemed completed, and the next slot will go into a new row list.
        """

        self.add_slot(slot=ROW_BREAK, index=index)

    def assign(self, widget: Widget, *, index: int = -1, apply: bool = True) -> None:
        """Assigns a widget to the slot at the specified index.

        Args:
            widget: The widget to assign.
            index: The target slot's index.
            apply: If set, `apply` will be called once the widget has been assigned.
        """

        slots = [slot for slot in self.slots if slot is not ROW_BREAK]
        if index > len(slots) - 1:
            return

        slot = slots[index]

        slot.content = widget

        if apply:
            self.apply()

    def apply(self) -> None:
        """Applies the layout to each slot."""

        position = list(self.terminal.origin)
        for row in self.build_rows():
            position[0] = 1

            for slot in row:
                slot.apply((position[0], position[1]))

                position[0] += slot.width.value

            position[1] += max(slot.height.value for slot in row)

    def __getattr__(self, attr: str) -> Slot:
        """Gets a slot by its (slugified) name."""

        def _snakeify(name: str) -> str:
            return name.lower().replace(" ", "_")

        for slot in self.slots:
            if _snakeify(slot.name) == attr:
                return slot

        raise AttributeError(f"Slot with name {attr!r} could not be found.")

terminal property

Returns the current global terminal instance.

__getattr__(attr)

Gets a slot by its (slugified) name.

Source code in pytermgui/window_manager/layouts.py
382
383
384
385
386
387
388
389
390
391
392
def __getattr__(self, attr: str) -> Slot:
    """Gets a slot by its (slugified) name."""

    def _snakeify(name: str) -> str:
        return name.lower().replace(" ", "_")

    for slot in self.slots:
        if _snakeify(slot.name) == attr:
            return slot

    raise AttributeError(f"Slot with name {attr!r} could not be found.")

__len__()

Gets the slot count of this layout.

Source code in pytermgui/window_manager/layouts.py
191
192
193
194
def __len__(self) -> int:
    """Gets the slot count of this layout."""

    return len(self.slots)

add_break(*, index=-1)

Adds ROW_BREAK to the given index.

This special slot is ignored for all intents and purposes, other than when breaking the slots into rows. In that context, when encountered, the current row is deemed completed, and the next slot will go into a new row list.

Source code in pytermgui/window_manager/layouts.py
338
339
340
341
342
343
344
345
346
def add_break(self, *, index: int = -1) -> None:
    """Adds `ROW_BREAK` to the given index.

    This special slot is ignored for all intents and purposes, other than when
    breaking the slots into rows. In that context, when encountered, the current
    row is deemed completed, and the next slot will go into a new row list.
    """

    self.add_slot(slot=ROW_BREAK, index=index)

add_slot(name='Slot', *, slot=None, width=None, height=None, index=-1)

Adds a new slot to the layout.

Parameters:

Name Type Description Default
name str

The snakeified name of the slot. A non-snake case name will cause issues when trying to retrieve the slot (see GH#147).

'Slot'
slot Slot | None

An already instantiated Slot instance. If this is given, the additional width & height arguments will be ignored.

None
width Dimension | int | float | None

The width for the new slot. See below for special types.

None
height Dimension | int | float | None

The height for the new slot. See below for special types.

None
index int

The index to add the new slot to.

-1

Returns:

Type Description
Slot

The just-added slot.

When defining dimensions, either width or height, some special value types can be given: - Dimension: Passed directly to the new slot. - None: An Auto dimension is created with no value. - int: A Static dimension is created with the given value. - float: A Relative dimension is created with the given value as its scale. Its bound attribute will default to the relevant part of the terminal's size.

Source code in pytermgui/window_manager/layouts.py
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
def add_slot(  # pylint: disable=too-many-arguments
    self,
    name: str = "Slot",
    *,
    slot: Slot | None = None,
    width: Dimension | int | float | None = None,
    height: Dimension | int | float | None = None,
    index: int = -1,
) -> Slot:
    """Adds a new slot to the layout.

    Args:
        name: The **snakeified** name of the slot. A non-snake case name
            will cause issues when trying to retrieve the slot (see GH#147).
        slot: An already instantiated `Slot` instance. If this is given,
            the additional width & height arguments will be ignored.
        width: The width for the new slot. See below for special types.
        height: The height for the new slot. See below for special types.
        index: The index to add the new slot to.

    Returns:
        The just-added slot.

    When defining dimensions, either width or height, some special value
    types can be given:
    - `Dimension`: Passed directly to the new slot.
    - `None`: An `Auto` dimension is created with no value.
    - `int`: A `Static` dimension is created with the given value.
    - `float`: A `Relative` dimension is created with the given value as its
        scale. Its `bound` attribute will default to the relevant part of the
        terminal's size.
    """

    if slot is None:
        if width is None:
            width = Auto()

        elif isinstance(width, int):
            width = Static(width)

        elif isinstance(width, float):
            width = Relative(width, bound=lambda: self.terminal.width)

        if height is None:
            height = Auto()

        elif isinstance(height, int):
            height = Static(height)

        elif isinstance(height, float):
            height = Relative(height, bound=lambda: self.terminal.height)

        slot = Slot(name, width=width, height=height)

    if index == -1:
        self.slots.append(slot)
        return slot

    self.slots.insert(index, slot)

    return slot

apply()

Applies the layout to each slot.

Source code in pytermgui/window_manager/layouts.py
368
369
370
371
372
373
374
375
376
377
378
379
380
def apply(self) -> None:
    """Applies the layout to each slot."""

    position = list(self.terminal.origin)
    for row in self.build_rows():
        position[0] = 1

        for slot in row:
            slot.apply((position[0], position[1]))

            position[0] += slot.width.value

        position[1] += max(slot.height.value for slot in row)

assign(widget, *, index=-1, apply=True)

Assigns a widget to the slot at the specified index.

Parameters:

Name Type Description Default
widget Widget

The widget to assign.

required
index int

The target slot's index.

-1
apply bool

If set, apply will be called once the widget has been assigned.

True
Source code in pytermgui/window_manager/layouts.py
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
def assign(self, widget: Widget, *, index: int = -1, apply: bool = True) -> None:
    """Assigns a widget to the slot at the specified index.

    Args:
        widget: The widget to assign.
        index: The target slot's index.
        apply: If set, `apply` will be called once the widget has been assigned.
    """

    slots = [slot for slot in self.slots if slot is not ROW_BREAK]
    if index > len(slots) - 1:
        return

    slot = slots[index]

    slot.content = widget

    if apply:
        self.apply()

build_rows()

Builds a list of slot rows, breaking them & applying automatic dimensions.

Returns:

Type Description
list[list[Slot]]

A list[list[Slot]], aka. a list of slot-rows.

Source code in pytermgui/window_manager/layouts.py
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
def build_rows(self) -> list[list[Slot]]:
    """Builds a list of slot rows, breaking them & applying automatic dimensions.

    Returns:
        A list[list[Slot]], aka. a list of slot-rows.
    """

    def _get_height(row: list[Slot]) -> int:
        defined = list(filter(lambda slot: not isinstance(slot.height, Auto), row))

        if len(defined) > 0:
            return max(slot.height.value for slot in defined)

        return 0

    def _calculate_widths(row: list[Slot]) -> tuple[int, int]:
        defined: list[Slot] = list(
            filter(lambda slt: not isinstance(slt.width, Auto), row)
        )
        undefined = list(filter(lambda slt: slt not in defined, row))

        available = self.terminal.width - sum(slot.width.value for slot in defined)

        return divmod(available, len(undefined) or 1)

    rows = self._to_rows()
    heights = [_get_height(row) for row in rows]

    occupied = sum(heights)
    auto_height, extra_height = divmod(
        self.terminal.height - occupied, heights.count(0) or 1
    )

    for row, height in zip(rows, heights):
        height = height or auto_height

        auto_width, extra_width = _calculate_widths(row)
        for slot in row:
            width = auto_width if isinstance(slot.width, Auto) else slot.width.value

            if isinstance(slot.height, Auto):
                slot.height.value = height + extra_height
                extra_height = 0

            if isinstance(slot.width, Auto):
                slot.width.value = width + extra_width
                extra_width = 0

    return rows

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 │",
        "└───┘",
    ]

LineLengthError

Bases: Exception

Raised when a widget line is not the expected length.

Source code in pytermgui/exceptions.py
25
26
class LineLengthError(Exception):
    """Raised when a widget line is not the expected length."""

MacroToken dataclass

Bases: Token

A binding of a Python function to a markup name.

See the docs on information about syntax & semantics.

Source code in pytermgui/markup/tokens.py
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
@dataclass(frozen=True, repr=False)
class MacroToken(Token):
    """A binding of a Python function to a markup name.

    See the docs on information about syntax & semantics.
    """

    __slots__ = ("value", "arguments")

    value: str
    arguments: tuple[str, ...]

    def __iter__(self) -> Iterator[Any]:
        return iter((self.value, self.arguments))

    @cached_property
    def prettified_markup(self) -> str:
        target = self.markup[1:]

        return f"[210 bold]![/]{target}"

    @cached_property
    def markup(self) -> str:
        return f"{self.value}" + (
            f"({':'.join(self.arguments)})" if len(self.arguments) > 0 else ""
        )

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"\[")

MarkupLanguage

A relatively simple object that binds context to TIM parsing functions.

Most of the job this class has is to pass along a ContextDict to various "lower level" functions, in order to maintain a sort of state. It also exposes ways to modify this state, namely the alias and define methods.

Source code in pytermgui/markup/language.py
 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
class MarkupLanguage:
    """A relatively simple object that binds context to TIM parsing functions.

    Most of the job this class has is to pass along a `ContextDict` to various
    "lower level" functions, in order to maintain a sort of state. It also exposes
    ways to modify this state, namely the `alias` and `define` methods.
    """

    def __init__(
        self,
        *,
        strict: bool = False,
        default_aliases: bool = True,
        default_macros: bool = True,
    ) -> None:
        self._cache: dict[tuple[str, bool, bool], tuple[str, list[Token], bool]] = {}

        self.context = create_context_dict()
        self._aliases = self.context["aliases"]
        self._macros = self.context["macros"]

        if default_aliases:
            apply_default_aliases(self)

        if default_macros:
            apply_default_macros(self)

        self.strict = strict or STRICT_MARKUP

    @property
    def aliases(self) -> dict[str, str]:
        """Returns a copy of the aliases defined in context."""

        return self._aliases.copy()

    @property
    def macros(self) -> dict[str, MacroType]:
        """Returns a copy of the macros defined in context."""

        return self._macros.copy()

    def clear_cache(self) -> None:
        """Clears the internal cache.

        Use this after re-defining aliases.
        """

        self._cache.clear()

    def define(self, name: str, method: MacroType) -> None:
        """Defines a markup macro.

        Macros are essentially function bindings callable within markup. They can be
        very useful to represent changing data and simplify TIM code.

        Args:
            name: The name that will be used within TIM to call the macro. Must start with
                a bang (`!`).
            method: The function bound to the name given above. This function will take
                any number of strings as arguments, and return a terminal-ready (i.e. parsed)
                string.
        """

        if not name.startswith("!"):
            raise ValueError("TIM macro names must be prefixed by `!`.")

        self._macros[name] = method

    def alias(self, name: str, value: str, *, generate_unsetter: bool = True) -> None:
        """Creates an alias from one custom name to a set of styles.

        These can be used to store and reference a set of tags using only one name.

        Aliases may reference other aliases, but only do this consciously, as it can become
        a hard to follow trail of sorrow very quickly!

        Args:
            name: The name this alias will be referenced by.
            value: The markup value that the alias will represent.
            generate_unsetter: Disable generating clearer aliases.

                For example:
                    ```
                    my-tag = 141 bold italic
                    ```

                will generate:
                    ```
                    /my-tag = /fg /bold /italic
                    ```
        """

        def _generate_unsetter() -> str:
            unsetter = ""
            no_alias = eval_alias(value, self.context)

            for tag in no_alias.split():
                if "(" in tag and ")" in tag:
                    tag = tag[: tag.find("(")]

                if tag in self._aliases or tag in self._macros:
                    unsetter += f" /{tag}"
                    continue

                try:
                    color = str_to_color(tag)
                    unsetter += f" /{'bg' if color.background else 'fg'}"

                except ColorSyntaxError:
                    unsetter += f" /{tag}"

            return unsetter.lstrip(" ")

        self._aliases[name] = value

        if generate_unsetter:
            self._aliases[f"/{name}"] = _generate_unsetter()

    def alias_multiple(self, *, generate_unsetter: bool = True, **items: str) -> None:
        """Runs `MarkupLanguage.alias` repeatedly for all arguments.

        The same `generate_unsetter` value will be used for all calls.

        You can use this in two forms:

        - Traditional keyword arguments:

            ```python
            lang.alias_multiple(my-tag1="bold", my-tag2="italic")
            ```

        - Keyword argument unpacking:

            ```python
            my_aliases = {"my-tag1": "bold", "my-tag2": "italic"}
            lang.alias_multiple(**my_aliases)
            ```
        """

        for name, value in items.items():
            self.alias(name, value, generate_unsetter=generate_unsetter)

    def parse(
        self,
        text: str,
        optimize: bool = False,
        append_reset: bool = True,
    ) -> str:
        """Parses some markup text.

        This is a thin wrapper around [markup.parsing.parse](/reference/
        pytermgui/markup/parsing#pytermgui.markup.parsing.parse). The main additions
        of this wrapper are a caching system, as well as state management.

        Ignoring caching, all calls to this function would be equivalent to:

        ```python3
        def parse(self, *args, **kwargs) -> str:
            kwargs["context"] = self.context

            return parse(*args, **kwargs)
        ```
        """

        key = (text, optimize, append_reset)

        cache_hit = self._cache.get(key)
        if cache_hit is not None:
            cached, tokens, has_macro = cache_hit

            # Re-parse using known tokens when macro is present
            #
            # This saves a tiny fraction of time (around 0.2ms) when parsing
            # macros, for a loss of an even smaller time for the general,
            # non-macro usecase.
            if has_macro:
                output = parse_tokens(
                    tokens,
                    optimize=optimize,
                    append_reset=append_reset,
                    context=self.context,
                    ignore_unknown_tags=not self.strict,
                )

                return output

            return cached

        tokens = list(tokenize_markup(text))

        output = parse_tokens(
            tokens,
            optimize=optimize,
            append_reset=append_reset,
            context=self.context,
            ignore_unknown_tags=not self.strict,
        )

        has_macro = any(token.is_macro() for token in tokens)

        self._cache[key] = (output, tokens, has_macro)

        return output

    # TODO: This should be deprecated.
    @staticmethod
    def get_markup(text: str) -> str:
        """DEPRECATED: Convert ANSI text into markup.

        This function does not use context, and thus is out of place here.
        """

        return tokens_to_markup(list(tokenize_ansi(text)))

    def group_styles(
        self, text: str, tokenizer: Tokenizer = tokenize_ansi
    ) -> Generator[StyledText, None, None]:
        """Generate StyledText-s from some text, using our context.

        See `StyledText.group_styles` for arguments.
        """

        yield from StyledText.group_styles(
            text, tokenizer=tokenizer, context=self.context
        )

    def print(self, *args, **kwargs) -> None:
        """Parse all arguments and pass them through to print, along with kwargs."""

        parsed = []
        for arg in args:
            parsed.append(self.parse(str(arg)))

        get_terminal().print(*parsed, **kwargs)

aliases property

Returns a copy of the aliases defined in context.

macros property

Returns a copy of the macros defined in context.

alias(name, value, *, generate_unsetter=True)

Creates an alias from one custom name to a set of styles.

These can be used to store and reference a set of tags using only one name.

Aliases may reference other aliases, but only do this consciously, as it can become a hard to follow trail of sorrow very quickly!

Parameters:

Name Type Description Default
name str

The name this alias will be referenced by.

required
value str

The markup value that the alias will represent.

required
generate_unsetter bool

Disable generating clearer aliases.

For example:

my-tag = 141 bold italic

will generate:

/my-tag = /fg /bold /italic

True
Source code in pytermgui/markup/language.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def alias(self, name: str, value: str, *, generate_unsetter: bool = True) -> None:
    """Creates an alias from one custom name to a set of styles.

    These can be used to store and reference a set of tags using only one name.

    Aliases may reference other aliases, but only do this consciously, as it can become
    a hard to follow trail of sorrow very quickly!

    Args:
        name: The name this alias will be referenced by.
        value: The markup value that the alias will represent.
        generate_unsetter: Disable generating clearer aliases.

            For example:
                ```
                my-tag = 141 bold italic
                ```

            will generate:
                ```
                /my-tag = /fg /bold /italic
                ```
    """

    def _generate_unsetter() -> str:
        unsetter = ""
        no_alias = eval_alias(value, self.context)

        for tag in no_alias.split():
            if "(" in tag and ")" in tag:
                tag = tag[: tag.find("(")]

            if tag in self._aliases or tag in self._macros:
                unsetter += f" /{tag}"
                continue

            try:
                color = str_to_color(tag)
                unsetter += f" /{'bg' if color.background else 'fg'}"

            except ColorSyntaxError:
                unsetter += f" /{tag}"

        return unsetter.lstrip(" ")

    self._aliases[name] = value

    if generate_unsetter:
        self._aliases[f"/{name}"] = _generate_unsetter()

alias_multiple(*, generate_unsetter=True, **items)

Runs MarkupLanguage.alias repeatedly for all arguments.

The same generate_unsetter value will be used for all calls.

You can use this in two forms:

  • Traditional keyword arguments:

    lang.alias_multiple(my-tag1="bold", my-tag2="italic")
    
  • Keyword argument unpacking:

    my_aliases = {"my-tag1": "bold", "my-tag2": "italic"}
    lang.alias_multiple(**my_aliases)
    
Source code in pytermgui/markup/language.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def alias_multiple(self, *, generate_unsetter: bool = True, **items: str) -> None:
    """Runs `MarkupLanguage.alias` repeatedly for all arguments.

    The same `generate_unsetter` value will be used for all calls.

    You can use this in two forms:

    - Traditional keyword arguments:

        ```python
        lang.alias_multiple(my-tag1="bold", my-tag2="italic")
        ```

    - Keyword argument unpacking:

        ```python
        my_aliases = {"my-tag1": "bold", "my-tag2": "italic"}
        lang.alias_multiple(**my_aliases)
        ```
    """

    for name, value in items.items():
        self.alias(name, value, generate_unsetter=generate_unsetter)

clear_cache()

Clears the internal cache.

Use this after re-defining aliases.

Source code in pytermgui/markup/language.py
93
94
95
96
97
98
99
def clear_cache(self) -> None:
    """Clears the internal cache.

    Use this after re-defining aliases.
    """

    self._cache.clear()

define(name, method)

Defines a markup macro.

Macros are essentially function bindings callable within markup. They can be very useful to represent changing data and simplify TIM code.

Parameters:

Name Type Description Default
name str

The name that will be used within TIM to call the macro. Must start with a bang (!).

required
method MacroType

The function bound to the name given above. This function will take any number of strings as arguments, and return a terminal-ready (i.e. parsed) string.

required
Source code in pytermgui/markup/language.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def define(self, name: str, method: MacroType) -> None:
    """Defines a markup macro.

    Macros are essentially function bindings callable within markup. They can be
    very useful to represent changing data and simplify TIM code.

    Args:
        name: The name that will be used within TIM to call the macro. Must start with
            a bang (`!`).
        method: The function bound to the name given above. This function will take
            any number of strings as arguments, and return a terminal-ready (i.e. parsed)
            string.
    """

    if not name.startswith("!"):
        raise ValueError("TIM macro names must be prefixed by `!`.")

    self._macros[name] = method

get_markup(text) staticmethod

DEPRECATED: Convert ANSI text into markup.

This function does not use context, and thus is out of place here.

Source code in pytermgui/markup/language.py
257
258
259
260
261
262
263
264
@staticmethod
def get_markup(text: str) -> str:
    """DEPRECATED: Convert ANSI text into markup.

    This function does not use context, and thus is out of place here.
    """

    return tokens_to_markup(list(tokenize_ansi(text)))

group_styles(text, tokenizer=tokenize_ansi)

Generate StyledText-s from some text, using our context.

See StyledText.group_styles for arguments.

Source code in pytermgui/markup/language.py
266
267
268
269
270
271
272
273
274
275
276
def group_styles(
    self, text: str, tokenizer: Tokenizer = tokenize_ansi
) -> Generator[StyledText, None, None]:
    """Generate StyledText-s from some text, using our context.

    See `StyledText.group_styles` for arguments.
    """

    yield from StyledText.group_styles(
        text, tokenizer=tokenizer, context=self.context
    )

parse(text, optimize=False, append_reset=True)

Parses some markup text.

This is a thin wrapper around markup.parsing.parse. The main additions of this wrapper are a caching system, as well as state management.

Ignoring caching, all calls to this function would be equivalent to:

def parse(self, *args, **kwargs) -> str:
    kwargs["context"] = self.context

    return parse(*args, **kwargs)
Source code in pytermgui/markup/language.py
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
def parse(
    self,
    text: str,
    optimize: bool = False,
    append_reset: bool = True,
) -> str:
    """Parses some markup text.

    This is a thin wrapper around [markup.parsing.parse](/reference/
    pytermgui/markup/parsing#pytermgui.markup.parsing.parse). The main additions
    of this wrapper are a caching system, as well as state management.

    Ignoring caching, all calls to this function would be equivalent to:

    ```python3
    def parse(self, *args, **kwargs) -> str:
        kwargs["context"] = self.context

        return parse(*args, **kwargs)
    ```
    """

    key = (text, optimize, append_reset)

    cache_hit = self._cache.get(key)
    if cache_hit is not None:
        cached, tokens, has_macro = cache_hit

        # Re-parse using known tokens when macro is present
        #
        # This saves a tiny fraction of time (around 0.2ms) when parsing
        # macros, for a loss of an even smaller time for the general,
        # non-macro usecase.
        if has_macro:
            output = parse_tokens(
                tokens,
                optimize=optimize,
                append_reset=append_reset,
                context=self.context,
                ignore_unknown_tags=not self.strict,
            )

            return output

        return cached

    tokens = list(tokenize_markup(text))

    output = parse_tokens(
        tokens,
        optimize=optimize,
        append_reset=append_reset,
        context=self.context,
        ignore_unknown_tags=not self.strict,
    )

    has_macro = any(token.is_macro() for token in tokens)

    self._cache[key] = (output, tokens, has_macro)

    return output

print(*args, **kwargs)

Parse all arguments and pass them through to print, along with kwargs.

Source code in pytermgui/markup/language.py
278
279
280
281
282
283
284
285
def print(self, *args, **kwargs) -> None:
    """Parse all arguments and pass them through to print, along with kwargs."""

    parsed = []
    for arg in args:
        parsed.append(self.parse(str(arg)))

    get_terminal().print(*parsed, **kwargs)

MarkupSyntaxError dataclass

Bases: ParserSyntaxError

Raised when parsed markup text contains an error.

Source code in pytermgui/exceptions.py
84
85
86
87
class MarkupSyntaxError(ParserSyntaxError):
    """Raised when parsed markup text contains an error."""

    _delimiters = ("[", "]")

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 ",
        "   ",
    ]

Palette dataclass

A harmonious color palette.

Running Palette.alias on a generated palette will create the following color aliases:

Main colors

These are the colors used by the majority of the application. Primary should make up around 50% percent of an average screen's colors, while secondary and tertiary should use the remaining 50% together (25% each).

Accents should be used sparingly to highlight specific details.

Items: primary, secondary, tertiary, accent

Semantic colors

These colors are all meant to convey some meaning. They shouldn't be used in situation where that meaning, e.g. success, isn't clearly related. When not given as an argument, they are generated by blending some default green, yellow and red with the primary color.

Items: success, warning, error

Neutral colors

These are colors meant to be used as a background to the main group. All of them are a blend of a default background color and one of the main colors: surface is generated from primary, surface2 comes from secondary and so on.

Items: surface, surface2, surface3, surface4

Source code in pytermgui/palettes.py
 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
@dataclass(repr=False)
class Palette:
    """A harmonious color palette.

    Running `Palette.alias` on a generated palette will create the following color
    aliases:

    !!! cite "Main colors"

        These are the colors used by the majority of the application. Primary should
        make up around 50% percent of an average screen's colors, while secondary and
        tertiary should use the remaining 50% together (25% each).

        Accents should be used sparingly to highlight specific details.

        **Items:** primary, secondary, tertiary, accent

    !!! cite "Semantic colors"

        These colors are all meant to convey some meaning. They shouldn't be used in
        situation where that meaning, e.g. success, isn't clearly related. When not given
        as an argument, they are generated by blending some default green, yellow and red
        with the primary color.

        **Items:** success, warning, error

    !!! cite "Neutral colors"

        These are colors meant to be used as a background to the main group. All of them
        are a blend of a default background color and one of the main colors: `surface`
        is generated from `primary`, `surface2` comes from secondary and so on.

        **Items:** surface, surface2, surface3, surface4
    """

    data: dict[str, str]

    def __init__(  # pylint: disable=too-many-locals,too-many-arguments
        self,
        *,
        primary: str,
        secondary: str | None = None,
        tertiary: str | None = None,
        accent: str | None = None,
        success: str | None = None,
        warning: str | None = None,
        error: str | None = None,
        surface: str | None = None,
        surface2: str | None = None,
        surface3: str | None = None,
        strategy: PaletteGeneratorStrategy = triadic,
    ) -> None:
        """Generates a color palette from the given primary color.

        If any other color arguments are passed, they will be parsed as a color
        and used as-is. Otherwise, they will be derived from the primary.

        See the class documentation for info on all arguments.

        Args:
            strategy: A strategy that will be used to derive colors.
        """

        self.data = self._generate_map(
            primary=primary,
            secondary=secondary,
            tertiary=tertiary,
            accent=accent,
            success=success,
            warning=warning,
            error=error,
            surface=surface,
            surface2=surface2,
            surface3=surface3,
            strategy=strategy,
        )

    def _generate_map(  # pylint: disable=too-many-locals,too-many-arguments
        self,
        *,
        primary: str,
        secondary: str | None = None,
        tertiary: str | None = None,
        accent: str | None = None,
        success: str | None = None,
        warning: str | None = None,
        error: str | None = None,
        surface: str | None = None,
        surface2: str | None = None,
        surface3: str | None = None,
        strategy: PaletteGeneratorStrategy = triadic,
    ) -> dict[str, str]:
        """Generates a map of color names to values.

        See `__init__` for more information.
        """

        if isinstance(strategy, str):
            old_strat = strategy
            strategy = STRATEGIES.get(strategy)

            if strategy is None:
                raise KeyError(
                    f"Unknown strategy {old_strat!r}. Please choose from"
                    + f" {list(STRATEGIES.keys())}."
                )

        c_primary = Color.parse(primary, localize=False)

        # Four main colors
        c_primary, *generated = strategy(c_primary)
        c_secondary = _parse_optional(secondary, generated[0])
        c_tertiary = _parse_optional(tertiary, generated[1])
        c_accent = _parse_optional(accent, generated[2])

        # Four surface colors, one for each main color
        c_surface = _parse_optional(surface, SURFACE.blend(c_primary, SURFACE_ALPHA))
        c_surface2 = _parse_optional(
            surface2, SURFACE.blend(c_secondary, SURFACE_ALPHA)
        )
        c_surface3 = _parse_optional(surface3, SURFACE.blend(c_tertiary, SURFACE_ALPHA))
        c_surface4 = _parse_optional(surface3, SURFACE.blend(c_accent, SURFACE_ALPHA))

        # Three semantic colors, blended from primary
        c_success = _parse_optional(success, SUCCESS.blend(c_primary, SEMANTIC_ALPHA))
        c_warning = _parse_optional(warning, WARNING.blend(c_primary, SEMANTIC_ALPHA))
        c_error = _parse_optional(error, ERROR.blend(c_primary, SEMANTIC_ALPHA))

        base_palette: dict[str, Color] = {
            "primary": c_primary,
            "secondary": c_secondary,
            "tertiary": c_tertiary,
            "accent": c_accent,
            "surface": c_surface,
            "surface2": c_surface2,
            "surface3": c_surface3,
            "surface4": c_surface4,
            "success": c_success,
            "warning": c_warning,
            "error": c_error,
        }

        black = Color.parse("#000000")
        white = Color.parse("#FFFFFF")

        data = {}

        for name, color in base_palette.items():
            for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
                if shadenumber > 0:
                    shadeindex = f"+{shadenumber}"
                    blended = color.blend(
                        white, SHADE_INCREMENT * shadenumber
                    )

                elif shadenumber == 0:
                    shadeindex = ""
                    blended = color

                else:
                    shadeindex = str(shadenumber)
                    blended = color.blend(
                        black, -SHADE_INCREMENT * shadenumber
                    )

                data[f"{name}{shadeindex}"] = blended

                bg_variant = deepcopy(blended)
                bg_variant.background = True
                data[f"@{name}{shadeindex}"] = bg_variant

        return {
            key: ("@" if color.background else "") + color.hex
            for key, color in data.items()
        }

    def regenerate(self, **kwargs: Any) -> Palette:
        """Generates a new palette and replaces self.data with its data.

        Args:
            **kwargs: All key word args passed to the new Palette. See `__init__`.

        Returns:
            This palette, after regeneration.
        """

        other = Palette(**kwargs)

        self.data = other.data
        self.alias()

        return self

    def base_keys(self) -> list[str]:
        """Returns the non-background, non-shade alias keys."""

        return [
            key
            for key in self.data
            if not "+" in key and not "-" in key and not key.startswith("@")
        ]

    def alias(self, lang: MarkupLanguage = tim) -> None:
        """Sets up aliases for the given language.

        Note that no unsetters will be generated.

        Args:
            lang: The language to run `alias_multiple` on.
        """

        lang.clear_cache()
        lang.alias_multiple(**self.data, generate_unsetter=False)

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Shows off the palette in a compact form."""

        yield f"<{type(self).__name__}"

        for name, value in [
            ("primary", self.data["primary"]),
            ("secondary", self.data["secondary"]),
            ("tertiary", self.data["tertiary"]),
            ("accent", self.data["accent"]),
        ]:
            yield {
                "text": f" {name}: [@{value} #auto]{value}[/]",
                "highlight": False,
            }

        yield ">\n\n"

        length = max(len(key) for key in self.base_keys()) + 2
        for name in self.base_keys():
            line = ""

            for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
                if shadenumber > 0:
                    shadeindex = f"+{shadenumber}"

                elif shadenumber == 0:
                    line += f"[@{self.data[name]} #auto] {name:^{length}} "
                    continue

                else:
                    shadeindex = str(shadenumber)

                line += f"[@{self.data[name + shadeindex]} #auto]    "

            yield {
                "text": tim.parse(line + "[/]\n"),
                "highlight": False,
            }

    def print(self) -> None:
        """Shows off the palette in an extended form."""

        length = max(len(key) for key in self.base_keys()) + 4
        keys = self.base_keys()

        for name in keys:
            names = []

            for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
                if shadenumber > 0:
                    shadeindex = f"+{shadenumber}"

                elif shadenumber == 0:
                    shadeindex = ""

                else:
                    shadeindex = str(shadenumber)

                shaded_name = name + shadeindex
                names.append(shaded_name)

            tim.print("".join(f"[@{self.data[name]}]{' ' * length}" for name in names))
            tim.print(
                "".join(
                    f"[@{self.data[name]} #auto]"
                    + (
                        f"[bold]{name:^{length}}[/]"
                        if name in keys
                        else name[-2:].center(length)
                    )
                    for name in names
                )
            )
            tim.print(
                "".join(
                    f"[@{self.data[name]} #auto]{self.data[name]:^{length}}"
                    for name in names
                )
            )
            tim.print("".join(f"[@{self.data[name]}]{' ' * length}" for name in names))

__fancy_repr__()

Shows off the palette in a compact form.

Source code in pytermgui/palettes.py
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
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Shows off the palette in a compact form."""

    yield f"<{type(self).__name__}"

    for name, value in [
        ("primary", self.data["primary"]),
        ("secondary", self.data["secondary"]),
        ("tertiary", self.data["tertiary"]),
        ("accent", self.data["accent"]),
    ]:
        yield {
            "text": f" {name}: [@{value} #auto]{value}[/]",
            "highlight": False,
        }

    yield ">\n\n"

    length = max(len(key) for key in self.base_keys()) + 2
    for name in self.base_keys():
        line = ""

        for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
            if shadenumber > 0:
                shadeindex = f"+{shadenumber}"

            elif shadenumber == 0:
                line += f"[@{self.data[name]} #auto] {name:^{length}} "
                continue

            else:
                shadeindex = str(shadenumber)

            line += f"[@{self.data[name + shadeindex]} #auto]    "

        yield {
            "text": tim.parse(line + "[/]\n"),
            "highlight": False,
        }

__init__(*, primary, secondary=None, tertiary=None, accent=None, success=None, warning=None, error=None, surface=None, surface2=None, surface3=None, strategy=triadic)

Generates a color palette from the given primary color.

If any other color arguments are passed, they will be parsed as a color and used as-is. Otherwise, they will be derived from the primary.

See the class documentation for info on all arguments.

Parameters:

Name Type Description Default
strategy PaletteGeneratorStrategy

A strategy that will be used to derive colors.

triadic
Source code in pytermgui/palettes.py
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
def __init__(  # pylint: disable=too-many-locals,too-many-arguments
    self,
    *,
    primary: str,
    secondary: str | None = None,
    tertiary: str | None = None,
    accent: str | None = None,
    success: str | None = None,
    warning: str | None = None,
    error: str | None = None,
    surface: str | None = None,
    surface2: str | None = None,
    surface3: str | None = None,
    strategy: PaletteGeneratorStrategy = triadic,
) -> None:
    """Generates a color palette from the given primary color.

    If any other color arguments are passed, they will be parsed as a color
    and used as-is. Otherwise, they will be derived from the primary.

    See the class documentation for info on all arguments.

    Args:
        strategy: A strategy that will be used to derive colors.
    """

    self.data = self._generate_map(
        primary=primary,
        secondary=secondary,
        tertiary=tertiary,
        accent=accent,
        success=success,
        warning=warning,
        error=error,
        surface=surface,
        surface2=surface2,
        surface3=surface3,
        strategy=strategy,
    )

alias(lang=tim)

Sets up aliases for the given language.

Note that no unsetters will be generated.

Parameters:

Name Type Description Default
lang MarkupLanguage

The language to run alias_multiple on.

tim
Source code in pytermgui/palettes.py
295
296
297
298
299
300
301
302
303
304
305
def alias(self, lang: MarkupLanguage = tim) -> None:
    """Sets up aliases for the given language.

    Note that no unsetters will be generated.

    Args:
        lang: The language to run `alias_multiple` on.
    """

    lang.clear_cache()
    lang.alias_multiple(**self.data, generate_unsetter=False)

base_keys()

Returns the non-background, non-shade alias keys.

Source code in pytermgui/palettes.py
286
287
288
289
290
291
292
293
def base_keys(self) -> list[str]:
    """Returns the non-background, non-shade alias keys."""

    return [
        key
        for key in self.data
        if not "+" in key and not "-" in key and not key.startswith("@")
    ]

print()

Shows off the palette in an extended form.

Source code in pytermgui/palettes.py
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
def print(self) -> None:
    """Shows off the palette in an extended form."""

    length = max(len(key) for key in self.base_keys()) + 4
    keys = self.base_keys()

    for name in keys:
        names = []

        for shadenumber in range(-SHADE_COUNT, SHADE_COUNT + 1):
            if shadenumber > 0:
                shadeindex = f"+{shadenumber}"

            elif shadenumber == 0:
                shadeindex = ""

            else:
                shadeindex = str(shadenumber)

            shaded_name = name + shadeindex
            names.append(shaded_name)

        tim.print("".join(f"[@{self.data[name]}]{' ' * length}" for name in names))
        tim.print(
            "".join(
                f"[@{self.data[name]} #auto]"
                + (
                    f"[bold]{name:^{length}}[/]"
                    if name in keys
                    else name[-2:].center(length)
                )
                for name in names
            )
        )
        tim.print(
            "".join(
                f"[@{self.data[name]} #auto]{self.data[name]:^{length}}"
                for name in names
            )
        )
        tim.print("".join(f"[@{self.data[name]}]{' ' * length}" for name in names))

regenerate(**kwargs)

Generates a new palette and replaces self.data with its data.

Parameters:

Name Type Description Default
**kwargs Any

All key word args passed to the new Palette. See __init__.

{}

Returns:

Type Description
Palette

This palette, after regeneration.

Source code in pytermgui/palettes.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def regenerate(self, **kwargs: Any) -> Palette:
    """Generates a new palette and replaces self.data with its data.

    Args:
        **kwargs: All key word args passed to the new Palette. See `__init__`.

    Returns:
        This palette, after regeneration.
    """

    other = Palette(**kwargs)

    self.data = other.data
    self.alias()

    return self

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

PlainToken dataclass

Bases: Token

A plain piece of text.

These are the parts of data in-between markup tag groups.

Source code in pytermgui/markup/tokens.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
@dataclass(frozen=True, repr=False)
class PlainToken(Token):
    """A plain piece of text.

    These are the parts of data in-between markup tag groups.
    """

    __slots__ = ("value",)

    value: str

    def __repr__(self) -> str:
        return f"<{type(self).__name__} markup: {self.markup!r}>"

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        yield f"<{type(self).__name__} markup: {self.markup!r}>"

PseudoToken dataclass

Bases: Token

A token that can modify it's context, but doesn't hold information of its own.

Source code in pytermgui/markup/tokens.py
136
137
138
139
140
141
142
143
144
@dataclass(frozen=True, repr=False)
class PseudoToken(Token):
    """A token that can modify it's context, but doesn't hold information of its own."""

    value: str

    @cached_property
    def prettified_markup(self) -> str:
        return f"[245 italic]{self.markup}[/]"

RGBColor dataclass

Bases: Color

An arbitrary RGB color.

Source code in pytermgui/colors.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
@dataclass(repr=False)
class RGBColor(Color):
    """An arbitrary RGB color."""

    system = ColorSystem.TRUE

    def __post_init__(self) -> None:
        """Ensures data validity."""

        if self.value.count(";") != 2:
            raise ValueError(
                "Invalid value passed to RGBColor."
                + f" Format has to be rrr;ggg;bbb, got {self.value!r}."
            )

        rgb = tuple(int(num) for num in self.value.split(";"))
        self._rgb = rgb[0], rgb[1], rgb[2]

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Yields a fancy looking string."""

        yield (
            f"<{type(self).__name__} red: {self.red}, green: {self.green},"
            + f" blue: {self.blue}, preview: "
        )

        yield {"text": f"{self:seq}{PREVIEW_CHAR}\x1b[0m", "highlight": False}

        yield ">"

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> RGBColor:
        """Returns an `RGBColor` from the given triplet."""

        return cls(";".join(map(str, rgb)))

    @property
    def red(self) -> float:
        """Returns the red component of this color."""

        return self.rgb[0]

    @property
    def green(self) -> float:
        """Returns the green component of this color."""

        return self.rgb[1]

    @property
    def blue(self) -> float:
        """Returns the blue component of this color."""

        return self.rgb[2]

    @property
    def sequence(self) -> str:
        """Returns the ANSI sequence representing this color."""

        return (
            "\x1b["
            + ("48" if self.background else "38")
            + ";2;"
            + ";".join(str(num) for num in self.rgb)
            + "m"
        )

blue property

Returns the blue component of this color.

green property

Returns the green component of this color.

red property

Returns the red component of this color.

sequence property

Returns the ANSI sequence representing this color.

__fancy_repr__()

Yields a fancy looking string.

Source code in pytermgui/colors.py
737
738
739
740
741
742
743
744
745
746
747
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Yields a fancy looking string."""

    yield (
        f"<{type(self).__name__} red: {self.red}, green: {self.green},"
        + f" blue: {self.blue}, preview: "
    )

    yield {"text": f"{self:seq}{PREVIEW_CHAR}\x1b[0m", "highlight": False}

    yield ">"

__post_init__()

Ensures data validity.

Source code in pytermgui/colors.py
725
726
727
728
729
730
731
732
733
734
735
def __post_init__(self) -> None:
    """Ensures data validity."""

    if self.value.count(";") != 2:
        raise ValueError(
            "Invalid value passed to RGBColor."
            + f" Format has to be rrr;ggg;bbb, got {self.value!r}."
        )

    rgb = tuple(int(num) for num in self.value.split(";"))
    self._rgb = rgb[0], rgb[1], rgb[2]

from_rgb(rgb) classmethod

Returns an RGBColor from the given triplet.

Source code in pytermgui/colors.py
749
750
751
752
753
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> RGBColor:
    """Returns an `RGBColor` from the given triplet."""

    return cls(";".join(map(str, rgb)))

Recorder

A class that records & exports terminal content.

Source code in pytermgui/term.py
 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
class Recorder:
    """A class that records & exports terminal content."""

    def __init__(self) -> None:
        """Initializes the Recorder."""

        self.recording: list[tuple[str, float]] = []
        self._start_stamp = time.time()

    @property
    def _content(self) -> str:
        """Returns the str part of self._recording"""

        return "".join(data for data, _ in self.recording)

    def write(self, data: str) -> None:
        """Writes to the recorder."""

        self.recording.append((data, time.time() - self._start_stamp))

    def export_text(self) -> str:
        """Exports current content as plain text."""

        return strip_ansi(self._content)

    def export_html(
        self, prefix: str | None = None, inline_styles: bool = False
    ) -> str:
        """Exports current content as HTML.

        For help on the arguments, see `pytermgui.html.to_html`.
        """

        from .exporters import to_html  # pylint: disable=import-outside-toplevel

        return to_html(self._content, prefix=prefix, inline_styles=inline_styles)

    def export_svg(
        self,
        prefix: str | None = None,
        inline_styles: bool = False,
        title: str = "PyTermGUI",
        chrome: bool = True,
    ) -> str:
        """Exports current content as SVG.

        For help on the arguments, see `pytermgui.html.to_svg`.
        """

        from .exporters import to_svg  # pylint: disable=import-outside-toplevel

        return to_svg(
            self._content,
            prefix=prefix,
            inline_styles=inline_styles,
            title=title,
            chrome=chrome,
        )

    def save_plain(self, filename: str) -> None:
        """Exports plain text content to the given file.

        Args:
            filename: The file to save to.
        """

        with open(filename, "w", encoding="utf-8") as file:
            file.write(self.export_text())

    def save_html(
        self,
        filename: str | None = None,
        prefix: str | None = None,
        inline_styles: bool = False,
    ) -> None:
        """Exports HTML content to the given file.

        For help on the arguments, see `pytermgui.exporters.to_html`.

        Args:
            filename: The file to save to. If the filename does not contain the '.html'
                extension it will be appended to the end.
        """

        if filename is None:
            filename = f"PTG_{time.time():%Y-%m-%d %H:%M:%S}.html"

        if not filename.endswith(".html"):
            filename += ".html"

        with open(filename, "w", encoding="utf-8") as file:
            file.write(self.export_html(prefix=prefix, inline_styles=inline_styles))

    def save_svg(  # pylint: disable=too-many-arguments, R0917
        self,
        filename: str | None = None,
        prefix: str | None = None,
        chrome: bool = True,
        inline_styles: bool = False,
        title: str = "PyTermGUI",
    ) -> None:
        """Exports SVG content to the given file.

        For help on the arguments, see `pytermgui.exporters.to_svg`.

        Args:
            filename: The file to save to. If the filename does not contain the '.svg'
                extension it will be appended to the end.
        """

        if filename is None:
            timeval = datetime.now()
            filename = f"PTG_{timeval:%Y-%m-%d_%H:%M:%S}.svg"

        if not filename.endswith(".svg"):
            filename += ".svg"

        with open(filename, "w", encoding="utf-8") as file:
            file.write(
                self.export_svg(
                    prefix=prefix,
                    inline_styles=inline_styles,
                    title=title,
                    chrome=chrome,
                )
            )

__init__()

Initializes the Recorder.

Source code in pytermgui/term.py
40
41
42
43
44
def __init__(self) -> None:
    """Initializes the Recorder."""

    self.recording: list[tuple[str, float]] = []
    self._start_stamp = time.time()

export_html(prefix=None, inline_styles=False)

Exports current content as HTML.

For help on the arguments, see pytermgui.html.to_html.

Source code in pytermgui/term.py
62
63
64
65
66
67
68
69
70
71
72
def export_html(
    self, prefix: str | None = None, inline_styles: bool = False
) -> str:
    """Exports current content as HTML.

    For help on the arguments, see `pytermgui.html.to_html`.
    """

    from .exporters import to_html  # pylint: disable=import-outside-toplevel

    return to_html(self._content, prefix=prefix, inline_styles=inline_styles)

export_svg(prefix=None, inline_styles=False, title='PyTermGUI', chrome=True)

Exports current content as SVG.

For help on the arguments, see pytermgui.html.to_svg.

Source code in pytermgui/term.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def export_svg(
    self,
    prefix: str | None = None,
    inline_styles: bool = False,
    title: str = "PyTermGUI",
    chrome: bool = True,
) -> str:
    """Exports current content as SVG.

    For help on the arguments, see `pytermgui.html.to_svg`.
    """

    from .exporters import to_svg  # pylint: disable=import-outside-toplevel

    return to_svg(
        self._content,
        prefix=prefix,
        inline_styles=inline_styles,
        title=title,
        chrome=chrome,
    )

export_text()

Exports current content as plain text.

Source code in pytermgui/term.py
57
58
59
60
def export_text(self) -> str:
    """Exports current content as plain text."""

    return strip_ansi(self._content)

save_html(filename=None, prefix=None, inline_styles=False)

Exports HTML content to the given file.

For help on the arguments, see pytermgui.exporters.to_html.

Parameters:

Name Type Description Default
filename str | None

The file to save to. If the filename does not contain the '.html' extension it will be appended to the end.

None
Source code in pytermgui/term.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def save_html(
    self,
    filename: str | None = None,
    prefix: str | None = None,
    inline_styles: bool = False,
) -> None:
    """Exports HTML content to the given file.

    For help on the arguments, see `pytermgui.exporters.to_html`.

    Args:
        filename: The file to save to. If the filename does not contain the '.html'
            extension it will be appended to the end.
    """

    if filename is None:
        filename = f"PTG_{time.time():%Y-%m-%d %H:%M:%S}.html"

    if not filename.endswith(".html"):
        filename += ".html"

    with open(filename, "w", encoding="utf-8") as file:
        file.write(self.export_html(prefix=prefix, inline_styles=inline_styles))

save_plain(filename)

Exports plain text content to the given file.

Parameters:

Name Type Description Default
filename str

The file to save to.

required
Source code in pytermgui/term.py
 96
 97
 98
 99
100
101
102
103
104
def save_plain(self, filename: str) -> None:
    """Exports plain text content to the given file.

    Args:
        filename: The file to save to.
    """

    with open(filename, "w", encoding="utf-8") as file:
        file.write(self.export_text())

save_svg(filename=None, prefix=None, chrome=True, inline_styles=False, title='PyTermGUI')

Exports SVG content to the given file.

For help on the arguments, see pytermgui.exporters.to_svg.

Parameters:

Name Type Description Default
filename str | None

The file to save to. If the filename does not contain the '.svg' extension it will be appended to the end.

None
Source code in pytermgui/term.py
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
def save_svg(  # pylint: disable=too-many-arguments, R0917
    self,
    filename: str | None = None,
    prefix: str | None = None,
    chrome: bool = True,
    inline_styles: bool = False,
    title: str = "PyTermGUI",
) -> None:
    """Exports SVG content to the given file.

    For help on the arguments, see `pytermgui.exporters.to_svg`.

    Args:
        filename: The file to save to. If the filename does not contain the '.svg'
            extension it will be appended to the end.
    """

    if filename is None:
        timeval = datetime.now()
        filename = f"PTG_{timeval:%Y-%m-%d_%H:%M:%S}.svg"

    if not filename.endswith(".svg"):
        filename += ".svg"

    with open(filename, "w", encoding="utf-8") as file:
        file.write(
            self.export_svg(
                prefix=prefix,
                inline_styles=inline_styles,
                title=title,
                chrome=chrome,
            )
        )

write(data)

Writes to the recorder.

Source code in pytermgui/term.py
52
53
54
55
def write(self, data: str) -> None:
    """Writes to the recorder."""

    self.recording.append((data, time.time() - self._start_stamp))

RegexHighlighter dataclass

A class to highlight strings using regular expressions.

This class must be provided with a list of styles. These styles are really just a tuple of the markup alias name, and their associated RE patterns. If all aliases in the instance use the same prefix, it can be given under the prefix key and ommitted from the style names.

On construction, the instance will combine all of its patterns into a monster regex including named capturing groups. The general format is something like:

(?P<{name1}>{pattern1})|(?P<{name2}>{pattern2})|...

Calling this instance will then replace all matches, going in the order of definition, with style-injected versions. These follow the format:

[{prefix?}{name}]{content}[/{prefix}{name}]

Oddities to keep in mind: - Regex replace goes in the order of the defined groups, and is non-overlapping. Two groups cannot match the same text. - Because of how capturing groups work, everything within the patterns will be matched. To look for context around a match, look-around assertions can be used.

Source code in pytermgui/highlighters.py
 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
@dataclass
class RegexHighlighter:
    """A class to highlight strings using regular expressions.

    This class must be provided with a list of styles. These styles are really just a
    tuple of the markup alias name, and their associated RE patterns. If *all* aliases
    in the instance use the same prefix, it can be given under the `prefix` key and
    ommitted from the style names.

    On construction, the instance will combine all of its patterns into a monster regex
    including named capturing groups. The general format is something like:

        (?P<{name1}>{pattern1})|(?P<{name2}>{pattern2})|...

    Calling this instance will then replace all matches, going in the order of
    definition, with style-injected versions. These follow the format:

        [{prefix?}{name}]{content}[/{prefix}{name}]

    Oddities to keep in mind:
    - Regex replace goes in the order of the defined groups, and is non-overlapping. Two
        groups cannot match the same text.
    - Because of how capturing groups work, everything within the patterns will be
        matched. To look for context around a match, look-around assertions can be used.
    """

    styles: list[tuple[str, str]]
    """A list of tuples of (style_alias, pattern_str)."""

    prefix: str = ""
    """Some string to insert before each style alias."""

    pre_formatter: Callable[[str], str] | None = None
    """A callable that formats the input string, before any highlighting is done to it."""

    match_formatter: Callable[[Match, str], str] | None = None
    """A callable of (match, content) that gets called on every match.

    Its return value will be used as the content that the already set highlighting will apply
    to. Useful to trim text, or apply other transformations before inserting it back.
    """

    re_flags: int = 0
    """All regex flags to apply when compiling the generated pattern, OR-d (|) together."""

    _pattern: Pattern = field(init=False)
    _highlight_cache: dict[str, str] = field(init=False, default_factory=dict)

    def __post_init__(self) -> None:
        """Combines all styles into one pattern."""

        pattern = ""
        names: list[str] = []
        for name, ptrn in self.styles:
            pattern += f"(?P<{name}>{ptrn})|"
            names.append(name)

        pattern = pattern[:-1]

        self._pattern = re.compile(pattern, flags=self.re_flags)

    def __call__(self, text: str, cache: bool = True) -> str:
        """Highlights the given text, using the combined regex pattern."""

        if self.pre_formatter is not None:
            text = self.pre_formatter(text)

        if cache and text in self._highlight_cache:
            return self._highlight_cache[text]

        cache_key = text

        def _insert_style(matchobj: Match) -> str:
            """Returns the match inserted into a markup style."""

            groups = matchobj.groupdict()

            name = matchobj.lastgroup
            content = groups.get(str(name), None)

            if self.match_formatter is not None:
                content = self.match_formatter(matchobj, content)

                if content == "":
                    return ""

            tag = f"{self.prefix}{name}"
            style = f"[{tag}]{{}}[/{tag}]"

            return style.format(content)

        text = self._pattern.sub(_insert_style, text)
        self._highlight_cache[cache_key] = text

        return text

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Yields some fancy looking repr text."""

        preview = self("highlight_python()") + "\x1b[0m"
        pattern = self._pattern.pattern

        if len(pattern) > 40:
            pattern = pattern[:38] + "..."

        yield f"<{type(self).__name__} pattern: {pattern!r}, preview: "
        yield {"text": str(preview), "highlight": False}

        yield ">"

match_formatter = None class-attribute instance-attribute

A callable of (match, content) that gets called on every match.

Its return value will be used as the content that the already set highlighting will apply to. Useful to trim text, or apply other transformations before inserting it back.

pre_formatter = None class-attribute instance-attribute

A callable that formats the input string, before any highlighting is done to it.

prefix = '' class-attribute instance-attribute

Some string to insert before each style alias.

re_flags = 0 class-attribute instance-attribute

All regex flags to apply when compiling the generated pattern, OR-d (|) together.

styles instance-attribute

A list of tuples of (style_alias, pattern_str).

__call__(text, cache=True)

Highlights the given text, using the combined regex pattern.

Source code in pytermgui/highlighters.py
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
def __call__(self, text: str, cache: bool = True) -> str:
    """Highlights the given text, using the combined regex pattern."""

    if self.pre_formatter is not None:
        text = self.pre_formatter(text)

    if cache and text in self._highlight_cache:
        return self._highlight_cache[text]

    cache_key = text

    def _insert_style(matchobj: Match) -> str:
        """Returns the match inserted into a markup style."""

        groups = matchobj.groupdict()

        name = matchobj.lastgroup
        content = groups.get(str(name), None)

        if self.match_formatter is not None:
            content = self.match_formatter(matchobj, content)

            if content == "":
                return ""

        tag = f"{self.prefix}{name}"
        style = f"[{tag}]{{}}[/{tag}]"

        return style.format(content)

    text = self._pattern.sub(_insert_style, text)
    self._highlight_cache[cache_key] = text

    return text

__fancy_repr__()

Yields some fancy looking repr text.

Source code in pytermgui/highlighters.py
135
136
137
138
139
140
141
142
143
144
145
146
147
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Yields some fancy looking repr text."""

    preview = self("highlight_python()") + "\x1b[0m"
    pattern = self._pattern.pattern

    if len(pattern) > 40:
        pattern = pattern[:38] + "..."

    yield f"<{type(self).__name__} pattern: {pattern!r}, preview: "
    yield {"text": str(preview), "highlight": False}

    yield ">"

__post_init__()

Combines all styles into one pattern.

Source code in pytermgui/highlighters.py
87
88
89
90
91
92
93
94
95
96
97
98
def __post_init__(self) -> None:
    """Combines all styles into one pattern."""

    pattern = ""
    names: list[str] = []
    for name, ptrn in self.styles:
        pattern += f"(?P<{name}>{ptrn})|"
        names.append(name)

    pattern = pattern[:-1]

    self._pattern = re.compile(pattern, flags=self.re_flags)

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

Serializer

A class to facilitate loading & dumping widgets.

By default it is only aware of pytermgui objects, however if needed it can be made aware of custom widgets using Serializer.register.

It can dump any widget type, but can only load ones it knows.

All styles (except for char styles) are converted to markup during the dump process. This is done to make the end-result more readable, as well as more universally usable. As a result, all widgets use markup_style for their affected styles.

Source code in pytermgui/serialization.py
 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
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
class Serializer:
    """A class to facilitate loading & dumping widgets.

    By default it is only aware of pytermgui objects, however
    if needed it can be made aware of custom widgets using
    `Serializer.register`.

    It can dump any widget type, but can only load ones it knows.

    All styles (except for char styles) are converted to markup
    during the dump process. This is done to make the end-result
    more readable, as well as more universally usable. As a result,
    all widgets use `markup_style` for their affected styles."""

    def __init__(self) -> None:
        """Sets up known widgets."""

        self.known_widgets = self.get_widgets()
        self.known_boxes = vars(widgets.boxes)
        self.register(Window)

        self.bound_methods: dict[str, Callable[..., Any]] = {}

    @staticmethod
    def get_widgets() -> WidgetDict:
        """Gets all widgets from the module."""

        known = {}
        for name, item in vars(widgets).items():
            if not isinstance(item, type):
                continue

            if issubclass(item, Widget):
                known[name] = item

        return known

    @staticmethod
    def dump_to_dict(obj: Widget) -> dict[str, Any]:
        """Dump widget to a dict.

        This is an alias for `obj.serialize`.

        Args:
            obj: The widget to dump.

        Returns:
            `obj.serialize()`.
        """

        return obj.serialize()

    def register_box(self, name: str, box: widgets.boxes.Box) -> None:
        """Registers a new Box type.

        Args:
            name: The name of the box.
            box: The box instance.
        """

        self.known_boxes[name] = box

    def register(self, cls: Type[Widget]) -> None:
        """Makes object aware of a custom widget class, so
        it can be serialized.

        Args:
            cls: The widget type to register.

        Raises:
            TypeError: The object is not a type.
        """

        if not isinstance(cls, type):
            raise TypeError("Registered object must be a type.")

        self.known_widgets[cls.__name__] = cls

    def bind(self, name: str, method: Callable[..., Any]) -> None:
        """Binds a name to a method.

        These method callables are substituted into all fields that follow
        the `method:<method_name>` syntax. If `method_name` is not bound,
        an exception will be raised during loading.

        Args:
            name: The name of the method, as referenced in the loaded
                files.
            method: The callable to bind.
        """

        self.bound_methods[name] = method

    def from_dict(  # pylint: disable=too-many-locals, too-many-branches
        self, data: dict[str, Any], widget_type: str | None = None
    ) -> Widget:
        """Loads a widget from a dictionary.

        Args:
            data: The data to load from.
            widget_type: Substitute for when data has no `type` field.

        Returns:
            A widget from the given data.
        """

        def _apply_markup(value: CharType) -> CharType:
            """Apply markup style to obj's key"""

            formatted: CharType
            if isinstance(value, list):
                formatted = [tim.parse(val) for val in value]
            else:
                formatted = tim.parse(value)

            return formatted

        if widget_type is not None:
            data["type"] = widget_type

        obj_class_name = data.get("type")
        if obj_class_name is None:
            raise ValueError("Object with type None could not be loaded.")

        if obj_class_name not in self.known_widgets:
            raise ValueError(
                f'Object of type "{obj_class_name}" is not known!'
                + f" Register it with `serializer.register({obj_class_name})`."
            )

        del data["type"]

        obj_class = self.known_widgets.get(obj_class_name)
        assert obj_class is not None

        obj = obj_class()

        for key, value in data.items():
            if key.startswith("widgets"):
                for inner in value:
                    name, widget = list(inner.items())[0]
                    new = self.from_dict(widget, widget_type=name)
                    assert hasattr(obj, "__iadd__")

                    # this object can be added to, since
                    # it has an __iadd__ method.
                    obj += new  # type: ignore

                continue

            if isinstance(value, str) and value.startswith("method:"):
                name = value[7:]

                if name not in self.bound_methods:
                    raise KeyError(f'Reference to unbound method: "{name}".')

                value = self.bound_methods[name]

            if key == "chars":
                chars: dict[str, CharType] = {}
                for name, char in value.items():
                    chars[name] = _apply_markup(char)

                setattr(obj, "chars", chars)
                continue

            if key == "styles":
                for name, markup_str in value.items():
                    obj.styles[name] = markup_str

                continue

            setattr(obj, key, value)

        return obj

    def from_file(self, file: IO[str]) -> Widget:
        """Loads widget from a file object.

        Args:
            file: An IO object.

        Returns:
            The loaded widget.
        """

        return self.from_dict(json.load(file))

    def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
        """Dumps widget to a file object.

        Args:
            obj: The widget to dump.
            file: The file object it gets written to.
            **json_args: Arguments passed to `json.dump`.
        """

        data = self.dump_to_dict(obj)
        if "separators" not in json_args:
            # this is a sub-element of a dict[str, Any], so this
            # should work.
            json_args["separators"] = (",", ":")  # type: ignore

        # ** is supposed to be a dict, not a positional arg
        json.dump(data, file, **json_args)  # type: ignore

__init__()

Sets up known widgets.

Source code in pytermgui/serialization.py
36
37
38
39
40
41
42
43
def __init__(self) -> None:
    """Sets up known widgets."""

    self.known_widgets = self.get_widgets()
    self.known_boxes = vars(widgets.boxes)
    self.register(Window)

    self.bound_methods: dict[str, Callable[..., Any]] = {}

bind(name, method)

Binds a name to a method.

These method callables are substituted into all fields that follow the method:<method_name> syntax. If method_name is not bound, an exception will be raised during loading.

Parameters:

Name Type Description Default
name str

The name of the method, as referenced in the loaded files.

required
method Callable[..., Any]

The callable to bind.

required
Source code in pytermgui/serialization.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def bind(self, name: str, method: Callable[..., Any]) -> None:
    """Binds a name to a method.

    These method callables are substituted into all fields that follow
    the `method:<method_name>` syntax. If `method_name` is not bound,
    an exception will be raised during loading.

    Args:
        name: The name of the method, as referenced in the loaded
            files.
        method: The callable to bind.
    """

    self.bound_methods[name] = method

dump_to_dict(obj) staticmethod

Dump widget to a dict.

This is an alias for obj.serialize.

Parameters:

Name Type Description Default
obj Widget

The widget to dump.

required

Returns:

Type Description
dict[str, Any]

obj.serialize().

Source code in pytermgui/serialization.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@staticmethod
def dump_to_dict(obj: Widget) -> dict[str, Any]:
    """Dump widget to a dict.

    This is an alias for `obj.serialize`.

    Args:
        obj: The widget to dump.

    Returns:
        `obj.serialize()`.
    """

    return obj.serialize()

from_dict(data, widget_type=None)

Loads a widget from a dictionary.

Parameters:

Name Type Description Default
data dict[str, Any]

The data to load from.

required
widget_type str | None

Substitute for when data has no type field.

None

Returns:

Type Description
Widget

A widget from the given data.

Source code in pytermgui/serialization.py
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
def from_dict(  # pylint: disable=too-many-locals, too-many-branches
    self, data: dict[str, Any], widget_type: str | None = None
) -> Widget:
    """Loads a widget from a dictionary.

    Args:
        data: The data to load from.
        widget_type: Substitute for when data has no `type` field.

    Returns:
        A widget from the given data.
    """

    def _apply_markup(value: CharType) -> CharType:
        """Apply markup style to obj's key"""

        formatted: CharType
        if isinstance(value, list):
            formatted = [tim.parse(val) for val in value]
        else:
            formatted = tim.parse(value)

        return formatted

    if widget_type is not None:
        data["type"] = widget_type

    obj_class_name = data.get("type")
    if obj_class_name is None:
        raise ValueError("Object with type None could not be loaded.")

    if obj_class_name not in self.known_widgets:
        raise ValueError(
            f'Object of type "{obj_class_name}" is not known!'
            + f" Register it with `serializer.register({obj_class_name})`."
        )

    del data["type"]

    obj_class = self.known_widgets.get(obj_class_name)
    assert obj_class is not None

    obj = obj_class()

    for key, value in data.items():
        if key.startswith("widgets"):
            for inner in value:
                name, widget = list(inner.items())[0]
                new = self.from_dict(widget, widget_type=name)
                assert hasattr(obj, "__iadd__")

                # this object can be added to, since
                # it has an __iadd__ method.
                obj += new  # type: ignore

            continue

        if isinstance(value, str) and value.startswith("method:"):
            name = value[7:]

            if name not in self.bound_methods:
                raise KeyError(f'Reference to unbound method: "{name}".')

            value = self.bound_methods[name]

        if key == "chars":
            chars: dict[str, CharType] = {}
            for name, char in value.items():
                chars[name] = _apply_markup(char)

            setattr(obj, "chars", chars)
            continue

        if key == "styles":
            for name, markup_str in value.items():
                obj.styles[name] = markup_str

            continue

        setattr(obj, key, value)

    return obj

from_file(file)

Loads widget from a file object.

Parameters:

Name Type Description Default
file IO[str]

An IO object.

required

Returns:

Type Description
Widget

The loaded widget.

Source code in pytermgui/serialization.py
198
199
200
201
202
203
204
205
206
207
208
def from_file(self, file: IO[str]) -> Widget:
    """Loads widget from a file object.

    Args:
        file: An IO object.

    Returns:
        The loaded widget.
    """

    return self.from_dict(json.load(file))

get_widgets() staticmethod

Gets all widgets from the module.

Source code in pytermgui/serialization.py
45
46
47
48
49
50
51
52
53
54
55
56
57
@staticmethod
def get_widgets() -> WidgetDict:
    """Gets all widgets from the module."""

    known = {}
    for name, item in vars(widgets).items():
        if not isinstance(item, type):
            continue

        if issubclass(item, Widget):
            known[name] = item

    return known

register(cls)

Makes object aware of a custom widget class, so it can be serialized.

Parameters:

Name Type Description Default
cls Type[Widget]

The widget type to register.

required

Raises:

Type Description
TypeError

The object is not a type.

Source code in pytermgui/serialization.py
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def register(self, cls: Type[Widget]) -> None:
    """Makes object aware of a custom widget class, so
    it can be serialized.

    Args:
        cls: The widget type to register.

    Raises:
        TypeError: The object is not a type.
    """

    if not isinstance(cls, type):
        raise TypeError("Registered object must be a type.")

    self.known_widgets[cls.__name__] = cls

register_box(name, box)

Registers a new Box type.

Parameters:

Name Type Description Default
name str

The name of the box.

required
box Box

The box instance.

required
Source code in pytermgui/serialization.py
74
75
76
77
78
79
80
81
82
def register_box(self, name: str, box: widgets.boxes.Box) -> None:
    """Registers a new Box type.

    Args:
        name: The name of the box.
        box: The box instance.
    """

    self.known_boxes[name] = box

to_file(obj, file, **json_args)

Dumps widget to a file object.

Parameters:

Name Type Description Default
obj Widget

The widget to dump.

required
file IO[str]

The file object it gets written to.

required
**json_args dict[str, Any]

Arguments passed to json.dump.

{}
Source code in pytermgui/serialization.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def to_file(self, obj: Widget, file: IO[str], **json_args: dict[str, Any]) -> None:
    """Dumps widget to a file object.

    Args:
        obj: The widget to dump.
        file: The file object it gets written to.
        **json_args: Arguments passed to `json.dump`.
    """

    data = self.dump_to_dict(obj)
    if "separators" not in json_args:
        # this is a sub-element of a dict[str, Any], so this
        # should work.
        json_args["separators"] = (",", ":")  # type: ignore

    # ** is supposed to be a dict, not a positional arg
    json.dump(data, file, **json_args)  # type: ignore

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.

Slider

Bases: Widget

A Widget to display & configure scalable data.

By default, this Widget will act like a slider you might find in a settings page, allowing percentage-based selection of magnitude. Using WindowManager it can even be dragged around by the user using the mouse.

Source code in pytermgui/widgets/slider.py
 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
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
class Slider(Widget):  # pylint: disable=too-many-instance-attributes
    """A Widget to display & configure scalable data.

    By default, this Widget will act like a slider you might find in a
    settings page, allowing percentage-based selection of magnitude.
    Using `WindowManager` it can even be dragged around by the user using
    the mouse.
    """

    locked: bool
    """Disallow mouse input, hide cursor and lock current state"""

    chars = {"cursor": "", "rail": "━", "delimiter": ["[", "]"]}

    styles = w_styles.StyleManager(
        delimiter="surface",
        filled="surface+1",
        cursor="primary",
        filled_selected="primary",
        unfilled="surface-1",
        unfilled_selected="surface",
    )

    keys = {
        "increase": {keys.RIGHT, keys.CTRL_F, "l", "+"},
        "decrease": {keys.LEFT, keys.CTRL_B, "h", "-"},
    }

    def __init__(
        self,
        onchange: Callable[[float], Any] | None = None,
        locked: bool = False,
        **attrs: Any,
    ) -> None:
        """Initializes a Slider.

        Args:
            onchange: The callable called every time the value
                is updated.
            locked: Whether this Slider should accept value changes.
        """

        self._value = 0.0

        super().__init__(**attrs)
        self._selectables_length = 1

        self.is_locked = locked
        self.onchange = onchange

    @property
    def value(self) -> float:
        """Returns the value of this Slider.

        Returns:
            A floating point number between 0.0 and 1.0.
        """

        return self._value

    @value.setter
    def value(self, new: float) -> None:
        """Updates the value."""

        if self.is_locked:
            return

        self._value = max(0.0, min(new, 1.0))

        if self.onchange is not None:
            self.onchange(self._value)

    def handle_key(self, key: str) -> bool:
        """Moves the slider cursor."""

        if self.execute_binding(key):
            return True

        if key in self.keys["increase"]:
            self.value += 0.1
            return True

        if key in self.keys["decrease"]:
            self.value -= 0.1
            return True

        return False

    def handle_mouse(self, event: MouseEvent) -> bool:
        """Moves the slider cursor."""

        delimiter = self._get_char("delimiter")[0]

        if event.action in [MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG]:
            offset = event.position[0] - self.pos[0] + 1 - real_length(delimiter)
            self.value = max(0, min(offset / self.width, 1.0))
            return True

        return False

    def get_lines(self) -> list[str]:
        """Gets slider lines."""

        rail = self._get_char("rail")
        cursor = self._get_char("cursor") or rail
        delimiters = self._get_char("delimiter")

        assert isinstance(delimiters, list)
        assert isinstance(cursor, str)
        assert isinstance(rail, str)

        cursor = self._get_style("cursor")(cursor)
        unfilled = self.styles.unfilled(rail)

        if self.selected_index is None:
            filled = self.styles.filled(rail)
        else:
            filled = self.styles.filled_selected(rail)

            for i, char in enumerate(delimiters):
                delimiters[i] = self.styles.filled_selected(char)

        for i, delimiter in enumerate(delimiters):
            delimiters[i] = self.styles.delimiter(delimiter)

        width = self.width - real_length("".join(delimiters))
        count = width * self.value - 1

        chars = [delimiters[0]]

        for i in range(width):
            if i == count and not self.is_locked and self.selected_index is not None:
                chars.append(cursor)
                continue

            if i <= count:
                chars.append(filled)
                continue

            chars.append(unfilled)

        chars.append(delimiters[1])
        line = "".join(chars)
        self.width = real_length(line)

        return [line]

locked instance-attribute

Disallow mouse input, hide cursor and lock current state

value property writable

Returns the value of this Slider.

Returns:

Type Description
float

A floating point number between 0.0 and 1.0.

__init__(onchange=None, locked=False, **attrs)

Initializes a Slider.

Parameters:

Name Type Description Default
onchange Callable[[float], Any] | None

The callable called every time the value is updated.

None
locked bool

Whether this Slider should accept value changes.

False
Source code in pytermgui/widgets/slider.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def __init__(
    self,
    onchange: Callable[[float], Any] | None = None,
    locked: bool = False,
    **attrs: Any,
) -> None:
    """Initializes a Slider.

    Args:
        onchange: The callable called every time the value
            is updated.
        locked: Whether this Slider should accept value changes.
    """

    self._value = 0.0

    super().__init__(**attrs)
    self._selectables_length = 1

    self.is_locked = locked
    self.onchange = onchange

get_lines()

Gets slider lines.

Source code in pytermgui/widgets/slider.py
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
def get_lines(self) -> list[str]:
    """Gets slider lines."""

    rail = self._get_char("rail")
    cursor = self._get_char("cursor") or rail
    delimiters = self._get_char("delimiter")

    assert isinstance(delimiters, list)
    assert isinstance(cursor, str)
    assert isinstance(rail, str)

    cursor = self._get_style("cursor")(cursor)
    unfilled = self.styles.unfilled(rail)

    if self.selected_index is None:
        filled = self.styles.filled(rail)
    else:
        filled = self.styles.filled_selected(rail)

        for i, char in enumerate(delimiters):
            delimiters[i] = self.styles.filled_selected(char)

    for i, delimiter in enumerate(delimiters):
        delimiters[i] = self.styles.delimiter(delimiter)

    width = self.width - real_length("".join(delimiters))
    count = width * self.value - 1

    chars = [delimiters[0]]

    for i in range(width):
        if i == count and not self.is_locked and self.selected_index is not None:
            chars.append(cursor)
            continue

        if i <= count:
            chars.append(filled)
            continue

        chars.append(unfilled)

    chars.append(delimiters[1])
    line = "".join(chars)
    self.width = real_length(line)

    return [line]

handle_key(key)

Moves the slider cursor.

Source code in pytermgui/widgets/slider.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def handle_key(self, key: str) -> bool:
    """Moves the slider cursor."""

    if self.execute_binding(key):
        return True

    if key in self.keys["increase"]:
        self.value += 0.1
        return True

    if key in self.keys["decrease"]:
        self.value -= 0.1
        return True

    return False

handle_mouse(event)

Moves the slider cursor.

Source code in pytermgui/widgets/slider.py
106
107
108
109
110
111
112
113
114
115
116
def handle_mouse(self, event: MouseEvent) -> bool:
    """Moves the slider cursor."""

    delimiter = self._get_char("delimiter")[0]

    if event.action in [MouseAction.LEFT_CLICK, MouseAction.LEFT_DRAG]:
        offset = event.position[0] - self.pos[0] + 1 - real_length(delimiter)
        self.value = max(0, min(offset / self.width, 1.0))
        return True

    return False

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

StandardColor dataclass

Bases: IndexedColor

A color in the xterm-16 palette.

Source code in pytermgui/colors.py
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
class StandardColor(IndexedColor):
    """A color in the xterm-16 palette."""

    system = ColorSystem.STANDARD

    @property
    def name(self) -> str:
        """Returns the markup-compatible name for this color."""

        index = name = int(self.value)

        # Normal colors
        if 30 <= index <= 47:
            name -= 30

        elif 90 <= index <= 107:
            name -= 82

        return ("@" if self.background else "") + str(name)

    @classmethod
    def from_ansi(cls, code: str) -> StandardColor:
        """Creates a standard color from the given ANSI code.

        These codes have to be a digit ranging between 31 and 47.
        """

        if not code.isdigit():
            raise ColorSyntaxError(
                f"Standard color codes must be digits, not {code!r}."
            )

        code_int = int(code)

        if not 30 <= code_int <= 47 and not 90 <= code_int <= 107:
            raise ColorSyntaxError(
                f"Standard color codes must be in the range ]30;47[ or ]90;107[, got {code_int!r}."
            )

        is_background = 40 <= code_int <= 47 or 100 <= code_int <= 107

        if is_background:
            code_int -= 10

        return cls(str(code_int), background=is_background)

    @classmethod
    def from_rgb(cls, rgb: RGBTriplet) -> StandardColor:
        """Creates a color with the closest-matching xterm index, based on rgb.

        Args:
            rgb: The target color.
        """

        if rgb in _COLOR_MATCH_CACHE:
            color = _COLOR_MATCH_CACHE[rgb]

            if color.system is ColorSystem.STANDARD:
                assert isinstance(color, StandardColor)
                return color

        # Find the least-different color in the table
        index = min(range(16), key=lambda i: _get_color_difference(rgb, COLOR_TABLE[i]))

        if index > 7:
            index += 82
        else:
            index += 30

        color = cls(str(index))

        _COLOR_MATCH_CACHE[rgb] = color

        return color

    @property
    def sequence(self) -> str:
        r"""Returns an ANSI sequence representing this color."""

        index = int(self.value)

        if self.background:
            index += 10

        return f"\x1b[{index}m"

    @cached_property
    def rgb(self) -> RGBTriplet:
        """Returns an RGB representation of this color."""

        index = int(self.value)

        if 30 <= index <= 47:
            index -= 30

        elif 90 <= index <= 107:
            index -= 82

        rgb = COLOR_TABLE[index]

        return (rgb[0], rgb[1], rgb[2])

name property

Returns the markup-compatible name for this color.

rgb cached property

Returns an RGB representation of this color.

sequence property

Returns an ANSI sequence representing this color.

from_ansi(code) classmethod

Creates a standard color from the given ANSI code.

These codes have to be a digit ranging between 31 and 47.

Source code in pytermgui/colors.py
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
@classmethod
def from_ansi(cls, code: str) -> StandardColor:
    """Creates a standard color from the given ANSI code.

    These codes have to be a digit ranging between 31 and 47.
    """

    if not code.isdigit():
        raise ColorSyntaxError(
            f"Standard color codes must be digits, not {code!r}."
        )

    code_int = int(code)

    if not 30 <= code_int <= 47 and not 90 <= code_int <= 107:
        raise ColorSyntaxError(
            f"Standard color codes must be in the range ]30;47[ or ]90;107[, got {code_int!r}."
        )

    is_background = 40 <= code_int <= 47 or 100 <= code_int <= 107

    if is_background:
        code_int -= 10

    return cls(str(code_int), background=is_background)

from_rgb(rgb) classmethod

Creates a color with the closest-matching xterm index, based on rgb.

Parameters:

Name Type Description Default
rgb RGBTriplet

The target color.

required
Source code in pytermgui/colors.py
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
@classmethod
def from_rgb(cls, rgb: RGBTriplet) -> StandardColor:
    """Creates a color with the closest-matching xterm index, based on rgb.

    Args:
        rgb: The target color.
    """

    if rgb in _COLOR_MATCH_CACHE:
        color = _COLOR_MATCH_CACHE[rgb]

        if color.system is ColorSystem.STANDARD:
            assert isinstance(color, StandardColor)
            return color

    # Find the least-different color in the table
    index = min(range(16), key=lambda i: _get_color_difference(rgb, COLOR_TABLE[i]))

    if index > 7:
        index += 82
    else:
        index += 30

    color = cls(str(index))

    _COLOR_MATCH_CACHE[rgb] = color

    return color

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

StyleToken dataclass

Bases: Token

A terminal-style identifier.

Most terminals support a set of 9 styles:

  • bold
  • dim
  • italic
  • underline
  • blink
  • blink2
  • inverse
  • invisible
  • strikethrough

This token will store the style it represents by its name in the value field. Note that other, less widely supported styles may be available; for an up-to-date list, run ptg -i pytermgui.markup.style_maps.STYLES. ```

Source code in pytermgui/markup/tokens.py
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
@dataclass(frozen=True, repr=False)
class StyleToken(Token):
    """A terminal-style identifier.

    Most terminals support a set of 9 styles:

    - bold
    - dim
    - italic
    - underline
    - blink
    - blink2
    - inverse
    - invisible
    - strikethrough

    This token will store the style it represents by its name in the `value` field. Note
    that other, less widely supported styles *may* be available; for an up-to-date list,
    run `ptg -i pytermgui.markup.style_maps.STYLES`.
    ```

    """

    __slots__ = ("value",)

    value: str

StyledText dataclass

An ANSI style-infused string.

This is a sort of helper to handle ANSI texts in a more semantic manner. It keeps track of a sequence and a plain part.

Calling len() will return the length of the printable, non-ANSI part, and indexing will return the characters at the given slice, but also include the sequences that are applied to them.

To generate StyledText-s, it is recommended to use the StyledText.group_styles classmethod.

Source code in pytermgui/markup/language.py
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
@dataclass(frozen=True)
class StyledText:
    """An ANSI style-infused string.

    This is a sort of helper to handle ANSI texts in a more semantic manner. It
    keeps track of a sequence and a plain part.

    Calling `len()` will return the length of the printable, non-ANSI part, and
    indexing will return the characters at the given slice, but also include the
    sequences that are applied to them.

    To generate StyledText-s, it is recommended to use the `StyledText.group_styles`
    classmethod.
    """

    __slots__ = ("plain", "sequences", "tokens", "link", "__dict__")

    sequences: str
    plain: str
    tokens: list[Token]
    link: str | None

    @cached_property
    def foreground(self) -> Color | None:
        """Returns the foreground color of this object."""

        colors = [
            tkn
            for tkn in self.tokens
            if Token.is_color(tkn) and not tkn.color.background
        ]

        if len(colors) == 0:
            return None

        return colors[-1].color

    @cached_property
    def background(self) -> Color | None:
        """Returns the background color of this object."""

        colors = [
            tkn for tkn in self.tokens if Token.is_color(tkn) and tkn.color.background
        ]

        if len(colors) == 0:
            return None

        return colors[-1].color

    @cached_property
    def bold(self) -> bool:
        """Returns this text is bold."""

        return any(Token.is_style(tkn) and tkn.markup == "bold" for tkn in self.tokens)

    @cached_property
    def dim(self) -> bool:
        """Returns this text is dimmed."""

        return any(Token.is_style(tkn) and tkn.markup == "dim" for tkn in self.tokens)

    @cached_property
    def italic(self) -> bool:
        """Returns this text is italicized."""

        return any(
            Token.is_style(tkn) and tkn.markup == "italic" for tkn in self.tokens
        )

    @cached_property
    def underline(self) -> bool:
        """Returns this text is underlined."""

        return any(
            Token.is_style(tkn) and tkn.markup == "underline" for tkn in self.tokens
        )

    @cached_property
    def blink(self) -> bool:
        """Returns this text is blinking."""

        return any(Token.is_style(tkn) and tkn.markup == "blink" for tkn in self.tokens)

    @cached_property
    def blink2(self) -> bool:
        """Returns this text is alternate-blinking."""

        return any(
            Token.is_style(tkn) and tkn.markup == "blink2" for tkn in self.tokens
        )

    @cached_property
    def strikethrough(self) -> bool:
        """Returns this text is striked out."""

        return any(
            Token.is_style(tkn) and tkn.markup == "strikethrough" for tkn in self.tokens
        )

    @cached_property
    def inverse(self) -> bool:
        """Returns this text has its colors inversed."""

        return any(
            Token.is_style(tkn) and tkn.markup == "inverse" for tkn in self.tokens
        )

    @cached_property
    def overline(self) -> bool:
        """Returns this text is overlined."""

        return any(
            Token.is_style(tkn) and tkn.markup == "overline" for tkn in self.tokens
        )

    @staticmethod
    def group_styles(
        text: str,
        tokenizer: Tokenizer = tokenize_ansi,
        context: ContextDict | None = None,
    ) -> Generator[StyledText, None, None]:
        """Yields StyledTexts from an ANSI coded string.

        A new StyledText will be created each time a non-plain token follows a
        plain token, thus all texts will represent a single (ANSI)PLAIN group
        of characters.
        """

        context = context if context is not None else create_context_dict()

        parsers = PARSERS
        link = None

        def _parse(token: Token) -> str:
            nonlocal link

            if token.is_macro():
                return token.markup

            if token.is_hyperlink():
                link = token
                return ""

            if link is not None and Token.is_clear(token) and token.targets(link):
                link = None

            if token.is_clear() and token.value not in CLEARERS:
                return token.markup

            # The full text (last arg) is not relevant here, as ANSI parsing doesn't
            # use any context-defined tags, so no errors will occur.
            return parsers[type(token)](token, context, lambda: "")  # type: ignore

        tokens: list[Token] = []
        token: Token

        for token in tokenizer(text):
            if token.is_plain():
                yield StyledText(
                    "".join(_parse(tkn) for tkn in tokens),
                    token.value,
                    tokens + [token],
                    link.value if link is not None else None,
                )

                tokens = [tkn for tkn in tokens if not tkn.is_cursor()]
                continue

            if Token.is_clear(token):
                tokens = [tkn for tkn in tokens if not token.targets(tkn)]

                if len(tokens) > 0 and tokens[-1] == token:
                    continue

            if len(tokens) > 0 and all(tkn.is_clear() for tkn in tokens):
                tokens = []

            tokens.append(token)

        # if len(tokens) > 0:
        #     token = PlainToken("")

        #     yield StyledText(
        #         "".join(_parse(tkn) for tkn in tokens),
        #         token.value,
        #         tokens + [token],
        #         link.value if link is not None else None,
        #     )

    @classmethod
    def first_of(cls, text: str) -> StyledText | None:
        """Returns the first element of cls.group_styles(text)."""

        for item in cls.group_styles(text):
            return item

        return None

    def __len__(self) -> int:
        return len(self.plain)

    def __str__(self) -> str:
        return self.sequences + self.plain

    def __getitem__(self, sli: int | slice) -> str:
        return self.sequences + self.plain[sli]

background cached property

Returns the background color of this object.

Returns this text is blinking.

blink2 cached property

Returns this text is alternate-blinking.

bold cached property

Returns this text is bold.

dim cached property

Returns this text is dimmed.

foreground cached property

Returns the foreground color of this object.

inverse cached property

Returns this text has its colors inversed.

italic cached property

Returns this text is italicized.

overline cached property

Returns this text is overlined.

strikethrough cached property

Returns this text is striked out.

underline cached property

Returns this text is underlined.

first_of(text) classmethod

Returns the first element of cls.group_styles(text).

Source code in pytermgui/markup/language.py
481
482
483
484
485
486
487
488
@classmethod
def first_of(cls, text: str) -> StyledText | None:
    """Returns the first element of cls.group_styles(text)."""

    for item in cls.group_styles(text):
        return item

    return None

group_styles(text, tokenizer=tokenize_ansi, context=None) staticmethod

Yields StyledTexts from an ANSI coded string.

A new StyledText will be created each time a non-plain token follows a plain token, thus all texts will represent a single (ANSI)PLAIN group of characters.

Source code in pytermgui/markup/language.py
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
@staticmethod
def group_styles(
    text: str,
    tokenizer: Tokenizer = tokenize_ansi,
    context: ContextDict | None = None,
) -> Generator[StyledText, None, None]:
    """Yields StyledTexts from an ANSI coded string.

    A new StyledText will be created each time a non-plain token follows a
    plain token, thus all texts will represent a single (ANSI)PLAIN group
    of characters.
    """

    context = context if context is not None else create_context_dict()

    parsers = PARSERS
    link = None

    def _parse(token: Token) -> str:
        nonlocal link

        if token.is_macro():
            return token.markup

        if token.is_hyperlink():
            link = token
            return ""

        if link is not None and Token.is_clear(token) and token.targets(link):
            link = None

        if token.is_clear() and token.value not in CLEARERS:
            return token.markup

        # The full text (last arg) is not relevant here, as ANSI parsing doesn't
        # use any context-defined tags, so no errors will occur.
        return parsers[type(token)](token, context, lambda: "")  # type: ignore

    tokens: list[Token] = []
    token: Token

    for token in tokenizer(text):
        if token.is_plain():
            yield StyledText(
                "".join(_parse(tkn) for tkn in tokens),
                token.value,
                tokens + [token],
                link.value if link is not None else None,
            )

            tokens = [tkn for tkn in tokens if not tkn.is_cursor()]
            continue

        if Token.is_clear(token):
            tokens = [tkn for tkn in tokens if not token.targets(tkn)]

            if len(tokens) > 0 and tokens[-1] == token:
                continue

        if len(tokens) > 0 and all(tkn.is_clear() for tkn in tokens):
            tokens = []

        tokens.append(token)

SupportsFancyRepr

Bases: Protocol

An object that supports the __fancy_repr__ dunder.

Source code in pytermgui/fancy_repr.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class SupportsFancyRepr(Protocol):  # pylint: disable=too-few-public-methods
    """An object that supports the `__fancy_repr__` dunder."""

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Yields some fancy text.

        Each value yielded can be one of two types. If a dictionary is yielded,
        it will be assumed to have `text` and `highlight` fields. `text` will be
        the string included in the repr, and `highlight` will be a boolean describing
        whether the part should be highlighted. At the moment highlighting is done by
        `highlight_python`, but this might be configurable once more highlighters are
        available.

        If a `str` is yielded, it is assumed to be a shorthand for:

            {"text": <your_text>, "highlight": True}
        """

__fancy_repr__()

Yields some fancy text.

Each value yielded can be one of two types. If a dictionary is yielded, it will be assumed to have text and highlight fields. text will be the string included in the repr, and highlight will be a boolean describing whether the part should be highlighted. At the moment highlighting is done by highlight_python, but this might be configurable once more highlighters are available.

If a str is yielded, it is assumed to be a shorthand for:

{"text": <your_text>, "highlight": True}
Source code in pytermgui/fancy_repr.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Yields some fancy text.

    Each value yielded can be one of two types. If a dictionary is yielded,
    it will be assumed to have `text` and `highlight` fields. `text` will be
    the string included in the repr, and `highlight` will be a boolean describing
    whether the part should be highlighted. At the moment highlighting is done by
    `highlight_python`, but this might be configurable once more highlighters are
    available.

    If a `str` is yielded, it is assumed to be a shorthand for:

        {"text": <your_text>, "highlight": True}
    """

Terminal

A class to store & access data about a terminal.

Source code in pytermgui/term.py
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
class Terminal:  # pylint: disable=too-many-instance-attributes
    """A class to store & access data about a terminal."""

    RESIZE = 0
    """Event sent out when the terminal has been resized.

    Arguments passed:
    - New size: tuple[int, int]
    """

    margins = [0, 0, 0, 0]
    """Not quite sure what this does at the moment."""

    displayhook_installed: bool = False
    """This is set to True when `pretty.install` is called."""

    origin: tuple[int, int] = (1, 1)
    """Origin of the internal coordinate system."""

    def __init__(
        self,
        stream: TextIO | None = None,
        *,
        size: tuple[int, int] | None = None,
    ) -> None:
        """Initialize `Terminal` class."""

        if stream is None:
            stream = sys.stdout

        self._size = size
        self._stream = stream or sys.stdout

        self._recorder: Recorder | None = None

        self.size: tuple[int, int] = self._get_size()
        self.forced_colorsystem: ColorSystem | None = _get_env_colorsys()

        self._listeners: dict[int, list[Callable[..., Any]]] = {}

        # Async-signal-safe resize mechanism
        self._resize_pending = threading.Event()

        if hasattr(signal, "SIGWINCH"):
            signal.signal(signal.SIGWINCH, self._update_size)
        else:
            from threading import Thread  # pylint: disable=import-outside-toplevel

            Thread(
                name="windows_terminal_resize",
                target=self._window_terminal_resize,
                daemon=True,
            ).start()

        self._diff_buffer = [
            ["" for _ in range(self.width)] for y in range(self.height)
        ]

    def _window_terminal_resize(self) -> None:
        from time import sleep  # pylint: disable=import-outside-toplevel

        _previous = get_terminal_size()
        while True:
            _next = get_terminal_size()
            if _previous != _next:
                self._resize_pending.set()
                _previous = _next
            sleep(0.01)

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        """Returns a cool looking repr."""

        name = type(self).__name__

        yield f"<{name} stream={self._stream} size={self.size}>"

    @cached_property
    def resolution(self) -> tuple[int, int]:
        """Returns the terminal's pixel based resolution.

        Only evaluated on demand.
        """

        if self.isatty():
            sys.stdout.write("\x1b[14t")
            sys.stdout.flush()

            # Some terminals may not respond to a pixel size query, so we send
            # a timed-out getch call with a default response of 1280x720.
            output = getch_timeout(0.1, default="\x1b[4;720;1280t")
            match = RE_PIXEL_SIZE.match(output)

            if match is not None:
                return (int(match[2]), int(match[1]))

        return (0, 0)

    @property
    def pixel_size(self) -> tuple[int, int]:
        """DEPRECATED: Returns the terminal's pixel resolution.

        Prefer terminal.resolution.
        """

        return self.resolution

    def _call_listener(self, event: int, data: Any) -> None:
        """Calls callbacks for event.

        Args:
            event: A terminal event.
            data: Arbitrary data passed to the callback.
        """

        if event in self._listeners:
            for callback in self._listeners[event]:
                callback(data)

    def _get_size(self) -> tuple[int, int]:
        """Gets the screen size with origin substracted."""

        if self._size is not None:
            return self._size

        size = get_terminal_size()
        return (size[0], size[1])

    def _update_size(self, *_: Any) -> None:
        """Signal handler for SIGWINCH - ONLY sets flag (async-signal-safe)."""

        self._resize_pending.set()

    def process_pending_resize(self) -> bool:
        """Process pending resize event if one is queued.

        Call this periodically from the main event loop.

        :returns: True if a resize was processed.
        """
        if not self._resize_pending.is_set():
            return False

        self._resize_pending.clear()

        # Check __dict__ directly to avoid triggering the cached_property getter,
        # which uses signals and can only run in the main thread
        if "resolution" in self.__dict__:
            del self.__dict__["resolution"]

        self.size = self._get_size()
        self._call_listener(self.RESIZE, self.size)

        # Wipe the screen in case anything got messed up
        self.write("\x1b[H\x1b[2J")

        return True

    @property
    def width(self) -> int:
        """Gets the current width of the terminal."""

        return self.size[0]

    @property
    def height(self) -> int:
        """Gets the current height of the terminal."""

        return self.size[1]

    @staticmethod
    def is_interactive() -> bool:
        """Determines whether shell is interactive.

        A shell is interactive if it is run from `python3` or `python3 -i`.
        """

        return hasattr(sys, "ps1")

    @property
    def forced_colorsystem(self) -> ColorSystem | None:
        """Forces a color system type on this terminal."""

        return self._forced_colorsystem

    @forced_colorsystem.setter
    def forced_colorsystem(self, new: ColorSystem | None) -> None:
        """Sets a colorsystem, clears colorsystem cache."""

        self._forced_colorsystem = new

    @property
    def colorsystem(self) -> ColorSystem:
        """Gets the current terminal's supported color system."""

        if self.forced_colorsystem is not None:
            return self.forced_colorsystem

        if os.getenv("NO_COLOR") is not None:
            return ColorSystem.NO_COLOR

        term = os.getenv("TERM", "")
        color_term = os.getenv("COLORTERM", "").strip().lower()

        if color_term == "":
            color_term = term.split("xterm-")[-1]

        if color_term in ["24bit", "truecolor"]:
            return ColorSystem.TRUE

        if color_term == "256color":
            return ColorSystem.EIGHT_BIT

        return ColorSystem.STANDARD

    @contextmanager
    def record(self) -> Generator[Recorder, None, None]:
        """Records the terminal's stream."""

        if self._recorder is not None:
            raise RuntimeError(f"{self!r} is already recording.")

        try:
            self._recorder = Recorder()
            yield self._recorder

        finally:
            self._recorder = None

    @contextmanager
    def no_record(self) -> Generator[None, None, None]:
        """Pauses recording for the duration of the context."""

        recorder = self._recorder

        try:
            self._recorder = None
            yield

        finally:
            self._recorder = recorder

    @contextmanager
    def frame(self) -> Generator[StringIO, None, None]:
        """Notifies the emulator of the inner content being a single frame.

        See https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036!
        """

        buffer = StringIO()

        try:
            # Write directly to stream to avoid write()'s auto-clear behavior
            self._stream.write("\x1b[?2026h")
            yield buffer

        finally:
            content = buffer.getvalue()
            self._stream.write(content)

            # Frame contents bypass write() so they are sent atomically to the output
            # stream. Forward the visual content to an active recorder explicitly.
            if self._recorder is not None:
                self._recorder.write(content)

            self._stream.write("\x1b[?2026l")
            self._stream.flush()

    @staticmethod
    def isatty() -> bool:
        """Returns whether sys.stdin is a tty."""

        return sys.stdin.isatty()

    def replay(self, recorder: Recorder) -> None:
        """Replays a recording."""

        last_time = 0.0
        for data, delay in recorder.recording:
            if last_time > 0.0:
                time.sleep(delay - last_time)

            self.write(data, flush=True)
            last_time = delay

    def subscribe(self, event: int, callback: Callable[..., Any]) -> None:
        """Subcribes a callback to be called when event occurs.

        Args:
            event: The terminal event that calls callback.
            callback: The callable to be called. The signature of this
                callable is dependent on the event. See the documentation
                of the specific event for more information.
        """

        if not event in self._listeners:
            self._listeners[event] = []

        self._listeners[event].append(callback)

    def write(
        self,
        data: str,
        pos: tuple[int, int] | None = None,
        flush: bool = False,
        slice_too_long: bool = True,
    ) -> None:
        """Writes the given data to the terminal's stream.

        Args:
            data: The data to write.
            pos: Terminal-character space position to write the data to, (x, y).
            flush: If set, `flush` will be called on the stream after reading.
            slice_too_long: If set, lines that are outside of the terminal will be
                sliced to fit. Involves a sizable performance hit.
        """

        def _slice(line: str, maximum: int) -> str:
            length = 0
            sliced = ""
            for char in line:
                sliced += char
                if char == "\x1b":
                    continue

                if (
                    length > maximum
                    and real_length(sliced) > maximum
                    and not has_open_sequence(sliced)
                ):
                    break

                length += 1

            return sliced

        # Truncate pending buffer on clear (may help on Windows)
        if "\x1b[2J" in data:
            self.clear_stream()

        if pos is not None:
            xpos, ypos = pos
            xpos += self.origin[0]
            ypos += self.origin[1]

            if slice_too_long:
                if not self.height + self.origin[1] + 1 > ypos >= 0:
                    return

                maximum = self.width - xpos

                xpos = max(xpos, self.origin[0])

                sliced = _slice(data, maximum) if len(data) > maximum else data

                data = f"\x1b[{ypos};{xpos}H{sliced}\x1b[0m"

            else:
                data = f"\x1b[{ypos};{xpos}H{data}"

        self._stream.write(data)

        if self._recorder is not None:
            self._recorder.write(data)

        if flush:
            self._stream.flush()

    def clear_stream(self) -> None:
        """Clears the terminal screen.

        Attempts to truncate any buffered stream data (may work on Windows),
        then moves cursor to home position and clears the entire screen.
        """

        try:
            self._stream.truncate(0)

        except OSError as error:
            if error.errno != errno.EINVAL and os.name != "nt":
                raise

        self._stream.write("\x1b[H\x1b[2J")

    def print(
        self,
        *items,
        pos: tuple[int, int] | None = None,
        sep: str = " ",
        end="\n",
        flush: bool = True,
    ) -> None:
        """Prints items to the stream.

        All arguments not mentioned here are analogous to `print`.

        Args:
            pos: Terminal-character space position to write the data to, (x, y).

        """

        self.write(sep.join(map(str, items)) + end, pos=pos, flush=flush)

    def flush(self) -> None:
        """Flushes self._stream."""

        self._stream.flush()

RESIZE = 0 class-attribute instance-attribute

Event sent out when the terminal has been resized.

Arguments passed: - New size: tuple[int, int]

colorsystem property

Gets the current terminal's supported color system.

displayhook_installed = False class-attribute instance-attribute

This is set to True when pretty.install is called.

forced_colorsystem property writable

Forces a color system type on this terminal.

height property

Gets the current height of the terminal.

margins = [0, 0, 0, 0] class-attribute instance-attribute

Not quite sure what this does at the moment.

origin = (1, 1) class-attribute instance-attribute

Origin of the internal coordinate system.

pixel_size property

DEPRECATED: Returns the terminal's pixel resolution.

Prefer terminal.resolution.

resolution cached property

Returns the terminal's pixel based resolution.

Only evaluated on demand.

width property

Gets the current width of the terminal.

__fancy_repr__()

Returns a cool looking repr.

Source code in pytermgui/term.py
296
297
298
299
300
301
def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
    """Returns a cool looking repr."""

    name = type(self).__name__

    yield f"<{name} stream={self._stream} size={self.size}>"

__init__(stream=None, *, size=None)

Initialize Terminal class.

Source code in pytermgui/term.py
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
def __init__(
    self,
    stream: TextIO | None = None,
    *,
    size: tuple[int, int] | None = None,
) -> None:
    """Initialize `Terminal` class."""

    if stream is None:
        stream = sys.stdout

    self._size = size
    self._stream = stream or sys.stdout

    self._recorder: Recorder | None = None

    self.size: tuple[int, int] = self._get_size()
    self.forced_colorsystem: ColorSystem | None = _get_env_colorsys()

    self._listeners: dict[int, list[Callable[..., Any]]] = {}

    # Async-signal-safe resize mechanism
    self._resize_pending = threading.Event()

    if hasattr(signal, "SIGWINCH"):
        signal.signal(signal.SIGWINCH, self._update_size)
    else:
        from threading import Thread  # pylint: disable=import-outside-toplevel

        Thread(
            name="windows_terminal_resize",
            target=self._window_terminal_resize,
            daemon=True,
        ).start()

    self._diff_buffer = [
        ["" for _ in range(self.width)] for y in range(self.height)
    ]

clear_stream()

Clears the terminal screen.

Attempts to truncate any buffered stream data (may work on Windows), then moves cursor to home position and clears the entire screen.

Source code in pytermgui/term.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
def clear_stream(self) -> None:
    """Clears the terminal screen.

    Attempts to truncate any buffered stream data (may work on Windows),
    then moves cursor to home position and clears the entire screen.
    """

    try:
        self._stream.truncate(0)

    except OSError as error:
        if error.errno != errno.EINVAL and os.name != "nt":
            raise

    self._stream.write("\x1b[H\x1b[2J")

flush()

Flushes self._stream.

Source code in pytermgui/term.py
629
630
631
632
def flush(self) -> None:
    """Flushes self._stream."""

    self._stream.flush()

frame()

Notifies the emulator of the inner content being a single frame.

See https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036!

Source code in pytermgui/term.py
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
@contextmanager
def frame(self) -> Generator[StringIO, None, None]:
    """Notifies the emulator of the inner content being a single frame.

    See https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036!
    """

    buffer = StringIO()

    try:
        # Write directly to stream to avoid write()'s auto-clear behavior
        self._stream.write("\x1b[?2026h")
        yield buffer

    finally:
        content = buffer.getvalue()
        self._stream.write(content)

        # Frame contents bypass write() so they are sent atomically to the output
        # stream. Forward the visual content to an active recorder explicitly.
        if self._recorder is not None:
            self._recorder.write(content)

        self._stream.write("\x1b[?2026l")
        self._stream.flush()

is_interactive() staticmethod

Determines whether shell is interactive.

A shell is interactive if it is run from python3 or python3 -i.

Source code in pytermgui/term.py
396
397
398
399
400
401
402
403
@staticmethod
def is_interactive() -> bool:
    """Determines whether shell is interactive.

    A shell is interactive if it is run from `python3` or `python3 -i`.
    """

    return hasattr(sys, "ps1")

isatty() staticmethod

Returns whether sys.stdin is a tty.

Source code in pytermgui/term.py
494
495
496
497
498
@staticmethod
def isatty() -> bool:
    """Returns whether sys.stdin is a tty."""

    return sys.stdin.isatty()

no_record()

Pauses recording for the duration of the context.

Source code in pytermgui/term.py
455
456
457
458
459
460
461
462
463
464
465
466
@contextmanager
def no_record(self) -> Generator[None, None, None]:
    """Pauses recording for the duration of the context."""

    recorder = self._recorder

    try:
        self._recorder = None
        yield

    finally:
        self._recorder = recorder

print(*items, pos=None, sep=' ', end='\n', flush=True)

Prints items to the stream.

All arguments not mentioned here are analogous to print.

Parameters:

Name Type Description Default
pos tuple[int, int] | None

Terminal-character space position to write the data to, (x, y).

None
Source code in pytermgui/term.py
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
def print(
    self,
    *items,
    pos: tuple[int, int] | None = None,
    sep: str = " ",
    end="\n",
    flush: bool = True,
) -> None:
    """Prints items to the stream.

    All arguments not mentioned here are analogous to `print`.

    Args:
        pos: Terminal-character space position to write the data to, (x, y).

    """

    self.write(sep.join(map(str, items)) + end, pos=pos, flush=flush)

process_pending_resize()

Process pending resize event if one is queued.

Call this periodically from the main event loop.

:returns: True if a resize was processed.

Source code in pytermgui/term.py
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
def process_pending_resize(self) -> bool:
    """Process pending resize event if one is queued.

    Call this periodically from the main event loop.

    :returns: True if a resize was processed.
    """
    if not self._resize_pending.is_set():
        return False

    self._resize_pending.clear()

    # Check __dict__ directly to avoid triggering the cached_property getter,
    # which uses signals and can only run in the main thread
    if "resolution" in self.__dict__:
        del self.__dict__["resolution"]

    self.size = self._get_size()
    self._call_listener(self.RESIZE, self.size)

    # Wipe the screen in case anything got messed up
    self.write("\x1b[H\x1b[2J")

    return True

record()

Records the terminal's stream.

Source code in pytermgui/term.py
441
442
443
444
445
446
447
448
449
450
451
452
453
@contextmanager
def record(self) -> Generator[Recorder, None, None]:
    """Records the terminal's stream."""

    if self._recorder is not None:
        raise RuntimeError(f"{self!r} is already recording.")

    try:
        self._recorder = Recorder()
        yield self._recorder

    finally:
        self._recorder = None

replay(recorder)

Replays a recording.

Source code in pytermgui/term.py
500
501
502
503
504
505
506
507
508
509
def replay(self, recorder: Recorder) -> None:
    """Replays a recording."""

    last_time = 0.0
    for data, delay in recorder.recording:
        if last_time > 0.0:
            time.sleep(delay - last_time)

        self.write(data, flush=True)
        last_time = delay

subscribe(event, callback)

Subcribes a callback to be called when event occurs.

Parameters:

Name Type Description Default
event int

The terminal event that calls callback.

required
callback Callable[..., Any]

The callable to be called. The signature of this callable is dependent on the event. See the documentation of the specific event for more information.

required
Source code in pytermgui/term.py
511
512
513
514
515
516
517
518
519
520
521
522
523
524
def subscribe(self, event: int, callback: Callable[..., Any]) -> None:
    """Subcribes a callback to be called when event occurs.

    Args:
        event: The terminal event that calls callback.
        callback: The callable to be called. The signature of this
            callable is dependent on the event. See the documentation
            of the specific event for more information.
    """

    if not event in self._listeners:
        self._listeners[event] = []

    self._listeners[event].append(callback)

write(data, pos=None, flush=False, slice_too_long=True)

Writes the given data to the terminal's stream.

Parameters:

Name Type Description Default
data str

The data to write.

required
pos tuple[int, int] | None

Terminal-character space position to write the data to, (x, y).

None
flush bool

If set, flush will be called on the stream after reading.

False
slice_too_long bool

If set, lines that are outside of the terminal will be sliced to fit. Involves a sizable performance hit.

True
Source code in pytermgui/term.py
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
def write(
    self,
    data: str,
    pos: tuple[int, int] | None = None,
    flush: bool = False,
    slice_too_long: bool = True,
) -> None:
    """Writes the given data to the terminal's stream.

    Args:
        data: The data to write.
        pos: Terminal-character space position to write the data to, (x, y).
        flush: If set, `flush` will be called on the stream after reading.
        slice_too_long: If set, lines that are outside of the terminal will be
            sliced to fit. Involves a sizable performance hit.
    """

    def _slice(line: str, maximum: int) -> str:
        length = 0
        sliced = ""
        for char in line:
            sliced += char
            if char == "\x1b":
                continue

            if (
                length > maximum
                and real_length(sliced) > maximum
                and not has_open_sequence(sliced)
            ):
                break

            length += 1

        return sliced

    # Truncate pending buffer on clear (may help on Windows)
    if "\x1b[2J" in data:
        self.clear_stream()

    if pos is not None:
        xpos, ypos = pos
        xpos += self.origin[0]
        ypos += self.origin[1]

        if slice_too_long:
            if not self.height + self.origin[1] + 1 > ypos >= 0:
                return

            maximum = self.width - xpos

            xpos = max(xpos, self.origin[0])

            sliced = _slice(data, maximum) if len(data) > maximum else data

            data = f"\x1b[{ypos};{xpos}H{sliced}\x1b[0m"

        else:
            data = f"\x1b[{ypos};{xpos}H{data}"

    self._stream.write(data)

    if self._recorder is not None:
        self._recorder.write(data)

    if flush:
        self._stream.flush()

Toggle

Bases: Checkbox

A specialized checkbox showing either of two states

Source code in pytermgui/widgets/toggle.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Toggle(Checkbox):
    """A specialized checkbox showing either of two states"""

    chars = {**Checkbox.chars, **{"delimiter": [" ", " "], "checked": "choose"}}

    def __init__(
        self,
        states: tuple[str, str],
        callback: Callable[[str], Any] | None = None,
        **attrs: Any,
    ) -> None:
        """Initialize object"""

        self.states = states

        self.set_char("checked", states[0])
        self.set_char("unchecked", states[1])

        super().__init__(callback, **attrs)
        self.toggle(run_callback=False)

    def _run_callback(self) -> None:
        """Run the toggle callback with the label as its argument"""

        if self.callback is not None:
            self.callback(self.label)

__init__(states, callback=None, **attrs)

Initialize object

Source code in pytermgui/widgets/toggle.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def __init__(
    self,
    states: tuple[str, str],
    callback: Callable[[str], Any] | None = None,
    **attrs: Any,
) -> None:
    """Initialize object"""

    self.states = states

    self.set_char("checked", states[0])
    self.set_char("unchecked", states[1])

    super().__init__(callback, **attrs)
    self.toggle(run_callback=False)

Token

A piece of markup information.

All tokens must have at least a value field, and have markup and prettified_markup properties derived from it in some manner.

They are meant to be immutable (frozen), and generated by some tokenization. They are also static representations of the data in its pre-parsed form.

Source code in pytermgui/markup/tokens.py
 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
class Token:
    """A piece of markup information.

    All tokens must have at least a `value` field, and have `markup` and `prettified_markup`
    properties derived from it in some manner.

    They are meant to be immutable (frozen), and generated by some tokenization. They are also
    static representations of the data in its pre-parsed form.
    """

    value: str

    @cached_property
    def markup(self) -> str:
        """Returns markup representing this token."""

        return self.value

    @cached_property
    def prettified_markup(self) -> str:
        """Returns syntax-highlighted markup representing this token."""

        return f"[{self.markup}]{self.markup}[/{self.markup}]"

    def __eq__(self, other: object) -> bool:
        return isinstance(other, type(self)) and other.value == self.value

    def __repr__(self) -> str:
        return f"<{type(self).__name__} markup: '{self.markup}'>"

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        yield f"<{type(self).__name__} markup: "
        yield {
            "text": self.prettified_markup,
            "highlight": False,
        }
        yield ">"

    def is_plain(self) -> TypeGuard["PlainToken"]:
        """Returns True if this token is an instance of PlainToken."""

        return isinstance(self, PlainToken)

    def is_pseudo(self) -> TypeGuard["PseudoToken"]:
        """Returns True if this token is an instance of PseudoToken."""

        return isinstance(self, PseudoToken)

    def is_color(self) -> TypeGuard["ColorToken"]:
        """Returns True if this token is an instance of ColorToken."""

        return isinstance(self, ColorToken)

    def is_style(self) -> TypeGuard["StyleToken"]:
        """Returns True if this token is an instance of StyleToken."""

        return isinstance(self, StyleToken)

    def is_alias(self) -> TypeGuard["AliasToken"]:
        """Returns True if this token is an instance of AliasToken."""

        return isinstance(self, AliasToken)

    def is_macro(self) -> TypeGuard["MacroToken"]:
        """Returns True if this token is an instance of MacroToken."""

        return isinstance(self, MacroToken)

    def is_clear(self) -> TypeGuard["ClearToken"]:
        """Returns True if this token is an instance of ClearToken."""

        return isinstance(self, ClearToken)

    def is_hyperlink(self) -> TypeGuard["HLinkToken"]:
        """Returns True if this token is an instance of HLinkToken."""

        return isinstance(self, HLinkToken)

    def is_cursor(self) -> TypeGuard["CursorToken"]:
        """Returns True if this token is an instance of CursorToken."""

        return isinstance(self, CursorToken)

markup cached property

Returns markup representing this token.

prettified_markup cached property

Returns syntax-highlighted markup representing this token.

is_alias()

Returns True if this token is an instance of AliasToken.

Source code in pytermgui/markup/tokens.py
92
93
94
95
def is_alias(self) -> TypeGuard["AliasToken"]:
    """Returns True if this token is an instance of AliasToken."""

    return isinstance(self, AliasToken)

is_clear()

Returns True if this token is an instance of ClearToken.

Source code in pytermgui/markup/tokens.py
102
103
104
105
def is_clear(self) -> TypeGuard["ClearToken"]:
    """Returns True if this token is an instance of ClearToken."""

    return isinstance(self, ClearToken)

is_color()

Returns True if this token is an instance of ColorToken.

Source code in pytermgui/markup/tokens.py
82
83
84
85
def is_color(self) -> TypeGuard["ColorToken"]:
    """Returns True if this token is an instance of ColorToken."""

    return isinstance(self, ColorToken)

is_cursor()

Returns True if this token is an instance of CursorToken.

Source code in pytermgui/markup/tokens.py
112
113
114
115
def is_cursor(self) -> TypeGuard["CursorToken"]:
    """Returns True if this token is an instance of CursorToken."""

    return isinstance(self, CursorToken)

Returns True if this token is an instance of HLinkToken.

Source code in pytermgui/markup/tokens.py
107
108
109
110
def is_hyperlink(self) -> TypeGuard["HLinkToken"]:
    """Returns True if this token is an instance of HLinkToken."""

    return isinstance(self, HLinkToken)

is_macro()

Returns True if this token is an instance of MacroToken.

Source code in pytermgui/markup/tokens.py
 97
 98
 99
100
def is_macro(self) -> TypeGuard["MacroToken"]:
    """Returns True if this token is an instance of MacroToken."""

    return isinstance(self, MacroToken)

is_plain()

Returns True if this token is an instance of PlainToken.

Source code in pytermgui/markup/tokens.py
72
73
74
75
def is_plain(self) -> TypeGuard["PlainToken"]:
    """Returns True if this token is an instance of PlainToken."""

    return isinstance(self, PlainToken)

is_pseudo()

Returns True if this token is an instance of PseudoToken.

Source code in pytermgui/markup/tokens.py
77
78
79
80
def is_pseudo(self) -> TypeGuard["PseudoToken"]:
    """Returns True if this token is an instance of PseudoToken."""

    return isinstance(self, PseudoToken)

is_style()

Returns True if this token is an instance of StyleToken.

Source code in pytermgui/markup/tokens.py
87
88
89
90
def is_style(self) -> TypeGuard["StyleToken"]:
    """Returns True if this token is an instance of StyleToken."""

    return isinstance(self, StyleToken)

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.

WidgetNamespace dataclass

Class to hold data on loaded namespace.

Source code in pytermgui/file_loaders.py
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
@dataclass
class WidgetNamespace:
    """Class to hold data on loaded namespace."""

    # No clue why `widgets` is seen as undefined here,
    # but not in the code below. It only seems to happen
    # in certain pylint configs as well.
    config: dict[
        Type[widgets_m.Widget], dict[str, Any]  # pylint: disable=undefined-variable
    ]
    widgets: dict[str, widgets_m.Widget]
    boxes: dict[str, widgets_m.boxes.Box] = field(default_factory=dict)

    @classmethod
    def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
        """Creates a namespace from config data.

        Args:
            data: A dictionary of config data.
            loader: The `FileLoader` instance that should be used.

        Returns:
            A new WidgetNamespace with the given config.
        """

        namespace = WidgetNamespace({}, {})
        for name, config in data.items():
            obj = loader.serializer.known_widgets.get(name)
            if obj is None:
                raise KeyError(f"Unknown widget type {name}.")

            namespace.config[obj] = {
                "styles": obj.styles,
                "chars": obj.chars.copy(),
            }

            for category, inner in config.items():
                value: str | widgets_m.styles.MarkupFormatter

                if category not in namespace.config[obj]:
                    setattr(obj, category, inner)
                    continue

                for key, value in inner.items():
                    namespace.config[obj][category][key] = value

        namespace.apply_config()
        return namespace

    @staticmethod
    def _apply_section(
        widget: Type[widgets_m.Widget], title: str, section: dict[str, str]
    ) -> None:
        """Applies configuration section to the widget."""

        for key, value in section.items():
            if title == "styles":
                widget.set_style(key, value)
                continue

            widget.set_char(key, value)

    def apply_to(self, widget: widgets_m.Widget) -> None:
        """Applies namespace config to the widget.

        Args:
            widget: The widget in question.
        """

        def _apply_sections(
            data: dict[str, dict[str, str]], widget: widgets_m.Widget
        ) -> None:
            """Applies sections from data to the widget."""

            for title, section in data.items():
                self._apply_section(type(widget), title, section)

        data = self.config.get(type(widget))
        if data is None:
            return

        _apply_sections(data, widget)

        if hasattr(widget, "_widgets"):
            for inner in widget:
                inner_section = self.config.get(type(inner))

                if inner_section is None:
                    continue

                _apply_sections(inner_section, inner)

    def apply_config(self) -> None:
        """Apply self.config to current namespace."""

        for widget, settings in self.config.items():
            for title, section in settings.items():
                self._apply_section(widget, title, section)

    def __getattr__(self, attr: str) -> widgets_m.Widget:
        """Get widget by name from widget list."""

        if attr in self.widgets:
            return self.widgets[attr]

        return self.__dict__[attr]

__getattr__(attr)

Get widget by name from widget list.

Source code in pytermgui/file_loaders.py
246
247
248
249
250
251
252
def __getattr__(self, attr: str) -> widgets_m.Widget:
    """Get widget by name from widget list."""

    if attr in self.widgets:
        return self.widgets[attr]

    return self.__dict__[attr]

apply_config()

Apply self.config to current namespace.

Source code in pytermgui/file_loaders.py
239
240
241
242
243
244
def apply_config(self) -> None:
    """Apply self.config to current namespace."""

    for widget, settings in self.config.items():
        for title, section in settings.items():
            self._apply_section(widget, title, section)

apply_to(widget)

Applies namespace config to the widget.

Parameters:

Name Type Description Default
widget Widget

The widget in question.

required
Source code in pytermgui/file_loaders.py
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
def apply_to(self, widget: widgets_m.Widget) -> None:
    """Applies namespace config to the widget.

    Args:
        widget: The widget in question.
    """

    def _apply_sections(
        data: dict[str, dict[str, str]], widget: widgets_m.Widget
    ) -> None:
        """Applies sections from data to the widget."""

        for title, section in data.items():
            self._apply_section(type(widget), title, section)

    data = self.config.get(type(widget))
    if data is None:
        return

    _apply_sections(data, widget)

    if hasattr(widget, "_widgets"):
        for inner in widget:
            inner_section = self.config.get(type(inner))

            if inner_section is None:
                continue

            _apply_sections(inner_section, inner)

from_config(data, loader) classmethod

Creates a namespace from config data.

Parameters:

Name Type Description Default
data dict[Any, Any]

A dictionary of config data.

required
loader FileLoader

The FileLoader instance that should be used.

required

Returns:

Type Description
WidgetNamespace

A new WidgetNamespace with the given config.

Source code in pytermgui/file_loaders.py
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
@classmethod
def from_config(cls, data: dict[Any, Any], loader: FileLoader) -> WidgetNamespace:
    """Creates a namespace from config data.

    Args:
        data: A dictionary of config data.
        loader: The `FileLoader` instance that should be used.

    Returns:
        A new WidgetNamespace with the given config.
    """

    namespace = WidgetNamespace({}, {})
    for name, config in data.items():
        obj = loader.serializer.known_widgets.get(name)
        if obj is None:
            raise KeyError(f"Unknown widget type {name}.")

        namespace.config[obj] = {
            "styles": obj.styles,
            "chars": obj.chars.copy(),
        }

        for category, inner in config.items():
            value: str | widgets_m.styles.MarkupFormatter

            if category not in namespace.config[obj]:
                setattr(obj, category, inner)
                continue

            for key, value in inner.items():
                namespace.config[obj][category][key] = value

    namespace.apply_config()
    return namespace

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."""

Window

Bases: Container

A class representing a window.

Windows are essentially fancy pytermgui.widgets.Container-s. They build on top of them to store and display various widgets, while allowing some custom functionality.

Source code in pytermgui/window_manager/window.py
 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
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
class Window(Container):  # pylint: disable=too-many-instance-attributes
    """A class representing a window.

    Windows are essentially fancy `pytermgui.widgets.Container`-s. They build on top of them
    to store and display various widgets, while allowing some custom functionality.
    """

    overflow = Overflow.HIDE

    title = ""
    """Title shown in left-top corner."""

    is_static = False
    """Static windows cannot be moved using the mouse."""

    is_modal = False
    """Modal windows stay on top of every other window and block interactions with other windows."""

    is_noblur = False
    """No-blur windows will always appear to stay in focus, even if they functionally don't."""

    is_noresize = False
    """No-resize windows cannot be resized using the mouse."""

    is_dirty = False
    """Controls whether the window should be redrawn in the next frame."""

    is_persistent = False
    """Persistent windows will be set noblur automatically, and remain clickable even through
    modals.

    While the library core doesn't do this for various reasons, it also might be useful to disable
    some behaviour (e.g. closing) for persistent windows on an implementation level.
    """

    chars = Container.chars.copy()

    styles = w_styles.StyleManager(
        border="surface",
        corner="surface",
        fill="background",
        border_focused="surface",
        corner_focused="surface",
        border_blurred="surface-2",
        corner_blurred="surface-2",
    )

    def __init__(self, *widgets: Any, **attrs: Any) -> None:
        """Initializes object.

        Args:
            *widgets: Widgets to add to this window after initilization.
            **attrs: Attributes that are passed to the constructor.
        """

        self._min_width: int | None = None
        self._auto_min_width: int | None = None
        self._auto_height = "height" not in attrs

        self.styles.border_focused = type(self).styles.border
        self.styles.corner_focused = type(self).styles.corner

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

        super().__init__(*widgets, **attrs)

        self.has_focus: bool = False

        self.manager: "WindowManager" | None = None

        # -------------------------  position ----- width x height
        self._restore_data: tuple[tuple[int, int], tuple[int, int]] | None = None

        if self.title != "":
            self.set_title(self.title)

        if self.is_persistent:
            self.is_noblur = True

    @property
    def min_width(self) -> int | None:
        """Minimum width of the window.

        If set to none, _auto_min_width will be calculated based on the maximum width of
        inner widgets.

        This is accurate enough for general use, but tends to lean to the safer side,
        i.e. it often overshoots the 'real' minimum width possible.

        If you find this to be the case, **AND** you can ensure that your window will
        not break, you may set this value manually.

        Returns:
            The calculated, or given minimum width of this object.
        """

        return self._min_width or self._auto_min_width

    @min_width.setter
    def min_width(self, new: int | None) -> None:
        """Sets a new minimum width."""

        self._min_width = new

    @property
    def rect(self) -> tuple[int, int, int, int]:
        """Returns the tuple of positions that define this window.

        Returns:
            A tuple of integers, in the order (left, top, right, bottom).
        """

        left, top = self.pos
        return (left, top, left + self.width, top + self.height)

    @rect.setter
    def rect(self, new: tuple[int, int, int, int]) -> None:
        """Sets new position, width and height of this window.

        This method also checks for the minimum width this window can be, and
        if the new width doesn't comply with that setting the changes are thrown
        away.

        Args:
            new: A tuple of integers in the order (left, top, right, bottom).
        """

        left, top, right, bottom = new
        minimum = self.min_width or 0

        if right - left < minimum:
            return

        # Update size policy to fill to resize inner objects properly
        self.size_policy = SizePolicy.FILL
        self.pos = (left, top)
        self.width = right - left
        self.height = bottom - top

        # Restore original size policy
        self.size_policy = SizePolicy.STATIC

    def __iadd__(self, other: object) -> Window:
        """Calls self._add_widget(other) and returns self."""

        self._add_widget(other)
        return self

    def __add__(self, other: object) -> Window:
        """Calls self._add_widget(other) and returns self."""

        self._add_widget(other)
        return self

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

        Args:
            other: The widget-like to add.
            run_get_lines: Whether self.get_lines should be ran after adding.
        """

        added = super()._add_widget(other, run_get_lines)

        if len(self._widgets) > 0:
            self._auto_min_width = max(widget.width for widget in self._widgets)
            self._auto_min_width += self.sidelength

        if self.overflow != Overflow.SCROLL or self._auto_height:
            self.height += added.height

        return added

    @classmethod
    def set_focus_styles(
        cls,
        *,
        focused: tuple[w_styles.StyleValue, w_styles.StyleValue],
        blurred: tuple[w_styles.StyleValue, w_styles.StyleValue],
    ) -> None:
        """Sets focused & blurred border & corner styles.

        Args:
            focused: A tuple of border_focused, corner_focused styles.
            blurred: A tuple of border_blurred, corner_blurred styles.
        """

        cls.styles.border_focused, cls.styles.corner_focused = focused
        cls.styles.border_blurred, cls.styles.corner_blurred = blurred

    def focus(self) -> None:
        """Focuses this window."""

        self.has_focus = True

        if not self.is_noblur:
            self.styles.border = self.styles.border_focused
            self.styles.corner = self.styles.corner_focused

    def blur(self) -> None:
        """Blurs (unfocuses) this window."""

        self.has_focus = False
        self.select(None)
        self.handle_mouse(MouseEvent(MouseAction.RELEASE, (0, 0)))

        if not self.is_noblur:
            self.styles.border = self.styles.border_blurred
            self.styles.corner = self.styles.corner_blurred

    def clear_cache(self) -> None:
        """Clears manager compositor's cached blur state."""

        if self.manager is not None:
            self.manager.clear_cache(self)

    def contains(self, pos: tuple[int, int]) -> bool:
        """Determines whether widget contains `pos`.

        This method uses window.rect to get the positions.

        Args:
            pos: Position to compare.

        Returns:
            Boolean describing whether the position is inside
                this widget.
        """

        left, top, right, bottom = self.rect

        return left <= pos[0] < right and top <= pos[1] < bottom

    def set_title(self, title: str, position: int = 0, pad: bool = True) -> Window:
        """Sets the window's title.

        Args:
            title: The string to set as the window title.
            position: An integer indexing into ["left", "top", "right", "bottom"],
                determining where the title is applied.
            pad: Whether there should be an extra space before and after the given title.
                defaults to True.
        """

        corners = self._get_char("corner")
        assert isinstance(corners, list)

        # Delete (both cases) of current title from the corner
        corners[position] = (
            corners[position].replace(f" {self.title} ", "").replace(self.title, "")
        )

        self.title = title

        if pad:
            title = " " + title + " "

        if position % 2 == 0:
            corners[position] += title

        else:
            current = corners[position]
            corners[position] = title + current

        self.set_char("corner", corners)

        return self

    def center(
        self, where: CenteringPolicy | None = None, store: bool = True
    ) -> Window:
        """Center window"""

        super().center(where, store)
        return self

    def close(self, animate: bool = True) -> None:
        """Instruct window manager to close object"""

        assert self.manager is not None

        self.manager.remove(self, animate=animate)

is_dirty = False class-attribute instance-attribute

Controls whether the window should be redrawn in the next frame.

is_modal = False class-attribute instance-attribute

Modal windows stay on top of every other window and block interactions with other windows.

is_noblur = False class-attribute instance-attribute

No-blur windows will always appear to stay in focus, even if they functionally don't.

is_noresize = False class-attribute instance-attribute

No-resize windows cannot be resized using the mouse.

is_persistent = False class-attribute instance-attribute

Persistent windows will be set noblur automatically, and remain clickable even through modals.

While the library core doesn't do this for various reasons, it also might be useful to disable some behaviour (e.g. closing) for persistent windows on an implementation level.

is_static = False class-attribute instance-attribute

Static windows cannot be moved using the mouse.

min_width property writable

Minimum width of the window.

If set to none, _auto_min_width will be calculated based on the maximum width of inner widgets.

This is accurate enough for general use, but tends to lean to the safer side, i.e. it often overshoots the 'real' minimum width possible.

If you find this to be the case, AND you can ensure that your window will not break, you may set this value manually.

Returns:

Type Description
int | None

The calculated, or given minimum width of this object.

rect property writable

Returns the tuple of positions that define this window.

Returns:

Type Description
tuple[int, int, int, int]

A tuple of integers, in the order (left, top, right, bottom).

title = '' class-attribute instance-attribute

Title shown in left-top corner.

__add__(other)

Calls self._add_widget(other) and returns self.

Source code in pytermgui/window_manager/window.py
166
167
168
169
170
def __add__(self, other: object) -> Window:
    """Calls self._add_widget(other) and returns self."""

    self._add_widget(other)
    return self

__iadd__(other)

Calls self._add_widget(other) and returns self.

Source code in pytermgui/window_manager/window.py
160
161
162
163
164
def __iadd__(self, other: object) -> Window:
    """Calls self._add_widget(other) and returns self."""

    self._add_widget(other)
    return self

__init__(*widgets, **attrs)

Initializes object.

Parameters:

Name Type Description Default
*widgets Any

Widgets to add to this window after initilization.

()
**attrs Any

Attributes that are passed to the constructor.

{}
Source code in pytermgui/window_manager/window.py
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
def __init__(self, *widgets: Any, **attrs: Any) -> None:
    """Initializes object.

    Args:
        *widgets: Widgets to add to this window after initilization.
        **attrs: Attributes that are passed to the constructor.
    """

    self._min_width: int | None = None
    self._auto_min_width: int | None = None
    self._auto_height = "height" not in attrs

    self.styles.border_focused = type(self).styles.border
    self.styles.corner_focused = type(self).styles.corner

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

    super().__init__(*widgets, **attrs)

    self.has_focus: bool = False

    self.manager: "WindowManager" | None = None

    # -------------------------  position ----- width x height
    self._restore_data: tuple[tuple[int, int], tuple[int, int]] | None = None

    if self.title != "":
        self.set_title(self.title)

    if self.is_persistent:
        self.is_noblur = True

blur()

Blurs (unfocuses) this window.

Source code in pytermgui/window_manager/window.py
217
218
219
220
221
222
223
224
225
226
def blur(self) -> None:
    """Blurs (unfocuses) this window."""

    self.has_focus = False
    self.select(None)
    self.handle_mouse(MouseEvent(MouseAction.RELEASE, (0, 0)))

    if not self.is_noblur:
        self.styles.border = self.styles.border_blurred
        self.styles.corner = self.styles.corner_blurred

center(where=None, store=True)

Center window

Source code in pytermgui/window_manager/window.py
286
287
288
289
290
291
292
def center(
    self, where: CenteringPolicy | None = None, store: bool = True
) -> Window:
    """Center window"""

    super().center(where, store)
    return self

clear_cache()

Clears manager compositor's cached blur state.

Source code in pytermgui/window_manager/window.py
228
229
230
231
232
def clear_cache(self) -> None:
    """Clears manager compositor's cached blur state."""

    if self.manager is not None:
        self.manager.clear_cache(self)

close(animate=True)

Instruct window manager to close object

Source code in pytermgui/window_manager/window.py
294
295
296
297
298
299
def close(self, animate: bool = True) -> None:
    """Instruct window manager to close object"""

    assert self.manager is not None

    self.manager.remove(self, animate=animate)

contains(pos)

Determines whether widget contains pos.

This method uses window.rect to get the positions.

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/window_manager/window.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def contains(self, pos: tuple[int, int]) -> bool:
    """Determines whether widget contains `pos`.

    This method uses window.rect to get the positions.

    Args:
        pos: Position to compare.

    Returns:
        Boolean describing whether the position is inside
            this widget.
    """

    left, top, right, bottom = self.rect

    return left <= pos[0] < right and top <= pos[1] < bottom

focus()

Focuses this window.

Source code in pytermgui/window_manager/window.py
208
209
210
211
212
213
214
215
def focus(self) -> None:
    """Focuses this window."""

    self.has_focus = True

    if not self.is_noblur:
        self.styles.border = self.styles.border_focused
        self.styles.corner = self.styles.corner_focused

set_focus_styles(*, focused, blurred) classmethod

Sets focused & blurred border & corner styles.

Parameters:

Name Type Description Default
focused tuple[StyleValue, StyleValue]

A tuple of border_focused, corner_focused styles.

required
blurred tuple[StyleValue, StyleValue]

A tuple of border_blurred, corner_blurred styles.

required
Source code in pytermgui/window_manager/window.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
@classmethod
def set_focus_styles(
    cls,
    *,
    focused: tuple[w_styles.StyleValue, w_styles.StyleValue],
    blurred: tuple[w_styles.StyleValue, w_styles.StyleValue],
) -> None:
    """Sets focused & blurred border & corner styles.

    Args:
        focused: A tuple of border_focused, corner_focused styles.
        blurred: A tuple of border_blurred, corner_blurred styles.
    """

    cls.styles.border_focused, cls.styles.corner_focused = focused
    cls.styles.border_blurred, cls.styles.corner_blurred = blurred

set_title(title, position=0, pad=True)

Sets the window's title.

Parameters:

Name Type Description Default
title str

The string to set as the window title.

required
position int

An integer indexing into ["left", "top", "right", "bottom"], determining where the title is applied.

0
pad bool

Whether there should be an extra space before and after the given title. defaults to True.

True
Source code in pytermgui/window_manager/window.py
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
def set_title(self, title: str, position: int = 0, pad: bool = True) -> Window:
    """Sets the window's title.

    Args:
        title: The string to set as the window title.
        position: An integer indexing into ["left", "top", "right", "bottom"],
            determining where the title is applied.
        pad: Whether there should be an extra space before and after the given title.
            defaults to True.
    """

    corners = self._get_char("corner")
    assert isinstance(corners, list)

    # Delete (both cases) of current title from the corner
    corners[position] = (
        corners[position].replace(f" {self.title} ", "").replace(self.title, "")
    )

    self.title = title

    if pad:
        title = " " + title + " "

    if position % 2 == 0:
        corners[position] += title

    else:
        current = corners[position]
        corners[position] = title + current

    self.set_char("corner", corners)

    return self

WindowManager

Bases: Widget

The manager of windows.

This class can be used, or even subclassed in order to create full-screen applications, using the pytermgui.window_manager.window.Window class and the general Widget API.

Source code in pytermgui/window_manager/manager.py
 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
class WindowManager(Widget):  # pylint: disable=too-many-instance-attributes
    """The manager of windows.

    This class can be used, or even subclassed in order to create full-screen applications,
    using the `pytermgui.window_manager.window.Window` class and the general Widget API.
    """

    focusing_actions = (MouseAction.LEFT_CLICK, MouseAction.RIGHT_CLICK)
    """These mouse actions will focus the window they are acted upon."""

    autorun = True

    def __init__(
        self,
        *,
        layout_type: Type[Layout] = Layout,
        framerate: int = 60,
        autorun: bool | None = None,
    ) -> None:
        """Initialize the manager."""

        super().__init__()

        self._is_running = False
        self._windows: list[Window] = []
        self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}

        self.focused: Window | None = None

        if autorun is not None:
            self.autorun = autorun

        self.layout = layout_type()
        self.compositor = Compositor(self._windows, framerate=framerate)
        self.mouse_translator: MouseTranslator | None = None

        self._mouse_target: Window | None = None
        self._focus_index = 0
        self._drag_offsets: tuple[int, int] = (0, 0)
        self._drag_target: tuple[Window, Edge] | None = None

        # This isn't quite implemented at the moment.
        self.restrict_within_bounds = True

        terminal.subscribe(terminal.RESIZE, self.on_resize)

    def __iadd__(self, other: object) -> WindowManager:
        """Adds a window to the manager."""

        if not isinstance(other, Window):
            raise ValueError("You may only add windows to a WindowManager.")

        return self.add(other)

    def __isub__(self, other: object) -> WindowManager:
        """Removes a window from the manager."""

        if not isinstance(other, Window):
            raise ValueError("You may only add windows to a WindowManager.")

        return self.remove(other)

    def __enter__(self) -> WindowManager:
        """Starts context manager."""

        return self

    def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
        """Ends context manager."""

        # Run the manager if it hasnt been run before.
        if self.autorun and exception is None and self.mouse_translator is None:
            self.run()

        if exception is not None:
            self.stop()
            raise exception

        return True

    def __iter__(self) -> Iterator[Window]:
        """Iterates this manager's windows."""

        return iter(self._windows)

    def _run_input_loop(self) -> None:
        """The main input loop of the WindowManager."""

        with enable_virtual_processing():
            while self._is_running:
                key = getch(interrupts=False)

                # Windows getch is non-blocking so the manager can be stopped from
                # another thread. Avoid spinning while no console input is pending.
                if key == "":
                    sleep(0.01)
                    continue

                if key == chr(3):
                    self.stop()
                    break

                if self.handle_key(key):
                    continue

                self.process_mouse(key)

    def get_lines(self) -> list[str]:
        """Gets the empty list."""

        # TODO: Allow using WindowManager as a widget.

        return []

    def clear_cache(self, window: Window) -> None:
        """Clears the compositor's cache related to the given window."""

        self.compositor.clear_cache(window)

    def on_resize(self, size: tuple[int, int]) -> None:
        """Correctly updates window positions & prints when terminal gets resized.

        Args:
            size: The new terminal size.
        """

        width, height = size

        for window in self._windows:
            newx = max(0, min(window.pos[0], width - window.width))
            newy = max(0, min(window.pos[1], height - window.height + 1))

            window.pos = (newx, newy)

        self.layout.apply()
        self.compositor.redraw()

    def run(self, mouse_events: list[str] | None = None) -> None:
        """Starts the WindowManager.

        Args:
            mouse_events: A list of mouse event types to listen to. See
                `pytermgui.ansi_interface.report_mouse` for more information.
                Defaults to `["press_hold", "hover"]`.

        Returns:
            The WindowManager's compositor instance.
        """

        self._is_running = True

        if mouse_events is None:
            mouse_events = ["all"]

        with alt_buffer(cursor=False, echo=False):
            with mouse_handler(mouse_events, "decimal_xterm") as translate:
                self.mouse_translator = translate
                self.compositor.run()

                self._run_input_loop()

    def stop(self) -> None:
        """Stops the WindowManager and its compositor."""

        self.compositor.stop()
        self._is_running = False

        feed(chr(3))

    def add(
        self, window: Window, assign: str | bool = True, animate: bool = True
    ) -> WindowManager:
        """Adds a window to the manager.

        Args:
            window: The window to add.
            assign: The name of the slot the new window should be assigned to, or a
                boolean. If it is given a str, it is treated as the name of a slot. When
                given True, the next non-filled slot will be assigned, and when given
                False no assignment will be done.
            animate: If set, an animation will be played on the window once it's added.
        """

        self._windows.insert(0, window)
        window.manager = self

        if assign:
            if isinstance(assign, str):
                getattr(self.layout, assign).content = window

            elif len(self._windows) <= len(self.layout.slots):
                self.layout.assign(window, index=len(self._windows) - 1)

            self.layout.apply()

        # New windows take focus-precedence over already
        # existing ones, even if they are modal.
        self.focus(window)

        if not animate:
            return self

        if window.height > 1:
            animator.animate_attr(
                target=window,
                attr="height",
                start=0,
                end=window.height,
                duration=300,
                on_step=_center_during_animation,
            )

        return self

    def remove(
        self,
        window: Window,
        autostop: bool = True,
        animate: bool = True,
    ) -> WindowManager:
        """Removes a window from the manager.

        Args:
            window: The window to remove.
            autostop: If set, the manager will be stopped if the length of its windows
                hits 0.
        """

        def _on_finish(_: AttrAnimation | None) -> bool:
            self._windows.remove(window)

            if autostop and len(self._windows) == 0:
                self.stop()
            else:
                self.focus(self._windows[0])

            return True

        if not animate:
            _on_finish(None)
            return self

        animator.animate_attr(
            target=window,
            attr="height",
            end=0,
            duration=300,
            on_step=_center_during_animation,
            on_finish=_on_finish,
        )

        return self

    def focus(self, window: Window | None) -> None:
        """Focuses a window by moving it to the first index in _windows."""

        if self.focused is not None:
            self.focused.blur()

        self.focused = window

        if window is not None:
            self._focus_index = self._windows.index(window)

            window.focus()

    def focus_next(self, step: int = 1) -> Window | None:
        """Focuses the next window in focus order, looping to first at the end.

        Args:
            step: The direction to step through windows. +1 for next, -1 for previous.
        """

        if len(self._windows) == 0:
            return None

        self._focus_index = (self._focus_index + step) % len(self._windows)

        if self.focused is not None:
            self.focused.blur()

        window = self._windows[-self._focus_index]

        window.focus()
        self.focused = window

        return window

    def handle_key(self, key: str) -> bool:
        """Processes a keypress.

        Args:
            key: The key to handle.

        Returns:
            True if the given key could be processed, False otherwise.
        """

        # Apply WindowManager bindings
        if self.execute_binding(key):
            return True

        # Apply focused window binding, or send to InputField
        if self.focused is not None:
            if self.focused.execute_binding(key):
                return True

            if self.focused.handle_key(key):
                return True

        return False

    # I prefer having the _click, _drag and _release helpers within this function, for
    # easier readability.
    def process_mouse(self, key: str) -> None:  # pylint: disable=too-many-statements
        """Processes (potential) mouse input.

        Args:
            key: Input to handle.
        """

        window: Window

        def _clamp_pos(pos: tuple[int, int], index: int) -> int:
            """Clamp a value using index to address x/y & width/height"""

            offset = self._drag_offsets[index]

            # TODO: This -2 is a very magical number. Not good.
            maximum = terminal.size[index] - ((window.width, window.height)[index] - 2)

            start_margin_index = abs(index - 1)

            if self.restrict_within_bounds:
                return max(
                    index + terminal.margins[start_margin_index],
                    min(
                        pos[index] - offset,
                        maximum
                        - terminal.margins[start_margin_index + 2]
                        - terminal.origin[index],
                    ),
                )

            return pos[index] - offset

        def _click(pos: tuple[int, int], window: Window) -> bool:
            """Process clicking a window."""

            left, top, right, bottom = window.rect
            borders = window.chars.get("border", [" "] * 4)

            if real_length(borders[1]) > 0 and pos[1] == top and left <= pos[0] < right:
                self._drag_target = (window, Edge.TOP)

            elif (
                real_length(borders[3]) > 0
                and pos[1] == bottom - 1
                and left <= pos[0] < right
            ):
                self._drag_target = (window, Edge.BOTTOM)

            elif (
                real_length(borders[0]) > 0
                and pos[0] == left
                and top <= pos[1] < bottom
            ):
                self._drag_target = (window, Edge.LEFT)

            elif (
                real_length(borders[2]) > 0
                and pos[0] == right - 1
                and top <= pos[1] < bottom
            ):
                self._drag_target = (window, Edge.RIGHT)

            else:
                return False

            self._drag_offsets = (
                pos[0] - window.pos[0],
                pos[1] - window.pos[1],
            )

            return True

        def _drag(pos: tuple[int, int], window: Window) -> bool:
            """Process dragging a window"""

            if self._drag_target is None:
                return False

            target_window, edge = self._drag_target
            handled = False

            if window is not target_window:
                return False

            left, top, right, bottom = window.rect

            if not window.is_static and edge is Edge.TOP:
                window.pos = (
                    _clamp_pos(pos, 0),
                    _clamp_pos(pos, 1),
                )

                handled = True

            # TODO: Why are all these arbitrary offsets needed?
            elif not window.is_noresize:
                if edge is Edge.RIGHT:
                    window.rect = (left, top, pos[0] + 1, bottom)
                    handled = True

                elif edge is Edge.LEFT:
                    window.rect = (pos[0], top, right, bottom)
                    handled = True

                elif edge is Edge.BOTTOM:
                    window.rect = (left, top, right, pos[1] + 1)
                    handled = True

            if handled:
                window.is_dirty = True
                self.compositor.set_redraw()

            return handled

        def _release(_: tuple[int, int], __: Window) -> bool:
            """Process release of key"""

            self._drag_target = None

            # This return False so Window can handle the mouse action as well,
            # as not much is done in this callback.
            return False

        handlers = {
            MouseAction.LEFT_CLICK: _click,
            MouseAction.LEFT_DRAG: _drag,
            MouseAction.RELEASE: _release,
        }

        translate = self.mouse_translator
        event_list = None if translate is None else translate(key)

        if event_list is None:
            return

        for event in event_list:
            # Ignore null-events
            if event is None:
                continue

            for window in self._windows:
                contains = window.contains(event.position)

                if event.action in self.focusing_actions:
                    self.focus(window)

                if event.action in handlers and handlers[event.action](
                    event.position, window
                ):
                    break

                if contains:
                    if self._mouse_target is not None:
                        self._mouse_target.handle_mouse(
                            MouseEvent(MouseAction.RELEASE, event.position)
                        )

                    self._mouse_target = window
                    window.handle_mouse(event)
                    break

                if window.is_modal:
                    break

            # Unset drag_target if no windows received the input
            else:
                self._drag_target = None
                if self._mouse_target is not None:
                    self._mouse_target.handle_mouse(
                        MouseEvent(MouseAction.RELEASE, event.position)
                    )

                self._mouse_target = None

    def screenshot(self, title: str, filename: str = "screenshot.svg") -> None:
        """Takes a screenshot of the current state.

        See `pytermgui.exporters.to_svg` for more information.

        Args:
            filename: The name of the file.
        """

        self.compositor.capture(title=title, filename=filename)

    def show_positions(self) -> None:
        """Shows the positions of each Window's widgets."""

        def _show_positions(widget, color_base: int = 60) -> None:
            """Show positions of widget."""

            if isinstance(widget, Container):
                for i, subwidget in enumerate(widget):
                    _show_positions(subwidget, color_base + i)

                return

            if not widget.is_selectable:
                return

            debug = widget.debug()
            color = str_to_color(f"@{color_base}")
            buff = color(" ", reset=False)

            for i in range(min(widget.width, real_length(debug)) - 1):
                buff += debug[i]

            self.terminal.write(buff, pos=widget.pos)

        for widget in self._windows:
            _show_positions(widget)
        self.terminal.flush()

        getch()

    def alert(self, *items: Any, center: bool = True, **attributes: Any) -> Window:
        """Creates a modal popup of the given elements and attributes.

        Args:
            *items: All widget-convertable objects passed as children of the new window.
            center: If set, `pytermgui.window_manager.window.center` is called on the window.
            **attributes: kwargs passed as the new window's attributes.
        """

        window = Window(*items, is_modal=True, **attributes)

        if center:
            window.center()

        self.add(window, assign=False)

        return window

    def toast(
        self,
        *items: Any,
        offset: int = 0,
        duration: int = 300,
        delay: int = 1000,
        **attributes: Any,
    ) -> Window:
        """Creates a Material UI-inspired toast window of the given elements and attributes.

        Args:
            *items: All widget-convertable objects passed as children of the new window.
            delay: The amount of time before the window will start animating out.
            **attributes: kwargs passed as the new window's attributes.
        """

        # pylint: disable=no-value-for-parameter

        toast = Window(*items, is_noblur=True, **attributes)

        target_height = toast.height
        toast.overflow = Overflow.HIDE

        def _finish(_: Animation) -> None:
            self.remove(toast, animate=False)

        def _progressively_show(anim: Animation, invert: bool = False) -> bool:
            height = int(anim.state * target_height)

            toast.center()

            if invert:
                toast.height = target_height - 1 - height
                toast.pos = (
                    toast.pos[0],
                    self.terminal.height - toast.height + 1 - offset,
                )
                return False

            toast.height = height
            toast.pos = (toast.pos[0], self.terminal.height - toast.height + 1 - offset)

            return False

        def _animate_toast_out(_: Animation) -> None:
            animator.schedule(
                FloatAnimation(
                    delay,
                    on_finish=lambda *_: animator.schedule(
                        FloatAnimation(
                            duration,
                            on_step=lambda anim: _progressively_show(anim, invert=True),
                            on_finish=_finish,
                        )
                    ),
                )
            )

        leadup = FloatAnimation(
            duration, on_step=_progressively_show, on_finish=_animate_toast_out
        )

        # pylint: enable=no-value-for-parameter

        self.add(toast.center(), animate=False, assign=False)
        self.focus(toast)
        animator.schedule(leadup)

        return toast

focusing_actions = (MouseAction.LEFT_CLICK, MouseAction.RIGHT_CLICK) class-attribute instance-attribute

These mouse actions will focus the window they are acted upon.

__enter__()

Starts context manager.

Source code in pytermgui/window_manager/manager.py
108
109
110
111
def __enter__(self) -> WindowManager:
    """Starts context manager."""

    return self

__exit__(_, exception, __)

Ends context manager.

Source code in pytermgui/window_manager/manager.py
113
114
115
116
117
118
119
120
121
122
123
124
def __exit__(self, _: Any, exception: Exception, __: Any) -> bool:
    """Ends context manager."""

    # Run the manager if it hasnt been run before.
    if self.autorun and exception is None and self.mouse_translator is None:
        self.run()

    if exception is not None:
        self.stop()
        raise exception

    return True

__iadd__(other)

Adds a window to the manager.

Source code in pytermgui/window_manager/manager.py
92
93
94
95
96
97
98
def __iadd__(self, other: object) -> WindowManager:
    """Adds a window to the manager."""

    if not isinstance(other, Window):
        raise ValueError("You may only add windows to a WindowManager.")

    return self.add(other)

__init__(*, layout_type=Layout, framerate=60, autorun=None)

Initialize the manager.

Source code in pytermgui/window_manager/manager.py
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
def __init__(
    self,
    *,
    layout_type: Type[Layout] = Layout,
    framerate: int = 60,
    autorun: bool | None = None,
) -> None:
    """Initialize the manager."""

    super().__init__()

    self._is_running = False
    self._windows: list[Window] = []
    self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {}

    self.focused: Window | None = None

    if autorun is not None:
        self.autorun = autorun

    self.layout = layout_type()
    self.compositor = Compositor(self._windows, framerate=framerate)
    self.mouse_translator: MouseTranslator | None = None

    self._mouse_target: Window | None = None
    self._focus_index = 0
    self._drag_offsets: tuple[int, int] = (0, 0)
    self._drag_target: tuple[Window, Edge] | None = None

    # This isn't quite implemented at the moment.
    self.restrict_within_bounds = True

    terminal.subscribe(terminal.RESIZE, self.on_resize)

__isub__(other)

Removes a window from the manager.

Source code in pytermgui/window_manager/manager.py
100
101
102
103
104
105
106
def __isub__(self, other: object) -> WindowManager:
    """Removes a window from the manager."""

    if not isinstance(other, Window):
        raise ValueError("You may only add windows to a WindowManager.")

    return self.remove(other)

__iter__()

Iterates this manager's windows.

Source code in pytermgui/window_manager/manager.py
126
127
128
129
def __iter__(self) -> Iterator[Window]:
    """Iterates this manager's windows."""

    return iter(self._windows)

add(window, assign=True, animate=True)

Adds a window to the manager.

Parameters:

Name Type Description Default
window Window

The window to add.

required
assign str | bool

The name of the slot the new window should be assigned to, or a boolean. If it is given a str, it is treated as the name of a slot. When given True, the next non-filled slot will be assigned, and when given False no assignment will be done.

True
animate bool

If set, an animation will be played on the window once it's added.

True
Source code in pytermgui/window_manager/manager.py
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
def add(
    self, window: Window, assign: str | bool = True, animate: bool = True
) -> WindowManager:
    """Adds a window to the manager.

    Args:
        window: The window to add.
        assign: The name of the slot the new window should be assigned to, or a
            boolean. If it is given a str, it is treated as the name of a slot. When
            given True, the next non-filled slot will be assigned, and when given
            False no assignment will be done.
        animate: If set, an animation will be played on the window once it's added.
    """

    self._windows.insert(0, window)
    window.manager = self

    if assign:
        if isinstance(assign, str):
            getattr(self.layout, assign).content = window

        elif len(self._windows) <= len(self.layout.slots):
            self.layout.assign(window, index=len(self._windows) - 1)

        self.layout.apply()

    # New windows take focus-precedence over already
    # existing ones, even if they are modal.
    self.focus(window)

    if not animate:
        return self

    if window.height > 1:
        animator.animate_attr(
            target=window,
            attr="height",
            start=0,
            end=window.height,
            duration=300,
            on_step=_center_during_animation,
        )

    return self

alert(*items, center=True, **attributes)

Creates a modal popup of the given elements and attributes.

Parameters:

Name Type Description Default
*items Any

All widget-convertable objects passed as children of the new window.

()
center bool

If set, pytermgui.window_manager.window.center is called on the window.

True
**attributes Any

kwargs passed as the new window's attributes.

{}
Source code in pytermgui/window_manager/manager.py
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
def alert(self, *items: Any, center: bool = True, **attributes: Any) -> Window:
    """Creates a modal popup of the given elements and attributes.

    Args:
        *items: All widget-convertable objects passed as children of the new window.
        center: If set, `pytermgui.window_manager.window.center` is called on the window.
        **attributes: kwargs passed as the new window's attributes.
    """

    window = Window(*items, is_modal=True, **attributes)

    if center:
        window.center()

    self.add(window, assign=False)

    return window

clear_cache(window)

Clears the compositor's cache related to the given window.

Source code in pytermgui/window_manager/manager.py
160
161
162
163
def clear_cache(self, window: Window) -> None:
    """Clears the compositor's cache related to the given window."""

    self.compositor.clear_cache(window)

focus(window)

Focuses a window by moving it to the first index in _windows.

Source code in pytermgui/window_manager/manager.py
299
300
301
302
303
304
305
306
307
308
309
310
def focus(self, window: Window | None) -> None:
    """Focuses a window by moving it to the first index in _windows."""

    if self.focused is not None:
        self.focused.blur()

    self.focused = window

    if window is not None:
        self._focus_index = self._windows.index(window)

        window.focus()

focus_next(step=1)

Focuses the next window in focus order, looping to first at the end.

Parameters:

Name Type Description Default
step int

The direction to step through windows. +1 for next, -1 for previous.

1
Source code in pytermgui/window_manager/manager.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
def focus_next(self, step: int = 1) -> Window | None:
    """Focuses the next window in focus order, looping to first at the end.

    Args:
        step: The direction to step through windows. +1 for next, -1 for previous.
    """

    if len(self._windows) == 0:
        return None

    self._focus_index = (self._focus_index + step) % len(self._windows)

    if self.focused is not None:
        self.focused.blur()

    window = self._windows[-self._focus_index]

    window.focus()
    self.focused = window

    return window

get_lines()

Gets the empty list.

Source code in pytermgui/window_manager/manager.py
153
154
155
156
157
158
def get_lines(self) -> list[str]:
    """Gets the empty list."""

    # TODO: Allow using WindowManager as a widget.

    return []

handle_key(key)

Processes a keypress.

Parameters:

Name Type Description Default
key str

The key to handle.

required

Returns:

Type Description
bool

True if the given key could be processed, False otherwise.

Source code in pytermgui/window_manager/manager.py
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
def handle_key(self, key: str) -> bool:
    """Processes a keypress.

    Args:
        key: The key to handle.

    Returns:
        True if the given key could be processed, False otherwise.
    """

    # Apply WindowManager bindings
    if self.execute_binding(key):
        return True

    # Apply focused window binding, or send to InputField
    if self.focused is not None:
        if self.focused.execute_binding(key):
            return True

        if self.focused.handle_key(key):
            return True

    return False

on_resize(size)

Correctly updates window positions & prints when terminal gets resized.

Parameters:

Name Type Description Default
size tuple[int, int]

The new terminal size.

required
Source code in pytermgui/window_manager/manager.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def on_resize(self, size: tuple[int, int]) -> None:
    """Correctly updates window positions & prints when terminal gets resized.

    Args:
        size: The new terminal size.
    """

    width, height = size

    for window in self._windows:
        newx = max(0, min(window.pos[0], width - window.width))
        newy = max(0, min(window.pos[1], height - window.height + 1))

        window.pos = (newx, newy)

    self.layout.apply()
    self.compositor.redraw()

process_mouse(key)

Processes (potential) mouse input.

Parameters:

Name Type Description Default
key str

Input to handle.

required
Source code in pytermgui/window_manager/manager.py
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
def process_mouse(self, key: str) -> None:  # pylint: disable=too-many-statements
    """Processes (potential) mouse input.

    Args:
        key: Input to handle.
    """

    window: Window

    def _clamp_pos(pos: tuple[int, int], index: int) -> int:
        """Clamp a value using index to address x/y & width/height"""

        offset = self._drag_offsets[index]

        # TODO: This -2 is a very magical number. Not good.
        maximum = terminal.size[index] - ((window.width, window.height)[index] - 2)

        start_margin_index = abs(index - 1)

        if self.restrict_within_bounds:
            return max(
                index + terminal.margins[start_margin_index],
                min(
                    pos[index] - offset,
                    maximum
                    - terminal.margins[start_margin_index + 2]
                    - terminal.origin[index],
                ),
            )

        return pos[index] - offset

    def _click(pos: tuple[int, int], window: Window) -> bool:
        """Process clicking a window."""

        left, top, right, bottom = window.rect
        borders = window.chars.get("border", [" "] * 4)

        if real_length(borders[1]) > 0 and pos[1] == top and left <= pos[0] < right:
            self._drag_target = (window, Edge.TOP)

        elif (
            real_length(borders[3]) > 0
            and pos[1] == bottom - 1
            and left <= pos[0] < right
        ):
            self._drag_target = (window, Edge.BOTTOM)

        elif (
            real_length(borders[0]) > 0
            and pos[0] == left
            and top <= pos[1] < bottom
        ):
            self._drag_target = (window, Edge.LEFT)

        elif (
            real_length(borders[2]) > 0
            and pos[0] == right - 1
            and top <= pos[1] < bottom
        ):
            self._drag_target = (window, Edge.RIGHT)

        else:
            return False

        self._drag_offsets = (
            pos[0] - window.pos[0],
            pos[1] - window.pos[1],
        )

        return True

    def _drag(pos: tuple[int, int], window: Window) -> bool:
        """Process dragging a window"""

        if self._drag_target is None:
            return False

        target_window, edge = self._drag_target
        handled = False

        if window is not target_window:
            return False

        left, top, right, bottom = window.rect

        if not window.is_static and edge is Edge.TOP:
            window.pos = (
                _clamp_pos(pos, 0),
                _clamp_pos(pos, 1),
            )

            handled = True

        # TODO: Why are all these arbitrary offsets needed?
        elif not window.is_noresize:
            if edge is Edge.RIGHT:
                window.rect = (left, top, pos[0] + 1, bottom)
                handled = True

            elif edge is Edge.LEFT:
                window.rect = (pos[0], top, right, bottom)
                handled = True

            elif edge is Edge.BOTTOM:
                window.rect = (left, top, right, pos[1] + 1)
                handled = True

        if handled:
            window.is_dirty = True
            self.compositor.set_redraw()

        return handled

    def _release(_: tuple[int, int], __: Window) -> bool:
        """Process release of key"""

        self._drag_target = None

        # This return False so Window can handle the mouse action as well,
        # as not much is done in this callback.
        return False

    handlers = {
        MouseAction.LEFT_CLICK: _click,
        MouseAction.LEFT_DRAG: _drag,
        MouseAction.RELEASE: _release,
    }

    translate = self.mouse_translator
    event_list = None if translate is None else translate(key)

    if event_list is None:
        return

    for event in event_list:
        # Ignore null-events
        if event is None:
            continue

        for window in self._windows:
            contains = window.contains(event.position)

            if event.action in self.focusing_actions:
                self.focus(window)

            if event.action in handlers and handlers[event.action](
                event.position, window
            ):
                break

            if contains:
                if self._mouse_target is not None:
                    self._mouse_target.handle_mouse(
                        MouseEvent(MouseAction.RELEASE, event.position)
                    )

                self._mouse_target = window
                window.handle_mouse(event)
                break

            if window.is_modal:
                break

        # Unset drag_target if no windows received the input
        else:
            self._drag_target = None
            if self._mouse_target is not None:
                self._mouse_target.handle_mouse(
                    MouseEvent(MouseAction.RELEASE, event.position)
                )

            self._mouse_target = None

remove(window, autostop=True, animate=True)

Removes a window from the manager.

Parameters:

Name Type Description Default
window Window

The window to remove.

required
autostop bool

If set, the manager will be stopped if the length of its windows hits 0.

True
Source code in pytermgui/window_manager/manager.py
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
def remove(
    self,
    window: Window,
    autostop: bool = True,
    animate: bool = True,
) -> WindowManager:
    """Removes a window from the manager.

    Args:
        window: The window to remove.
        autostop: If set, the manager will be stopped if the length of its windows
            hits 0.
    """

    def _on_finish(_: AttrAnimation | None) -> bool:
        self._windows.remove(window)

        if autostop and len(self._windows) == 0:
            self.stop()
        else:
            self.focus(self._windows[0])

        return True

    if not animate:
        _on_finish(None)
        return self

    animator.animate_attr(
        target=window,
        attr="height",
        end=0,
        duration=300,
        on_step=_center_during_animation,
        on_finish=_on_finish,
    )

    return self

run(mouse_events=None)

Starts the WindowManager.

Parameters:

Name Type Description Default
mouse_events list[str] | None

A list of mouse event types to listen to. See pytermgui.ansi_interface.report_mouse for more information. Defaults to ["press_hold", "hover"].

None

Returns:

Type Description
None

The WindowManager's compositor instance.

Source code in pytermgui/window_manager/manager.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def run(self, mouse_events: list[str] | None = None) -> None:
    """Starts the WindowManager.

    Args:
        mouse_events: A list of mouse event types to listen to. See
            `pytermgui.ansi_interface.report_mouse` for more information.
            Defaults to `["press_hold", "hover"]`.

    Returns:
        The WindowManager's compositor instance.
    """

    self._is_running = True

    if mouse_events is None:
        mouse_events = ["all"]

    with alt_buffer(cursor=False, echo=False):
        with mouse_handler(mouse_events, "decimal_xterm") as translate:
            self.mouse_translator = translate
            self.compositor.run()

            self._run_input_loop()

screenshot(title, filename='screenshot.svg')

Takes a screenshot of the current state.

See pytermgui.exporters.to_svg for more information.

Parameters:

Name Type Description Default
filename str

The name of the file.

'screenshot.svg'
Source code in pytermgui/window_manager/manager.py
534
535
536
537
538
539
540
541
542
543
def screenshot(self, title: str, filename: str = "screenshot.svg") -> None:
    """Takes a screenshot of the current state.

    See `pytermgui.exporters.to_svg` for more information.

    Args:
        filename: The name of the file.
    """

    self.compositor.capture(title=title, filename=filename)

show_positions()

Shows the positions of each Window's widgets.

Source code in pytermgui/window_manager/manager.py
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
def show_positions(self) -> None:
    """Shows the positions of each Window's widgets."""

    def _show_positions(widget, color_base: int = 60) -> None:
        """Show positions of widget."""

        if isinstance(widget, Container):
            for i, subwidget in enumerate(widget):
                _show_positions(subwidget, color_base + i)

            return

        if not widget.is_selectable:
            return

        debug = widget.debug()
        color = str_to_color(f"@{color_base}")
        buff = color(" ", reset=False)

        for i in range(min(widget.width, real_length(debug)) - 1):
            buff += debug[i]

        self.terminal.write(buff, pos=widget.pos)

    for widget in self._windows:
        _show_positions(widget)
    self.terminal.flush()

    getch()

stop()

Stops the WindowManager and its compositor.

Source code in pytermgui/window_manager/manager.py
207
208
209
210
211
212
213
def stop(self) -> None:
    """Stops the WindowManager and its compositor."""

    self.compositor.stop()
    self._is_running = False

    feed(chr(3))

toast(*items, offset=0, duration=300, delay=1000, **attributes)

Creates a Material UI-inspired toast window of the given elements and attributes.

Parameters:

Name Type Description Default
*items Any

All widget-convertable objects passed as children of the new window.

()
delay int

The amount of time before the window will start animating out.

1000
**attributes Any

kwargs passed as the new window's attributes.

{}
Source code in pytermgui/window_manager/manager.py
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
def toast(
    self,
    *items: Any,
    offset: int = 0,
    duration: int = 300,
    delay: int = 1000,
    **attributes: Any,
) -> Window:
    """Creates a Material UI-inspired toast window of the given elements and attributes.

    Args:
        *items: All widget-convertable objects passed as children of the new window.
        delay: The amount of time before the window will start animating out.
        **attributes: kwargs passed as the new window's attributes.
    """

    # pylint: disable=no-value-for-parameter

    toast = Window(*items, is_noblur=True, **attributes)

    target_height = toast.height
    toast.overflow = Overflow.HIDE

    def _finish(_: Animation) -> None:
        self.remove(toast, animate=False)

    def _progressively_show(anim: Animation, invert: bool = False) -> bool:
        height = int(anim.state * target_height)

        toast.center()

        if invert:
            toast.height = target_height - 1 - height
            toast.pos = (
                toast.pos[0],
                self.terminal.height - toast.height + 1 - offset,
            )
            return False

        toast.height = height
        toast.pos = (toast.pos[0], self.terminal.height - toast.height + 1 - offset)

        return False

    def _animate_toast_out(_: Animation) -> None:
        animator.schedule(
            FloatAnimation(
                delay,
                on_finish=lambda *_: animator.schedule(
                    FloatAnimation(
                        duration,
                        on_step=lambda anim: _progressively_show(anim, invert=True),
                        on_finish=_finish,
                    )
                ),
            )
        )

    leadup = FloatAnimation(
        duration, on_step=_progressively_show, on_finish=_animate_toast_out
    )

    # pylint: enable=no-value-for-parameter

    self.add(toast.center(), animate=False, assign=False)
    self.focus(toast)
    animator.schedule(leadup)

    return toast

YamlLoader

Bases: FileLoader

YAML specific loader subclass.

Source code in pytermgui/file_loaders.py
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
class YamlLoader(FileLoader):
    """YAML specific loader subclass."""

    def __init__(self, serializer: Serializer | None = None) -> None:
        """Initialize object, check for installation of PyYAML."""

        if _yaml_error is not None:
            raise RuntimeError(
                "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
            ) from _yaml_error

        super().__init__()

    def parse(self, data: str) -> dict[Any, Any]:
        """Parse YAML str.

        Args:
            data: YAML formatted string.

        Returns:
            Loadable dictionary.
        """

        assert yaml is not None
        return yaml.safe_load(data)

__init__(serializer=None)

Initialize object, check for installation of PyYAML.

Source code in pytermgui/file_loaders.py
412
413
414
415
416
417
418
419
420
def __init__(self, serializer: Serializer | None = None) -> None:
    """Initialize object, check for installation of PyYAML."""

    if _yaml_error is not None:
        raise RuntimeError(
            "YAML implementation module not found. Please install `PyYAML` to use `YamlLoader`."
        ) from _yaml_error

    super().__init__()

parse(data)

Parse YAML str.

Parameters:

Name Type Description Default
data str

YAML formatted string.

required

Returns:

Type Description
dict[Any, Any]

Loadable dictionary.

Source code in pytermgui/file_loaders.py
422
423
424
425
426
427
428
429
430
431
432
433
def parse(self, data: str) -> dict[Any, Any]:
    """Parse YAML str.

    Args:
        data: YAML formatted string.

    Returns:
        Loadable dictionary.
    """

    assert yaml is not None
    return yaml.safe_load(data)

analogous(base)

Colors that sit next to eachother on the colorwheel.

Note that the order of primary and secondary colors are swapped by this function. This is done so the colors, when laid out next to eachother, complete a gradient.

Parameters:

Name Type Description Default
base Color

The color used for derivations.

required

Analogous strategy

Source code in pytermgui/palettes.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def analogous(base: Color) -> tuple[Color, Color, Color, Color]:
    """Colors that sit next to eachother on the colorwheel.

    Note that the order of primary and secondary colors are swapped
    by this function. This is done so the colors, when laid out next
    to eachother, complete a gradient.

    Args:
        base: The color used for derivations.

    ![Analogous strategy](../../assets/analogous.svg)
    """

    before, _, after = base.analogous

    return before, base, after, base.complement

auto(data, **widget_args)

Creates a widget from specific data structures.

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

You can read up on the syntacies for each builtin widget within the widget documentation.

Parameters:

Name Type Description Default
data Any

The structure to convert. See below for formats.

required
**widget_args Any

Arguments passed straight to the widget constructor.

{}

Returns:

Type Description
Optional[Widget | list[Splitter]]

The widget or list of widgets created, or None if the passed structure could

Optional[Widget | list[Splitter]]

not be converted.

Example:

from pytermgui import Container
form = (
    Container(id="form")
    + "[157 bold]This is a title"
    + ""
    + {"[72 italic]Label1": "[210]Button1"}
    + {"[72 italic]Label2": "[210]Button2"}
    + {"[72 italic]Label3": "[210]Button3"}
    + ""
    + ["Submit", lambda _, button, your_submit_handler(button.parent)]
)
Source code in pytermgui/__init__.py
 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
def auto(data: Any, **widget_args: Any) -> Optional[Widget | list[Splitter]]:
    """Creates a widget from specific data structures.

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

    You can read up on the syntacies for each builtin widget within the widget
    [documentation](/widgets/builtins).

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

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

    Example:

    ```python3
    from pytermgui import Container
    form = (
        Container(id="form")
        + "[157 bold]This is a title"
        + ""
        + {"[72 italic]Label1": "[210]Button1"}
        + {"[72 italic]Label2": "[210]Button2"}
        + {"[72 italic]Label3": "[210]Button3"}
        + ""
        + ["Submit", lambda _, button, your_submit_handler(button.parent)]
    )
    ```
    """
    # In my opinion, returning immediately after construction is much more readable.
    # pylint: disable=too-many-return-statements

    # Nothing to do.
    if isinstance(data, Widget):
        # Set all **widget_args
        for key, value in widget_args.items():
            setattr(data, key, value)

        return data

    # Label
    if isinstance(data, str):
        return Label(data, **widget_args)

    # Splitter
    if isinstance(data, tuple):
        return Splitter(*data, **widget_args)

    # buttons
    if isinstance(data, list):
        label = data[0]
        onclick = None
        if len(data) > 1:
            onclick = data[1]

        # Checkbox
        if isinstance(label, bool):
            return Checkbox(onclick, checked=label, **widget_args)

        # Toggle
        if isinstance(label, tuple):
            assert len(label) == 2
            return Toggle(label, onclick, **widget_args)

        return Button(label, onclick, **widget_args)

    # prompt splitter
    if isinstance(data, dict):
        rows: list[Splitter] = []

        for key, value in data.items():
            left = auto(key, parent_align=HorizontalAlignment.LEFT)
            right = auto(value, parent_align=HorizontalAlignment.RIGHT)

            rows.append(Splitter(left, right, **widget_args))

        if len(rows) == 1:
            return rows[0]

        return rows

    return None

background(text, color, reset=True)

Sets the background color of the given text.

Note that the given color will be forced into background = True.

Parameters:

Name Type Description Default
text str

The text to color.

required
color str | Color

The color to use. See pytermgui.colors.str_to_color for accepted str formats.

required
reset bool

Whether the return value should include a reset sequence at the end.

True

Returns:

Type Description
str

The colored text, including a reset if set.

Source code in pytermgui/colors.py
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
def background(text: str, color: str | Color, reset: bool = True) -> str:
    """Sets the background color of the given text.

    Note that the given color will be forced into `background = True`.

    Args:
        text: The text to color.
        color: The color to use. See `pytermgui.colors.str_to_color` for accepted
            str formats.
        reset: Whether the return value should include a reset sequence at the end.

    Returns:
        The colored text, including a reset if set.
    """

    if not isinstance(color, Color):
        color = str_to_color(color)

    color.background = True

    return color(text, reset=reset)

Returns text blinking.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Source code in pytermgui/ansi_interface.py
682
683
684
685
686
687
688
689
690
def blink(text: str, reset_style: Optional[bool] = True) -> str:
    """Returns text blinking.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.
    """

    return set_mode("blink", False) + text + (reset() if reset_style else "")

bold(text, reset_style=True)

Returns text in bold.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Source code in pytermgui/ansi_interface.py
638
639
640
641
642
643
644
645
646
def bold(text: str, reset_style: Optional[bool] = True) -> str:
    """Returns text in bold.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.
    """

    return set_mode("bold", False) + text + (reset() if reset_style else "")

break_line(line, limit, non_first_limit=None, fill=None)

Breaks a line into a list[str] with maximum limit length per line.

Uses wcwidth.wrap() for proper word-boundary breaking, grapheme cluster handling, and wide character support. ANSI sequences are preserved and propagated across line breaks.

Parameters:

Name Type Description Default
line str

The line to split. May or may not contain ANSI sequences.

required
limit int

The maximum amount of characters allowed in each line, excluding non-printing sequences.

required
non_first_limit int | None

The limit after the first line. If not given, defaults to limit.

None
fill str | None

Optional character to pad lines to the limit width.

None
Source code in pytermgui/helpers.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
def break_line(
    line: str, limit: int, non_first_limit: int | None = None, fill: str | None = None
) -> Iterator[str]:
    """Breaks a line into a `list[str]` with maximum `limit` length per line.

    Uses wcwidth.wrap() for proper word-boundary breaking, grapheme cluster
    handling, and wide character support. ANSI sequences are preserved and
    propagated across line breaks.

    Args:
        line: The line to split. May or may not contain ANSI sequences.
        limit: The maximum amount of characters allowed in each line, excluding
            non-printing sequences.
        non_first_limit: The limit after the first line. If not given, defaults
            to `limit`.
        fill: Optional character to pad lines to the limit width.
    """

    if line in ["", "\x1b[0m"]:
        yield ""
        return

    def _pad_line(text: str, width: int) -> str:
        if fill is None:
            return text

        count = width - real_length(text)
        if count > 0:
            return text + count * fill
        return text

    if non_first_limit is None:
        non_first_limit = limit

    for segment in line.split("\n"):
        if not segment:
            yield _pad_line("", limit)
            limit = non_first_limit
            continue

        wrapped = wcwidth_wrap(segment, limit)

        for wrapped_line in wrapped:
            yield _pad_line(wrapped_line, limit)
            limit = non_first_limit

build_fancy_repr(obj)

Interprets objects with the __fancy_repr__ protocol.

Source code in pytermgui/fancy_repr.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def build_fancy_repr(obj: SupportsFancyRepr) -> str:
    """Interprets objects with the `__fancy_repr__` protocol."""

    output = ""
    for item in obj.__fancy_repr__():
        if isinstance(item, str):
            output += highlight_python(item)
            continue

        text = item["text"]
        assert isinstance(text, str)

        highlight = item["highlight"]

        if highlight:
            text = highlight_python(text)

        output += text

    return output

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

clear_color_cache()

Clears _COLOR_CACHE and _COLOR_MATCH_CACHE.

Source code in pytermgui/colors.py
88
89
90
91
92
def clear_color_cache() -> None:
    """Clears `_COLOR_CACHE` and `_COLOR_MATCH_CACHE`."""

    _COLOR_CACHE.clear()
    _COLOR_MATCH_CACHE.clear()

consume_tag(tag)

Consumes a tag text, returns the associated Token.

Source code in pytermgui/markup/parsing.py
 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
def consume_tag(tag: str) -> Token:  # pylint: disable=too-many-return-statements
    """Consumes a tag text, returns the associated Token."""

    if tag in STYLES:
        return StyleToken(tag)

    if tag.startswith("/"):
        return ClearToken(tag)

    if tag.startswith("!"):
        matchobj = RE_MACRO.match(tag)

        if matchobj is not None:
            name, args = matchobj.groups()

            if args is None:
                return MacroToken(name, tuple())

            return MacroToken(name, tuple(args.split(":")))

    if tag.startswith("~"):
        return HLinkToken(tag[1:])

    if tag.startswith("(") and tag.endswith(")"):
        values = tag[1:-1].split(";")
        if len(values) != 2:
            raise MarkupSyntaxError(
                tag,
                f"should have exactly 2 values separated by `;`, not {len(values)}",
                "",
            )

        return CursorToken(tag[1:-1], *map(int, values))

    if tag in PSEUDO_TOKENS:
        return PseudoToken(tag)

    token: Token
    try:
        token = ColorToken(tag, Color.parse(tag, localize=False))

    except ColorSyntaxError:
        token = AliasToken(tag)

    return token

create_context_dict()

Creates a new context dictionary, initializing its sub-dicts.

Returns:

Type Description
ContextDict

A dictionary with aliases and macros defined as empty sub-dicts.

Source code in pytermgui/markup/parsing.py
83
84
85
86
87
88
89
90
def create_context_dict() -> ContextDict:
    """Creates a new context dictionary, initializing its sub-dicts.

    Returns:
        A dictionary with `aliases` and `macros` defined as empty sub-dicts.
    """

    return {"aliases": {}, "macros": {}}

cursor_column(num=0)

Moves the cursor to the num-th character of the current line.

Parameters:

Name Type Description Default
num int

The new cursor position.

0
Note

This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
276
277
278
279
280
281
282
283
284
285
286
287
def cursor_column(num: int = 0) -> None:
    """Moves the cursor to the `num`-th character of the current line.

    Args:
        num: The new cursor position.

    Note:
        This does not flush the terminal for performance reasons. You
        can do it manually with `sys.stdout.flush()`.
    """

    get_terminal().write(f"\x1b[{num}G")

cursor_down(num=1)

Moves the cursor up by num lines.

Parameters:

Name Type Description Default
num int

How many lines the cursor should move by. Must be positive, to move in the opposite direction use cursor_up.

1

Note: This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
206
207
208
209
210
211
212
213
214
215
216
217
def cursor_down(num: int = 1) -> None:
    """Moves the cursor up by `num` lines.

    Args:
        num: How many lines the cursor should move by. Must be positive,
            to move in the opposite direction use `cursor_up`.
    Note:
        This does not flush the terminal for performance reasons. You
        can do it manually with `sys.stdout.flush()`.
    """

    get_terminal().write(f"\x1b[{num}B")

cursor_home()

Moves cursor to get_terminal().origin.

Note

This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
290
291
292
293
294
295
296
297
298
def cursor_home() -> None:
    """Moves cursor to `get_terminal().origin`.

    Note:
        This does not flush the terminal for performance reasons. You
        can do it manually with `sys.stdout.flush()`.
    """

    get_terminal().write("\x1b[H")

cursor_left(num=1)

Moves the cursor left by num lines.

Parameters:

Name Type Description Default
num int

How many characters the cursor should move by. Must be positive, to move in the opposite direction use cursor_right.

1

Note: This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
234
235
236
237
238
239
240
241
242
243
244
245
def cursor_left(num: int = 1) -> None:
    """Moves the cursor left by `num` lines.

    Args:
        num: How many characters the cursor should move by. Must be positive,
            to move in the opposite direction use `cursor_right`.
    Note:
        This does not flush the terminal for performance reasons. You
        can do it manually with `sys.stdout.flush()`.
    """

    get_terminal().write(f"\x1b[{num}D")

cursor_next_line(num=1)

Moves the cursor to the beginning of the num-th line downwards.

Parameters:

Name Type Description Default
num int

The amount the cursor should move by. Must be positive, to move in the opposite direction use cursor_prev_line.

1

Note: This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
248
249
250
251
252
253
254
255
256
257
258
259
def cursor_next_line(num: int = 1) -> None:
    """Moves the cursor to the beginning of the `num`-th line downwards.

    Args:
        num: The amount the cursor should move by. Must be positive, to move
            in the opposite direction use `cursor_prev_line`.
    Note:
        This does not flush the terminal for performance reasons. You
        can do it manually with `sys.stdout.flush()`.
    """

    get_terminal().write(f"\x1b[{num}E")

cursor_prev_line(num=1)

Moves the cursor to the beginning of the num-th line upwards.

Parameters:

Name Type Description Default
num int

The amount the cursor should move by. Must be positive, to move in the opposite direction use cursor_next_line.

1

Note: This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
262
263
264
265
266
267
268
269
270
271
272
273
def cursor_prev_line(num: int = 1) -> None:
    """Moves the cursor to the beginning of the `num`-th line upwards.

    Args:
        num: The amount the cursor should move by. Must be positive, to move
            in the opposite direction use `cursor_next_line`.
    Note:
        This does not flush the terminal for performance reasons. You
        can do it manually with `sys.stdout.flush()`.
    """

    get_terminal().write(f"\x1b[{num}F")

cursor_right(num=1)

Moves the cursor right by num lines.

Parameters:

Name Type Description Default
num int

How many characters the cursor should move by. Must be positive, to move in the opposite direction use cursor_left.

1

Note: This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
220
221
222
223
224
225
226
227
228
229
230
231
def cursor_right(num: int = 1) -> None:
    """Moves the cursor right by `num` lines.

    Args:
        num: How many characters the cursor should move by. Must be positive,
            to move in the opposite direction use `cursor_left`.
    Note:
        This does not flush the terminal for performance reasons. You
        can do it manually with `sys.stdout.flush()`.
    """

    get_terminal().write(f"\x1b[{num}C")

cursor_up(num=1)

Moves the cursor up by num lines.

Parameters:

Name Type Description Default
num int

How many lines the cursor should move by. Must be positive, to move in the opposite direction use cursor_down.

1

Note: This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
192
193
194
195
196
197
198
199
200
201
202
203
def cursor_up(num: int = 1) -> None:
    """Moves the cursor up by `num` lines.

    Args:
        num: How many lines the cursor should move by. Must be positive,
            to move in the opposite direction use `cursor_down`.
    Note:
        This does not flush the terminal for performance reasons. You
        can do it manually with `sys.stdout.flush()`.
    """

    get_terminal().write(f"\x1b[{num}A")

dim(text, reset_style=True)

Returns text in dim.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Source code in pytermgui/ansi_interface.py
649
650
651
652
653
654
655
656
657
def dim(text: str, reset_style: Optional[bool] = True) -> str:
    """Returns text in dim.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.
    """

    return set_mode("dim", False) + text + (reset() if reset_style else "")

escape(text)

Escapes any markup found within the given text.

Source code in pytermgui/markup/language.py
41
42
43
44
45
46
47
48
49
def escape(text: str) -> str:
    """Escapes any markup found within the given text."""

    def _repl(matchobj: Match) -> str:
        full, *_ = matchobj.groups()

        return f"\\{full}"

    return RE_MARKUP.sub(_repl, text)

escape_markup(text)

Escapes any potential markup to avoid double-parsing.

Use this when treating already parsed markup.

Source code in pytermgui/regex.py
79
80
81
82
83
84
85
86
87
88
89
90
def escape_markup(text: str) -> str:
    """Escapes any potential markup to avoid double-parsing.

    Use this when treating already parsed markup.
    """

    def _escape(mtch: Match) -> str:
        full, *_ = mtch.groups()

        return full.replace("[", r"\[")

    return RE_MARKUP.sub(_escape, text)

feed(text)

Manually feeds some text to be read by getch.

This can be used to emulate input, as well as to "interrupt" a blocking getch call (though getch_timeout works better for that scenario).

Source code in pytermgui/input.py
81
82
83
84
85
86
87
88
89
def feed(text: str) -> None:
    """Manually feeds some text to be read by `getch`.

    This can be used to emulate input, as well as to "interrupt" a blocking `getch`
    call (though `getch_timeout` works better for that scenario).
    """

    feeder_stream.write(text)
    feeder_stream.seek(0)

foreground(text, color, reset=True)

Sets the foreground color of the given text.

Note that the given color will be forced into background = True.

Parameters:

Name Type Description Default
text str

The text to color.

required
color str | Color

The color to use. See pytermgui.colors.str_to_color for accepted str formats.

required
reset bool

Whether the return value should include a reset sequence at the end.

True

Returns:

Type Description
str

The colored text, including a reset if set.

Source code in pytermgui/colors.py
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
def foreground(text: str, color: str | Color, reset: bool = True) -> str:
    """Sets the foreground color of the given text.

    Note that the given color will be forced into `background = True`.

    Args:
        text: The text to color.
        color: The color to use. See `pytermgui.colors.str_to_color` for accepted
            str formats.
        reset: Whether the return value should include a reset sequence at the end.

    Returns:
        The colored text, including a reset if set.
    """

    if not isinstance(color, Color):
        color = str_to_color(color)

    color.background = False

    return color(text, reset=reset)

get_markup(text)

Gets the markup representing an ANSI-coded string.

Source code in pytermgui/markup/parsing.py
581
582
583
584
def get_markup(text: str) -> str:
    """Gets the markup representing an ANSI-coded string."""

    return tokens_to_markup(list(tokenize_ansi(text)))

get_terminal()

Gets the default terminal instance used by the module.

Source code in pytermgui/term.py
645
646
647
648
def get_terminal() -> Terminal:
    """Gets the default terminal instance used by the module."""

    return terminal

getch(printable=False, interrupts=True, windows_raise_timeout=False)

Wrapper to call the platform-appropriate character getter.

Parameters:

Name Type Description Default
printable bool

When set, printable versions of the input are returned.

False
interrupts bool

If not set, KeyboardInterrupt is silenced and chr(3) (CTRL_C) is returned.

True
windows_raise_timeout bool

If set, TimeoutException (raised by Windows' getch when no input is available) isn't silenced.

False
Source code in pytermgui/input.py
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
def getch(
    printable: bool = False,
    interrupts: bool = True,
    windows_raise_timeout: bool = False,
) -> str:
    """Wrapper to call the platform-appropriate character getter.

    Args:
        printable: When set, printable versions of the input are returned.
        interrupts: If not set, `KeyboardInterrupt` is silenced and `chr(3)` (`CTRL_C`)
            is returned.
        windows_raise_timeout: If set, `TimeoutException` (raised by Windows' getch when
            no input is available) isn't silenced.
    """

    fed_text = feeder_stream.getvalue()

    if fed_text != "":
        feeder_stream.seek(0)
        feeder_stream.truncate(0)
        return fed_text

    try:
        key = _getch()

        # msvcrt.getch returns CTRL_C as a character, unlike UNIX systems
        # where an interrupt is raised. Thus, we need to manually raise
        # the interrupt.
        if key == chr(3):
            raise KeyboardInterrupt

    except KeyboardInterrupt as error:
        if interrupts:
            raise KeyboardInterrupt("Unhandled interrupt") from error

        key = chr(3)

    except TimeoutException:
        if windows_raise_timeout:
            raise

        key = ""

    if printable:
        key = key.encode("unicode_escape").decode("utf-8")

    return key

getch_timeout(duration, default='', printable=False, interrupts=True)

Calls getch, returns default if timeout passes before getting input.

No timeout is applied on Windows systems, as there is no support for SIGALRM. Instead, it will return immediately if no input is provided, since the Windows APIs expose a way to detect that case.

Parameters:

Name Type Description Default
duration float

How long the call should wait for input.

required
default str

The value to return if timeout occured.

''
Source code in pytermgui/input.py
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
def getch_timeout(
    duration: float, default: str = "", printable: bool = False, interrupts: bool = True
) -> Any:
    """Calls `getch`, returns `default` if timeout passes before getting input.

    No timeout is applied on Windows systems, as there is no support for
    `SIGALRM`. Instead, it will return immediately if no input is provided, since the
    Windows APIs expose a way to detect that case.

    Args:
        duration: How long the call should wait for input.
        default: The value to return if timeout occured.
    """

    if isinstance(_getch, _GetchWindows):
        try:
            return getch(windows_raise_timeout=True)

        except TimeoutException:
            return default

    with timeout(duration):
        return getch(printable=printable, interrupts=interrupts)

    return default

hide_cursor()

Stops printing the cursor.

Source code in pytermgui/ansi_interface.py
129
130
131
132
def hide_cursor() -> None:
    """Stops printing the cursor."""

    get_terminal().write("\x1b[?25l")

highlight_tim(text, cache=True)

Highlights some TIM code.

Source code in pytermgui/highlighters.py
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
def highlight_tim(text: str, cache: bool = True) -> str:
    """Highlights some TIM code."""

    @lru_cache(1048)
    def _highlight(txt: str) -> str:
        output = ""
        cursor = 0
        active_tokens: list[Token] = []

        def _get_active_markup() -> str:
            active_markup = " ".join(tkn.markup for tkn in active_tokens)

            if active_markup == "":
                return ""

            return f"[{active_markup}]"

        for matchobj in RE_MARKUP.finditer(txt):
            start, end = matchobj.span()

            if cursor < start:
                if cursor > 0:
                    output += "]"

                output += _get_active_markup()
                output += f"{txt[cursor:start]}[/]"

            *_, tags = matchobj.groups()

            output += "["
            for tag in tags.split():
                token = consume_tag(tag)
                output += f"{token.prettified_markup} "

                if Token.is_clear(token):
                    active_tokens = [
                        tkn for tkn in active_tokens if not token.targets(tkn)
                    ]

                else:
                    active_tokens.append(token)

            output = output.rstrip()
            cursor = end

        if cursor < len(txt) - 1:
            if cursor > 0:
                output += "]"

            output += _get_active_markup()
            output += f"{txt[cursor:]}"

            if len(active_tokens) > 0:
                output += "[/]"

        if output.count("[") != output.count("]"):
            output += "]"

        return output

    if cache:
        return _highlight(text)

    return _highlight.__wrapped__(text)

inspect(target, **inspector_args)

Inspects an object.

Parameters:

Name Type Description Default
target object

The object to inspect.

required
**inspector_args Any

See Inspector.__init__.

{}
Source code in pytermgui/inspector.py
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
def inspect(target: object, **inspector_args: Any) -> Inspector:
    """Inspects an object.

    Args:
        target: The object to inspect.
        **inspector_args: See `Inspector.__init__`.
    """

    def _conditionally_overwrite_kwarg(**kwargs) -> None:
        for key, value in kwargs.items():
            if inspector_args.get(key) is None:
                inspector_args[key] = value

    if ismodule(target):
        _conditionally_overwrite_kwarg(
            show_dunder=False,
            show_private=False,
            show_full_doc=False,
            show_methods=True,
            show_qualname=False,
        )

    elif isclass(target):
        _conditionally_overwrite_kwarg(
            show_dunder=False,
            show_private=False,
            show_full_doc=True,
            show_methods=True,
            show_qualname=False,
        )

    elif callable(target) or isbuiltin(target):
        _conditionally_overwrite_kwarg(
            show_dunder=False,
            show_private=False,
            show_full_doc=True,
            show_methods=False,
            show_qualname=True,
        )

    else:
        _conditionally_overwrite_kwarg(
            show_dunder=False,
            show_private=False,
            show_full_doc=True,
            show_methods=True,
            show_qualname=False,
        )

    inspector = Inspector(**inspector_args).inspect(target)

    return inspector

inverse(text, reset_style=True)

Returns text inverse-colored.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Source code in pytermgui/ansi_interface.py
693
694
695
696
697
698
699
700
701
def inverse(text: str, reset_style: Optional[bool] = True) -> str:
    """Returns text inverse-colored.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.
    """

    return set_mode("inverse", False) + text + (reset() if reset_style else "")

invisible(text, reset_style=True)

Returns text as invisible.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Note

This isn't very widely supported.

Source code in pytermgui/ansi_interface.py
704
705
706
707
708
709
710
711
712
713
714
715
def invisible(text: str, reset_style: Optional[bool] = True) -> str:
    """Returns text as invisible.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.

    Note:
        This isn't very widely supported.
    """

    return set_mode("invisible", False) + text + (reset() if reset_style else "")

is_animated(target, attribute)

Determines whether the given object.attribute is animated.

This looks for __ptg_animated__, and whether it contains the given attribute.

Source code in pytermgui/animations.py
59
60
61
62
63
64
65
66
67
68
69
70
def is_animated(target: object, attribute: str) -> bool:
    """Determines whether the given object.attribute is animated.

    This looks for `__ptg_animated__`, and whether it contains the given attribute.
    """

    if not hasattr(target, "__ptg_animated__"):
        return False

    animated = getattr(target, "__ptg_animated__")

    return attribute in animated

italic(text, reset_style=True)

Returns text in italic.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Source code in pytermgui/ansi_interface.py
660
661
662
663
664
665
666
667
668
def italic(text: str, reset_style: Optional[bool] = True) -> str:
    """Returns text in italic.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.
    """

    return set_mode("italic", False) + text + (reset() if reset_style else "")

move_cursor(pos)

Moves the cursor.

Parameters:

Name Type Description Default
pos tuple[int, int]

Tuple of that the cursor will be moved to.

required

This does not flush the terminal for performance reasons. You can do it manually with sys.stdout.flush().

Source code in pytermgui/ansi_interface.py
178
179
180
181
182
183
184
185
186
187
188
189
def move_cursor(pos: tuple[int, int]) -> None:
    """Moves the cursor.

    Args:
        pos: Tuple of that the cursor will be moved to.

    This does not flush the terminal for performance reasons. You
    can do it manually with `sys.stdout.flush()`.
    """

    posx, posy = pos
    get_terminal().write(f"\x1b[{posy};{posx}H")

optimize_markup(markup)

Optimizes markup by tokenizing it, optimizing the tokens and converting it back to markup.

Source code in pytermgui/markup/parsing.py
587
588
589
590
def optimize_markup(markup: str) -> str:
    """Optimizes markup by tokenizing it, optimizing the tokens and converting it back to markup."""

    return tokens_to_markup(list(optimize_tokens(list(tokenize_markup(markup)))))

optimize_tokens(tokens)

Optimizes a stream of tokens, only yielding functionally relevant ones.

Parameters:

Name Type Description Default
tokens list[Token]

Any list of Token objects. Usually obtained from tokenize_markup or tokenize_ansi.

required

Yields:

Type Description
Token

All those tokens within the input iterator that are functionally relevant, keeping their order.

Source code in pytermgui/markup/parsing.py
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
def optimize_tokens(tokens: list[Token]) -> Iterator[Token]:
    """Optimizes a stream of tokens, only yielding functionally relevant ones.

    Args:
        tokens: Any list of Token objects. Usually obtained from `tokenize_markup`
            or `tokenize_ansi`.

    Yields:
        All those tokens within the input iterator that are functionally relevant,
            keeping their order.
    """

    previous: list[Token] = []
    current_tag_group: list[Token] = []

    def _diff_previous() -> Iterator[Token]:
        """Find difference from the previously active list of tokens."""

        applied = previous.copy()

        for tkn in current_tag_group:
            targets = []

            clearer = Token.is_clear(tkn)
            if Token.is_clear(tkn):
                targets = [tkn.targets(tag) for tag in applied]

            if tkn in previous and not clearer:
                continue

            if clearer and not any(targets):
                continue

            applied.append(tkn)
            yield tkn

    def _remove_redundant_color(token: Token, new: Color) -> None:
        """Removes non-functional colors.

        These happen in the following ways:
        - Multiple colors of the same channel (fg/bg) are present.
        - A color is applied, then a clearer clears it.
        """

        for applied in current_tag_group.copy():
            if Token.is_clear(applied) and applied.targets(token):
                current_tag_group.remove(applied)

            if not Token.is_color(applied):
                continue

            old = applied.color

            if old.background == new.background:
                current_tag_group.remove(applied)

    for token in tokens:
        if Token.is_plain(token):
            yield from _diff_previous()
            yield token

            previous = current_tag_group.copy()

            continue

        if Token.is_color(token):
            new = token.color

            _remove_redundant_color(token, new)

            if not any(token.markup == applied.markup for applied in current_tag_group):
                current_tag_group.append(token)

            continue

        if token.is_style():
            if not any(token == tag for tag in current_tag_group):
                current_tag_group.append(token)

            continue

        if Token.is_clear(token):
            applied = False
            for tag in current_tag_group.copy():
                if token.targets(tag) or token == tag:
                    current_tag_group.remove(tag)
                    applied = True

            if not applied:
                continue

        current_tag_group.append(token)

    yield from _diff_previous()

overline(text, reset_style=True)

Return text overlined.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Note

This isnt' very widely supported.

Source code in pytermgui/ansi_interface.py
729
730
731
732
733
734
735
736
737
738
739
740
def overline(text: str, reset_style: Optional[bool] = True) -> str:
    """Return text overlined.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.

    Note:
        This isnt' very widely supported.
    """

    return set_mode("overline", False) + text + (reset() if reset_style else "")

parse(text, optimize=False, context=None, append_reset=True, ignore_unknown_tags=True)

Parses markup into the ANSI-coded string it represents.

Parameters:

Name Type Description Default
text str

Any valid markup.

required
optimize bool

If set, optimize_tokens will optimize the tokens found within the input markup before usage. This will incur a (minor) performance hit.

False
context ContextDict | None

The context that aliases and macros found within the markup will be searched in.

None
append_reset bool

If set, [/] will be appended to the token iterator, clearing all styles.

True
ignore_unknown_tags bool

If set, the MarkupSyntaxError coming from unknown tags will be silenced.

True

Returns:

Type Description
str

The ANSI-coded string that the markup represents.

Source code in pytermgui/markup/parsing.py
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
def parse(
    text: str,
    optimize: bool = False,
    context: ContextDict | None = None,
    append_reset: bool = True,
    ignore_unknown_tags: bool = True,
) -> str:
    """Parses markup into the ANSI-coded string it represents.

    Args:
        text: Any valid markup.
        optimize: If set, `optimize_tokens` will optimize the tokens found within the
            input markup before usage. This will incur a (minor) performance hit.
        context: The context that aliases and macros found within the markup will be
            searched in.
        append_reset: If set, `[/]` will be appended to the token iterator, clearing all
            styles.
        ignore_unknown_tags: If set, the `MarkupSyntaxError` coming from unknown tags
            will be silenced.

    Returns:
        The ANSI-coded string that the markup represents.
    """

    if context is None:
        context = create_context_dict()

    if append_reset and not text.endswith("/]"):
        text += "[/]"

    tokens = list(tokenize_markup(text))

    return parse_tokens(
        tokens,
        optimize=optimize,
        context=context,
        append_reset=append_reset,
        ignore_unknown_tags=ignore_unknown_tags,
    )

parse_tokens(tokens, *, optimize=False, context=None, append_reset=True, ignore_unknown_tags=True)

Parses a stream of tokens into the ANSI-coded string they represent.

Parameters:

Name Type Description Default
tokens list[Token]

Any list of Tokens, usually obtained from either tokenize_ansi or tokenize_markup.

required
optimize bool

If set, optimize_tokens will optimize the input iterator before usage. This will incur a (minor) performance hit.

False
context ContextDict | None

The context that aliases and macros found within the tokens will be searched in.

None
append_reset bool

If set, ClearToken("/") will be appended to the token iterator, clearing all styles.

True
ignore_unknown_tags bool

If set, the MarkupSyntaxError coming from unknown tags will be silenced.

True

Returns:

Type Description
str

The ANSI-coded string that the token stream represents.

Source code in pytermgui/markup/parsing.py
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
def parse_tokens(  # pylint: disable=too-many-branches, too-many-locals, too-many-statements
    tokens: list[Token],
    *,
    optimize: bool = False,
    context: ContextDict | None = None,
    append_reset: bool = True,
    ignore_unknown_tags: bool = True,
) -> str:
    """Parses a stream of tokens into the ANSI-coded string they represent.

    Args:
        tokens: Any list of Tokens, usually obtained from either `tokenize_ansi` or
            `tokenize_markup`.
        optimize: If set, `optimize_tokens` will optimize the input iterator before
            usage. This will incur a (minor) performance hit.
        context: The context that aliases and macros found within the tokens will be
            searched in.
        append_reset: If set, `ClearToken("/")` will be appended to the token iterator,
            clearing all styles.
        ignore_unknown_tags: If set, the `MarkupSyntaxError` coming from unknown tags
            will be silenced.

    Returns:
        The ANSI-coded string that the token stream represents.
    """

    if context is None:
        context = create_context_dict()

    token_list = _sub_aliases(tokens, context)

    # It's more computationally efficient to create this lambda once and reuse it
    # every time. There is no need to define a full function, as it just returns
    # a function return.
    get_full = (
        lambda: tokens_to_markup(  # pylint: disable=unnecessary-lambda-assignment
            token_list
        )
    )

    if optimize:
        token_list = list(optimize_tokens(token_list))

    if append_reset:
        token_list.append(ClearToken("/"))

    link = None
    output = ""
    segment = ""
    background = Color.parse("#000000")
    macros: list[MacroToken] = []
    unknown_aliases: list[Token] = []

    save_state: list[Token] = []

    for i, token in enumerate(token_list):
        if token.is_plain():
            value = _apply_macros(
                token.value, (parse_macro(macro, context, get_full) for macro in macros)
            )

            if len(unknown_aliases) > 0:
                output += f"[{' '.join(tkn.value for tkn in unknown_aliases)}]"
                unknown_aliases = []

            output += segment + (
                value if link is None else LINK_TEMPLATE.format(uri=link, label=value)
            )

            segment = ""
            continue

        if token.is_hyperlink():
            link = token.value
            continue

        if Token.is_macro(token):
            macros.append(token)
            continue

        if Token.is_clear(token):
            if token.value in ("/", "/~"):
                link = None

                if token.value == "/~":
                    continue

            found = False
            for macro in macros.copy():
                if token.targets(macro):
                    macros.remove(macro)
                    found = True
                    break

            if found and token.value != "/":
                continue

            if token.value.startswith("/!"):
                raise MarkupSyntaxError(
                    token.value, "has nothing to target", get_full()
                )

        if Token.is_color(token) and token.color.background:
            background = token.color

        if Token.is_pseudo(token):
            if token.value in STATE_PSEUDOS:
                segment += parse_state_pseudo(token, tokens, i, save_state, context)
                continue

            if token.value == "#auto":
                token = ColorToken("#auto", background.contrast)

        try:
            segment += PARSERS[type(token)](token, context, get_full)  # type: ignore

        except MarkupSyntaxError:
            if not ignore_unknown_tags:
                raise

            unknown_aliases.append(token)

    if len(unknown_aliases) > 0:
        output += f"[{' '.join(tkn.value for tkn in unknown_aliases)}]"

    output += segment

    return output

prettify(target, indent=2, force_markup=False, expand_all=False, parse=True)

Prettifies any Python object.

This uses a set of pre-defined aliases for the styling, and as such is fully customizable.

The aliases are:

  • str: Applied to all strings, so long as they do not contain TIM code.
  • int: Applied to all integers and booleans. The latter are included as they subclass int.
  • type: Applied to all types.
  • none: Applied to NoneType. Note that when using pytermgui.pretty or any of its printers, a single None return value will not be printed, only when part of a more complex structure.

Parameters:

Name Type Description Default
target Any

The object to prettify. Can be any type.

required
indent int

The indentation used for multi-line objects, like containers. When set to 0, these will be collapsed. By default, container types with len() == 1 are always collapsed, regardless of this value. See expand_all to overwrite that behaviour.

2
force_markup bool

When this is set every ANSI-sequence string will be turned into markup and syntax highlighted.

False
expand_all bool

When set, objects that would normally be force-collapsed are also going to be expanded.

False
parse bool

If not set, the return value will be a plain markup string, not yet parsed.

True

Returns:

Type Description
str

A pretty string of the given target.

Source code in pytermgui/prettifiers.py
 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
def prettify(  # pylint: disable=too-many-branches
    target: Any,
    indent: int = 2,
    force_markup: bool = False,
    expand_all: bool = False,
    parse: bool = True,
) -> str:
    """Prettifies any Python object.

    This uses a set of pre-defined aliases for the styling, and as such is fully
    customizable.

    The aliases are:

    - `str`: Applied to all strings, so long as they do not contain TIM code.
    - `int`: Applied to all integers and booleans. The latter are included as they
        subclass int.
    - `type`: Applied to all types.
    - `none`: Applied to NoneType. Note that when using `pytermgui.pretty` or any
        of its printers, a single `None` return value will not be printed, only when
        part of a more complex structure.

    Args:
        target: The object to prettify. Can be any type.
        indent: The indentation used for multi-line objects, like containers. When
            set to 0, these will be collapsed. By default, container types with
            `len() == 1` are always collapsed, regardless of this value. See
            `expand_all` to overwrite that behaviour.
        force_markup: When this is set every ANSI-sequence string will be turned
            into markup and syntax highlighted.
        expand_all: When set, objects that would normally be force-collapsed are
            also going to be expanded.
        parse: If not set, the return value will be a plain markup string, not yet
            parsed.

    Returns:
        A pretty string of the given target.
    """

    if isinstance(target, str):
        if RE_MARKUP.match(target) is not None:
            try:
                highlighted_target = highlight_tim(target)

                if parse:
                    return f'"{tim.parse(highlighted_target)}"'

                return highlighted_target + "[/]"

            except MarkupSyntaxError:
                pass

        if RE_ANSI.match(target) is not None:
            return target + "\x1b[0m"

        target = repr(target)

    if isinstance(target, CONTAINER_TYPES):
        if len(target) < 2 and not expand_all:
            indent = 0

        indent_str = ("\n" if indent > 0 else "") + indent * " "

        chars = str(target)[0], str(target)[-1]
        buff = chars[0]

        if isinstance(target, (dict, UserDict)):
            for i, (key, value) in enumerate(target.items()):
                if i > 0:
                    buff += ", "

                buff += indent_str + highlight_python(f"{key!r}: ")

                pretty = prettify(
                    value,
                    indent=indent,
                    expand_all=expand_all,
                    force_markup=force_markup,
                    parse=False,
                )

                lines = pretty.splitlines()
                buff += lines[0]

                for line in lines[1:]:
                    buff += indent_str + line

        else:
            for i, value in enumerate(target):
                if i > 0:
                    buff += ", "

                pretty = prettify(
                    value,
                    indent=indent,
                    expand_all=expand_all,
                    force_markup=force_markup,
                    parse=False,
                )

                lines = pretty.splitlines()

                for line in lines:
                    buff += indent_str + line

        if indent > 0:
            buff += "\n"

        buff += chars[1]

        if force_markup:
            return buff

        return tim.parse(buff)

    if supports_fancy_repr(target):
        buff = build_fancy_repr(target)

    else:
        buff = highlight_python(str(target))

    return tim.parse(buff) if parse else buff

print_to(pos, *args, **kwargs)

Prints text to given pos.

Note

This method passes through all arguments (except for pos) to the print method.

Source code in pytermgui/ansi_interface.py
620
621
622
623
624
625
626
627
628
629
def print_to(pos: tuple[int, int], *args: Any, **kwargs: Any) -> None:
    """Prints text to given `pos`.

    Note:
        This method passes through all arguments (except for `pos`) to the `print`
        method.
    """

    move_cursor(pos)
    print(*args, **kwargs)

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)

report_cursor()

Gets position of cursor.

Returns:

Type Description
'Optional[tuple[int, int]]'

A tuple of integers, (columns, rows), describing the

'Optional[tuple[int, int]]'

current (printing) cursor's position. Returns None if

'Optional[tuple[int, int]]'

this could not be determined.

'Optional[tuple[int, int]]'

Note that this position is not the mouse position. See

'Optional[tuple[int, int]]'

report_mouse if that is what you are interested in.

Source code in pytermgui/ansi_interface.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def report_cursor() -> "Optional[tuple[int, int]]":
    """Gets position of cursor.

    Returns:
        A tuple of integers, (columns, rows), describing the
        current (printing) cursor's position. Returns None if
        this could not be determined.

        Note that this position is **not** the mouse position. See
        `report_mouse` if that is what you are interested in.
    """

    get_terminal().write("\x1b[6n", flush=True)
    chars = getch()
    posy, posx = chars[2:-1].split(";")

    if not posx.isdigit() or not posy.isdigit():
        return None

    return int(posx), int(posy)

report_mouse(event, method='decimal_xterm', stop=False)

Starts reporting of mouse events.

You can specify multiple events to report on.

Parameters:

Name Type Description Default
event str

The type of event to report on. See below for options.

required
method Optional[str]

The method of reporting to use. See below for options.

'decimal_xterm'
stop bool

If set to True, the stopping code is written to stdout.

False

Raises:

Type Description
NotImplementedError

The given event is not supported.

Note

If you need this functionality, you're probably better off using the wrapper pytermgui.context_managers.mouse_handler, which allows listening on multiple events, gives a translator method and handles exceptions.

Possible events
  • press: Report when the mouse is clicked, left or right button.
  • highlight: Report highlighting.
  • press_hold: Report with a left or right click, as well as both left & right drag and release.
  • all: Report every event, even hover.

Functions:

Name Description
- **None**

Non-decimal xterm method. Limited in coordinates.

- **decimal_xterm**

The default setting. Most universally supported.

- **decimal_urxvt**

Older, less compatible, but useful on some systems.

- **decimal_utf8**

Apparently not too stable.

More information here.

Source code in pytermgui/ansi_interface.py
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
def report_mouse(
    event: str, method: Optional[str] = "decimal_xterm", stop: bool = False
) -> None:
    """Starts reporting of mouse events.

    You can specify multiple events to report on.

    Args:
        event: The type of event to report on. See below for options.
        method: The method of reporting to use. See below for options.
        stop: If set to True, the stopping code is written to stdout.

    Raises:
        NotImplementedError: The given event is not supported.

    Note:
        If you need this functionality, you're probably better off using the wrapper
        `pytermgui.context_managers.mouse_handler`, which allows listening on multiple
        events, gives a translator method and handles exceptions.

    Possible events:
        - **press**: Report when the mouse is clicked, left or right button.
        - **highlight**: Report highlighting.
        - **press_hold**: Report with a left or right click, as well as both
            left & right drag and release.
        - **all**: Report every event, even hover.

    Methods:
        - **None**: Non-decimal xterm method. Limited in coordinates.
        - **decimal_xterm**: The default setting. Most universally supported.
        - **decimal_urxvt**: Older, less compatible, but useful on some systems.
        - **decimal_utf8**:  Apparently not too stable.

    More information <a href='https://stackoverflow.com/a/5970472'>here</a>.
    """

    terminal = get_terminal()

    if event == "press":
        terminal.write("\x1b[?1000")

    elif event == "highlight":
        terminal.write("\x1b[?1001")

    elif event == "press_hold":
        terminal.write("\x1b[?1002")

    elif event == "all":
        terminal.write("\x1b[?1003")

    else:
        raise NotImplementedError(f"Mouse report event {event!r} is not supported!")

    terminal.write("l" if stop else "h")

    if method == "decimal_utf8":
        terminal.write("\x1b[?1005")

    elif method == "decimal_xterm":
        terminal.write("\x1b[?1006")

    elif method == "decimal_urxvt":
        terminal.write("\x1b[?1015")

    elif method is None:
        return

    else:
        raise NotImplementedError(f"Mouse report method {method} is not supported!")

    terminal.write("l" if stop else "h", flush=True)

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)

restore_cursor()

Restore cursor position as saved by save_cursor.

Source code in pytermgui/ansi_interface.py
150
151
152
153
def restore_cursor() -> None:
    """Restore cursor position as saved by `save_cursor`."""

    get_terminal().write("\x1b[u")

restore_screen()

Restores the contents of the screen saved by save_screen().

Source code in pytermgui/ansi_interface.py
83
84
85
86
def restore_screen() -> None:
    """Restores the contents of the screen saved by `save_screen()`."""

    print("\x1b[?47l")

save_cursor()

Saves the current cursor position.

Use restore_cursor to restore it.

Source code in pytermgui/ansi_interface.py
141
142
143
144
145
146
147
def save_cursor() -> None:
    """Saves the current cursor position.

    Use `restore_cursor` to restore it.
    """

    get_terminal().write("\x1b[s")

save_screen()

Saves the contents of the screen, and wipes it.

Use restore_screen() to get them back.

Source code in pytermgui/ansi_interface.py
74
75
76
77
78
79
80
def save_screen() -> None:
    """Saves the contents of the screen, and wipes it.

    Use `restore_screen()` to get them back.
    """

    print("\x1b[?47h")

set_alt_buffer()

Starts an alternate buffer.

Source code in pytermgui/ansi_interface.py
89
90
91
92
def set_alt_buffer() -> None:
    """Starts an alternate buffer."""

    print("\x1b[?1049h")

set_echo()

Starts echoing of user input.

Note

This is currently only available on POSIX.

Source code in pytermgui/ansi_interface.py
351
352
353
354
355
356
357
358
359
360
361
def set_echo() -> None:
    """Starts echoing of user input.

    Note:
        This is currently only available on POSIX.
    """

    if not _name == "posix":
        return

    system("stty echo")

set_global_terminal(new)

Sets the terminal instance to be used by the module.

Source code in pytermgui/term.py
639
640
641
642
def set_global_terminal(new: Terminal) -> None:
    """Sets the terminal instance to be used by the module."""

    globals()["terminal"] = new

set_mode(mode, write=True)

Sets terminal display mode.

This is better left internal. To use these modes, you can call their specific functions, such as bold("text") or italic("text").

Parameters:

Name Type Description Default
mode Union[str, int]

One of the available modes. Strings and integers both work.

required
write bool

Boolean that determines whether the output should be written to stdout.

True

Returns:

Type Description
str

A string that sets the given mode.

Available modes
  • 0: reset
  • 1: bold
  • 2: dim
  • 3: italic
  • 4: underline
  • 5: blink
  • 7: inverse
  • 8: invisible
  • 9: strikethrough
  • 53: overline
Source code in pytermgui/ansi_interface.py
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
def set_mode(mode: Union[str, int], write: bool = True) -> str:
    """Sets terminal display mode.

    This is better left internal. To use these modes, you can call their
    specific functions, such as `bold("text")` or `italic("text")`.

    Args:
        mode: One of the available modes. Strings and integers both work.
        write: Boolean that determines whether the output should be written
            to stdout.

    Returns:
        A string that sets the given mode.

    Available modes:
        - 0: reset
        - 1: bold
        - 2: dim
        - 3: italic
        - 4: underline
        - 5: blink
        - 7: inverse
        - 8: invisible
        - 9: strikethrough
        - 53: overline
    """

    options = {
        "reset": 0,
        "bold": 1,
        "dim": 2,
        "italic": 3,
        "underline": 4,
        "blink": 5,
        "inverse": 7,
        "invisible": 8,
        "strikethrough": 9,
        "overline": 53,
    }

    if not str(mode).isdigit():
        mode = options[str(mode)]

    code = f"\x1b[{mode}m"
    if write:
        get_terminal().write(code)

    return code

show_cursor()

Starts printing the cursor.

Source code in pytermgui/ansi_interface.py
135
136
137
138
def show_cursor() -> None:
    """Starts printing the cursor."""

    get_terminal().write("\x1b[?25h")

str_to_color(text, is_background=False, localize=True, use_cache=True) cached

Creates a Color from the given text.

Accepted formats:

  • 0-255: IndexedColor.
  • 'rrr;ggg;bbb': RGBColor.
  • '(#)rrggbb': HEXColor. Leading hash is optional.

You can also add a leading '@' into the string to make the output represent a background color, such as @#123abc.

Parameters:

Name Type Description Default
text str

The string to format from.

required
is_background bool

Whether the output should be forced into a background color. Mostly used internally, when set will take precedence over syntax of leading '@' symbol.

False
localize bool

Whether get_localized should be called on the output color.

True
use_cache bool

Whether caching should be used.

True
Source code in pytermgui/colors.py
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
@lru_cache(maxsize=1024)
def str_to_color(
    text: str,
    is_background: bool = False,
    localize: bool = True,
    use_cache: bool = True,
) -> Color:
    """Creates a `Color` from the given text.

    Accepted formats:

    - 0-255: `IndexedColor`.
    - 'rrr;ggg;bbb': `RGBColor`.
    - '(#)rrggbb': `HEXColor`. Leading hash is optional.

    You can also add a leading '@' into the string to make the output represent a
    background color, such as `@#123abc`.

    Args:
        text: The string to format from.
        is_background: Whether the output should be forced into a background color.
            Mostly used internally, when set will take precedence over syntax of leading
            '@' symbol.
        localize: Whether `get_localized` should be called on the output color.
        use_cache: Whether caching should be used.
    """

    def _trim_code(code: str) -> str:
        """Trims the given color code."""

        if not all(char.isdigit() or char in "m;" for char in code):
            return code

        is_background = code.startswith("48;")

        if (code.startswith("38;5;") or code.startswith("48;5;")) or (
            code.startswith("38;2;") or code.startswith("48;2;")
        ):
            code = code[5:]

        if code.endswith("m"):
            code = code[:-1]

        if is_background:
            code = "@" + code

        return code

    text = _trim_code(text)

    if not use_cache:
        str_to_color.cache_clear()

    if text.startswith("@"):
        is_background = True
        text = text[1:]

    if text in NAMED_COLORS:
        return str_to_color(str(NAMED_COLORS[text]), is_background=is_background)

    color: Color

    # This code is not pretty, but having these separate branches for each type
    # should improve the performance by quite a large margin.
    match = RE_256.match(text)
    if match is not None:
        # Note: At the moment, all colors become an `IndexedColor`, due to a large
        #       amount of problems a separated `StandardColor` class caused. Not
        #       sure if there are any real drawbacks to doing it this way, bar the
        #       extra characters that 255 colors use up compared to xterm-16.
        color = IndexedColor(match[0], background=is_background)

        return color.get_localized() if localize else color

    match = RE_HEX.match(text)
    if match is not None:
        color = HEXColor(match[0], background=is_background)

        return color.get_localized() if localize else color

    match = RE_RGB.match(text)
    if match is not None:
        color = RGBColor(match[0], background=is_background)

        return color.get_localized() if localize else color

    raise ColorSyntaxError(f"Could not convert {text!r} into a `Color`.")

strikethrough(text, reset_style=True)

Return text as strikethrough.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Source code in pytermgui/ansi_interface.py
718
719
720
721
722
723
724
725
726
def strikethrough(text: str, reset_style: Optional[bool] = True) -> str:
    """Return text as strikethrough.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.
    """

    return set_mode("strikethrough", False) + text + (reset() if reset_style else "")

strip_ansi(text) cached

Removes ANSI sequences from text.

Parameters:

Name Type Description Default
text str

A string or bytes object containing 0 or more ANSI sequences.

required

Returns:

Type Description
str

The text without any ANSI sequences.

Source code in pytermgui/regex.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@lru_cache()
def strip_ansi(text: str) -> str:
    """Removes ANSI sequences from text.

    Args:
        text: A string or bytes object containing 0 or more ANSI sequences.

    Returns:
        The text without any ANSI sequences.
    """

    if hasattr(text, "plain"):
        return text.plain  # type: ignore

    return RE_ANSI.sub("", text)

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)

supports_fancy_repr(obj)

Determines whether the given object supports the fancy repl protocol.

Source code in pytermgui/fancy_repr.py
37
38
39
40
def supports_fancy_repr(obj: object) -> bool:
    """Determines whether the given object supports the fancy repl protocol."""

    return hasattr(obj, "__fancy_repr__") and not isinstance(obj, type)

to_html(obj, prefix=None, inline_styles=False, include_background=True, vertical_offset=0.0, horizontal_offset=0.0, formatter=HTML_FORMAT, joiner='\n')

Creates a static HTML representation of the given object.

Note that the output HTML will not be very attractive or easy to read. This is because these files probably aren't meant to be read by a human anyways, so file sizes are more important.

If you do care about the visual style of the output, you can run it through some prettifiers to get the result you are looking for.

Parameters:

Name Type Description Default
obj Widget | StyledText | str

The object to represent. Takes either a Widget or some markup text.

required
prefix str | None

The prefix included in the generated classes, e.g. instead of ptg-0, you would get ptg-my-prefix-0.

None
inline_styles bool

If set, styles will be set for each span using the inline style argument, otherwise a full style section is constructed.

False
include_background bool

Whether to include the terminal's background color in the output.

True
Source code in pytermgui/exporters.py
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
def to_html(  # pylint: disable=too-many-arguments, too-many-locals, R0917
    obj: Widget | StyledText | str,
    prefix: str | None = None,
    inline_styles: bool = False,
    include_background: bool = True,
    vertical_offset: float = 0.0,
    horizontal_offset: float = 0.0,
    formatter: str = HTML_FORMAT,
    joiner: str = "\n",
) -> str:
    """Creates a static HTML representation of the given object.

    Note that the output HTML will not be very attractive or easy to read. This is
    because these files probably aren't meant to be read by a human anyways, so file
    sizes are more important.

    If you do care about the visual style of the output, you can run it through some
    prettifiers to get the result you are looking for.

    Args:
        obj: The object to represent. Takes either a Widget or some markup text.
        prefix: The prefix included in the generated classes, e.g. instead of `ptg-0`,
            you would get `ptg-my-prefix-0`.
        inline_styles: If set, styles will be set for each span using the inline `style`
            argument, otherwise a full style section is constructed.
        include_background: Whether to include the terminal's background color in the
            output.
    """

    document_styles: list[list[str]] = []

    if isinstance(obj, Widget):
        data = obj.get_lines()

    elif isinstance(obj, str):
        data = obj.splitlines()

    else:
        data = str(obj).splitlines()

    lines = []
    for dataline in data:
        line = ""

        for span, styles in _get_spans(
            dataline, vertical_offset, horizontal_offset, include_background
        ):
            index = _generate_index_in(document_styles, styles)
            if index == len(document_styles):
                document_styles.append(styles)

            if inline_styles:
                stylesheet = ";".join(styles)
                line += span.format(f" styles='{stylesheet}'")

            else:
                line += span.format(" class='" + _get_cls(prefix or "", index) + "'")

        # Close any previously not closed divs
        line += "</div>" * (line.count("<div") - line.count("</div"))
        lines.append(line)

    stylesheet = ""
    if not inline_styles:
        stylesheet = _generate_stylesheet(document_styles, prefix)

    document = formatter.format(
        foreground=Color.get_default_foreground().hex,
        background=Color.get_default_background().hex if include_background else "",
        content=joiner.join(lines),
        styles=stylesheet,
        font_size=FONT_SIZE,
    )

    return document

token_to_css(token, invert=False)

Finds the CSS representation of a token.

Parameters:

Name Type Description Default
token Token

The token to represent.

required
invert bool

If set, the role of background & foreground colors are flipped.

False
Source code in pytermgui/exporters.py
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
def token_to_css(token: Token, invert: bool = False) -> str:
    """Finds the CSS representation of a token.

    Args:
        token: The token to represent.
        invert: If set, the role of background & foreground colors
            are flipped.
    """

    if Token.is_color(token):
        color = token.color

        style = "color:" + color.hex

        if invert:
            color.background = not color.background

        if color.background:
            style = "background-" + style

        return style

    if token.is_style() and token.value in _STYLE_TO_CSS:
        return _STYLE_TO_CSS[token.value]

    return ""

tokenize_ansi(text)

Converts some ANSI-coded text into a stream of tokens.

Parameters:

Name Type Description Default
text str

Any valid ANSI-coded text.

required

Yields:

Type Description
Token

The generated tokens, in the order they occur within the text.

Source code in pytermgui/markup/parsing.py
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
def tokenize_ansi(  # pylint: disable=too-many-locals, too-many-branches, too-many-statements
    text: str,
) -> Iterator[Token]:
    """Converts some ANSI-coded text into a stream of tokens.

    Args:
        text: Any valid ANSI-coded text.

    Yields:
        The generated tokens, in the order they occur within the text.
    """

    cursor = 0

    for matchobj in RE_ANSI.finditer(text):
        start, end = matchobj.span()

        csi = matchobj.groups()[0:2]
        link_osc = matchobj.groups()[2:4]

        if cursor < start:
            yield PlainToken(text[cursor:start])

        if matchobj.groups()[4] is not None:
            cursor = end
            yield ClearToken("/~")
            continue

        if link_osc != (None, None):
            cursor = end
            uri, label = link_osc

            yield HLinkToken(uri)
            yield PlainToken(label)
            yield ClearToken("/~")

            continue

        full, content = csi

        cursor = end

        code = ""

        # Position
        posmatch = RE_POSITION.match(full)

        if posmatch is not None:
            ypos, xpos = posmatch.groups()
            if not ypos and not xpos:
                raise ValueError(
                    f"Cannot parse cursor when no position is supplied. Match: {posmatch!r}"
                )

            yield CursorToken(content, int(ypos) or None, int(xpos) or None)
            continue

        parts = content.split(";")

        state = None
        color_code = ""
        for part in parts:
            if state is None:
                if part in REVERSE_STYLES:
                    yield StyleToken(REVERSE_STYLES[part])
                    continue

                if part in REVERSE_CLEARERS:
                    yield ClearToken(REVERSE_CLEARERS[part])
                    continue

                if part in ("38", "48"):
                    state = "COLOR"
                    color_code += part + ";"
                    continue

                # standard colors
                try:
                    yield ColorToken(part, Color.parse(part, localize=False))
                    continue

                except ColorSyntaxError as exc:
                    raise ValueError(f"Could not parse color tag {part!r}.") from exc

            if state != "COLOR":
                continue

            color_code += part + ";"

            # Ignore incomplete RGB colors
            if (
                color_code.startswith(("38;2;", "48;2;"))
                and len(color_code.split(";")) != 6
            ):
                continue

            try:
                code = color_code

                if code.startswith(("38;2;", "48;2;", "38;5;", "48;5;")):
                    stripped = code[5:-1]

                    if code.startswith("4"):
                        stripped = "@" + stripped

                    code = stripped

                yield ColorToken(code, Color.parse(code, localize=False))

            except ColorSyntaxError:
                continue

            state = None
            color_code = ""

    remaining = text[cursor:]
    if len(remaining) > 0:
        yield PlainToken(remaining)

tokenize_markup(text)

Converts some markup text into a stream of tokens.

Parameters:

Name Type Description Default
text str

Any valid markup.

required

Yields:

Type Description
Token

The generated tokens, in the order they occur within the markup.

Source code in pytermgui/markup/parsing.py
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
def tokenize_markup(text: str) -> Iterator[Token]:
    """Converts some markup text into a stream of tokens.

    Args:
        text: Any valid markup.

    Yields:
        The generated tokens, in the order they occur within the markup.
    """

    cursor = 0
    length = len(text)
    has_inverse = False
    for matchobj in RE_MARKUP.finditer(text):
        full, escapes, content = matchobj.groups()
        start, end = matchobj.span()

        if cursor < start:
            yield PlainToken(text[cursor:start])

        if not escapes == "":
            _, remaining = divmod(len(escapes), 2)

            yield PlainToken(full[max(1 - remaining, 1) :])
            cursor = end

            continue

        for tag in content.split():
            if tag == "inverse":
                has_inverse = True

            if tag == "/inverse":
                has_inverse = False

            consumed = consume_tag(tag)
            if has_inverse:
                if consumed.markup == "/fg":
                    consumed = ClearToken("/fg")

                elif consumed.markup == "/bg":
                    consumed = ClearToken("/bg")

            yield consumed

        cursor = end

    if cursor < length:
        yield PlainToken(text[cursor:length])

tokens_to_markup(tokens)

Converts a token stream into the markup of its tokens.

Parameters:

Name Type Description Default
tokens list[Token]

Any list of Token objects. Usually obtained from tokenize_markup or tokenize_ansi.

required

Returns:

Type Description
str

The markup the given tokens represent.

Source code in pytermgui/markup/parsing.py
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
def tokens_to_markup(tokens: list[Token]) -> str:
    """Converts a token stream into the markup of its tokens.

    Args:
        tokens: Any list of Token objects. Usually obtained from `tokenize_markup` or
            `tokenize_ansi`.

    Returns:
        The markup the given tokens represent.
    """

    tags: list[Token] = []
    markup = ""

    for token in tokens:
        if token.is_plain():
            if len(tags) > 0:
                markup += f"[{' '.join(tag.markup for tag in tags)}]"

            markup += token.value

            tags = []

        else:
            tags.append(token)

    if len(tags) > 0:
        markup += f"[{' '.join(tag.markup for tag in tags)}]"

    return markup

translate_mouse(code, method)

Translates the output of produced by setting report_mouse into MouseEvents.

This method currently only supports decimal_xterm and decimal_urxvt.

Parameters:

Name Type Description Default
code str

The string of mouse code(s) to translate.

required
method str

The reporting method to translate. One of decimal_xterm, decimal_urxvt.

required

Returns:

Type Description
list[MouseEvent | None] | None

A list of optional mouse events obtained from the code argument. If the code was malformed,

list[MouseEvent | None] | None

and no codes could be determined None is returned.

Source code in pytermgui/ansi_interface.py
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
def translate_mouse(code: str, method: str) -> list[MouseEvent | None] | None:
    """Translates the output of produced by setting `report_mouse` into MouseEvents.

    This method currently only supports `decimal_xterm` and `decimal_urxvt`.

    Args:
        code: The string of mouse code(s) to translate.
        method: The reporting method to translate. One of `decimal_xterm`, `decimal_urxvt`.

    Returns:
        A list of optional mouse events obtained from the code argument. If the code was malformed,
        and no codes could be determined None is returned.
    """

    if code == "\x1b":
        return None

    mouse_codes = {
        "decimal_xterm": {
            "0M": MouseAction.LEFT_CLICK,
            "0m": MouseAction.RELEASE,
            "2M": MouseAction.RIGHT_CLICK,
            "2m": MouseAction.RELEASE,
            "32": MouseAction.LEFT_DRAG,
            "34": MouseAction.RIGHT_DRAG,
            "35": MouseAction.HOVER,
            "64": MouseAction.SCROLL_UP,
            "65": MouseAction.SCROLL_DOWN,
            "68": MouseAction.SHIFT_SCROLL_UP,
            "69": MouseAction.SHIFT_SCROLL_DOWN,
        },
        "decimal_urxvt": {
            "32": MouseAction.LEFT_CLICK,
            "34": MouseAction.RIGHT_CLICK,
            "35": MouseAction.RELEASE,
            "64": MouseAction.LEFT_DRAG,
            "66": MouseAction.RIGHT_DRAG,
            "96": MouseAction.SCROLL_UP,
            "97": MouseAction.SCROLL_DOWN,
        },
    }

    mapping = mouse_codes[method]
    pattern: Pattern = RE_MOUSE[method]

    events: list[MouseEvent | None] = []

    for sequence in code.split("\x1b"):
        if len(sequence) == 0:
            continue

        matches = list(pattern.finditer(sequence))
        if len(matches) == 0:
            return None

        for match in matches:
            identifier, *pos, release_code = match.groups()

            # decimal_xterm uses the last character's
            # capitalization to signify press/release state
            if len(release_code) > 0 and identifier in ["0", "2"]:
                identifier += release_code

            if identifier in mapping:
                action = mapping[identifier]
                assert isinstance(action, MouseAction)

                events.append(MouseEvent(action, (int(pos[0]), int(pos[1]))))
                continue

            events.append(None)

    return events

triadic(base)

Three complementary colors.

Each color is offset 120 degrees from the previous one on the colorwheel. If plotted on the colorwheel, they make up a regular triangle.

Parameters:

Name Type Description Default
base Color

The color used for derivations.

required

Triadic strategy

Source code in pytermgui/palettes.py
54
55
56
57
58
59
60
61
62
63
64
65
66
def triadic(base: Color) -> tuple[Color, Color, Color, Color]:
    """Three complementary colors.

    Each color is offset 120 degrees from the previous one on the colorwheel. If
    plotted on the colorwheel, they make up a regular triangle.

    Args:
        base: The color used for derivations.

    ![Triadic strategy](../../assets/triadic.svg)
    """

    return (*base.triadic, base.complement)

underline(text, reset_style=True)

Returns text underlined.

Parameters:

Name Type Description Default
reset_style Optional[bool]

Boolean that determines whether a reset character should be appended to the end of the string.

True
Source code in pytermgui/ansi_interface.py
671
672
673
674
675
676
677
678
679
def underline(text: str, reset_style: Optional[bool] = True) -> str:
    """Returns text underlined.

    Args:
        reset_style: Boolean that determines whether a reset character should
            be appended to the end of the string.
    """

    return set_mode("underline", False) + text + (reset() if reset_style else "")

unset_alt_buffer()

Returns to main buffer, restoring its original state.

Source code in pytermgui/ansi_interface.py
95
96
97
98
def unset_alt_buffer() -> None:
    """Returns to main buffer, restoring its original state."""

    print("\x1b[?1049l")

unset_echo()

Stops echoing of user input.

Note

This is currently only available on POSIX.

Source code in pytermgui/ansi_interface.py
364
365
366
367
368
369
370
371
372
373
374
def unset_echo() -> None:
    """Stops echoing of user input.

    Note:
        This is currently only available on POSIX.
    """

    if not _name == "posix":
        return

    system("stty -echo")