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`"""
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}]")
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.
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.
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}"
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.
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.
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 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.
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
View Source
347MARKUP = lambda depth, item: tim.parse(item)
Style that parses value as markup. Used by most text labels, like pytermgui.widgets.Label
View Source
341FOREGROUND = lambda _, item: item
Standard foreground style, currently unused by the library
View Source
344BACKGROUND = 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