Skip to content

styles

Conveniences for styling widgets

All styles have a depth and item argument. depth is an int that represents that "deep" the Widget is within the hierarchy, and item is the string that the style is applied to.

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

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"\[")

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