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`"""
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}]")
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.
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.
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}"
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.
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.
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 whenbranch
is called on it. This is done so any changes made to the original data between themerge
call and the actual usage of the instance will be reflected.
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
348MARKUP = lambda depth, item: tim.parse(item)
Style that parses value as markup. Used by most text labels, like pytermgui.widgets.Label
342FOREGROUND = lambda _, item: item
Standard foreground style, currently unused by the library
345BACKGROUND = lambda _, item: item
Standard background, used by most fill
styles
Style for inactive clickable things, such as pytermgui.widgets.Button
Style for active clickable things, such as pytermgui.widgets.Button