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

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

A style that highlights the items given to it.

See pytermgui.highlighters for more information.

HighlighterStyle(highlighter: pytermgui.highlighters.Highlighter)
@dataclass
class StyleCall:
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

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

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

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

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

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.

def branch( self, parent: Union[pytermgui.widgets.base.Widget, Type[pytermgui.widgets.base.Widget]]) -> pytermgui.widgets.styles.StyleManager:
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)

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)
348MARKUP = lambda depth, item: tim.parse(item)

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

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

Standard foreground style, currently unused by the library

def BACKGROUND(_, item)
345BACKGROUND = 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