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.

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

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

A style that highlights the items given to it.

See pytermgui.highlighters for more information.

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

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

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

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

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

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.

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

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

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

#   def FOREGROUND(_, item):
View Source
341FOREGROUND = lambda _, item: item

Standard foreground style, currently unused by the library

#   def BACKGROUND(_, item):
View Source
344BACKGROUND = 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