pytermgui.widgets.base
The basic building blocks making up the Widget system.
View Source
0""" 1The basic building blocks making up the Widget system. 2""" 3 4# The classes defined here need more than 7 instance attributes, 5# and there is no cyclic import during runtime. 6# pylint: disable=too-many-instance-attributes, cyclic-import 7 8from __future__ import annotations 9 10from copy import deepcopy 11from inspect import signature 12from typing import Callable, Optional, Type, Iterator, Any, Union 13 14from ..input import keys 15from ..parser import markup 16from ..regex import real_length 17from ..helpers import break_line 18from ..ansi_interface import MouseEvent 19from ..terminal import get_terminal, Terminal 20from ..enums import SizePolicy, HorizontalAlignment, WidgetChange 21 22from . import styles as w_styles 23 24__all__ = ["Widget", "Label"] 25 26BoundCallback = Callable[..., Any] 27WidgetType = Union["Widget", Type["Widget"]] 28 29 30def _set_obj_or_cls_style( 31 obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.StyleType 32) -> Type[Widget] | Widget: 33 """Sets a style for an object or class 34 35 Args: 36 obj_or_cls: The Widget instance or type to update. 37 key: The style key. 38 value: The new style. 39 40 Returns: 41 Type[Widget] | Widget: The updated class. 42 43 Raises: 44 See `pytermgui.widgets.styles.StyleManager`. 45 """ 46 47 obj_or_cls.styles[key] = value 48 49 return obj_or_cls 50 51 52def _set_obj_or_cls_char( 53 obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.CharType 54) -> Type[Widget] | Widget: 55 """Sets a char for an object or class 56 57 Args: 58 obj_or_cls: The Widget instance or type to update. 59 key: The char key. 60 value: The new char. 61 62 Returns: 63 Type[Widget] | Widget: The updated class. 64 65 Raises: 66 KeyError: The char key provided is invalid. 67 """ 68 69 if not key in obj_or_cls.chars.keys(): 70 raise KeyError(f"Char {key} is not valid for {obj_or_cls}!") 71 72 obj_or_cls.chars[key] = value 73 74 return obj_or_cls 75 76 77class Widget: # pylint: disable=too-many-public-methods 78 """The base of the Widget system""" 79 80 set_style = classmethod(_set_obj_or_cls_style) 81 set_char = classmethod(_set_obj_or_cls_char) 82 83 styles = w_styles.StyleManager() 84 """Default styles for this class""" 85 86 chars: dict[str, w_styles.CharType] = {} 87 """Default characters for this class""" 88 89 keys: dict[str, set[str]] = {} 90 """Groups of keys that are used in `handle_key`""" 91 92 serialized: list[str] = [ 93 "id", 94 "pos", 95 "depth", 96 "width", 97 "height", 98 "selected_index", 99 "selectables_length", 100 ] 101 """Fields of widget that shall be serialized by `pytermgui.serializer.Serializer`""" 102 103 # This class is loaded after this module, 104 # and thus mypy doesn't see its existence. 105 _id_manager: Optional["_IDManager"] = None # type: ignore 106 107 is_bindable = False 108 """Allow binding support""" 109 110 size_policy = SizePolicy.get_default() 111 """`pytermgui.enums.SizePolicy` to set widget's width according to""" 112 113 parent_align = HorizontalAlignment.get_default() 114 """`pytermgui.enums.HorizontalAlignment` to align widget by""" 115 116 from_data: Callable[..., Widget | list[Widget] | None] 117 118 # We cannot import boxes here due to cyclic imports. 119 box: Any 120 121 def __init__(self, **attrs: Any) -> None: 122 """Initialize object""" 123 124 self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value) 125 self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value) 126 127 self.width = 1 128 self.height = 1 129 self.pos = self.terminal.origin 130 131 self.depth = 0 132 133 self.styles = type(self).styles.branch(self) 134 self.chars = type(self).chars.copy() 135 136 self.parent: Widget | None = None 137 self.selected_index: int | None = None 138 139 self._selectables_length = 0 140 self._id: Optional[str] = None 141 self._serialized_fields = type(self).serialized 142 self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {} 143 self._relative_width: float | None = None 144 self._previous_state: tuple[tuple[int, int], list[str]] | None = None 145 146 for attr, value in attrs.items(): 147 setattr(self, attr, value) 148 149 def __repr__(self) -> str: 150 """Return repr string of this widget. 151 152 Returns: 153 Whatever this widget's `debug` method gives. 154 """ 155 156 return self.debug() 157 158 def __iter__(self) -> Iterator[Widget]: 159 """Return self for iteration""" 160 161 yield self 162 163 @property 164 def bindings(self) -> dict[str | Type[MouseEvent], tuple[BoundCallback, str]]: 165 """Gets a copy of the bindings internal dictionary. 166 167 Returns: 168 A copy of the internal bindings dictionary, such as: 169 170 ``` 171 { 172 "*": (star_callback, "This is a callback activated when '*' is pressed.") 173 } 174 ``` 175 """ 176 177 return self._bindings.copy() 178 179 @property 180 def id(self) -> Optional[str]: # pylint: disable=invalid-name 181 """Gets this widget's id property 182 183 Returns: 184 The id string if one is present, None otherwise. 185 """ 186 187 return self._id 188 189 @id.setter 190 def id(self, value: str) -> None: # pylint: disable=invalid-name 191 """Registers a widget to the Widget._id_manager. 192 193 If this widget already had an id, the old value is deregistered 194 before the new one is assigned. 195 196 Args: 197 value: The new id this widget will be registered as. 198 """ 199 200 if self._id == value: 201 return 202 203 manager = Widget._id_manager 204 assert manager is not None 205 206 old = manager.get_id(self) 207 if old is not None: 208 manager.deregister(old) 209 210 self._id = value 211 manager.register(self) 212 213 @property 214 def selectables_length(self) -> int: 215 """Gets how many selectables this widget contains. 216 217 Returns: 218 An integer describing the amount of selectables in this widget. 219 """ 220 221 return self._selectables_length 222 223 @property 224 def selectables(self) -> list[tuple[Widget, int]]: 225 """Gets a list of all selectables within this widget 226 227 Returns: 228 A list of tuples. In the default implementation this will be 229 a list of one tuple, containing a reference to `self`, as well 230 as the lowest index, 0. 231 """ 232 233 return [(self, 0)] 234 235 @property 236 def is_selectable(self) -> bool: 237 """Determines whether this widget has any selectables. 238 239 Returns: 240 A boolean, representing `self.selectables_length != 0`. 241 """ 242 243 return self.selectables_length != 0 244 245 @property 246 def static_width(self) -> int: 247 """Allows for a shorter way of setting a width, and SizePolicy.STATIC. 248 249 Args: 250 value: The new width integer. 251 252 Returns: 253 None, as this is setter only. 254 """ 255 256 return None # type: ignore 257 258 @static_width.setter 259 def static_width(self, value: int) -> None: 260 """See the static_width getter.""" 261 262 self.width = value 263 self.size_policy = SizePolicy.STATIC 264 265 @property 266 def relative_width(self) -> float | None: 267 """Sets this widget's relative width, and changes size_policy to RELATIVE. 268 269 The value is clamped to 1.0. 270 271 If a Container holds a width of 30, and it has a subwidget with a relative 272 width of 0.5, it will be resized to 15. 273 274 Args: 275 value: The multiplier to apply to the parent's width. 276 277 Returns: 278 The current relative_width. 279 """ 280 281 return self._relative_width 282 283 @relative_width.setter 284 def relative_width(self, value: float) -> None: 285 """See the relative_width getter.""" 286 287 self.size_policy = SizePolicy.RELATIVE 288 self._relative_width = min(1.0, value) 289 290 @property 291 def terminal(self) -> Terminal: 292 """Returns the current global terminal instance.""" 293 294 return get_terminal() 295 296 def get_change(self) -> WidgetChange | None: 297 """Determines whether widget lines changed since the last call to this function.""" 298 299 lines = self.get_lines() 300 301 if self._previous_state is None: 302 self._previous_state = (self.width, self.height), lines 303 return WidgetChange.LINES 304 305 lines = self.get_lines() 306 (old_width, old_height), old_lines = self._previous_state 307 308 self._previous_state = (self.width, self.height), lines 309 310 if old_width != self.width and old_height != self.height: 311 return WidgetChange.SIZE 312 313 if old_width != self.width: 314 return WidgetChange.WIDTH 315 316 if old_height != self.height: 317 return WidgetChange.HEIGHT 318 319 if old_lines != lines: 320 return WidgetChange.LINES 321 322 return None 323 324 def contains(self, pos: tuple[int, int]) -> bool: 325 """Determines whether widget contains `pos`. 326 327 Args: 328 pos: Position to compare. 329 330 Returns: 331 Boolean describing whether the position is inside 332 this widget. 333 """ 334 335 rect = self.pos, ( 336 self.pos[0] + self.width, 337 self.pos[1] + self.height, 338 ) 339 340 (left, top), (right, bottom) = rect 341 342 return left <= pos[0] < right and top <= pos[1] < bottom 343 344 def handle_mouse(self, event: MouseEvent) -> bool: 345 """Handles a mouse event, returning its success. 346 347 Args: 348 event: Object containing mouse event to handle. 349 350 Returns: 351 A boolean describing whether the mouse input was handled.""" 352 353 return False and hasattr(self, event) 354 355 def handle_key(self, key: str) -> bool: 356 """Handles a mouse event, returning its success. 357 358 Args: 359 key: String representation of input string. 360 The `pytermgui.input.keys` object can be 361 used to retrieve special keys. 362 363 Returns: 364 A boolean describing whether the key was handled. 365 """ 366 367 return False and hasattr(self, key) 368 369 def serialize(self) -> dict[str, Any]: 370 """Serializes a widget. 371 372 The fields looked at are defined `Widget.serialized`. Note that 373 this method is not very commonly used at the moment, so it might 374 not have full functionality in non-nuclear widgets. 375 376 Returns: 377 Dictionary of widget attributes. The dictionary will always 378 have a `type` field. Any styles are converted into markup 379 strings during serialization, so they can be loaded again in 380 their original form. 381 382 Example return: 383 ``` 384 { 385 "type": "Label", 386 "value": "[210 bold]I am a title", 387 "parent_align": 0, 388 ... 389 } 390 ``` 391 """ 392 393 fields = self._serialized_fields 394 395 out: dict[str, Any] = {"type": type(self).__name__} 396 for key in fields: 397 # Detect styled values 398 if key.startswith("*"): 399 style = True 400 key = key[1:] 401 else: 402 style = False 403 404 value = getattr(self, key) 405 406 # Convert styled value into markup 407 if style: 408 style_call = self._get_style(key) 409 if isinstance(value, list): 410 out[key] = [markup.get_markup(style_call(char)) for char in value] 411 else: 412 out[key] = markup.get_markup(style_call(value)) 413 414 continue 415 416 out[key] = value 417 418 # The chars need to be handled separately 419 out["chars"] = {} 420 for key, value in self.chars.items(): 421 style_call = self._get_style(key) 422 423 if isinstance(value, list): 424 out["chars"][key] = [ 425 markup.get_markup(style_call(char)) for char in value 426 ] 427 else: 428 out["chars"][key] = markup.get_markup(style_call(value)) 429 430 return out 431 432 def copy(self) -> Widget: 433 """Creates a deep copy of this widget""" 434 435 return deepcopy(self) 436 437 def _get_style(self, key: str) -> w_styles.DepthlessStyleType: 438 """Gets style call from its key. 439 440 This is analogous to using `self.styles.{key}` 441 442 Args: 443 key: A key into the widget's style manager. 444 445 Returns: 446 A `pytermgui.styles.StyleCall` object containing the referenced 447 style. StyleCall objects should only be used internally inside a 448 widget. 449 450 Raises: 451 KeyError: Style key is invalid. 452 """ 453 454 return self.styles[key] 455 456 def _get_char(self, key: str) -> w_styles.CharType: 457 """Gets character from its key. 458 459 Args: 460 key: A key into the widget's chars dictionary. 461 462 Returns: 463 Either a `list[str]` or a simple `str`, depending on the character. 464 465 Raises: 466 KeyError: Style key is invalid. 467 """ 468 469 chars = self.chars[key] 470 if isinstance(chars, str): 471 return chars 472 473 return chars.copy() 474 475 def get_lines(self) -> list[str]: 476 """Gets lines representing this widget. 477 478 These lines have to be equal to the widget in length. All 479 widgets must provide this method. Make sure to keep it performant, 480 as it will be called very often, often multiple times per WindowManager frame. 481 482 Any longer actions should be done outside of this method, and only their 483 result should be looked up here. 484 485 Returns: 486 Nothing by default. 487 488 Raises: 489 NotImplementedError: As this method is required for **all** widgets, not 490 having it defined will raise NotImplementedError. 491 """ 492 493 raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.") 494 495 def bind( 496 self, key: str, action: BoundCallback, description: Optional[str] = None 497 ) -> None: 498 """Binds an action to a keypress. 499 500 This function is only called by implementations above this layer. To use this 501 functionality use `pytermgui.window_manager.WindowManager`, or write your own 502 custom layer. 503 504 Special keys: 505 - keys.ANY_KEY: Any and all keypresses execute this binding. 506 - keys.MouseAction: Any and all mouse inputs execute this binding. 507 508 Args: 509 key: The key that the action will be bound to. 510 action: The action executed when the key is pressed. 511 description: An optional description for this binding. It is not really 512 used anywhere, but you can provide a helper menu and display them. 513 514 Raises: 515 TypeError: This widget is not bindable, i.e. widget.is_bindable == False. 516 """ 517 518 if not self.is_bindable: 519 raise TypeError(f"Widget of type {type(self)} does not accept bindings.") 520 521 if description is None: 522 description = f"Binding of {key} to {action}" 523 524 self._bindings[key] = (action, description) 525 526 def unbind(self, key: str) -> None: 527 """Unbinds the given key.""" 528 529 del self._bindings[key] 530 531 def execute_binding(self, key: Any) -> bool: 532 """Executes a binding belonging to key, when present. 533 534 Use this method inside custom widget `handle_keys` methods, or to run a callback 535 without its corresponding key having been pressed. 536 537 Args: 538 key: Usually a string, indexing into the `_bindings` dictionary. These are the 539 same strings as defined in `Widget.bind`. 540 541 Returns: 542 True if the binding was found, False otherwise. Bindings will always be 543 executed if they are found. 544 """ 545 546 # Execute special binding 547 if keys.ANY_KEY in self._bindings: 548 method, _ = self._bindings[keys.ANY_KEY] 549 method(self, key) 550 551 if key in self._bindings: 552 method, _ = self._bindings[key] 553 method(self, key) 554 555 return True 556 557 return False 558 559 def select(self, index: int | None = None) -> None: 560 """Selects a part of this Widget. 561 562 Args: 563 index: The index to select. 564 565 Raises: 566 TypeError: This widget has no selectables, i.e. widget.is_selectable == False. 567 """ 568 569 if not self.is_selectable: 570 raise TypeError(f"Object of type {type(self)} has no selectables.") 571 572 if index is not None: 573 index = min(max(0, index), self.selectables_length - 1) 574 self.selected_index = index 575 576 def print(self) -> None: 577 """Prints this widget""" 578 579 for line in self.get_lines(): 580 print(line) 581 582 def debug(self) -> str: 583 """Returns identifiable information about this widget. 584 585 This method is used to easily differentiate between widgets. By default, all widget's 586 __repr__ method is an alias to this. The signature of each widget is used to generate 587 the return value. 588 589 Returns: 590 A string almost exactly matching the line of code that could have defined the widget. 591 592 Example return: 593 594 ``` 595 Container(Label(value="This is a label", padding=0), 596 Button(label="This is a button", padding=0), **attrs) 597 ``` 598 599 """ 600 601 constructor = "(" 602 for name in signature(getattr(self, "__init__")).parameters: 603 current = "" 604 if name == "attrs": 605 current += "**attrs" 606 continue 607 608 if len(constructor) > 1: 609 current += ", " 610 611 current += name 612 613 attr = getattr(self, name, None) 614 if attr is None: 615 continue 616 617 current += "=" 618 619 if isinstance(attr, str): 620 current += f'"{attr}"' 621 else: 622 current += str(attr) 623 624 constructor += current 625 626 constructor += ")" 627 628 return type(self).__name__ + constructor 629 630 631class Label(Widget): 632 """A Widget to display a string 633 634 By default, this widget uses `pytermgui.widgets.styles.MARKUP`. This 635 allows it to house markup text that is parsed before display, such as: 636 637 ```python3 638 import pytermgui as ptg 639 640 with ptg.alt_buffer(): 641 root = ptg.Container( 642 ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!") 643 ) 644 root.print() 645 ptg.getch() 646 ``` 647 648 <p style="text-align: center"> 649 <img 650 src="https://github.com/bczsalba/pytermgui/blob/master/assets/docs/widgets/label.png?raw=true" 651 width=100%> 652 </p> 653 """ 654 655 serialized = Widget.serialized + ["*value", "align", "padding"] 656 styles = w_styles.StyleManager(value=w_styles.MARKUP) 657 658 def __init__( 659 self, 660 value: str = "", 661 style: str | w_styles.StyleValue = "", 662 padding: int = 0, 663 non_first_padding: int = 0, 664 **attrs: Any, 665 ) -> None: 666 """Initializes a Label. 667 668 Args: 669 value: The value of this string. Using the default value style 670 (`pytermgui.widgets.styles.MARKUP`), 671 style: A pre-set value for self.styles.value. 672 padding: The number of space (" ") characters to prepend to every line after 673 line breaking. 674 non_first_padding: The number of space characters to prepend to every 675 non-first line of `get_lines`. This is applied on top of `padding`. 676 """ 677 678 super().__init__(**attrs) 679 680 self.value = value 681 self.padding = padding 682 self.non_first_padding = non_first_padding 683 self.width = real_length(value) + self.padding 684 685 if style != "": 686 self.styles.value = style 687 688 def get_lines(self) -> list[str]: 689 """Get lines representing this Label, breaking lines as necessary""" 690 691 lines = [] 692 limit = self.width - self.padding 693 broken = break_line( 694 self.styles.value(self.value), 695 limit=limit, 696 non_first_limit=limit - self.non_first_padding, 697 ) 698 699 for i, line in enumerate(broken): 700 if i == 0: 701 lines.append(self.padding * " " + line) 702 continue 703 704 lines.append(self.padding * " " + self.non_first_padding * " " + line) 705 706 return lines or [""] 707 708 709class ScrollableWidget(Widget): 710 """A widget with some scrolling helper methods. 711 712 This is not an implementation of the scrolling behaviour itself, just the 713 user-facing API for it. 714 715 It provides a `_scroll_offset` attribute, which is an integer describing the current 716 scroll state offset from the top, as well as some methods to modify the state.""" 717 718 def __init__(self, **attrs: Any) -> None: 719 """Initializes the scrollable widget.""" 720 721 super().__init__(**attrs) 722 723 self._max_scroll = 0 724 self._scroll_offset = 0 725 726 def scroll(self, offset: int) -> bool: 727 """Scrolls to given offset, returns the new scroll_offset. 728 729 Args: 730 offset: The amount to scroll by. Positive offsets scroll down, 731 negative up. 732 733 Returns: 734 True if the scroll offset changed, False otherwise. 735 """ 736 737 base = self._scroll_offset 738 739 self._scroll_offset = min( 740 max(0, self._scroll_offset + offset), self._max_scroll 741 ) 742 743 return base != self._scroll_offset 744 745 def scroll_end(self, end: int) -> int: 746 """Scrolls to either top or bottom end of this object. 747 748 Args: 749 end: The offset to scroll to. 0 goes to the very top, -1 to the 750 very bottom. 751 752 Returns: 753 True if the scroll offset changed, False otherwise. 754 """ 755 756 base = self._scroll_offset 757 758 if end == 0: 759 self._scroll_offset = 0 760 761 elif end == -1: 762 self._scroll_offset = self._max_scroll 763 764 return base != self._scroll_offset 765 766 def get_lines(self) -> list[str]: 767 ...
View Source
78class Widget: # pylint: disable=too-many-public-methods 79 """The base of the Widget system""" 80 81 set_style = classmethod(_set_obj_or_cls_style) 82 set_char = classmethod(_set_obj_or_cls_char) 83 84 styles = w_styles.StyleManager() 85 """Default styles for this class""" 86 87 chars: dict[str, w_styles.CharType] = {} 88 """Default characters for this class""" 89 90 keys: dict[str, set[str]] = {} 91 """Groups of keys that are used in `handle_key`""" 92 93 serialized: list[str] = [ 94 "id", 95 "pos", 96 "depth", 97 "width", 98 "height", 99 "selected_index", 100 "selectables_length", 101 ] 102 """Fields of widget that shall be serialized by `pytermgui.serializer.Serializer`""" 103 104 # This class is loaded after this module, 105 # and thus mypy doesn't see its existence. 106 _id_manager: Optional["_IDManager"] = None # type: ignore 107 108 is_bindable = False 109 """Allow binding support""" 110 111 size_policy = SizePolicy.get_default() 112 """`pytermgui.enums.SizePolicy` to set widget's width according to""" 113 114 parent_align = HorizontalAlignment.get_default() 115 """`pytermgui.enums.HorizontalAlignment` to align widget by""" 116 117 from_data: Callable[..., Widget | list[Widget] | None] 118 119 # We cannot import boxes here due to cyclic imports. 120 box: Any 121 122 def __init__(self, **attrs: Any) -> None: 123 """Initialize object""" 124 125 self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value) 126 self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value) 127 128 self.width = 1 129 self.height = 1 130 self.pos = self.terminal.origin 131 132 self.depth = 0 133 134 self.styles = type(self).styles.branch(self) 135 self.chars = type(self).chars.copy() 136 137 self.parent: Widget | None = None 138 self.selected_index: int | None = None 139 140 self._selectables_length = 0 141 self._id: Optional[str] = None 142 self._serialized_fields = type(self).serialized 143 self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {} 144 self._relative_width: float | None = None 145 self._previous_state: tuple[tuple[int, int], list[str]] | None = None 146 147 for attr, value in attrs.items(): 148 setattr(self, attr, value) 149 150 def __repr__(self) -> str: 151 """Return repr string of this widget. 152 153 Returns: 154 Whatever this widget's `debug` method gives. 155 """ 156 157 return self.debug() 158 159 def __iter__(self) -> Iterator[Widget]: 160 """Return self for iteration""" 161 162 yield self 163 164 @property 165 def bindings(self) -> dict[str | Type[MouseEvent], tuple[BoundCallback, str]]: 166 """Gets a copy of the bindings internal dictionary. 167 168 Returns: 169 A copy of the internal bindings dictionary, such as: 170 171 ``` 172 { 173 "*": (star_callback, "This is a callback activated when '*' is pressed.") 174 } 175 ``` 176 """ 177 178 return self._bindings.copy() 179 180 @property 181 def id(self) -> Optional[str]: # pylint: disable=invalid-name 182 """Gets this widget's id property 183 184 Returns: 185 The id string if one is present, None otherwise. 186 """ 187 188 return self._id 189 190 @id.setter 191 def id(self, value: str) -> None: # pylint: disable=invalid-name 192 """Registers a widget to the Widget._id_manager. 193 194 If this widget already had an id, the old value is deregistered 195 before the new one is assigned. 196 197 Args: 198 value: The new id this widget will be registered as. 199 """ 200 201 if self._id == value: 202 return 203 204 manager = Widget._id_manager 205 assert manager is not None 206 207 old = manager.get_id(self) 208 if old is not None: 209 manager.deregister(old) 210 211 self._id = value 212 manager.register(self) 213 214 @property 215 def selectables_length(self) -> int: 216 """Gets how many selectables this widget contains. 217 218 Returns: 219 An integer describing the amount of selectables in this widget. 220 """ 221 222 return self._selectables_length 223 224 @property 225 def selectables(self) -> list[tuple[Widget, int]]: 226 """Gets a list of all selectables within this widget 227 228 Returns: 229 A list of tuples. In the default implementation this will be 230 a list of one tuple, containing a reference to `self`, as well 231 as the lowest index, 0. 232 """ 233 234 return [(self, 0)] 235 236 @property 237 def is_selectable(self) -> bool: 238 """Determines whether this widget has any selectables. 239 240 Returns: 241 A boolean, representing `self.selectables_length != 0`. 242 """ 243 244 return self.selectables_length != 0 245 246 @property 247 def static_width(self) -> int: 248 """Allows for a shorter way of setting a width, and SizePolicy.STATIC. 249 250 Args: 251 value: The new width integer. 252 253 Returns: 254 None, as this is setter only. 255 """ 256 257 return None # type: ignore 258 259 @static_width.setter 260 def static_width(self, value: int) -> None: 261 """See the static_width getter.""" 262 263 self.width = value 264 self.size_policy = SizePolicy.STATIC 265 266 @property 267 def relative_width(self) -> float | None: 268 """Sets this widget's relative width, and changes size_policy to RELATIVE. 269 270 The value is clamped to 1.0. 271 272 If a Container holds a width of 30, and it has a subwidget with a relative 273 width of 0.5, it will be resized to 15. 274 275 Args: 276 value: The multiplier to apply to the parent's width. 277 278 Returns: 279 The current relative_width. 280 """ 281 282 return self._relative_width 283 284 @relative_width.setter 285 def relative_width(self, value: float) -> None: 286 """See the relative_width getter.""" 287 288 self.size_policy = SizePolicy.RELATIVE 289 self._relative_width = min(1.0, value) 290 291 @property 292 def terminal(self) -> Terminal: 293 """Returns the current global terminal instance.""" 294 295 return get_terminal() 296 297 def get_change(self) -> WidgetChange | None: 298 """Determines whether widget lines changed since the last call to this function.""" 299 300 lines = self.get_lines() 301 302 if self._previous_state is None: 303 self._previous_state = (self.width, self.height), lines 304 return WidgetChange.LINES 305 306 lines = self.get_lines() 307 (old_width, old_height), old_lines = self._previous_state 308 309 self._previous_state = (self.width, self.height), lines 310 311 if old_width != self.width and old_height != self.height: 312 return WidgetChange.SIZE 313 314 if old_width != self.width: 315 return WidgetChange.WIDTH 316 317 if old_height != self.height: 318 return WidgetChange.HEIGHT 319 320 if old_lines != lines: 321 return WidgetChange.LINES 322 323 return None 324 325 def contains(self, pos: tuple[int, int]) -> bool: 326 """Determines whether widget contains `pos`. 327 328 Args: 329 pos: Position to compare. 330 331 Returns: 332 Boolean describing whether the position is inside 333 this widget. 334 """ 335 336 rect = self.pos, ( 337 self.pos[0] + self.width, 338 self.pos[1] + self.height, 339 ) 340 341 (left, top), (right, bottom) = rect 342 343 return left <= pos[0] < right and top <= pos[1] < bottom 344 345 def handle_mouse(self, event: MouseEvent) -> bool: 346 """Handles a mouse event, returning its success. 347 348 Args: 349 event: Object containing mouse event to handle. 350 351 Returns: 352 A boolean describing whether the mouse input was handled.""" 353 354 return False and hasattr(self, event) 355 356 def handle_key(self, key: str) -> bool: 357 """Handles a mouse event, returning its success. 358 359 Args: 360 key: String representation of input string. 361 The `pytermgui.input.keys` object can be 362 used to retrieve special keys. 363 364 Returns: 365 A boolean describing whether the key was handled. 366 """ 367 368 return False and hasattr(self, key) 369 370 def serialize(self) -> dict[str, Any]: 371 """Serializes a widget. 372 373 The fields looked at are defined `Widget.serialized`. Note that 374 this method is not very commonly used at the moment, so it might 375 not have full functionality in non-nuclear widgets. 376 377 Returns: 378 Dictionary of widget attributes. The dictionary will always 379 have a `type` field. Any styles are converted into markup 380 strings during serialization, so they can be loaded again in 381 their original form. 382 383 Example return: 384 ``` 385 { 386 "type": "Label", 387 "value": "[210 bold]I am a title", 388 "parent_align": 0, 389 ... 390 } 391 ``` 392 """ 393 394 fields = self._serialized_fields 395 396 out: dict[str, Any] = {"type": type(self).__name__} 397 for key in fields: 398 # Detect styled values 399 if key.startswith("*"): 400 style = True 401 key = key[1:] 402 else: 403 style = False 404 405 value = getattr(self, key) 406 407 # Convert styled value into markup 408 if style: 409 style_call = self._get_style(key) 410 if isinstance(value, list): 411 out[key] = [markup.get_markup(style_call(char)) for char in value] 412 else: 413 out[key] = markup.get_markup(style_call(value)) 414 415 continue 416 417 out[key] = value 418 419 # The chars need to be handled separately 420 out["chars"] = {} 421 for key, value in self.chars.items(): 422 style_call = self._get_style(key) 423 424 if isinstance(value, list): 425 out["chars"][key] = [ 426 markup.get_markup(style_call(char)) for char in value 427 ] 428 else: 429 out["chars"][key] = markup.get_markup(style_call(value)) 430 431 return out 432 433 def copy(self) -> Widget: 434 """Creates a deep copy of this widget""" 435 436 return deepcopy(self) 437 438 def _get_style(self, key: str) -> w_styles.DepthlessStyleType: 439 """Gets style call from its key. 440 441 This is analogous to using `self.styles.{key}` 442 443 Args: 444 key: A key into the widget's style manager. 445 446 Returns: 447 A `pytermgui.styles.StyleCall` object containing the referenced 448 style. StyleCall objects should only be used internally inside a 449 widget. 450 451 Raises: 452 KeyError: Style key is invalid. 453 """ 454 455 return self.styles[key] 456 457 def _get_char(self, key: str) -> w_styles.CharType: 458 """Gets character from its key. 459 460 Args: 461 key: A key into the widget's chars dictionary. 462 463 Returns: 464 Either a `list[str]` or a simple `str`, depending on the character. 465 466 Raises: 467 KeyError: Style key is invalid. 468 """ 469 470 chars = self.chars[key] 471 if isinstance(chars, str): 472 return chars 473 474 return chars.copy() 475 476 def get_lines(self) -> list[str]: 477 """Gets lines representing this widget. 478 479 These lines have to be equal to the widget in length. All 480 widgets must provide this method. Make sure to keep it performant, 481 as it will be called very often, often multiple times per WindowManager frame. 482 483 Any longer actions should be done outside of this method, and only their 484 result should be looked up here. 485 486 Returns: 487 Nothing by default. 488 489 Raises: 490 NotImplementedError: As this method is required for **all** widgets, not 491 having it defined will raise NotImplementedError. 492 """ 493 494 raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.") 495 496 def bind( 497 self, key: str, action: BoundCallback, description: Optional[str] = None 498 ) -> None: 499 """Binds an action to a keypress. 500 501 This function is only called by implementations above this layer. To use this 502 functionality use `pytermgui.window_manager.WindowManager`, or write your own 503 custom layer. 504 505 Special keys: 506 - keys.ANY_KEY: Any and all keypresses execute this binding. 507 - keys.MouseAction: Any and all mouse inputs execute this binding. 508 509 Args: 510 key: The key that the action will be bound to. 511 action: The action executed when the key is pressed. 512 description: An optional description for this binding. It is not really 513 used anywhere, but you can provide a helper menu and display them. 514 515 Raises: 516 TypeError: This widget is not bindable, i.e. widget.is_bindable == False. 517 """ 518 519 if not self.is_bindable: 520 raise TypeError(f"Widget of type {type(self)} does not accept bindings.") 521 522 if description is None: 523 description = f"Binding of {key} to {action}" 524 525 self._bindings[key] = (action, description) 526 527 def unbind(self, key: str) -> None: 528 """Unbinds the given key.""" 529 530 del self._bindings[key] 531 532 def execute_binding(self, key: Any) -> bool: 533 """Executes a binding belonging to key, when present. 534 535 Use this method inside custom widget `handle_keys` methods, or to run a callback 536 without its corresponding key having been pressed. 537 538 Args: 539 key: Usually a string, indexing into the `_bindings` dictionary. These are the 540 same strings as defined in `Widget.bind`. 541 542 Returns: 543 True if the binding was found, False otherwise. Bindings will always be 544 executed if they are found. 545 """ 546 547 # Execute special binding 548 if keys.ANY_KEY in self._bindings: 549 method, _ = self._bindings[keys.ANY_KEY] 550 method(self, key) 551 552 if key in self._bindings: 553 method, _ = self._bindings[key] 554 method(self, key) 555 556 return True 557 558 return False 559 560 def select(self, index: int | None = None) -> None: 561 """Selects a part of this Widget. 562 563 Args: 564 index: The index to select. 565 566 Raises: 567 TypeError: This widget has no selectables, i.e. widget.is_selectable == False. 568 """ 569 570 if not self.is_selectable: 571 raise TypeError(f"Object of type {type(self)} has no selectables.") 572 573 if index is not None: 574 index = min(max(0, index), self.selectables_length - 1) 575 self.selected_index = index 576 577 def print(self) -> None: 578 """Prints this widget""" 579 580 for line in self.get_lines(): 581 print(line) 582 583 def debug(self) -> str: 584 """Returns identifiable information about this widget. 585 586 This method is used to easily differentiate between widgets. By default, all widget's 587 __repr__ method is an alias to this. The signature of each widget is used to generate 588 the return value. 589 590 Returns: 591 A string almost exactly matching the line of code that could have defined the widget. 592 593 Example return: 594 595 ``` 596 Container(Label(value="This is a label", padding=0), 597 Button(label="This is a button", padding=0), **attrs) 598 ``` 599 600 """ 601 602 constructor = "(" 603 for name in signature(getattr(self, "__init__")).parameters: 604 current = "" 605 if name == "attrs": 606 current += "**attrs" 607 continue 608 609 if len(constructor) > 1: 610 current += ", " 611 612 current += name 613 614 attr = getattr(self, name, None) 615 if attr is None: 616 continue 617 618 current += "=" 619 620 if isinstance(attr, str): 621 current += f'"{attr}"' 622 else: 623 current += str(attr) 624 625 constructor += current 626 627 constructor += ")" 628 629 return type(self).__name__ + constructor
The base of the Widget system
View Source
122 def __init__(self, **attrs: Any) -> None: 123 """Initialize object""" 124 125 self.set_style = lambda key, value: _set_obj_or_cls_style(self, key, value) 126 self.set_char = lambda key, value: _set_obj_or_cls_char(self, key, value) 127 128 self.width = 1 129 self.height = 1 130 self.pos = self.terminal.origin 131 132 self.depth = 0 133 134 self.styles = type(self).styles.branch(self) 135 self.chars = type(self).chars.copy() 136 137 self.parent: Widget | None = None 138 self.selected_index: int | None = None 139 140 self._selectables_length = 0 141 self._id: Optional[str] = None 142 self._serialized_fields = type(self).serialized 143 self._bindings: dict[str | Type[MouseEvent], tuple[BoundCallback, str]] = {} 144 self._relative_width: float | None = None 145 self._previous_state: tuple[tuple[int, int], list[str]] | None = None 146 147 for attr, value in attrs.items(): 148 setattr(self, attr, value)
Initialize object
View Source
31def _set_obj_or_cls_style( 32 obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.StyleType 33) -> Type[Widget] | Widget: 34 """Sets a style for an object or class 35 36 Args: 37 obj_or_cls: The Widget instance or type to update. 38 key: The style key. 39 value: The new style. 40 41 Returns: 42 Type[Widget] | Widget: The updated class. 43 44 Raises: 45 See `pytermgui.widgets.styles.StyleManager`. 46 """ 47 48 obj_or_cls.styles[key] = value 49 50 return obj_or_cls
Sets a style for an object or class
Args
- obj_or_cls: The Widget instance or type to update.
- key: The style key.
- value: The new style.
Returns
Type[Widget] | Widget: The updated class.
Raises
View Source
53def _set_obj_or_cls_char( 54 obj_or_cls: Type[Widget] | Widget, key: str, value: w_styles.CharType 55) -> Type[Widget] | Widget: 56 """Sets a char for an object or class 57 58 Args: 59 obj_or_cls: The Widget instance or type to update. 60 key: The char key. 61 value: The new char. 62 63 Returns: 64 Type[Widget] | Widget: The updated class. 65 66 Raises: 67 KeyError: The char key provided is invalid. 68 """ 69 70 if not key in obj_or_cls.chars.keys(): 71 raise KeyError(f"Char {key} is not valid for {obj_or_cls}!") 72 73 obj_or_cls.chars[key] = value 74 75 return obj_or_cls
Sets a char for an object or class
Args
- obj_or_cls: The Widget instance or type to update.
- key: The char key.
- value: The new char.
Returns
Type[Widget] | Widget: The updated class.
Raises
- KeyError: The char key provided is invalid.
Default styles for this class
Default characters for this class
Groups of keys that are used in handle_key
Fields of widget that shall be serialized by pytermgui.serializer.Serializer
Allow binding support
pytermgui.enums.SizePolicy
to set widget's width according to
pytermgui.enums.HorizontalAlignment
to align widget by
View Source
47def auto(data: Any, **widget_args: Any) -> Optional[Widget | list[Splitter]]: 48 """Creates a widget from specific data structures. 49 50 This conversion includes various widget classes, as well as some shorthands for 51 more complex objects. This method is called implicitly whenever a non-widget is 52 attempted to be added to a Widget. 53 54 55 Args: 56 data: The structure to convert. See below for formats. 57 **widget_args: Arguments passed straight to the widget constructor. 58 59 Returns: 60 The widget or list of widgets created, or None if the passed structure could 61 not be converted. 62 63 <br> 64 <details style="text-align: left"> 65 <summary style="all: revert; cursor: pointer">Data structures:</summary> 66 67 `pytermgui.widgets.base.Label`: 68 69 * Created from `str` 70 * Syntax example: `"Label value"` 71 72 `pytermgui.widgets.extra.Splitter`: 73 74 * Created from `tuple[Any]` 75 * Syntax example: `(YourWidget(), "auto_syntax", ...)` 76 77 `pytermgui.widgets.extra.Splitter` prompt: 78 79 * Created from `dict[Any, Any]` 80 * Syntax example: `{YourWidget(): "auto_syntax"}` 81 82 `pytermgui.widgets.buttons.Button`: 83 84 * Created from `list[str, pytermgui.widgets.buttons.MouseCallback]` 85 * Syntax example: `["Button label", lambda target, caller: ...]` 86 87 `pytermgui.widgets.buttons.Checkbox`: 88 89 * Created from `list[bool, Callable[[bool], Any]]` 90 * Syntax example: `[True, lambda checked: ...]` 91 92 `pytermgui.widgets.buttons.Toggle`: 93 94 * Created from `list[tuple[str, str], Callable[[str], Any]]` 95 * Syntax example: `[("On", "Off"), lambda new_value: ...]` 96 </details> 97 98 Example: 99 100 ```python3 101 from pytermgui import Container 102 form = ( 103 Container(id="form") 104 + "[157 bold]This is a title" 105 + "" 106 + {"[72 italic]Label1": "[210]Button1"} 107 + {"[72 italic]Label2": "[210]Button2"} 108 + {"[72 italic]Label3": "[210]Button3"} 109 + "" 110 + ["Submit", lambda _, button, your_submit_handler(button.parent)] 111 ) 112 ``` 113 """ 114 # In my opinion, returning immediately after construction is much more readable. 115 # pylint: disable=too-many-return-statements 116 117 # Nothing to do. 118 if isinstance(data, Widget): 119 # Set all **widget_args 120 for key, value in widget_args.items(): 121 setattr(data, key, value) 122 123 return data 124 125 # Label 126 if isinstance(data, str): 127 return Label(data, **widget_args) 128 129 # Splitter 130 if isinstance(data, tuple): 131 return Splitter(*data, **widget_args) 132 133 # buttons 134 if isinstance(data, list): 135 label = data[0] 136 onclick = None 137 if len(data) > 1: 138 onclick = data[1] 139 140 # Checkbox 141 if isinstance(label, bool): 142 return Checkbox(onclick, checked=label, **widget_args) 143 144 # Toggle 145 if isinstance(label, tuple): 146 assert len(label) == 2 147 return Toggle(label, onclick, **widget_args) 148 149 return Button(label, onclick, **widget_args) 150 151 # prompt splitter 152 if isinstance(data, dict): 153 rows: list[Splitter] = [] 154 155 for key, value in data.items(): 156 left = auto(key, parent_align=HorizontalAlignment.LEFT) 157 right = auto(value, parent_align=HorizontalAlignment.RIGHT) 158 159 rows.append(Splitter(left, right, **widget_args)) 160 161 if len(rows) == 1: 162 return rows[0] 163 164 return rows 165 166 return None
Creates a widget from specific data structures.
This conversion includes various widget classes, as well as some shorthands for more complex objects. This method is called implicitly whenever a non-widget is attempted to be added to a Widget.
Args
- data: The structure to convert. See below for formats.
- **widget_args: Arguments passed straight to the widget constructor.
Returns
The widget or list of widgets created, or None if the passed structure could not be converted.
Data structures:
- Created from
str
- Syntax example:
"Label value"
pytermgui.widgets.extra.Splitter
:
- Created from
tuple[Any]
- Syntax example:
(YourWidget(), "auto_syntax", ...)
pytermgui.widgets.extra.Splitter
prompt:
- Created from
dict[Any, Any]
- Syntax example:
{YourWidget(): "auto_syntax"}
pytermgui.widgets.buttons.Button
:
- Created from
list[str, pytermgui.widgets.buttons.MouseCallback]
- Syntax example:
["Button label", lambda target, caller: ...]
pytermgui.widgets.buttons.Checkbox
:
- Created from
list[bool, Callable[[bool], Any]]
- Syntax example:
[True, lambda checked: ...]
pytermgui.widgets.buttons.Toggle
:
- Created from
list[tuple[str, str], Callable[[str], Any]]
- Syntax example:
[("On", "Off"), lambda new_value: ...]
Example:
from pytermgui import Container
form = (
Container(id="form")
+ "[157 bold]This is a title"
+ ""
+ {"[72 italic]Label1": "[210]Button1"}
+ {"[72 italic]Label2": "[210]Button2"}
+ {"[72 italic]Label3": "[210]Button3"}
+ ""
+ ["Submit", lambda _, button, your_submit_handler(button.parent)]
)
Gets a copy of the bindings internal dictionary.
Returns
A copy of the internal bindings dictionary, such as:
{ "*": (star_callback, "This is a callback activated when '*' is pressed.") }
Gets this widget's id property
Returns
The id string if one is present, None otherwise.
Gets how many selectables this widget contains.
Returns
An integer describing the amount of selectables in this widget.
Gets a list of all selectables within this widget
Returns
A list of tuples. In the default implementation this will be a list of one tuple, containing a reference to
self
, as well as the lowest index, 0.
Determines whether this widget has any selectables.
Returns
A boolean, representing
self.selectables_length != 0
.
Allows for a shorter way of setting a width, and SizePolicy.STATIC.
Args
- value: The new width integer.
Returns
None, as this is setter only.
Sets this widget's relative width, and changes size_policy to RELATIVE.
The value is clamped to 1.0.
If a Container holds a width of 30, and it has a subwidget with a relative width of 0.5, it will be resized to 15.
Args
- value: The multiplier to apply to the parent's width.
Returns
The current relative_width.
Returns the current global terminal instance.
View Source
297 def get_change(self) -> WidgetChange | None: 298 """Determines whether widget lines changed since the last call to this function.""" 299 300 lines = self.get_lines() 301 302 if self._previous_state is None: 303 self._previous_state = (self.width, self.height), lines 304 return WidgetChange.LINES 305 306 lines = self.get_lines() 307 (old_width, old_height), old_lines = self._previous_state 308 309 self._previous_state = (self.width, self.height), lines 310 311 if old_width != self.width and old_height != self.height: 312 return WidgetChange.SIZE 313 314 if old_width != self.width: 315 return WidgetChange.WIDTH 316 317 if old_height != self.height: 318 return WidgetChange.HEIGHT 319 320 if old_lines != lines: 321 return WidgetChange.LINES 322 323 return None
Determines whether widget lines changed since the last call to this function.
View Source
325 def contains(self, pos: tuple[int, int]) -> bool: 326 """Determines whether widget contains `pos`. 327 328 Args: 329 pos: Position to compare. 330 331 Returns: 332 Boolean describing whether the position is inside 333 this widget. 334 """ 335 336 rect = self.pos, ( 337 self.pos[0] + self.width, 338 self.pos[1] + self.height, 339 ) 340 341 (left, top), (right, bottom) = rect 342 343 return left <= pos[0] < right and top <= pos[1] < bottom
Determines whether widget contains pos
.
Args
- pos: Position to compare.
Returns
Boolean describing whether the position is inside this widget.
View Source
345 def handle_mouse(self, event: MouseEvent) -> bool: 346 """Handles a mouse event, returning its success. 347 348 Args: 349 event: Object containing mouse event to handle. 350 351 Returns: 352 A boolean describing whether the mouse input was handled.""" 353 354 return False and hasattr(self, event)
Handles a mouse event, returning its success.
Args
- event: Object containing mouse event to handle.
Returns
A boolean describing whether the mouse input was handled.
View Source
356 def handle_key(self, key: str) -> bool: 357 """Handles a mouse event, returning its success. 358 359 Args: 360 key: String representation of input string. 361 The `pytermgui.input.keys` object can be 362 used to retrieve special keys. 363 364 Returns: 365 A boolean describing whether the key was handled. 366 """ 367 368 return False and hasattr(self, key)
Handles a mouse event, returning its success.
Args
- key: String representation of input string.
The
pytermgui.input.keys
object can be used to retrieve special keys.
Returns
A boolean describing whether the key was handled.
View Source
370 def serialize(self) -> dict[str, Any]: 371 """Serializes a widget. 372 373 The fields looked at are defined `Widget.serialized`. Note that 374 this method is not very commonly used at the moment, so it might 375 not have full functionality in non-nuclear widgets. 376 377 Returns: 378 Dictionary of widget attributes. The dictionary will always 379 have a `type` field. Any styles are converted into markup 380 strings during serialization, so they can be loaded again in 381 their original form. 382 383 Example return: 384 ``` 385 { 386 "type": "Label", 387 "value": "[210 bold]I am a title", 388 "parent_align": 0, 389 ... 390 } 391 ``` 392 """ 393 394 fields = self._serialized_fields 395 396 out: dict[str, Any] = {"type": type(self).__name__} 397 for key in fields: 398 # Detect styled values 399 if key.startswith("*"): 400 style = True 401 key = key[1:] 402 else: 403 style = False 404 405 value = getattr(self, key) 406 407 # Convert styled value into markup 408 if style: 409 style_call = self._get_style(key) 410 if isinstance(value, list): 411 out[key] = [markup.get_markup(style_call(char)) for char in value] 412 else: 413 out[key] = markup.get_markup(style_call(value)) 414 415 continue 416 417 out[key] = value 418 419 # The chars need to be handled separately 420 out["chars"] = {} 421 for key, value in self.chars.items(): 422 style_call = self._get_style(key) 423 424 if isinstance(value, list): 425 out["chars"][key] = [ 426 markup.get_markup(style_call(char)) for char in value 427 ] 428 else: 429 out["chars"][key] = markup.get_markup(style_call(value)) 430 431 return out
Serializes a widget.
The fields looked at are defined Widget.serialized
. Note that
this method is not very commonly used at the moment, so it might
not have full functionality in non-nuclear widgets.
Returns
Dictionary of widget attributes. The dictionary will always have a
type
field. Any styles are converted into markup strings during serialization, so they can be loaded again in their original form.Example return:
{ "type": "Label", "value": "[210 bold]I am a title", "parent_align": 0, ... }
View Source
Creates a deep copy of this widget
View Source
476 def get_lines(self) -> list[str]: 477 """Gets lines representing this widget. 478 479 These lines have to be equal to the widget in length. All 480 widgets must provide this method. Make sure to keep it performant, 481 as it will be called very often, often multiple times per WindowManager frame. 482 483 Any longer actions should be done outside of this method, and only their 484 result should be looked up here. 485 486 Returns: 487 Nothing by default. 488 489 Raises: 490 NotImplementedError: As this method is required for **all** widgets, not 491 having it defined will raise NotImplementedError. 492 """ 493 494 raise NotImplementedError(f"get_lines() is not defined for type {type(self)}.")
Gets lines representing this widget.
These lines have to be equal to the widget in length. All widgets must provide this method. Make sure to keep it performant, as it will be called very often, often multiple times per WindowManager frame.
Any longer actions should be done outside of this method, and only their result should be looked up here.
Returns
Nothing by default.
Raises
- NotImplementedError: As this method is required for all widgets, not having it defined will raise NotImplementedError.
View Source
496 def bind( 497 self, key: str, action: BoundCallback, description: Optional[str] = None 498 ) -> None: 499 """Binds an action to a keypress. 500 501 This function is only called by implementations above this layer. To use this 502 functionality use `pytermgui.window_manager.WindowManager`, or write your own 503 custom layer. 504 505 Special keys: 506 - keys.ANY_KEY: Any and all keypresses execute this binding. 507 - keys.MouseAction: Any and all mouse inputs execute this binding. 508 509 Args: 510 key: The key that the action will be bound to. 511 action: The action executed when the key is pressed. 512 description: An optional description for this binding. It is not really 513 used anywhere, but you can provide a helper menu and display them. 514 515 Raises: 516 TypeError: This widget is not bindable, i.e. widget.is_bindable == False. 517 """ 518 519 if not self.is_bindable: 520 raise TypeError(f"Widget of type {type(self)} does not accept bindings.") 521 522 if description is None: 523 description = f"Binding of {key} to {action}" 524 525 self._bindings[key] = (action, description)
Binds an action to a keypress.
This function is only called by implementations above this layer. To use this
functionality use pytermgui.window_manager.WindowManager
, or write your own
custom layer.
Special keys:
- keys.ANY_KEY: Any and all keypresses execute this binding.
- keys.MouseAction: Any and all mouse inputs execute this binding.
Args
- key: The key that the action will be bound to.
- action: The action executed when the key is pressed.
- description: An optional description for this binding. It is not really used anywhere, but you can provide a helper menu and display them.
Raises
- TypeError: This widget is not bindable, i.e. widget.is_bindable == False.
View Source
Unbinds the given key.
View Source
532 def execute_binding(self, key: Any) -> bool: 533 """Executes a binding belonging to key, when present. 534 535 Use this method inside custom widget `handle_keys` methods, or to run a callback 536 without its corresponding key having been pressed. 537 538 Args: 539 key: Usually a string, indexing into the `_bindings` dictionary. These are the 540 same strings as defined in `Widget.bind`. 541 542 Returns: 543 True if the binding was found, False otherwise. Bindings will always be 544 executed if they are found. 545 """ 546 547 # Execute special binding 548 if keys.ANY_KEY in self._bindings: 549 method, _ = self._bindings[keys.ANY_KEY] 550 method(self, key) 551 552 if key in self._bindings: 553 method, _ = self._bindings[key] 554 method(self, key) 555 556 return True 557 558 return False
Executes a binding belonging to key, when present.
Use this method inside custom widget handle_keys
methods, or to run a callback
without its corresponding key having been pressed.
Args
- key: Usually a string, indexing into the
_bindings
dictionary. These are the same strings as defined inWidget.bind
.
Returns
True if the binding was found, False otherwise. Bindings will always be executed if they are found.
View Source
560 def select(self, index: int | None = None) -> None: 561 """Selects a part of this Widget. 562 563 Args: 564 index: The index to select. 565 566 Raises: 567 TypeError: This widget has no selectables, i.e. widget.is_selectable == False. 568 """ 569 570 if not self.is_selectable: 571 raise TypeError(f"Object of type {type(self)} has no selectables.") 572 573 if index is not None: 574 index = min(max(0, index), self.selectables_length - 1) 575 self.selected_index = index
Selects a part of this Widget.
Args
- index: The index to select.
Raises
- TypeError: This widget has no selectables, i.e. widget.is_selectable == False.
View Source
Prints this widget
View Source
583 def debug(self) -> str: 584 """Returns identifiable information about this widget. 585 586 This method is used to easily differentiate between widgets. By default, all widget's 587 __repr__ method is an alias to this. The signature of each widget is used to generate 588 the return value. 589 590 Returns: 591 A string almost exactly matching the line of code that could have defined the widget. 592 593 Example return: 594 595 ``` 596 Container(Label(value="This is a label", padding=0), 597 Button(label="This is a button", padding=0), **attrs) 598 ``` 599 600 """ 601 602 constructor = "(" 603 for name in signature(getattr(self, "__init__")).parameters: 604 current = "" 605 if name == "attrs": 606 current += "**attrs" 607 continue 608 609 if len(constructor) > 1: 610 current += ", " 611 612 current += name 613 614 attr = getattr(self, name, None) 615 if attr is None: 616 continue 617 618 current += "=" 619 620 if isinstance(attr, str): 621 current += f'"{attr}"' 622 else: 623 current += str(attr) 624 625 constructor += current 626 627 constructor += ")" 628 629 return type(self).__name__ + constructor
Returns identifiable information about this widget.
This method is used to easily differentiate between widgets. By default, all widget's __repr__ method is an alias to this. The signature of each widget is used to generate the return value.
Returns
A string almost exactly matching the line of code that could have defined the widget.
Example return:
Container(Label(value="This is a label", padding=0), Button(label="This is a button", padding=0), **attrs)
View Source
632class Label(Widget): 633 """A Widget to display a string 634 635 By default, this widget uses `pytermgui.widgets.styles.MARKUP`. This 636 allows it to house markup text that is parsed before display, such as: 637 638 ```python3 639 import pytermgui as ptg 640 641 with ptg.alt_buffer(): 642 root = ptg.Container( 643 ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!") 644 ) 645 root.print() 646 ptg.getch() 647 ``` 648 649 <p style="text-align: center"> 650 <img 651 src="https://github.com/bczsalba/pytermgui/blob/master/assets/docs/widgets/label.png?raw=true" 652 width=100%> 653 </p> 654 """ 655 656 serialized = Widget.serialized + ["*value", "align", "padding"] 657 styles = w_styles.StyleManager(value=w_styles.MARKUP) 658 659 def __init__( 660 self, 661 value: str = "", 662 style: str | w_styles.StyleValue = "", 663 padding: int = 0, 664 non_first_padding: int = 0, 665 **attrs: Any, 666 ) -> None: 667 """Initializes a Label. 668 669 Args: 670 value: The value of this string. Using the default value style 671 (`pytermgui.widgets.styles.MARKUP`), 672 style: A pre-set value for self.styles.value. 673 padding: The number of space (" ") characters to prepend to every line after 674 line breaking. 675 non_first_padding: The number of space characters to prepend to every 676 non-first line of `get_lines`. This is applied on top of `padding`. 677 """ 678 679 super().__init__(**attrs) 680 681 self.value = value 682 self.padding = padding 683 self.non_first_padding = non_first_padding 684 self.width = real_length(value) + self.padding 685 686 if style != "": 687 self.styles.value = style 688 689 def get_lines(self) -> list[str]: 690 """Get lines representing this Label, breaking lines as necessary""" 691 692 lines = [] 693 limit = self.width - self.padding 694 broken = break_line( 695 self.styles.value(self.value), 696 limit=limit, 697 non_first_limit=limit - self.non_first_padding, 698 ) 699 700 for i, line in enumerate(broken): 701 if i == 0: 702 lines.append(self.padding * " " + line) 703 continue 704 705 lines.append(self.padding * " " + self.non_first_padding * " " + line) 706 707 return lines or [""]
A Widget to display a string
By default, this widget uses pytermgui.widgets.styles.MARKUP
. This
allows it to house markup text that is parsed before display, such as:
import pytermgui as ptg
with ptg.alt_buffer():
root = ptg.Container(
ptg.Label("[italic 141 bold]This is some [green]fancy [white inverse]text!")
)
root.print()
ptg.getch()
View Source
659 def __init__( 660 self, 661 value: str = "", 662 style: str | w_styles.StyleValue = "", 663 padding: int = 0, 664 non_first_padding: int = 0, 665 **attrs: Any, 666 ) -> None: 667 """Initializes a Label. 668 669 Args: 670 value: The value of this string. Using the default value style 671 (`pytermgui.widgets.styles.MARKUP`), 672 style: A pre-set value for self.styles.value. 673 padding: The number of space (" ") characters to prepend to every line after 674 line breaking. 675 non_first_padding: The number of space characters to prepend to every 676 non-first line of `get_lines`. This is applied on top of `padding`. 677 """ 678 679 super().__init__(**attrs) 680 681 self.value = value 682 self.padding = padding 683 self.non_first_padding = non_first_padding 684 self.width = real_length(value) + self.padding 685 686 if style != "": 687 self.styles.value = style
Initializes a Label.
Args
- value: The value of this string. Using the default value style
(
pytermgui.widgets.styles.MARKUP
), - style: A pre-set value for self.styles.value.
- padding: The number of space (" ") characters to prepend to every line after line breaking.
- non_first_padding: The number of space characters to prepend to every
non-first line of
get_lines
. This is applied on top ofpadding
.
Fields of widget that shall be serialized by pytermgui.serializer.Serializer
Default styles for this class
View Source
689 def get_lines(self) -> list[str]: 690 """Get lines representing this Label, breaking lines as necessary""" 691 692 lines = [] 693 limit = self.width - self.padding 694 broken = break_line( 695 self.styles.value(self.value), 696 limit=limit, 697 non_first_limit=limit - self.non_first_padding, 698 ) 699 700 for i, line in enumerate(broken): 701 if i == 0: 702 lines.append(self.padding * " " + line) 703 continue 704 705 lines.append(self.padding * " " + self.non_first_padding * " " + line) 706 707 return lines or [""]
Get lines representing this Label, breaking lines as necessary