pytermgui.widgets.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.

  1"""
  2Conveniences for styling widgets
  3
  4All styles have a `depth` and `item` argument. `depth` is an int
  5that represents that "deep" the Widget is within the hierarchy, and
  6`item` is the string that the style is applied to.
  7"""
  8
  9# pylint: disable=unused-argument, too-many-instance-attributes
 10# pylint: disable=unnecessary-lambda-assignment
 11
 12from __future__ import annotations
 13
 14from collections import UserDict
 15from dataclasses import dataclass, field
 16from typing import TYPE_CHECKING, Any, Callable, List, Type, Union
 17
 18from ..highlighters import Highlighter
 19from ..markup import get_markup, tim
 20from ..regex import RE_MARKUP, strip_ansi
 21
 22__all__ = [
 23    "MarkupFormatter",
 24    "HighlighterStyle",
 25    "StyleCall",
 26    "StyleType",
 27    "StyleManager",
 28    "DepthlessStyleType",
 29    "CharType",
 30    "MARKUP",
 31    "FOREGROUND",
 32    "BACKGROUND",
 33    "CLICKABLE",
 34    "CLICKED",
 35]
 36
 37if TYPE_CHECKING:
 38    from .base import Widget
 39
 40StyleType = Callable[[int, str], str]
 41DepthlessStyleType = Callable[[str], str]
 42CharType = Union[List[str], str]
 43
 44StyleValue = Union[str, "MarkupFormatter", "HighlighterStyle", "StyleCall", StyleType]
 45
 46
 47@dataclass
 48class StyleCall:
 49    """A callable object that simplifies calling style methods.
 50
 51    Instances of this class are created within the `Widget._get_style`
 52    method, and this class should not be used outside of that context."""
 53
 54    obj: Widget | Type[Widget] | None
 55    method: StyleType
 56
 57    def __call__(self, item: str) -> str:
 58        """DepthlessStyleType: Apply style method to item, using depth"""
 59
 60        if self.obj is None:
 61            raise ValueError(
 62                f"Can not call {self.method!r}, as no object is assigned to this StyleCall."
 63            )
 64
 65        try:
 66            # mypy fails on one machine with this, but not on the other.
 67            return self.method(self.obj.depth, item)  # type: ignore
 68
 69        # this is purposefully broad, as anything can happen during these calls.
 70        except Exception as error:
 71            raise RuntimeError(
 72                f'Could not apply style {self.method} to "{item}": {error}'  # type: ignore
 73            ) from error
 74
 75    def __eq__(self, other: object) -> bool:
 76        if not isinstance(other, type(self)):
 77            return False
 78
 79        return other.method == self.method
 80
 81
 82@dataclass
 83class MarkupFormatter:
 84    """A style that formats depth & item into the given markup on call.
 85
 86    Useful in Widget styles, such as:
 87
 88    ```python3
 89    import pytermgui as ptg
 90
 91    root = ptg.Container()
 92
 93    # Set border style to be reactive to the widget's depth
 94    root.set_style("border", ptg.MarkupFactory("[35 @{depth}]{item}]")
 95    ```
 96    """
 97
 98    markup: str
 99    ensure_strip: bool = False
100
101    _markup_cache: dict[str, str] = field(init=False, default_factory=dict)
102
103    def __call__(self, depth: int, item: str) -> str:
104        """StyleType: Format depth & item into given markup template"""
105
106        if self.ensure_strip:
107            item = strip_ansi(item)
108
109        if item in self._markup_cache:
110            item = self._markup_cache[item]
111
112        else:
113            original = item
114            item = get_markup(item)
115            self._markup_cache[original] = item
116
117        return tim.parse(self.markup.format(depth=depth, item=item))
118
119    def __str__(self) -> str:
120        """Returns __repr__, but with markup escaped."""
121
122        return self.__repr__().replace("[", r"\[")
123
124
125@dataclass
126class HighlighterStyle:
127    """A style that highlights the items given to it.
128
129    See `pytermgui.highlighters` for more information.
130    """
131
132    highlighter: Highlighter
133
134    def __call__(self, _: int, item: str) -> str:
135        """Highlights the given string."""
136
137        return tim.parse(self.highlighter(item))
138
139
140# There is only a single ancestor here.
141class StyleManager(UserDict):  # pylint: disable=too-many-ancestors
142    """An fancy dictionary to manage a Widget's styles.
143
144    Individual styles can be accessed two ways:
145
146    ```python3
147    manager.styles.style_name == manager._get_style("style_name")
148    ```
149
150    Same with setting:
151
152    ```python3
153    widget.styles.style_name = ...
154    widget.set_style("style_name", ...)
155    ```
156
157    The `set` and `get` methods remain for backwards compatibility reasons, but all
158    newly written code should use the dot syntax.
159
160    It is also possible to set styles as markup shorthands. For example:
161
162    ```python3
163    widget.styles.border = "60 bold"
164    ```
165
166    ...is equivalent to:
167
168    ```python3
169    widget.styles.border = "[60 bold]{item}"
170    ```
171    """
172
173    def __init__(
174        self,
175        parent: Widget | Type[Widget] | None = None,
176        **base,
177    ) -> None:
178
179        """Initializes a `StyleManager`.
180
181        Args:
182            parent: The parent of this instance. It will be assigned in all
183                `StyleCall`-s created by it.
184        """
185
186        self.__dict__["_is_setup"] = False
187
188        self.parent = parent
189
190        super().__init__()
191
192        for key, value in base.items():
193            self._set_as_stylecall(key, value)
194
195        self.__dict__["_is_setup"] = self.parent is not None
196
197    @staticmethod
198    def expand_shorthand(shorthand: str) -> MarkupFormatter:
199        """Expands a shorthand string into a `MarkupFormatter` instance.
200
201        For example, all of these will expand into `MarkupFormatter([60]{item}')`:
202        - '60'
203        - '[60]'
204        - '[60]{item}'
205
206        Args:
207            shorthand: The short version of markup to expand.
208
209        Returns:
210            A `MarkupFormatter` with the expanded markup.
211        """
212
213        if len(shorthand) == 0:
214            return MarkupFormatter("{item}")
215
216        if RE_MARKUP.match(shorthand) is not None:
217            return MarkupFormatter(shorthand)
218
219        markup = "[" + shorthand + "]"
220
221        if not "{item}" in shorthand:
222            markup += "{item}"
223
224        return MarkupFormatter(markup)
225
226    @classmethod
227    def merge(cls, other: StyleManager, **styles) -> StyleManager:
228        """Creates a new manager that merges `other` with the passed in styles.
229
230        Args:
231            other: The style manager to base the new one from.
232            **styles: The additional styles the new instance should have.
233
234        Returns:
235            A new `StyleManager`. This instance will only gather its data when
236            `branch` is called on it. This is done so any changes made to the original
237            data between the `merge` call and the actual usage of the instance will be
238            reflected.
239        """
240
241        return cls(**{**other, **styles})
242
243    def branch(self, parent: Widget | Type[Widget]) -> StyleManager:
244        """Branch off from the `base` style dictionary.
245
246        This method should be called during widget construction. It creates a new
247        `StyleManager` based on self, but with its data detached from the original.
248
249        Args:
250            parent: The parent of the new instance.
251
252        Returns:
253            A new `StyleManager`, with detached instances of data. This can then be
254            modified without touching the original instance.
255        """
256
257        return type(self)(parent, **self.data)
258
259    def _set_as_stylecall(self, key: str, item: StyleValue) -> None:
260        """Sets `self.data[key]` as a `StyleCall` of the given item.
261
262        If the item is a string, it will be expanded into a `MarkupFormatter` before
263        being converted into the `StyleCall`, using `expand_shorthand`.
264        """
265
266        if isinstance(item, StyleCall):
267            self.data[key] = StyleCall(self.parent, item.method)
268            return
269
270        if isinstance(item, str):
271            item = self.expand_shorthand(item)
272
273        self.data[key] = StyleCall(self.parent, item)
274
275    def __setitem__(self, key: str, value: StyleValue) -> None:
276        """Sets an item in `self.data`.
277
278        If the item is a string, it will be expanded into a `MarkupFormatter` before
279        being converted into the `StyleCall`, using `expand_shorthand`.
280        """
281
282        self._set_as_stylecall(key, value)
283
284    def __setattr__(self, key: str, value: StyleValue) -> None:
285        """Sets an attribute.
286
287        It first looks if it can set inside self.data, and defaults back to
288        self.__dict__.
289
290        Raises:
291            KeyError: The given key is not a defined attribute, and is not part of this
292                object's style set.
293        """
294
295        found = False
296        if "data" in self.__dict__:
297            for part in key.split("__"):
298                if part in self.data:
299                    self._set_as_stylecall(part, value)
300                    found = True
301
302        if found:
303            return
304
305        if self.__dict__.get("_is_setup") and key not in self.__dict__:
306            raise KeyError(f"Style {key!r} was not defined during construction.")
307
308        self.__dict__[key] = value
309
310    def __getattr__(self, key: str) -> StyleCall:
311        """Allows styles.dot_syntax."""
312
313        if key in self.__dict__:
314            return self.__dict__[key]
315
316        if key in self.__dict__["data"]:
317            return self.__dict__["data"][key]
318
319        raise AttributeError(key, self.data)
320
321    def __call__(self, **styles: StyleValue) -> Any:
322        """Allows calling the manager and setting its styles.
323
324        For example:
325        ```
326        >>> Button("Hello").styles(label="@60")
327        ```
328        """
329
330        for key, value in styles.items():
331            self._set_as_stylecall(key, value)
332
333        return self.parent
334
335
336CLICKABLE = MarkupFormatter("[@238 72 bold]{item}")
337"""Style for inactive clickable things, such as `pytermgui.widgets.Button`"""
338
339CLICKED = MarkupFormatter("[238 @72 bold]{item}")
340"""Style for active clickable things, such as `pytermgui.widgets.Button`"""
341
342FOREGROUND = lambda _, item: item
343"""Standard foreground style, currently unused by the library"""
344
345BACKGROUND = lambda _, item: item
346"""Standard background, used by most `fill` styles"""
347
348MARKUP = lambda depth, item: tim.parse(item)
349"""Style that parses value as markup. Used by most text labels, like `pytermgui.widgets.Label`"""
@dataclass
class MarkupFormatter:
 83@dataclass
 84class MarkupFormatter:
 85    """A style that formats depth & item into the given markup on call.
 86
 87    Useful in Widget styles, such as:
 88
 89    ```python3
 90    import pytermgui as ptg
 91
 92    root = ptg.Container()
 93
 94    # Set border style to be reactive to the widget's depth
 95    root.set_style("border", ptg.MarkupFactory("[35 @{depth}]{item}]")
 96    ```
 97    """
 98
 99    markup: str
100    ensure_strip: bool = False
101
102    _markup_cache: dict[str, str] = field(init=False, default_factory=dict)
103
104    def __call__(self, depth: int, item: str) -> str:
105        """StyleType: Format depth & item into given markup template"""
106
107        if self.ensure_strip:
108            item = strip_ansi(item)
109
110        if item in self._markup_cache:
111            item = self._markup_cache[item]
112
113        else:
114            original = item
115            item = get_markup(item)
116            self._markup_cache[original] = item
117
118        return tim.parse(self.markup.format(depth=depth, item=item))
119
120    def __str__(self) -> str:
121        """Returns __repr__, but with markup escaped."""
122
123        return self.__repr__().replace("[", r"\[")

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}]")
MarkupFormatter(markup: str, ensure_strip: bool = False)
ensure_strip: bool = False
@dataclass
class HighlighterStyle:
126@dataclass
127class HighlighterStyle:
128    """A style that highlights the items given to it.
129
130    See `pytermgui.highlighters` for more information.
131    """
132
133    highlighter: Highlighter
134
135    def __call__(self, _: int, item: str) -> str:
136        """Highlights the given string."""
137
138        return tim.parse(self.highlighter(item))

A style that highlights the items given to it.

See pytermgui.highlighters for more information.

HighlighterStyle(highlighter: pytermgui.highlighters.Highlighter)
@dataclass
class StyleCall:
48@dataclass
49class StyleCall:
50    """A callable object that simplifies calling style methods.
51
52    Instances of this class are created within the `Widget._get_style`
53    method, and this class should not be used outside of that context."""
54
55    obj: Widget | Type[Widget] | None
56    method: StyleType
57
58    def __call__(self, item: str) -> str:
59        """DepthlessStyleType: Apply style method to item, using depth"""
60
61        if self.obj is None:
62            raise ValueError(
63                f"Can not call {self.method!r}, as no object is assigned to this StyleCall."
64            )
65
66        try:
67            # mypy fails on one machine with this, but not on the other.
68            return self.method(self.obj.depth, item)  # type: ignore
69
70        # this is purposefully broad, as anything can happen during these calls.
71        except Exception as error:
72            raise RuntimeError(
73                f'Could not apply style {self.method} to "{item}": {error}'  # type: ignore
74            ) from error
75
76    def __eq__(self, other: object) -> bool:
77        if not isinstance(other, type(self)):
78            return False
79
80        return other.method == self.method

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.

StyleCall( obj: Union[pytermgui.widgets.base.Widget, Type[pytermgui.widgets.base.Widget], NoneType], method: Callable[[int, str], str])
StyleType = typing.Callable[[int, str], str]
class StyleManager(collections.UserDict):
142class StyleManager(UserDict):  # pylint: disable=too-many-ancestors
143    """An fancy dictionary to manage a Widget's styles.
144
145    Individual styles can be accessed two ways:
146
147    ```python3
148    manager.styles.style_name == manager._get_style("style_name")
149    ```
150
151    Same with setting:
152
153    ```python3
154    widget.styles.style_name = ...
155    widget.set_style("style_name", ...)
156    ```
157
158    The `set` and `get` methods remain for backwards compatibility reasons, but all
159    newly written code should use the dot syntax.
160
161    It is also possible to set styles as markup shorthands. For example:
162
163    ```python3
164    widget.styles.border = "60 bold"
165    ```
166
167    ...is equivalent to:
168
169    ```python3
170    widget.styles.border = "[60 bold]{item}"
171    ```
172    """
173
174    def __init__(
175        self,
176        parent: Widget | Type[Widget] | None = None,
177        **base,
178    ) -> None:
179
180        """Initializes a `StyleManager`.
181
182        Args:
183            parent: The parent of this instance. It will be assigned in all
184                `StyleCall`-s created by it.
185        """
186
187        self.__dict__["_is_setup"] = False
188
189        self.parent = parent
190
191        super().__init__()
192
193        for key, value in base.items():
194            self._set_as_stylecall(key, value)
195
196        self.__dict__["_is_setup"] = self.parent is not None
197
198    @staticmethod
199    def expand_shorthand(shorthand: str) -> MarkupFormatter:
200        """Expands a shorthand string into a `MarkupFormatter` instance.
201
202        For example, all of these will expand into `MarkupFormatter([60]{item}')`:
203        - '60'
204        - '[60]'
205        - '[60]{item}'
206
207        Args:
208            shorthand: The short version of markup to expand.
209
210        Returns:
211            A `MarkupFormatter` with the expanded markup.
212        """
213
214        if len(shorthand) == 0:
215            return MarkupFormatter("{item}")
216
217        if RE_MARKUP.match(shorthand) is not None:
218            return MarkupFormatter(shorthand)
219
220        markup = "[" + shorthand + "]"
221
222        if not "{item}" in shorthand:
223            markup += "{item}"
224
225        return MarkupFormatter(markup)
226
227    @classmethod
228    def merge(cls, other: StyleManager, **styles) -> StyleManager:
229        """Creates a new manager that merges `other` with the passed in styles.
230
231        Args:
232            other: The style manager to base the new one from.
233            **styles: The additional styles the new instance should have.
234
235        Returns:
236            A new `StyleManager`. This instance will only gather its data when
237            `branch` is called on it. This is done so any changes made to the original
238            data between the `merge` call and the actual usage of the instance will be
239            reflected.
240        """
241
242        return cls(**{**other, **styles})
243
244    def branch(self, parent: Widget | Type[Widget]) -> StyleManager:
245        """Branch off from the `base` style dictionary.
246
247        This method should be called during widget construction. It creates a new
248        `StyleManager` based on self, but with its data detached from the original.
249
250        Args:
251            parent: The parent of the new instance.
252
253        Returns:
254            A new `StyleManager`, with detached instances of data. This can then be
255            modified without touching the original instance.
256        """
257
258        return type(self)(parent, **self.data)
259
260    def _set_as_stylecall(self, key: str, item: StyleValue) -> None:
261        """Sets `self.data[key]` as a `StyleCall` of the given item.
262
263        If the item is a string, it will be expanded into a `MarkupFormatter` before
264        being converted into the `StyleCall`, using `expand_shorthand`.
265        """
266
267        if isinstance(item, StyleCall):
268            self.data[key] = StyleCall(self.parent, item.method)
269            return
270
271        if isinstance(item, str):
272            item = self.expand_shorthand(item)
273
274        self.data[key] = StyleCall(self.parent, item)
275
276    def __setitem__(self, key: str, value: StyleValue) -> None:
277        """Sets an item in `self.data`.
278
279        If the item is a string, it will be expanded into a `MarkupFormatter` before
280        being converted into the `StyleCall`, using `expand_shorthand`.
281        """
282
283        self._set_as_stylecall(key, value)
284
285    def __setattr__(self, key: str, value: StyleValue) -> None:
286        """Sets an attribute.
287
288        It first looks if it can set inside self.data, and defaults back to
289        self.__dict__.
290
291        Raises:
292            KeyError: The given key is not a defined attribute, and is not part of this
293                object's style set.
294        """
295
296        found = False
297        if "data" in self.__dict__:
298            for part in key.split("__"):
299                if part in self.data:
300                    self._set_as_stylecall(part, value)
301                    found = True
302
303        if found:
304            return
305
306        if self.__dict__.get("_is_setup") and key not in self.__dict__:
307            raise KeyError(f"Style {key!r} was not defined during construction.")
308
309        self.__dict__[key] = value
310
311    def __getattr__(self, key: str) -> StyleCall:
312        """Allows styles.dot_syntax."""
313
314        if key in self.__dict__:
315            return self.__dict__[key]
316
317        if key in self.__dict__["data"]:
318            return self.__dict__["data"][key]
319
320        raise AttributeError(key, self.data)
321
322    def __call__(self, **styles: StyleValue) -> Any:
323        """Allows calling the manager and setting its styles.
324
325        For example:
326        ```
327        >>> Button("Hello").styles(label="@60")
328        ```
329        """
330
331        for key, value in styles.items():
332            self._set_as_stylecall(key, value)
333
334        return self.parent

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}"
StyleManager( parent: Union[pytermgui.widgets.base.Widget, Type[pytermgui.widgets.base.Widget], NoneType] = None, **base)
174    def __init__(
175        self,
176        parent: Widget | Type[Widget] | None = None,
177        **base,
178    ) -> None:
179
180        """Initializes a `StyleManager`.
181
182        Args:
183            parent: The parent of this instance. It will be assigned in all
184                `StyleCall`-s created by it.
185        """
186
187        self.__dict__["_is_setup"] = False
188
189        self.parent = parent
190
191        super().__init__()
192
193        for key, value in base.items():
194            self._set_as_stylecall(key, value)
195
196        self.__dict__["_is_setup"] = self.parent is not None

Initializes a StyleManager.

Args
  • parent: The parent of this instance. It will be assigned in all StyleCall-s created by it.
@staticmethod
def expand_shorthand(shorthand: str) -> pytermgui.widgets.styles.MarkupFormatter:
198    @staticmethod
199    def expand_shorthand(shorthand: str) -> MarkupFormatter:
200        """Expands a shorthand string into a `MarkupFormatter` instance.
201
202        For example, all of these will expand into `MarkupFormatter([60]{item}')`:
203        - '60'
204        - '[60]'
205        - '[60]{item}'
206
207        Args:
208            shorthand: The short version of markup to expand.
209
210        Returns:
211            A `MarkupFormatter` with the expanded markup.
212        """
213
214        if len(shorthand) == 0:
215            return MarkupFormatter("{item}")
216
217        if RE_MARKUP.match(shorthand) is not None:
218            return MarkupFormatter(shorthand)
219
220        markup = "[" + shorthand + "]"
221
222        if not "{item}" in shorthand:
223            markup += "{item}"
224
225        return MarkupFormatter(markup)

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.

@classmethod
def merge( cls, other: pytermgui.widgets.styles.StyleManager, **styles) -> pytermgui.widgets.styles.StyleManager:
227    @classmethod
228    def merge(cls, other: StyleManager, **styles) -> StyleManager:
229        """Creates a new manager that merges `other` with the passed in styles.
230
231        Args:
232            other: The style manager to base the new one from.
233            **styles: The additional styles the new instance should have.
234
235        Returns:
236            A new `StyleManager`. This instance will only gather its data when
237            `branch` is called on it. This is done so any changes made to the original
238            data between the `merge` call and the actual usage of the instance will be
239            reflected.
240        """
241
242        return cls(**{**other, **styles})

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.

244    def branch(self, parent: Widget | Type[Widget]) -> StyleManager:
245        """Branch off from the `base` style dictionary.
246
247        This method should be called during widget construction. It creates a new
248        `StyleManager` based on self, but with its data detached from the original.
249
250        Args:
251            parent: The parent of the new instance.
252
253        Returns:
254            A new `StyleManager`, with detached instances of data. This can then be
255            modified without touching the original instance.
256        """
257
258        return type(self)(parent, **self.data)

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.

Inherited Members
collections.UserDict
copy
fromkeys
collections.abc.MutableMapping
pop
popitem
clear
update
setdefault
collections.abc.Mapping
get
keys
items
values
DepthlessStyleType = typing.Callable[[str], str]
CharType = typing.Union[typing.List[str], str]
def MARKUP(depth, item)
349MARKUP = lambda depth, item: tim.parse(item)

Style that parses value as markup. Used by most text labels, like pytermgui.widgets.Label

def FOREGROUND(_, item)
343FOREGROUND = lambda _, item: item

Standard foreground style, currently unused by the library

def BACKGROUND(_, item)
346BACKGROUND = lambda _, item: item

Standard background, used by most fill styles

CLICKABLE = MarkupFormatter(markup='[@238 72 bold]{item}', ensure_strip=False, _markup_cache={})

Style for inactive clickable things, such as pytermgui.widgets.Button

CLICKED = MarkupFormatter(markup='[238 @72 bold]{item}', ensure_strip=False, _markup_cache={})

Style for active clickable things, such as pytermgui.widgets.Button