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