pytermgui.cmd
The command-line module of the library.
See ptg --help for more information.
1"""The command-line module of the library. 2 3See ptg --help for more information. 4""" 5 6from __future__ import annotations 7 8import os 9import sys 10import random 11import builtins 12import importlib 13from platform import platform 14from itertools import zip_longest 15from argparse import ArgumentParser, Namespace 16from typing import Any, Callable, Iterable, Type 17 18import pytermgui as ptg 19 20 21def _title() -> str: 22 """Returns 'PyTermGUI', formatted.""" 23 24 return "[!gradient(210) bold]PyTermGUI[/!gradient /]" 25 26 27class AppWindow(ptg.Window): 28 """A generic application window. 29 30 It contains a header with the app's title, as well as some global 31 settings. 32 """ 33 34 app_title: str 35 """The display title of the application.""" 36 37 app_id: str 38 """The short identifier used by ArgumentParser.""" 39 40 standalone: bool 41 """Whether this app was launched directly from the CLI.""" 42 43 overflow = ptg.Overflow.SCROLL 44 vertical_align = ptg.VerticalAlignment.TOP 45 46 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 47 super().__init__(**attrs) 48 49 self.standalone = bool(getattr(args, self.app_id, None)) 50 51 bottom = ptg.Container.chars["border"][-1] 52 header_box = ptg.boxes.Box( 53 [ 54 "", 55 " x ", 56 bottom * 3, 57 ] 58 ) 59 60 self._add_widget(ptg.Container(f"[ptg.title]{self.app_title}", box=header_box)) 61 self._add_widget("") 62 63 def setup(self) -> None: 64 """Centers window, sets its width & height.""" 65 66 self.width = int(self.terminal.width * 2 / 3) 67 self.height = int(self.terminal.height * 2 / 3) 68 self.center(store=False) 69 70 def on_exit(self) -> None: 71 """Called on application exit. 72 73 Should be used to print current application state to the user's shell. 74 """ 75 76 ptg.tim.print(f"{_title()} - [dim]{self.app_title}") 77 print() 78 79 80class GetchWindow(AppWindow): 81 """A window for the Getch utility.""" 82 83 app_title = "Getch" 84 app_id = "getch" 85 86 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 87 super().__init__(args, **attrs) 88 89 self.bind(ptg.keys.ANY_KEY, self._update) 90 91 self._content = ptg.Container("Press any key...", static_width=50) 92 self._add_widget(self._content) 93 94 self.setup() 95 96 def _update(self, _: ptg.Widget, key: str) -> None: 97 """Updates window contents on keypress.""" 98 99 self._content.set_widgets([]) 100 name = _get_key_name(key) 101 102 if name != ascii(key): 103 name = f"keys.{name}" 104 105 style = ptg.HighlighterStyle(ptg.highlight_python) 106 107 items = [ 108 "[ptg.title]Your output", 109 "", 110 {"[ptg.detail]key": ptg.Label(name, style=style)}, 111 {"[ptg.detail]value:": ptg.Label(ascii(key), style=style)}, 112 {"[ptg.detail]len()": ptg.Label(str(len(key)), style=style)}, 113 { 114 "[ptg.detail]real_length()": ptg.Label( 115 str(ptg.real_length(key)), style=style 116 ) 117 }, 118 ] 119 120 for item in items: 121 self._content += item 122 123 if self.standalone: 124 assert self.manager is not None 125 self.manager.stop() 126 127 def on_exit(self) -> None: 128 super().on_exit() 129 130 for line in self._content.get_lines(): 131 print(line) 132 133 134class ColorPickerWindow(AppWindow): 135 """A window to pick colors from the xterm-256 palette.""" 136 137 app_title = "ColorPicker" 138 app_id = "color" 139 140 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 141 super().__init__(args, **attrs) 142 143 self._chosen_rgb = ptg.str_to_color("black") 144 145 self._colorpicker = ptg.ColorPicker() 146 self._add_widget(ptg.Collapsible("xterm-256", "", self._colorpicker).expand()) 147 self._add_widget("") 148 self._add_widget( 149 ptg.Collapsible( 150 "RGB & HEX", "", self._create_rgb_picker(), static_width=81 151 ).expand(), 152 ) 153 154 self.setup() 155 156 def _create_rgb_picker(self) -> ptg.Container: 157 """Creates the RGB picker 'widget'.""" 158 159 root = ptg.Container(static_width=72) 160 161 matrix = ptg.DensePixelMatrix(68, 20) 162 hexdisplay = ptg.Label() 163 rgbdisplay = ptg.Label() 164 165 sliders = [ptg.Slider() for _ in range(3)] 166 167 def _get_rgb() -> tuple[int, int, int]: 168 """Computes the RGB value from the 3 sliders.""" 169 170 values = [int(255 * slider.value) for slider in sliders] 171 172 return values[0], values[1], values[2] 173 174 def _update(*_) -> None: 175 """Updates the matrix & displays with the current color.""" 176 177 color = self._chosen_rgb = ptg.RGBColor.from_rgb(_get_rgb()) 178 for row in range(matrix.rows): 179 for col in range(matrix.columns): 180 matrix[row, col] = color.hex 181 182 hexdisplay.value = f"[ptg.body]{color.hex}" 183 rgbdisplay.value = f"[ptg.body]rgb({', '.join(map(str, color.rgb))})" 184 matrix.build() 185 186 red, green, blue = sliders 187 188 # red.styles.filled_selected__cursor = "red" 189 # green.styles.filled_selected__cursor = "green" 190 # blue.styles.filled_selected__cursor = "blue" 191 192 for slider in sliders: 193 slider.onchange = _update 194 195 root += hexdisplay 196 root += rgbdisplay 197 root += "" 198 199 root += matrix 200 root += "" 201 202 root += red 203 root += green 204 root += blue 205 206 _update() 207 return root 208 209 def on_exit(self) -> None: 210 super().on_exit() 211 212 color = self._chosen_rgb 213 eightbit = " ".join( 214 button.get_lines()[0] for button in self._colorpicker.chosen 215 ) 216 217 ptg.tim.print("[ptg.title]Your colors:") 218 ptg.tim.print(f" [{color.hex}]{color.rgb}[/] // [{color.hex}]{color.hex}") 219 ptg.tim.print() 220 ptg.tim.print(f" {eightbit}") 221 222 223class TIMWindow(AppWindow): 224 """An application to play around with TIM.""" 225 226 app_title = "TIM Playground" 227 app_id = "tim" 228 229 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 230 super().__init__(args, **attrs) 231 232 if self.standalone: 233 self.bind( 234 ptg.keys.RETURN, 235 lambda *_: self.manager.stop() if self.manager is not None else None, 236 ) 237 238 self._generate_colors() 239 240 self._output = ptg.Label(parent_align=0) 241 242 self._input = ptg.InputField() 243 self._input.styles.value__fill = lambda _, item: item 244 245 self._showcase = self._create_showcase() 246 247 self._input.bind(ptg.keys.ANY_KEY, lambda *_: self._update_output()) 248 249 self._add_widget( 250 ptg.Container( 251 ptg.Container(self._output), 252 self._showcase, 253 ptg.Container(self._input), 254 box="EMPTY", 255 static_width=60, 256 ) 257 ) 258 259 self.bind(ptg.keys.CTRL_R, self._generate_colors) 260 261 self.setup() 262 263 self.select(0) 264 265 @staticmethod 266 def _random_rgb() -> ptg.Color: 267 """Returns a random Color.""" 268 269 rgb = tuple(random.randint(0, 255) for _ in range(3)) 270 271 return ptg.RGBColor.from_rgb(rgb) # type: ignore 272 273 def _update_output(self) -> None: 274 """Updates the output field.""" 275 276 self._output.value = self._input.value 277 278 def _generate_colors(self, *_) -> None: 279 """Generates self._example_{255,rgb,hex}.""" 280 281 ptg.tim.alias("ptg.timwindow.255", str(random.randint(16, 233))) 282 ptg.tim.alias("ptg.timwindow.rgb", ";".join(map(str, self._random_rgb().rgb))) 283 ptg.tim.alias("ptg.timwindow.hex", self._random_rgb().hex) 284 285 @staticmethod 286 def _create_showcase() -> ptg.Container: 287 """Creates the showcase container.""" 288 289 def _show_style(name: str) -> str: 290 return f"[{name}]{name}" 291 292 def _create_table(source: Iterable[tuple[str, str]]) -> ptg.Container: 293 root = ptg.Container() 294 295 for left, right in source: 296 row = ptg.Splitter( 297 ptg.Label(left, parent_align=0), ptg.Label(right, parent_align=2) 298 ).styles(separator="ptg.border") 299 300 row.set_char("separator", f" {ptg.Container.chars['border'][0]}") 301 302 root += row 303 304 return root 305 306 prefix = "ptg.timwindow" 307 tags = [_show_style(style) for style in ptg.tim.tags] 308 colors = [ 309 f"[[{prefix}.255]0-255[/]]", 310 f"[[{prefix}.hex]#RRGGBB[/]]", 311 f"[[{prefix}.rgb]RRR;GGG;BBB[/]]", 312 "", 313 f"[[inverse {prefix}.255]@0-255[/]]", 314 f"[[inverse {prefix}.hex]@#RRGGBB[/]]", 315 f"[[inverse {prefix}.rgb]@RRR;GGG;BBB[/]]", 316 ] 317 318 tag_container = _create_table(zip_longest(tags, colors, fillvalue="")) 319 user_container = _create_table( 320 (_show_style(tag), f"[!expand {tag}]{tag}") for tag in ptg.tim.user_tags 321 ) 322 323 return ptg.Container(tag_container, user_container, box="EMPTY") 324 325 def on_exit(self) -> None: 326 super().on_exit() 327 print(ptg.tim.prettify_markup(self._input.value)) 328 ptg.tim.print(self._input.value) 329 330 331class InspectorWindow(AppWindow): 332 """A window for the `inspect` utility.""" 333 334 app_title = "Inspector" 335 app_id = "inspect" 336 337 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 338 super().__init__(args, **attrs) 339 340 self._input = ptg.InputField(value="boxes.Box") 341 342 self._output = ptg.Container(box="EMPTY") 343 self._update() 344 345 self._input.bind(ptg.keys.ENTER, self._update) 346 347 self._add_widget( 348 ptg.Container( 349 self._output, 350 "", 351 ptg.Container(self._input), 352 box="EMPTY", 353 ) 354 ) 355 356 self.setup() 357 358 self.select(0) 359 360 @staticmethod 361 def obj_from_path(path: str) -> object | None: 362 """Retrieves an object from any valid import path. 363 364 An import path could be something like: 365 pytermgui.window_manager.compositor.Compositor 366 367 ...or if the library in question imports its parts within `__init__.py`-s: 368 pytermgui.Compositor 369 """ 370 371 parts = path.split(".") 372 373 if parts[0] in dir(builtins): 374 obj = getattr(builtins, parts[0]) 375 376 elif parts[0] in dir(ptg): 377 obj = getattr(ptg, parts[0]) 378 379 else: 380 try: 381 obj = importlib.import_module(".".join(parts[:-1])) 382 except (ValueError, ModuleNotFoundError) as error: 383 return ( 384 f"Could not import object at path {path!r}: {error}." 385 + " Maybe try using the --eval flag?" 386 ) 387 388 try: 389 obj = getattr(obj, parts[-1]) 390 except AttributeError: 391 return obj 392 393 return obj 394 395 def _update(self, *_) -> None: 396 """Updates output with new inspection result.""" 397 398 obj = self.obj_from_path(self._input.value) 399 400 self._output.vertical_align = ptg.VerticalAlignment.CENTER 401 self._output.set_widgets([ptg.inspect(obj)]) 402 403 def on_exit(self) -> None: 404 super().on_exit() 405 406 self._output.vertical_align = ptg.VerticalAlignment.TOP 407 for line in self._output.get_lines(): 408 print(line) 409 410 411APPLICATION_MAP = { 412 ("Getch", "getch"): GetchWindow, 413 ("Inspector", "inspect"): InspectorWindow, 414 ("ColorPicker", "color"): ColorPickerWindow, 415 ("TIM Playground", "tim"): TIMWindow, 416} 417 418 419def _app_from_short(short: str) -> Type[AppWindow]: 420 """Finds an AppWindow constructor from its short name.""" 421 422 for (_, name), app in APPLICATION_MAP.items(): 423 if name == short: 424 return app 425 426 raise KeyError(f"No app found for {short!r}") 427 428 429def process_args(argv: list[str] | None = None) -> Namespace: 430 """Processes command line arguments.""" 431 432 parser = ArgumentParser( 433 description=f"{ptg.tim.parse(_title())}'s command line environment." 434 ) 435 436 apps = [short for (_, short), _ in APPLICATION_MAP.items()] 437 438 app_group = parser.add_argument_group("Applications") 439 app_group.add_argument( 440 "--app", 441 type=str.lower, 442 help="Launch an app.", 443 metavar=f"{', '.join(app.capitalize() for app in apps)}", 444 choices=apps, 445 ) 446 447 app_group.add_argument( 448 "-g", "--getch", help="Launch the Getch app.", action="store_true" 449 ) 450 451 app_group.add_argument( 452 "-t", "--tim", help="Launch the TIM Playground app.", action="store_true" 453 ) 454 455 app_group.add_argument( 456 "-c", 457 "--color", 458 help="Launch the ColorPicker app.", 459 action="store_true", 460 ) 461 462 inspect_group = parser.add_argument_group("Inspection") 463 inspect_group.add_argument( 464 "-i", "--inspect", help="Inspect an object.", metavar="PATH_OR_CODE" 465 ) 466 inspect_group.add_argument( 467 "-e", 468 "--eval", 469 help="Evaluate the expression given to `--inspect` instead of treating it as a path.", 470 action="store_true", 471 ) 472 473 inspect_group.add_argument( 474 "--methods", help="Always show methods when inspecting.", action="store_true" 475 ) 476 inspect_group.add_argument( 477 "--dunder", 478 help="Always show __dunder__ methods when inspecting.", 479 action="store_true", 480 ) 481 inspect_group.add_argument( 482 "--private", 483 help="Always show _private methods when inspecting.", 484 action="store_true", 485 ) 486 487 util_group = parser.add_argument_group("Utilities") 488 util_group.add_argument( 489 "-s", 490 "--size", 491 help="Output the current terminal size in WxH format.", 492 action="store_true", 493 ) 494 495 util_group.add_argument( 496 "-v", 497 "--version", 498 help="Print version & system information.", 499 action="store_true", 500 ) 501 502 util_group.add_argument( 503 "--highlight", 504 help=( 505 "Highlight some python-like code syntax." 506 + " No argument or '-' will read STDIN." 507 ), 508 metavar="SYNTAX", 509 const="-", 510 nargs="?", 511 ) 512 513 util_group.add_argument( 514 "--exec", 515 help="Execute some Python code. No argument or '-' will read STDIN.", 516 const="-", 517 nargs="?", 518 ) 519 520 util_group.add_argument("-f", "--file", help="Interpret a PTG-YAML file.") 521 util_group.add_argument( 522 "--print-only", 523 help="When interpreting YAML, print the environment without running it interactively.", 524 action="store_true", 525 ) 526 527 export_group = parser.add_argument_group("Exporters") 528 529 export_group.add_argument( 530 "--export-svg", 531 help="Export the result of any non-interactive argument as an SVG file.", 532 metavar="FILE", 533 ) 534 export_group.add_argument( 535 "--export-html", 536 help="Export the result of any non-interactive argument as an HTML file.", 537 metavar="FILE", 538 ) 539 540 argv = argv or sys.argv[1:] 541 args = parser.parse_args(args=argv) 542 543 return args 544 545 546def screenshot(man: ptg.WindowManager) -> None: 547 """Opens a modal dialogue & saves a screenshot.""" 548 549 tempname = ".screenshot_temp.svg" 550 551 modal: ptg.Window 552 553 def _finish(*_: Any) -> None: 554 """Closes the modal and renames the window.""" 555 556 man.remove(modal) 557 filename = field.value or "screenshot" 558 559 if not filename.endswith(".svg"): 560 filename += ".svg" 561 562 os.rename(tempname, filename) 563 564 man.toast("[ptg.title]Screenshot saved!", "", f"[ptg.detail]{filename}") 565 566 title = sys.argv[0] 567 field = ptg.InputField(prompt="Save as: ") 568 569 man.screenshot(title=title, filename=tempname) 570 571 modal = man.alert( 572 "[ptg.title]Screenshot taken!", "", ptg.Container(field), "", ["Save!", _finish] 573 ) 574 575 576def _get_key_name(key: str) -> str: 577 """Gets canonical name of a key. 578 579 Arguments: 580 key: The key in question. 581 582 Returns: 583 The canonical-ish name of the key. 584 """ 585 586 name = ptg.keys.get_name(key) 587 if name is not None: 588 return name 589 590 return ascii(key) 591 592 593def _create_header() -> ptg.Window: 594 """Creates an application header window.""" 595 596 content = ptg.Splitter(ptg.Label("PyTermGUI", parent_align=0, padding=2)) 597 content.styles.fill = "ptg.header" 598 599 return ptg.Window(content, box="EMPTY", id="ptg.header", is_persistent=True) 600 601 602def _create_app_picker(manager: ptg.WindowManager) -> ptg.Window: 603 """Creates a dropdown that allows picking between applications.""" 604 605 existing_windows: list[ptg.Window] = [] 606 607 def _wrap(func: Callable[[ptg.Widget], Any]) -> Callable[[ptg.Widget], Any]: 608 def _inner(caller: ptg.Widget) -> None: 609 dropdown.collapse() 610 611 window: ptg.Window = func(caller) 612 if type(window) in map(type, manager): 613 return 614 615 existing_windows.append(window) 616 manager.add(window, assign="body") 617 618 body = manager.layout.body 619 620 body.content = window 621 manager.layout.apply() 622 623 return _inner 624 625 buttons = [ 626 ptg.Button(label, _wrap(lambda *_, app=app: app())) 627 for (label, _), app in APPLICATION_MAP.items() 628 ] 629 630 dropdown = ptg.Collapsible("Applications", *buttons, keyboard=True).styles( 631 fill="ptg.footer" 632 ) 633 634 return ptg.Window( 635 dropdown, 636 box="EMPTY", 637 id="ptg.header", 638 is_persistent=True, 639 overflow=ptg.Overflow.RESIZE, 640 ).styles(fill="ptg.header") 641 642 643def _create_footer(man: ptg.WindowManager) -> ptg.Window: 644 """Creates a footer based on the manager's bindings.""" 645 646 content = ptg.Splitter().styles(fill="ptg.footer") 647 for key, (callback, doc) in man.bindings.items(): 648 if doc == f"Binding of {key} to {callback}": 649 continue 650 651 content.lazy_add( 652 ptg.Button( 653 f"{_get_key_name(str(key))} - {doc}", 654 onclick=lambda *_, _callback=callback: _callback(man), 655 ) 656 ) 657 658 return ptg.Window(content, box="EMPTY", id="ptg.footer", is_persistent=True) 659 660 661def _create_layout() -> ptg.Layout: 662 """Creates the main layout.""" 663 664 layout = ptg.Layout() 665 666 layout.add_slot("Header", height=1) 667 layout.add_slot("Applications", width=20) 668 layout.add_break() 669 layout.add_slot("Body") 670 layout.add_break() 671 layout.add_slot("Footer", height=1) 672 673 return layout 674 675 676def _create_aliases() -> None: 677 """Creates all TIM alises used by the `ptg` utility. 678 679 Current aliases: 680 - ptg.title: Used for main titles. 681 - ptg.body: Used for body text. 682 - ptg.detail: Used for highlighting detail inside body text. 683 - ptg.accent: Used as an accent color in various places. 684 - ptg.header: Used for the header bar. 685 - ptg.footer: Used for the footer bar. 686 - ptg.border: Used for focused window borders & corners. 687 - ptg.border_blurred: Used for non-focused window borders & corners. 688 """ 689 690 ptg.tim.alias("ptg.title", "210 bold") 691 ptg.tim.alias("ptg.body", "247") 692 ptg.tim.alias("ptg.detail", "dim") 693 ptg.tim.alias("ptg.accent", "72") 694 695 ptg.tim.alias("ptg.header", "@235 242 bold") 696 ptg.tim.alias("ptg.footer", "@235") 697 698 ptg.tim.alias("ptg.border", "60") 699 ptg.tim.alias("ptg.border_blurred", "#373748") 700 701 702def _configure_widgets() -> None: 703 """Configures default widget attributes.""" 704 705 ptg.boxes.Box([" ", " x ", " "]).set_chars_of(ptg.Window) 706 ptg.boxes.SINGLE.set_chars_of(ptg.Container) 707 ptg.boxes.DOUBLE.set_chars_of(ptg.Window) 708 709 ptg.InputField.styles.cursor = "inverse ptg.accent" 710 ptg.InputField.styles.fill = "245" 711 ptg.Container.styles.border__corner = "ptg.border" 712 ptg.Splitter.set_char("separator", "") 713 ptg.Button.set_char("delimiter", [" ", " "]) 714 715 ptg.Window.styles.border__corner = "ptg.border" 716 ptg.Window.set_focus_styles( 717 focused=("ptg.border", "ptg.border"), 718 blurred=("ptg.border_blurred", "ptg.border_blurred"), 719 ) 720 721 722def run_environment(args: Namespace) -> None: 723 """Runs the WindowManager environment. 724 725 Args: 726 args: An argparse namespace containing relevant arguments. 727 """ 728 729 def _find_focused(manager: ptg.WindowManager) -> ptg.Window | None: 730 if manager.focused is None: 731 return None 732 733 # Find foremost non-persistent window 734 for window in manager: 735 if window.is_persistent: 736 continue 737 738 return window 739 740 return None 741 742 def _toggle_attachment(manager: ptg.WindowManager) -> None: 743 focused = _find_focused(manager) 744 745 if focused is None: 746 return 747 748 slot = manager.layout.body 749 if slot.content is None: 750 slot.content = focused 751 else: 752 slot.detach_content() 753 754 manager.layout.apply() 755 756 def _close_focused(manager: ptg.WindowManager) -> None: 757 focused = _find_focused(manager) 758 759 if focused is None: 760 return 761 762 focused.close() 763 764 _configure_widgets() 765 766 window: AppWindow | None = None 767 with ptg.WindowManager() as manager: 768 app_picker = _create_app_picker(manager) 769 770 manager.bind( 771 ptg.keys.CTRL_W, 772 lambda *_: _close_focused(manager), 773 "Close window", 774 ) 775 manager.bind( 776 ptg.keys.F12, 777 lambda *_: screenshot(manager), 778 "Screenshot", 779 ) 780 manager.bind( 781 ptg.keys.CTRL_F, 782 lambda *_: _toggle_attachment(manager), 783 "Toggle layout", 784 ) 785 786 manager.bind( 787 ptg.keys.CTRL_A, 788 lambda *_: { 789 manager.focus(app_picker), # type: ignore 790 app_picker.execute_binding(ptg.keys.CTRL_A), 791 }, 792 ) 793 manager.bind( 794 ptg.keys.ALT + ptg.keys.TAB, 795 lambda *_: manager.focus_next(), 796 ) 797 798 if not args.app: 799 manager.layout = _create_layout() 800 801 manager.add(_create_header(), assign="header") 802 manager.add(app_picker, assign="applications") 803 manager.add(_create_footer(manager), assign="footer") 804 805 manager.toast( 806 f"[ptg.title]Welcome to the {_title()} [ptg.title]CLI!", 807 offset=ptg.terminal.height // 2 - 3, 808 delay=700, 809 ) 810 811 else: 812 manager.layout.add_slot("Body") 813 814 app = _app_from_short(args.app) 815 window = app(args) 816 manager.add(window, assign="body") 817 818 window = window or manager.focused # type: ignore 819 if window is None or not isinstance(window, AppWindow): 820 return 821 822 window.on_exit() 823 824 825def _print_version() -> None: 826 """Prints version info.""" 827 828 def _print_aligned(left: str, right: str | None) -> None: 829 left += ":" 830 831 ptg.tim.print(f"[ptg.detail]{left:<19} [/ptg.detail 157]{right}") 832 833 ptg.tim.print( 834 f"[bold !gradient(210)]PyTermGUI[/ /!gradient] version [157]{ptg.__version__}" 835 ) 836 ptg.tim.print() 837 ptg.tim.print("[ptg.title]System details:") 838 839 _print_aligned(" Python version", sys.version.split()[0]) 840 _print_aligned(" $TERM", os.getenv("TERM")) 841 _print_aligned(" $COLORTERM", os.getenv("COLORTERM")) 842 _print_aligned(" Color support", str(ptg.terminal.colorsystem)) 843 _print_aligned(" OS Platform", platform()) 844 845 846def _run_inspect(args: Namespace) -> None: 847 """Inspects something in the CLI.""" 848 849 args.methods = args.methods or None 850 args.dunder = args.dunder or None 851 args.private = args.private or None 852 853 target = ( 854 eval(args.inspect) # pylint: disable=eval-used 855 if args.eval 856 else InspectorWindow.obj_from_path(args.inspect) 857 ) 858 859 if not args.eval and isinstance(target, str): 860 args.methods = False 861 862 inspector = ptg.inspect( 863 target, 864 show_methods=args.methods, 865 show_private=args.private, 866 show_dunder=args.dunder, 867 ) 868 869 ptg.terminal.print(inspector) 870 871 872def _interpret_file(args: Namespace) -> None: 873 """Interprets a PTG-YAML file.""" 874 875 with ptg.YamlLoader() as loader, open(args.file, "r", encoding="utf-8") as file: 876 namespace = loader.load(file) 877 878 if not args.print_only: 879 with ptg.WindowManager() as manager: 880 for widget in namespace.widgets.values(): 881 if not isinstance(widget, ptg.Window): 882 continue 883 884 manager.add(widget) 885 return 886 887 for widget in namespace.widgets.values(): 888 for line in widget.get_lines(): 889 ptg.terminal.print(line) 890 891 892def main(argv: list[str] | None = None) -> None: 893 """Runs the program. 894 895 Args: 896 argv: A list of arguments, not included the 0th element pointing to the 897 executable path. 898 """ 899 900 _create_aliases() 901 902 args = process_args(argv) 903 904 args.app = args.app or ( 905 "getch" 906 if args.getch 907 else ("tim" if args.tim else ("color" if args.color else None)) 908 ) 909 910 if args.app or len(sys.argv) == 1: 911 run_environment(args) 912 return 913 914 with ptg.terminal.record() as recording: 915 if args.size: 916 ptg.tim.print(f"{ptg.terminal.width}x{ptg.terminal.height}") 917 918 elif args.version: 919 _print_version() 920 921 elif args.inspect: 922 _run_inspect(args) 923 924 elif args.exec: 925 args.exec = sys.stdin.read() if args.exec == "-" else args.exec 926 927 for name in dir(ptg): 928 obj = getattr(ptg, name, None) 929 globals()[name] = obj 930 931 globals()["print"] = ptg.terminal.print 932 933 exec(args.exec, locals(), globals()) # pylint: disable=exec-used 934 935 elif args.highlight: 936 text = sys.stdin.read() if args.highlight == "-" else args.highlight 937 938 ptg.tim.print(ptg.highlight_python(text)) 939 940 elif args.file: 941 _interpret_file(args) 942 943 if args.export_svg: 944 recording.save_svg(args.export_svg) 945 946 elif args.export_html: 947 recording.save_html(args.export_html) 948 949 950if __name__ == "__main__": 951 main(sys.argv[1:])
28class AppWindow(ptg.Window): 29 """A generic application window. 30 31 It contains a header with the app's title, as well as some global 32 settings. 33 """ 34 35 app_title: str 36 """The display title of the application.""" 37 38 app_id: str 39 """The short identifier used by ArgumentParser.""" 40 41 standalone: bool 42 """Whether this app was launched directly from the CLI.""" 43 44 overflow = ptg.Overflow.SCROLL 45 vertical_align = ptg.VerticalAlignment.TOP 46 47 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 48 super().__init__(**attrs) 49 50 self.standalone = bool(getattr(args, self.app_id, None)) 51 52 bottom = ptg.Container.chars["border"][-1] 53 header_box = ptg.boxes.Box( 54 [ 55 "", 56 " x ", 57 bottom * 3, 58 ] 59 ) 60 61 self._add_widget(ptg.Container(f"[ptg.title]{self.app_title}", box=header_box)) 62 self._add_widget("") 63 64 def setup(self) -> None: 65 """Centers window, sets its width & height.""" 66 67 self.width = int(self.terminal.width * 2 / 3) 68 self.height = int(self.terminal.height * 2 / 3) 69 self.center(store=False) 70 71 def on_exit(self) -> None: 72 """Called on application exit. 73 74 Should be used to print current application state to the user's shell. 75 """ 76 77 ptg.tim.print(f"{_title()} - [dim]{self.app_title}") 78 print()
A generic application window.
It contains a header with the app's title, as well as some global settings.
47 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 48 super().__init__(**attrs) 49 50 self.standalone = bool(getattr(args, self.app_id, None)) 51 52 bottom = ptg.Container.chars["border"][-1] 53 header_box = ptg.boxes.Box( 54 [ 55 "", 56 " x ", 57 bottom * 3, 58 ] 59 ) 60 61 self._add_widget(ptg.Container(f"[ptg.title]{self.app_title}", box=header_box)) 62 self._add_widget("")
Initializes object.
Args
- widgets: Widgets to add to this window after initilization.
- attrs: Attributes that are passed to the constructor.
64 def setup(self) -> None: 65 """Centers window, sets its width & height.""" 66 67 self.width = int(self.terminal.width * 2 / 3) 68 self.height = int(self.terminal.height * 2 / 3) 69 self.center(store=False)
Centers window, sets its width & height.
71 def on_exit(self) -> None: 72 """Called on application exit. 73 74 Should be used to print current application state to the user's shell. 75 """ 76 77 ptg.tim.print(f"{_title()} - [dim]{self.app_title}") 78 print()
Called on application exit.
Should be used to print current application state to the user's shell.
Inherited Members
81class GetchWindow(AppWindow): 82 """A window for the Getch utility.""" 83 84 app_title = "Getch" 85 app_id = "getch" 86 87 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 88 super().__init__(args, **attrs) 89 90 self.bind(ptg.keys.ANY_KEY, self._update) 91 92 self._content = ptg.Container("Press any key...", static_width=50) 93 self._add_widget(self._content) 94 95 self.setup() 96 97 def _update(self, _: ptg.Widget, key: str) -> None: 98 """Updates window contents on keypress.""" 99 100 self._content.set_widgets([]) 101 name = _get_key_name(key) 102 103 if name != ascii(key): 104 name = f"keys.{name}" 105 106 style = ptg.HighlighterStyle(ptg.highlight_python) 107 108 items = [ 109 "[ptg.title]Your output", 110 "", 111 {"[ptg.detail]key": ptg.Label(name, style=style)}, 112 {"[ptg.detail]value:": ptg.Label(ascii(key), style=style)}, 113 {"[ptg.detail]len()": ptg.Label(str(len(key)), style=style)}, 114 { 115 "[ptg.detail]real_length()": ptg.Label( 116 str(ptg.real_length(key)), style=style 117 ) 118 }, 119 ] 120 121 for item in items: 122 self._content += item 123 124 if self.standalone: 125 assert self.manager is not None 126 self.manager.stop() 127 128 def on_exit(self) -> None: 129 super().on_exit() 130 131 for line in self._content.get_lines(): 132 print(line)
A window for the Getch utility.
87 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 88 super().__init__(args, **attrs) 89 90 self.bind(ptg.keys.ANY_KEY, self._update) 91 92 self._content = ptg.Container("Press any key...", static_width=50) 93 self._add_widget(self._content) 94 95 self.setup()
Initializes object.
Args
- widgets: Widgets to add to this window after initilization.
- attrs: Attributes that are passed to the constructor.
128 def on_exit(self) -> None: 129 super().on_exit() 130 131 for line in self._content.get_lines(): 132 print(line)
Called on application exit.
Should be used to print current application state to the user's shell.
Inherited Members
135class ColorPickerWindow(AppWindow): 136 """A window to pick colors from the xterm-256 palette.""" 137 138 app_title = "ColorPicker" 139 app_id = "color" 140 141 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 142 super().__init__(args, **attrs) 143 144 self._chosen_rgb = ptg.str_to_color("black") 145 146 self._colorpicker = ptg.ColorPicker() 147 self._add_widget(ptg.Collapsible("xterm-256", "", self._colorpicker).expand()) 148 self._add_widget("") 149 self._add_widget( 150 ptg.Collapsible( 151 "RGB & HEX", "", self._create_rgb_picker(), static_width=81 152 ).expand(), 153 ) 154 155 self.setup() 156 157 def _create_rgb_picker(self) -> ptg.Container: 158 """Creates the RGB picker 'widget'.""" 159 160 root = ptg.Container(static_width=72) 161 162 matrix = ptg.DensePixelMatrix(68, 20) 163 hexdisplay = ptg.Label() 164 rgbdisplay = ptg.Label() 165 166 sliders = [ptg.Slider() for _ in range(3)] 167 168 def _get_rgb() -> tuple[int, int, int]: 169 """Computes the RGB value from the 3 sliders.""" 170 171 values = [int(255 * slider.value) for slider in sliders] 172 173 return values[0], values[1], values[2] 174 175 def _update(*_) -> None: 176 """Updates the matrix & displays with the current color.""" 177 178 color = self._chosen_rgb = ptg.RGBColor.from_rgb(_get_rgb()) 179 for row in range(matrix.rows): 180 for col in range(matrix.columns): 181 matrix[row, col] = color.hex 182 183 hexdisplay.value = f"[ptg.body]{color.hex}" 184 rgbdisplay.value = f"[ptg.body]rgb({', '.join(map(str, color.rgb))})" 185 matrix.build() 186 187 red, green, blue = sliders 188 189 # red.styles.filled_selected__cursor = "red" 190 # green.styles.filled_selected__cursor = "green" 191 # blue.styles.filled_selected__cursor = "blue" 192 193 for slider in sliders: 194 slider.onchange = _update 195 196 root += hexdisplay 197 root += rgbdisplay 198 root += "" 199 200 root += matrix 201 root += "" 202 203 root += red 204 root += green 205 root += blue 206 207 _update() 208 return root 209 210 def on_exit(self) -> None: 211 super().on_exit() 212 213 color = self._chosen_rgb 214 eightbit = " ".join( 215 button.get_lines()[0] for button in self._colorpicker.chosen 216 ) 217 218 ptg.tim.print("[ptg.title]Your colors:") 219 ptg.tim.print(f" [{color.hex}]{color.rgb}[/] // [{color.hex}]{color.hex}") 220 ptg.tim.print() 221 ptg.tim.print(f" {eightbit}")
A window to pick colors from the xterm-256 palette.
141 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 142 super().__init__(args, **attrs) 143 144 self._chosen_rgb = ptg.str_to_color("black") 145 146 self._colorpicker = ptg.ColorPicker() 147 self._add_widget(ptg.Collapsible("xterm-256", "", self._colorpicker).expand()) 148 self._add_widget("") 149 self._add_widget( 150 ptg.Collapsible( 151 "RGB & HEX", "", self._create_rgb_picker(), static_width=81 152 ).expand(), 153 ) 154 155 self.setup()
Initializes object.
Args
- widgets: Widgets to add to this window after initilization.
- attrs: Attributes that are passed to the constructor.
210 def on_exit(self) -> None: 211 super().on_exit() 212 213 color = self._chosen_rgb 214 eightbit = " ".join( 215 button.get_lines()[0] for button in self._colorpicker.chosen 216 ) 217 218 ptg.tim.print("[ptg.title]Your colors:") 219 ptg.tim.print(f" [{color.hex}]{color.rgb}[/] // [{color.hex}]{color.hex}") 220 ptg.tim.print() 221 ptg.tim.print(f" {eightbit}")
Called on application exit.
Should be used to print current application state to the user's shell.
Inherited Members
224class TIMWindow(AppWindow): 225 """An application to play around with TIM.""" 226 227 app_title = "TIM Playground" 228 app_id = "tim" 229 230 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 231 super().__init__(args, **attrs) 232 233 if self.standalone: 234 self.bind( 235 ptg.keys.RETURN, 236 lambda *_: self.manager.stop() if self.manager is not None else None, 237 ) 238 239 self._generate_colors() 240 241 self._output = ptg.Label(parent_align=0) 242 243 self._input = ptg.InputField() 244 self._input.styles.value__fill = lambda _, item: item 245 246 self._showcase = self._create_showcase() 247 248 self._input.bind(ptg.keys.ANY_KEY, lambda *_: self._update_output()) 249 250 self._add_widget( 251 ptg.Container( 252 ptg.Container(self._output), 253 self._showcase, 254 ptg.Container(self._input), 255 box="EMPTY", 256 static_width=60, 257 ) 258 ) 259 260 self.bind(ptg.keys.CTRL_R, self._generate_colors) 261 262 self.setup() 263 264 self.select(0) 265 266 @staticmethod 267 def _random_rgb() -> ptg.Color: 268 """Returns a random Color.""" 269 270 rgb = tuple(random.randint(0, 255) for _ in range(3)) 271 272 return ptg.RGBColor.from_rgb(rgb) # type: ignore 273 274 def _update_output(self) -> None: 275 """Updates the output field.""" 276 277 self._output.value = self._input.value 278 279 def _generate_colors(self, *_) -> None: 280 """Generates self._example_{255,rgb,hex}.""" 281 282 ptg.tim.alias("ptg.timwindow.255", str(random.randint(16, 233))) 283 ptg.tim.alias("ptg.timwindow.rgb", ";".join(map(str, self._random_rgb().rgb))) 284 ptg.tim.alias("ptg.timwindow.hex", self._random_rgb().hex) 285 286 @staticmethod 287 def _create_showcase() -> ptg.Container: 288 """Creates the showcase container.""" 289 290 def _show_style(name: str) -> str: 291 return f"[{name}]{name}" 292 293 def _create_table(source: Iterable[tuple[str, str]]) -> ptg.Container: 294 root = ptg.Container() 295 296 for left, right in source: 297 row = ptg.Splitter( 298 ptg.Label(left, parent_align=0), ptg.Label(right, parent_align=2) 299 ).styles(separator="ptg.border") 300 301 row.set_char("separator", f" {ptg.Container.chars['border'][0]}") 302 303 root += row 304 305 return root 306 307 prefix = "ptg.timwindow" 308 tags = [_show_style(style) for style in ptg.tim.tags] 309 colors = [ 310 f"[[{prefix}.255]0-255[/]]", 311 f"[[{prefix}.hex]#RRGGBB[/]]", 312 f"[[{prefix}.rgb]RRR;GGG;BBB[/]]", 313 "", 314 f"[[inverse {prefix}.255]@0-255[/]]", 315 f"[[inverse {prefix}.hex]@#RRGGBB[/]]", 316 f"[[inverse {prefix}.rgb]@RRR;GGG;BBB[/]]", 317 ] 318 319 tag_container = _create_table(zip_longest(tags, colors, fillvalue="")) 320 user_container = _create_table( 321 (_show_style(tag), f"[!expand {tag}]{tag}") for tag in ptg.tim.user_tags 322 ) 323 324 return ptg.Container(tag_container, user_container, box="EMPTY") 325 326 def on_exit(self) -> None: 327 super().on_exit() 328 print(ptg.tim.prettify_markup(self._input.value)) 329 ptg.tim.print(self._input.value)
An application to play around with TIM.
230 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 231 super().__init__(args, **attrs) 232 233 if self.standalone: 234 self.bind( 235 ptg.keys.RETURN, 236 lambda *_: self.manager.stop() if self.manager is not None else None, 237 ) 238 239 self._generate_colors() 240 241 self._output = ptg.Label(parent_align=0) 242 243 self._input = ptg.InputField() 244 self._input.styles.value__fill = lambda _, item: item 245 246 self._showcase = self._create_showcase() 247 248 self._input.bind(ptg.keys.ANY_KEY, lambda *_: self._update_output()) 249 250 self._add_widget( 251 ptg.Container( 252 ptg.Container(self._output), 253 self._showcase, 254 ptg.Container(self._input), 255 box="EMPTY", 256 static_width=60, 257 ) 258 ) 259 260 self.bind(ptg.keys.CTRL_R, self._generate_colors) 261 262 self.setup() 263 264 self.select(0)
Initializes object.
Args
- widgets: Widgets to add to this window after initilization.
- attrs: Attributes that are passed to the constructor.
326 def on_exit(self) -> None: 327 super().on_exit() 328 print(ptg.tim.prettify_markup(self._input.value)) 329 ptg.tim.print(self._input.value)
Called on application exit.
Should be used to print current application state to the user's shell.
Inherited Members
332class InspectorWindow(AppWindow): 333 """A window for the `inspect` utility.""" 334 335 app_title = "Inspector" 336 app_id = "inspect" 337 338 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 339 super().__init__(args, **attrs) 340 341 self._input = ptg.InputField(value="boxes.Box") 342 343 self._output = ptg.Container(box="EMPTY") 344 self._update() 345 346 self._input.bind(ptg.keys.ENTER, self._update) 347 348 self._add_widget( 349 ptg.Container( 350 self._output, 351 "", 352 ptg.Container(self._input), 353 box="EMPTY", 354 ) 355 ) 356 357 self.setup() 358 359 self.select(0) 360 361 @staticmethod 362 def obj_from_path(path: str) -> object | None: 363 """Retrieves an object from any valid import path. 364 365 An import path could be something like: 366 pytermgui.window_manager.compositor.Compositor 367 368 ...or if the library in question imports its parts within `__init__.py`-s: 369 pytermgui.Compositor 370 """ 371 372 parts = path.split(".") 373 374 if parts[0] in dir(builtins): 375 obj = getattr(builtins, parts[0]) 376 377 elif parts[0] in dir(ptg): 378 obj = getattr(ptg, parts[0]) 379 380 else: 381 try: 382 obj = importlib.import_module(".".join(parts[:-1])) 383 except (ValueError, ModuleNotFoundError) as error: 384 return ( 385 f"Could not import object at path {path!r}: {error}." 386 + " Maybe try using the --eval flag?" 387 ) 388 389 try: 390 obj = getattr(obj, parts[-1]) 391 except AttributeError: 392 return obj 393 394 return obj 395 396 def _update(self, *_) -> None: 397 """Updates output with new inspection result.""" 398 399 obj = self.obj_from_path(self._input.value) 400 401 self._output.vertical_align = ptg.VerticalAlignment.CENTER 402 self._output.set_widgets([ptg.inspect(obj)]) 403 404 def on_exit(self) -> None: 405 super().on_exit() 406 407 self._output.vertical_align = ptg.VerticalAlignment.TOP 408 for line in self._output.get_lines(): 409 print(line)
A window for the inspect
utility.
338 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 339 super().__init__(args, **attrs) 340 341 self._input = ptg.InputField(value="boxes.Box") 342 343 self._output = ptg.Container(box="EMPTY") 344 self._update() 345 346 self._input.bind(ptg.keys.ENTER, self._update) 347 348 self._add_widget( 349 ptg.Container( 350 self._output, 351 "", 352 ptg.Container(self._input), 353 box="EMPTY", 354 ) 355 ) 356 357 self.setup() 358 359 self.select(0)
Initializes object.
Args
- widgets: Widgets to add to this window after initilization.
- attrs: Attributes that are passed to the constructor.
361 @staticmethod 362 def obj_from_path(path: str) -> object | None: 363 """Retrieves an object from any valid import path. 364 365 An import path could be something like: 366 pytermgui.window_manager.compositor.Compositor 367 368 ...or if the library in question imports its parts within `__init__.py`-s: 369 pytermgui.Compositor 370 """ 371 372 parts = path.split(".") 373 374 if parts[0] in dir(builtins): 375 obj = getattr(builtins, parts[0]) 376 377 elif parts[0] in dir(ptg): 378 obj = getattr(ptg, parts[0]) 379 380 else: 381 try: 382 obj = importlib.import_module(".".join(parts[:-1])) 383 except (ValueError, ModuleNotFoundError) as error: 384 return ( 385 f"Could not import object at path {path!r}: {error}." 386 + " Maybe try using the --eval flag?" 387 ) 388 389 try: 390 obj = getattr(obj, parts[-1]) 391 except AttributeError: 392 return obj 393 394 return obj
Retrieves an object from any valid import path.
An import path could be something like
...or if the library in question imports its parts within __init__.py
-s:
pytermgui.Compositor
404 def on_exit(self) -> None: 405 super().on_exit() 406 407 self._output.vertical_align = ptg.VerticalAlignment.TOP 408 for line in self._output.get_lines(): 409 print(line)
Called on application exit.
Should be used to print current application state to the user's shell.
Inherited Members
430def process_args(argv: list[str] | None = None) -> Namespace: 431 """Processes command line arguments.""" 432 433 parser = ArgumentParser( 434 description=f"{ptg.tim.parse(_title())}'s command line environment." 435 ) 436 437 apps = [short for (_, short), _ in APPLICATION_MAP.items()] 438 439 app_group = parser.add_argument_group("Applications") 440 app_group.add_argument( 441 "--app", 442 type=str.lower, 443 help="Launch an app.", 444 metavar=f"{', '.join(app.capitalize() for app in apps)}", 445 choices=apps, 446 ) 447 448 app_group.add_argument( 449 "-g", "--getch", help="Launch the Getch app.", action="store_true" 450 ) 451 452 app_group.add_argument( 453 "-t", "--tim", help="Launch the TIM Playground app.", action="store_true" 454 ) 455 456 app_group.add_argument( 457 "-c", 458 "--color", 459 help="Launch the ColorPicker app.", 460 action="store_true", 461 ) 462 463 inspect_group = parser.add_argument_group("Inspection") 464 inspect_group.add_argument( 465 "-i", "--inspect", help="Inspect an object.", metavar="PATH_OR_CODE" 466 ) 467 inspect_group.add_argument( 468 "-e", 469 "--eval", 470 help="Evaluate the expression given to `--inspect` instead of treating it as a path.", 471 action="store_true", 472 ) 473 474 inspect_group.add_argument( 475 "--methods", help="Always show methods when inspecting.", action="store_true" 476 ) 477 inspect_group.add_argument( 478 "--dunder", 479 help="Always show __dunder__ methods when inspecting.", 480 action="store_true", 481 ) 482 inspect_group.add_argument( 483 "--private", 484 help="Always show _private methods when inspecting.", 485 action="store_true", 486 ) 487 488 util_group = parser.add_argument_group("Utilities") 489 util_group.add_argument( 490 "-s", 491 "--size", 492 help="Output the current terminal size in WxH format.", 493 action="store_true", 494 ) 495 496 util_group.add_argument( 497 "-v", 498 "--version", 499 help="Print version & system information.", 500 action="store_true", 501 ) 502 503 util_group.add_argument( 504 "--highlight", 505 help=( 506 "Highlight some python-like code syntax." 507 + " No argument or '-' will read STDIN." 508 ), 509 metavar="SYNTAX", 510 const="-", 511 nargs="?", 512 ) 513 514 util_group.add_argument( 515 "--exec", 516 help="Execute some Python code. No argument or '-' will read STDIN.", 517 const="-", 518 nargs="?", 519 ) 520 521 util_group.add_argument("-f", "--file", help="Interpret a PTG-YAML file.") 522 util_group.add_argument( 523 "--print-only", 524 help="When interpreting YAML, print the environment without running it interactively.", 525 action="store_true", 526 ) 527 528 export_group = parser.add_argument_group("Exporters") 529 530 export_group.add_argument( 531 "--export-svg", 532 help="Export the result of any non-interactive argument as an SVG file.", 533 metavar="FILE", 534 ) 535 export_group.add_argument( 536 "--export-html", 537 help="Export the result of any non-interactive argument as an HTML file.", 538 metavar="FILE", 539 ) 540 541 argv = argv or sys.argv[1:] 542 args = parser.parse_args(args=argv) 543 544 return args
Processes command line arguments.
547def screenshot(man: ptg.WindowManager) -> None: 548 """Opens a modal dialogue & saves a screenshot.""" 549 550 tempname = ".screenshot_temp.svg" 551 552 modal: ptg.Window 553 554 def _finish(*_: Any) -> None: 555 """Closes the modal and renames the window.""" 556 557 man.remove(modal) 558 filename = field.value or "screenshot" 559 560 if not filename.endswith(".svg"): 561 filename += ".svg" 562 563 os.rename(tempname, filename) 564 565 man.toast("[ptg.title]Screenshot saved!", "", f"[ptg.detail]{filename}") 566 567 title = sys.argv[0] 568 field = ptg.InputField(prompt="Save as: ") 569 570 man.screenshot(title=title, filename=tempname) 571 572 modal = man.alert( 573 "[ptg.title]Screenshot taken!", "", ptg.Container(field), "", ["Save!", _finish] 574 )
Opens a modal dialogue & saves a screenshot.
723def run_environment(args: Namespace) -> None: 724 """Runs the WindowManager environment. 725 726 Args: 727 args: An argparse namespace containing relevant arguments. 728 """ 729 730 def _find_focused(manager: ptg.WindowManager) -> ptg.Window | None: 731 if manager.focused is None: 732 return None 733 734 # Find foremost non-persistent window 735 for window in manager: 736 if window.is_persistent: 737 continue 738 739 return window 740 741 return None 742 743 def _toggle_attachment(manager: ptg.WindowManager) -> None: 744 focused = _find_focused(manager) 745 746 if focused is None: 747 return 748 749 slot = manager.layout.body 750 if slot.content is None: 751 slot.content = focused 752 else: 753 slot.detach_content() 754 755 manager.layout.apply() 756 757 def _close_focused(manager: ptg.WindowManager) -> None: 758 focused = _find_focused(manager) 759 760 if focused is None: 761 return 762 763 focused.close() 764 765 _configure_widgets() 766 767 window: AppWindow | None = None 768 with ptg.WindowManager() as manager: 769 app_picker = _create_app_picker(manager) 770 771 manager.bind( 772 ptg.keys.CTRL_W, 773 lambda *_: _close_focused(manager), 774 "Close window", 775 ) 776 manager.bind( 777 ptg.keys.F12, 778 lambda *_: screenshot(manager), 779 "Screenshot", 780 ) 781 manager.bind( 782 ptg.keys.CTRL_F, 783 lambda *_: _toggle_attachment(manager), 784 "Toggle layout", 785 ) 786 787 manager.bind( 788 ptg.keys.CTRL_A, 789 lambda *_: { 790 manager.focus(app_picker), # type: ignore 791 app_picker.execute_binding(ptg.keys.CTRL_A), 792 }, 793 ) 794 manager.bind( 795 ptg.keys.ALT + ptg.keys.TAB, 796 lambda *_: manager.focus_next(), 797 ) 798 799 if not args.app: 800 manager.layout = _create_layout() 801 802 manager.add(_create_header(), assign="header") 803 manager.add(app_picker, assign="applications") 804 manager.add(_create_footer(manager), assign="footer") 805 806 manager.toast( 807 f"[ptg.title]Welcome to the {_title()} [ptg.title]CLI!", 808 offset=ptg.terminal.height // 2 - 3, 809 delay=700, 810 ) 811 812 else: 813 manager.layout.add_slot("Body") 814 815 app = _app_from_short(args.app) 816 window = app(args) 817 manager.add(window, assign="body") 818 819 window = window or manager.focused # type: ignore 820 if window is None or not isinstance(window, AppWindow): 821 return 822 823 window.on_exit()
Runs the WindowManager environment.
Args
- args: An argparse namespace containing relevant arguments.
893def main(argv: list[str] | None = None) -> None: 894 """Runs the program. 895 896 Args: 897 argv: A list of arguments, not included the 0th element pointing to the 898 executable path. 899 """ 900 901 _create_aliases() 902 903 args = process_args(argv) 904 905 args.app = args.app or ( 906 "getch" 907 if args.getch 908 else ("tim" if args.tim else ("color" if args.color else None)) 909 ) 910 911 if args.app or len(sys.argv) == 1: 912 run_environment(args) 913 return 914 915 with ptg.terminal.record() as recording: 916 if args.size: 917 ptg.tim.print(f"{ptg.terminal.width}x{ptg.terminal.height}") 918 919 elif args.version: 920 _print_version() 921 922 elif args.inspect: 923 _run_inspect(args) 924 925 elif args.exec: 926 args.exec = sys.stdin.read() if args.exec == "-" else args.exec 927 928 for name in dir(ptg): 929 obj = getattr(ptg, name, None) 930 globals()[name] = obj 931 932 globals()["print"] = ptg.terminal.print 933 934 exec(args.exec, locals(), globals()) # pylint: disable=exec-used 935 936 elif args.highlight: 937 text = sys.stdin.read() if args.highlight == "-" else args.highlight 938 939 ptg.tim.print(ptg.highlight_python(text)) 940 941 elif args.file: 942 _interpret_file(args) 943 944 if args.export_svg: 945 recording.save_svg(args.export_svg) 946 947 elif args.export_html: 948 recording.save_html(args.export_html)
Runs the program.
Args
- argv: A list of arguments, not included the 0th element pointing to the executable path.