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