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