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 builtins 9import importlib 10import os 11import random 12import sys 13from argparse import ArgumentParser, Namespace 14from itertools import zip_longest 15from platform import platform 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}" # .replace("'", "") 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.markup.style_maps.STYLES] 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"[{tag}]{value}") 321 for tag, value in ptg.tim.aliases.items() 322 if not tag.startswith("/") 323 ) 324 325 return ptg.Container(tag_container, user_container, box="EMPTY") 326 327 def on_exit(self) -> None: 328 super().on_exit() 329 ptg.tim.print(ptg.highlight_tim(self._input.value)) 330 ptg.tim.print(self._input.value) 331 332 333class InspectorWindow(AppWindow): 334 """A window for the `inspect` utility.""" 335 336 app_title = "Inspector" 337 app_id = "inspect" 338 339 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 340 super().__init__(args, **attrs) 341 342 self._input = ptg.InputField(value="boxes.Box") 343 344 self._output = ptg.Container(box="EMPTY") 345 self._update() 346 347 self._input.bind(ptg.keys.ENTER, self._update) 348 349 self._add_widget( 350 ptg.Container( 351 self._output, 352 "", 353 ptg.Container(self._input), 354 box="EMPTY", 355 ) 356 ) 357 358 self.setup() 359 360 self.select(0) 361 362 @staticmethod 363 def obj_from_path(path: str) -> object | None: 364 """Retrieves an object from any valid import path. 365 366 An import path could be something like: 367 pytermgui.window_manager.compositor.Compositor 368 369 ...or if the library in question imports its parts within `__init__.py`-s: 370 pytermgui.Compositor 371 """ 372 373 parts = path.split(".") 374 375 if parts[0] in dir(builtins): 376 obj = getattr(builtins, parts[0]) 377 378 elif parts[0] in dir(ptg): 379 obj = getattr(ptg, parts[0]) 380 381 else: 382 try: 383 obj = importlib.import_module(".".join(parts[:-1])) 384 except (ValueError, ModuleNotFoundError) as error: 385 return ( 386 f"Could not import object at path {path!r}: {error}." 387 + " Maybe try using the --eval flag?" 388 ) 389 390 try: 391 obj = getattr(obj, parts[-1]) 392 except AttributeError: 393 return obj 394 395 return obj 396 397 def _update(self, *_) -> None: 398 """Updates output with new inspection result.""" 399 400 obj = self.obj_from_path(self._input.value) 401 402 self._output.vertical_align = ptg.VerticalAlignment.CENTER 403 self._output.set_widgets([ptg.inspect(obj)]) 404 405 def on_exit(self) -> None: 406 super().on_exit() 407 408 self._output.vertical_align = ptg.VerticalAlignment.TOP 409 for line in self._output.get_lines(): 410 print(line) 411 412 413APPLICATION_MAP = { 414 ("Getch", "getch"): GetchWindow, 415 ("Inspector", "inspect"): InspectorWindow, 416 ("ColorPicker", "color"): ColorPickerWindow, 417 ("TIM Playground", "tim"): TIMWindow, 418} 419 420 421def _app_from_short(short: str) -> Type[AppWindow]: 422 """Finds an AppWindow constructor from its short name.""" 423 424 for (_, name), app in APPLICATION_MAP.items(): 425 if name == short: 426 return app 427 428 raise KeyError(f"No app found for {short!r}") 429 430 431def process_args(argv: list[str] | None = None) -> Namespace: 432 """Processes command line arguments.""" 433 434 parser = ArgumentParser( 435 description=f"{ptg.tim.parse(_title())}'s command line environment." 436 ) 437 438 apps = [short for (_, short), _ in APPLICATION_MAP.items()] 439 440 app_group = parser.add_argument_group("Applications") 441 app_group.add_argument( 442 "--app", 443 type=str.lower, 444 help="Launch an app.", 445 metavar=f"{', '.join(app.capitalize() for app in apps)}", 446 choices=apps, 447 ) 448 449 app_group.add_argument( 450 "-g", "--getch", help="Launch the Getch app.", action="store_true" 451 ) 452 453 app_group.add_argument( 454 "-t", "--tim", help="Launch the TIM Playground app.", action="store_true" 455 ) 456 457 app_group.add_argument( 458 "-c", 459 "--color", 460 help="Launch the ColorPicker app.", 461 action="store_true", 462 ) 463 464 inspect_group = parser.add_argument_group("Inspection") 465 inspect_group.add_argument( 466 "-i", "--inspect", help="Inspect an object.", metavar="PATH_OR_CODE" 467 ) 468 inspect_group.add_argument( 469 "-e", 470 "--eval", 471 help="Evaluate the expression given to `--inspect` instead of treating it as a path.", 472 action="store_true", 473 ) 474 475 inspect_group.add_argument( 476 "--methods", help="Always show methods when inspecting.", action="store_true" 477 ) 478 inspect_group.add_argument( 479 "--dunder", 480 help="Always show __dunder__ methods when inspecting.", 481 action="store_true", 482 ) 483 inspect_group.add_argument( 484 "--private", 485 help="Always show _private methods when inspecting.", 486 action="store_true", 487 ) 488 489 util_group = parser.add_argument_group("Utilities") 490 util_group.add_argument( 491 "-s", 492 "--size", 493 help="Output the current terminal size in WxH format.", 494 action="store_true", 495 ) 496 497 util_group.add_argument( 498 "-v", 499 "--version", 500 help="Print version & system information.", 501 action="store_true", 502 ) 503 504 util_group.add_argument( 505 "--highlight", 506 help=( 507 "Highlight some python-like code syntax." 508 + " No argument or '-' will read STDIN." 509 ), 510 metavar="SYNTAX", 511 const="-", 512 nargs="?", 513 ) 514 515 util_group.add_argument( 516 "--exec", 517 help="Execute some Python code. No argument or '-' will read STDIN.", 518 const="-", 519 nargs="?", 520 ) 521 522 util_group.add_argument("-f", "--file", help="Interpret a PTG-YAML file.") 523 util_group.add_argument( 524 "--print-only", 525 help="When interpreting YAML, print the environment without running it interactively.", 526 action="store_true", 527 ) 528 529 export_group = parser.add_argument_group("Exporters") 530 531 export_group.add_argument( 532 "--export-svg", 533 help="Export the result of any non-interactive argument as an SVG file.", 534 metavar="FILE", 535 ) 536 export_group.add_argument( 537 "--export-html", 538 help="Export the result of any non-interactive argument as an HTML file.", 539 metavar="FILE", 540 ) 541 542 argv = argv or sys.argv[1:] 543 args = parser.parse_args(args=argv) 544 545 return args 546 547 548def screenshot(man: ptg.WindowManager) -> None: 549 """Opens a modal dialogue & saves a screenshot.""" 550 551 tempname = ".screenshot_temp.svg" 552 553 modal: ptg.Window 554 555 def _finish(*_: Any) -> None: 556 """Closes the modal and renames the window.""" 557 558 man.remove(modal) 559 filename = field.value or "screenshot" 560 561 if not filename.endswith(".svg"): 562 filename += ".svg" 563 564 os.rename(tempname, filename) 565 566 man.toast("[ptg.title]Screenshot saved!", "", f"[ptg.detail]{filename}") 567 568 title = sys.argv[0] 569 field = ptg.InputField(prompt="Save as: ") 570 571 man.screenshot(title=title, filename=tempname) 572 573 modal = man.alert( 574 "[ptg.title]Screenshot taken!", "", ptg.Container(field), "", ["Save!", _finish] 575 ) 576 577 578def _get_key_name(key: str) -> str: 579 """Gets canonical name of a key. 580 581 Arguments: 582 key: The key in question. 583 584 Returns: 585 The canonical-ish name of the key. 586 """ 587 588 name = ptg.keys.get_name(key) 589 if name is not None: 590 return name 591 592 return ascii(key) 593 594 595def _create_header() -> ptg.Window: 596 """Creates an application header window.""" 597 598 content = ptg.Splitter(ptg.Label("PyTermGUI", parent_align=0, padding=2)) 599 content.styles.fill = "ptg.header" 600 601 return ptg.Window(content, box="EMPTY", id="ptg.header", is_persistent=True) 602 603 604def _create_app_picker(manager: ptg.WindowManager) -> ptg.Window: 605 """Creates a dropdown that allows picking between applications.""" 606 607 existing_windows: list[ptg.Window] = [] 608 609 def _wrap(func: Callable[[ptg.Widget], Any]) -> Callable[[ptg.Widget], Any]: 610 def _inner(caller: ptg.Widget) -> None: 611 dropdown.collapse() 612 613 window: ptg.Window = func(caller) 614 if type(window) in map(type, manager): 615 return 616 617 existing_windows.append(window) 618 manager.add(window, assign="body") 619 620 body = manager.layout.body 621 622 body.content = window 623 manager.layout.apply() 624 625 return _inner 626 627 buttons = [ 628 ptg.Button(label, _wrap(lambda *_, app=app: app())) 629 for (label, _), app in APPLICATION_MAP.items() 630 ] 631 632 dropdown = ptg.Collapsible("Applications", *buttons, keyboard=True).styles( 633 fill="ptg.footer" 634 ) 635 636 return ptg.Window( 637 dropdown, 638 box="EMPTY", 639 id="ptg.header", 640 is_persistent=True, 641 overflow=ptg.Overflow.RESIZE, 642 ).styles(fill="ptg.header") 643 644 645def _create_footer(man: ptg.WindowManager) -> ptg.Window: 646 """Creates a footer based on the manager's bindings.""" 647 648 content = ptg.Splitter().styles(fill="ptg.footer") 649 for key, (callback, doc) in man.bindings.items(): 650 if doc == f"Binding of {key} to {callback}": 651 continue 652 653 content.lazy_add( 654 ptg.Button( 655 f"{_get_key_name(str(key))} - {doc}", 656 onclick=lambda *_, _callback=callback: _callback(man), 657 ) 658 ) 659 660 return ptg.Window(content, box="EMPTY", id="ptg.footer", is_persistent=True) 661 662 663def _create_layout() -> ptg.Layout: 664 """Creates the main layout.""" 665 666 layout = ptg.Layout() 667 668 layout.add_slot("Header", height=1) 669 layout.add_slot("Applications", width=20) 670 layout.add_break() 671 layout.add_slot("Body") 672 layout.add_break() 673 layout.add_slot("Footer", height=1) 674 675 return layout 676 677 678def _create_aliases() -> None: 679 """Creates all TIM alises used by the `ptg` utility. 680 681 Current aliases: 682 - ptg.title: Used for main titles. 683 - ptg.body: Used for body text. 684 - ptg.detail: Used for highlighting detail inside body text. 685 - ptg.accent: Used as an accent color in various places. 686 - ptg.header: Used for the header bar. 687 - ptg.footer: Used for the footer bar. 688 - ptg.border: Used for focused window borders & corners. 689 - ptg.border_blurred: Used for non-focused window borders & corners. 690 """ 691 692 ptg.tim.alias("ptg.title", "210 bold") 693 ptg.tim.alias("ptg.brand_title", "!gradient(210) bold") 694 ptg.tim.alias("ptg.body", "247") 695 ptg.tim.alias("ptg.detail", "dim") 696 ptg.tim.alias("ptg.accent", "72") 697 698 ptg.tim.alias("ptg.header", "@235 242 bold") 699 ptg.tim.alias("ptg.footer", "@235") 700 701 ptg.tim.alias("ptg.border", "60") 702 ptg.tim.alias("ptg.border_blurred", "#373748") 703 704 705def _configure_widgets() -> None: 706 """Configures default widget attributes.""" 707 708 ptg.boxes.Box([" ", " x ", " "]).set_chars_of(ptg.Window) 709 ptg.boxes.SINGLE.set_chars_of(ptg.Container) 710 ptg.boxes.DOUBLE.set_chars_of(ptg.Window) 711 712 ptg.InputField.styles.cursor = "inverse ptg.accent" 713 ptg.InputField.styles.fill = "245" 714 ptg.Container.styles.border__corner = "ptg.border" 715 ptg.Splitter.set_char("separator", "") 716 ptg.Button.set_char("delimiter", [" ", " "]) 717 718 ptg.Window.styles.border__corner = "ptg.border" 719 ptg.Window.set_focus_styles( 720 focused=("ptg.border", "ptg.border"), 721 blurred=("ptg.border_blurred", "ptg.border_blurred"), 722 ) 723 724 725def run_environment(args: Namespace) -> None: 726 """Runs the WindowManager environment. 727 728 Args: 729 args: An argparse namespace containing relevant arguments. 730 """ 731 732 def _find_focused(manager: ptg.WindowManager) -> ptg.Window | None: 733 if manager.focused is None: 734 return None 735 736 # Find foremost non-persistent window 737 for window in manager: 738 if window.is_persistent: 739 continue 740 741 return window 742 743 return None 744 745 def _toggle_attachment(manager: ptg.WindowManager) -> None: 746 focused = _find_focused(manager) 747 748 if focused is None: 749 return 750 751 slot = manager.layout.body 752 if slot.content is None: 753 slot.content = focused 754 else: 755 slot.detach_content() 756 757 manager.layout.apply() 758 759 def _close_focused(manager: ptg.WindowManager) -> None: 760 focused = _find_focused(manager) 761 762 if focused is None: 763 return 764 765 focused.close() 766 767 _configure_widgets() 768 769 window: AppWindow | None = None 770 with ptg.WindowManager() as manager: 771 app_picker = _create_app_picker(manager) 772 773 manager.bind( 774 ptg.keys.CTRL_W, 775 lambda *_: _close_focused(manager), 776 "Close window", 777 ) 778 manager.bind( 779 ptg.keys.F12, 780 lambda *_: screenshot(manager), 781 "Screenshot", 782 ) 783 manager.bind( 784 ptg.keys.CTRL_F, 785 lambda *_: _toggle_attachment(manager), 786 "Toggle layout", 787 ) 788 789 manager.bind( 790 ptg.keys.CTRL_A, 791 lambda *_: { 792 manager.focus(app_picker), # type: ignore 793 app_picker.execute_binding(ptg.keys.CTRL_A), 794 }, 795 ) 796 manager.bind( 797 ptg.keys.ALT + ptg.keys.TAB, 798 lambda *_: manager.focus_next(), 799 ) 800 801 if not args.app: 802 manager.layout = _create_layout() 803 804 manager.add(_create_header(), assign="header") 805 manager.add(app_picker, assign="applications") 806 manager.add(_create_footer(manager), assign="footer") 807 808 manager.toast( 809 "[ptg.title]Welcome to the [ptg.brand_title]" 810 + "PyTermGUI[/ptg.brand_title ptg.title] CLI!", 811 offset=ptg.terminal.height // 2 - 3, 812 delay=700, 813 ) 814 815 else: 816 manager.layout.add_slot("Body") 817 818 app = _app_from_short(args.app) 819 window = app(args) 820 manager.add(window, assign="body") 821 822 window = window or manager.focused # type: ignore 823 if window is None or not isinstance(window, AppWindow): 824 return 825 826 window.on_exit() 827 828 829def _print_version() -> None: 830 """Prints version info.""" 831 832 def _print_aligned(left: str, right: str | None) -> None: 833 left += ":" 834 835 ptg.tim.print(f"[ptg.detail]{left:<19} [/ptg.detail 157]{right}") 836 837 ptg.tim.print( 838 f"[bold !gradient(210)]PyTermGUI[/ /!gradient] version [157]{ptg.__version__}" 839 ) 840 ptg.tim.print() 841 ptg.tim.print("[ptg.title]System details:") 842 843 _print_aligned(" Python version", sys.version.split()[0]) 844 _print_aligned(" $TERM", os.getenv("TERM")) 845 _print_aligned(" $COLORTERM", os.getenv("COLORTERM")) 846 _print_aligned(" Color support", str(ptg.terminal.colorsystem)) 847 _print_aligned(" OS Platform", platform()) 848 849 850def _run_inspect(args: Namespace) -> None: 851 """Inspects something in the CLI.""" 852 853 args.methods = args.methods or None 854 args.dunder = args.dunder or None 855 args.private = args.private or None 856 857 target = ( 858 eval(args.inspect) # pylint: disable=eval-used 859 if args.eval 860 else InspectorWindow.obj_from_path(args.inspect) 861 ) 862 863 if not args.eval and isinstance(target, str): 864 args.methods = False 865 866 inspector = ptg.inspect( 867 target, 868 show_methods=args.methods, 869 show_private=args.private, 870 show_dunder=args.dunder, 871 ) 872 873 ptg.terminal.print(inspector) 874 875 876def _interpret_file(args: Namespace) -> None: 877 """Interprets a PTG-YAML file.""" 878 879 with ptg.YamlLoader() as loader, open(args.file, "r", encoding="utf-8") as file: 880 namespace = loader.load(file) 881 882 if not args.print_only: 883 with ptg.WindowManager() as manager: 884 for widget in namespace.widgets.values(): 885 if not isinstance(widget, ptg.Window): 886 continue 887 888 manager.add(widget) 889 return 890 891 for widget in namespace.widgets.values(): 892 for line in widget.get_lines(): 893 ptg.terminal.print(line) 894 895 896def main(argv: list[str] | None = None) -> None: 897 """Runs the program. 898 899 Args: 900 argv: A list of arguments, not included the 0th element pointing to the 901 executable path. 902 """ 903 904 _create_aliases() 905 906 args = process_args(argv) 907 908 args.app = args.app or ( 909 "getch" 910 if args.getch 911 else ("tim" if args.tim else ("color" if args.color else None)) 912 ) 913 914 if args.app or len(sys.argv) == 1: 915 run_environment(args) 916 return 917 918 with ptg.terminal.record() as recording: 919 if args.size: 920 ptg.tim.print(f"{ptg.terminal.width}x{ptg.terminal.height}") 921 922 elif args.version: 923 _print_version() 924 925 elif args.inspect: 926 _run_inspect(args) 927 928 elif args.exec: 929 args.exec = sys.stdin.read() if args.exec == "-" else args.exec 930 931 for name in dir(ptg): 932 obj = getattr(ptg, name, None) 933 globals()[name] = obj 934 935 globals()["print"] = ptg.terminal.print 936 937 exec(args.exec, locals(), globals()) # pylint: disable=exec-used 938 939 elif args.highlight: 940 text = sys.stdin.read() if args.highlight == "-" else args.highlight 941 942 ptg.tim.print(ptg.highlight_python(text)) 943 944 elif args.file: 945 _interpret_file(args) 946 947 if args.export_svg: 948 recording.save_svg(args.export_svg) 949 950 elif args.export_html: 951 recording.save_html(args.export_html) 952 953 954if __name__ == "__main__": 955 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}" # .replace("'", "") 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.markup.style_maps.STYLES] 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"[{tag}]{value}") 322 for tag, value in ptg.tim.aliases.items() 323 if not tag.startswith("/") 324 ) 325 326 return ptg.Container(tag_container, user_container, box="EMPTY") 327 328 def on_exit(self) -> None: 329 super().on_exit() 330 ptg.tim.print(ptg.highlight_tim(self._input.value)) 331 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.
328 def on_exit(self) -> None: 329 super().on_exit() 330 ptg.tim.print(ptg.highlight_tim(self._input.value)) 331 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
334class InspectorWindow(AppWindow): 335 """A window for the `inspect` utility.""" 336 337 app_title = "Inspector" 338 app_id = "inspect" 339 340 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 341 super().__init__(args, **attrs) 342 343 self._input = ptg.InputField(value="boxes.Box") 344 345 self._output = ptg.Container(box="EMPTY") 346 self._update() 347 348 self._input.bind(ptg.keys.ENTER, self._update) 349 350 self._add_widget( 351 ptg.Container( 352 self._output, 353 "", 354 ptg.Container(self._input), 355 box="EMPTY", 356 ) 357 ) 358 359 self.setup() 360 361 self.select(0) 362 363 @staticmethod 364 def obj_from_path(path: str) -> object | None: 365 """Retrieves an object from any valid import path. 366 367 An import path could be something like: 368 pytermgui.window_manager.compositor.Compositor 369 370 ...or if the library in question imports its parts within `__init__.py`-s: 371 pytermgui.Compositor 372 """ 373 374 parts = path.split(".") 375 376 if parts[0] in dir(builtins): 377 obj = getattr(builtins, parts[0]) 378 379 elif parts[0] in dir(ptg): 380 obj = getattr(ptg, parts[0]) 381 382 else: 383 try: 384 obj = importlib.import_module(".".join(parts[:-1])) 385 except (ValueError, ModuleNotFoundError) as error: 386 return ( 387 f"Could not import object at path {path!r}: {error}." 388 + " Maybe try using the --eval flag?" 389 ) 390 391 try: 392 obj = getattr(obj, parts[-1]) 393 except AttributeError: 394 return obj 395 396 return obj 397 398 def _update(self, *_) -> None: 399 """Updates output with new inspection result.""" 400 401 obj = self.obj_from_path(self._input.value) 402 403 self._output.vertical_align = ptg.VerticalAlignment.CENTER 404 self._output.set_widgets([ptg.inspect(obj)]) 405 406 def on_exit(self) -> None: 407 super().on_exit() 408 409 self._output.vertical_align = ptg.VerticalAlignment.TOP 410 for line in self._output.get_lines(): 411 print(line)
A window for the inspect
utility.
340 def __init__(self, args: Namespace | None = None, **attrs: Any) -> None: 341 super().__init__(args, **attrs) 342 343 self._input = ptg.InputField(value="boxes.Box") 344 345 self._output = ptg.Container(box="EMPTY") 346 self._update() 347 348 self._input.bind(ptg.keys.ENTER, self._update) 349 350 self._add_widget( 351 ptg.Container( 352 self._output, 353 "", 354 ptg.Container(self._input), 355 box="EMPTY", 356 ) 357 ) 358 359 self.setup() 360 361 self.select(0)
Initializes object.
Args
- widgets: Widgets to add to this window after initilization.
- attrs: Attributes that are passed to the constructor.
363 @staticmethod 364 def obj_from_path(path: str) -> object | None: 365 """Retrieves an object from any valid import path. 366 367 An import path could be something like: 368 pytermgui.window_manager.compositor.Compositor 369 370 ...or if the library in question imports its parts within `__init__.py`-s: 371 pytermgui.Compositor 372 """ 373 374 parts = path.split(".") 375 376 if parts[0] in dir(builtins): 377 obj = getattr(builtins, parts[0]) 378 379 elif parts[0] in dir(ptg): 380 obj = getattr(ptg, parts[0]) 381 382 else: 383 try: 384 obj = importlib.import_module(".".join(parts[:-1])) 385 except (ValueError, ModuleNotFoundError) as error: 386 return ( 387 f"Could not import object at path {path!r}: {error}." 388 + " Maybe try using the --eval flag?" 389 ) 390 391 try: 392 obj = getattr(obj, parts[-1]) 393 except AttributeError: 394 return obj 395 396 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
406 def on_exit(self) -> None: 407 super().on_exit() 408 409 self._output.vertical_align = ptg.VerticalAlignment.TOP 410 for line in self._output.get_lines(): 411 print(line)
Called on application exit.
Should be used to print current application state to the user's shell.
Inherited Members
432def process_args(argv: list[str] | None = None) -> Namespace: 433 """Processes command line arguments.""" 434 435 parser = ArgumentParser( 436 description=f"{ptg.tim.parse(_title())}'s command line environment." 437 ) 438 439 apps = [short for (_, short), _ in APPLICATION_MAP.items()] 440 441 app_group = parser.add_argument_group("Applications") 442 app_group.add_argument( 443 "--app", 444 type=str.lower, 445 help="Launch an app.", 446 metavar=f"{', '.join(app.capitalize() for app in apps)}", 447 choices=apps, 448 ) 449 450 app_group.add_argument( 451 "-g", "--getch", help="Launch the Getch app.", action="store_true" 452 ) 453 454 app_group.add_argument( 455 "-t", "--tim", help="Launch the TIM Playground app.", action="store_true" 456 ) 457 458 app_group.add_argument( 459 "-c", 460 "--color", 461 help="Launch the ColorPicker app.", 462 action="store_true", 463 ) 464 465 inspect_group = parser.add_argument_group("Inspection") 466 inspect_group.add_argument( 467 "-i", "--inspect", help="Inspect an object.", metavar="PATH_OR_CODE" 468 ) 469 inspect_group.add_argument( 470 "-e", 471 "--eval", 472 help="Evaluate the expression given to `--inspect` instead of treating it as a path.", 473 action="store_true", 474 ) 475 476 inspect_group.add_argument( 477 "--methods", help="Always show methods when inspecting.", action="store_true" 478 ) 479 inspect_group.add_argument( 480 "--dunder", 481 help="Always show __dunder__ methods when inspecting.", 482 action="store_true", 483 ) 484 inspect_group.add_argument( 485 "--private", 486 help="Always show _private methods when inspecting.", 487 action="store_true", 488 ) 489 490 util_group = parser.add_argument_group("Utilities") 491 util_group.add_argument( 492 "-s", 493 "--size", 494 help="Output the current terminal size in WxH format.", 495 action="store_true", 496 ) 497 498 util_group.add_argument( 499 "-v", 500 "--version", 501 help="Print version & system information.", 502 action="store_true", 503 ) 504 505 util_group.add_argument( 506 "--highlight", 507 help=( 508 "Highlight some python-like code syntax." 509 + " No argument or '-' will read STDIN." 510 ), 511 metavar="SYNTAX", 512 const="-", 513 nargs="?", 514 ) 515 516 util_group.add_argument( 517 "--exec", 518 help="Execute some Python code. No argument or '-' will read STDIN.", 519 const="-", 520 nargs="?", 521 ) 522 523 util_group.add_argument("-f", "--file", help="Interpret a PTG-YAML file.") 524 util_group.add_argument( 525 "--print-only", 526 help="When interpreting YAML, print the environment without running it interactively.", 527 action="store_true", 528 ) 529 530 export_group = parser.add_argument_group("Exporters") 531 532 export_group.add_argument( 533 "--export-svg", 534 help="Export the result of any non-interactive argument as an SVG file.", 535 metavar="FILE", 536 ) 537 export_group.add_argument( 538 "--export-html", 539 help="Export the result of any non-interactive argument as an HTML file.", 540 metavar="FILE", 541 ) 542 543 argv = argv or sys.argv[1:] 544 args = parser.parse_args(args=argv) 545 546 return args
Processes command line arguments.
549def screenshot(man: ptg.WindowManager) -> None: 550 """Opens a modal dialogue & saves a screenshot.""" 551 552 tempname = ".screenshot_temp.svg" 553 554 modal: ptg.Window 555 556 def _finish(*_: Any) -> None: 557 """Closes the modal and renames the window.""" 558 559 man.remove(modal) 560 filename = field.value or "screenshot" 561 562 if not filename.endswith(".svg"): 563 filename += ".svg" 564 565 os.rename(tempname, filename) 566 567 man.toast("[ptg.title]Screenshot saved!", "", f"[ptg.detail]{filename}") 568 569 title = sys.argv[0] 570 field = ptg.InputField(prompt="Save as: ") 571 572 man.screenshot(title=title, filename=tempname) 573 574 modal = man.alert( 575 "[ptg.title]Screenshot taken!", "", ptg.Container(field), "", ["Save!", _finish] 576 )
Opens a modal dialogue & saves a screenshot.
726def run_environment(args: Namespace) -> None: 727 """Runs the WindowManager environment. 728 729 Args: 730 args: An argparse namespace containing relevant arguments. 731 """ 732 733 def _find_focused(manager: ptg.WindowManager) -> ptg.Window | None: 734 if manager.focused is None: 735 return None 736 737 # Find foremost non-persistent window 738 for window in manager: 739 if window.is_persistent: 740 continue 741 742 return window 743 744 return None 745 746 def _toggle_attachment(manager: ptg.WindowManager) -> None: 747 focused = _find_focused(manager) 748 749 if focused is None: 750 return 751 752 slot = manager.layout.body 753 if slot.content is None: 754 slot.content = focused 755 else: 756 slot.detach_content() 757 758 manager.layout.apply() 759 760 def _close_focused(manager: ptg.WindowManager) -> None: 761 focused = _find_focused(manager) 762 763 if focused is None: 764 return 765 766 focused.close() 767 768 _configure_widgets() 769 770 window: AppWindow | None = None 771 with ptg.WindowManager() as manager: 772 app_picker = _create_app_picker(manager) 773 774 manager.bind( 775 ptg.keys.CTRL_W, 776 lambda *_: _close_focused(manager), 777 "Close window", 778 ) 779 manager.bind( 780 ptg.keys.F12, 781 lambda *_: screenshot(manager), 782 "Screenshot", 783 ) 784 manager.bind( 785 ptg.keys.CTRL_F, 786 lambda *_: _toggle_attachment(manager), 787 "Toggle layout", 788 ) 789 790 manager.bind( 791 ptg.keys.CTRL_A, 792 lambda *_: { 793 manager.focus(app_picker), # type: ignore 794 app_picker.execute_binding(ptg.keys.CTRL_A), 795 }, 796 ) 797 manager.bind( 798 ptg.keys.ALT + ptg.keys.TAB, 799 lambda *_: manager.focus_next(), 800 ) 801 802 if not args.app: 803 manager.layout = _create_layout() 804 805 manager.add(_create_header(), assign="header") 806 manager.add(app_picker, assign="applications") 807 manager.add(_create_footer(manager), assign="footer") 808 809 manager.toast( 810 "[ptg.title]Welcome to the [ptg.brand_title]" 811 + "PyTermGUI[/ptg.brand_title ptg.title] CLI!", 812 offset=ptg.terminal.height // 2 - 3, 813 delay=700, 814 ) 815 816 else: 817 manager.layout.add_slot("Body") 818 819 app = _app_from_short(args.app) 820 window = app(args) 821 manager.add(window, assign="body") 822 823 window = window or manager.focused # type: ignore 824 if window is None or not isinstance(window, AppWindow): 825 return 826 827 window.on_exit()
Runs the WindowManager environment.
Args
- args: An argparse namespace containing relevant arguments.
897def main(argv: list[str] | None = None) -> None: 898 """Runs the program. 899 900 Args: 901 argv: A list of arguments, not included the 0th element pointing to the 902 executable path. 903 """ 904 905 _create_aliases() 906 907 args = process_args(argv) 908 909 args.app = args.app or ( 910 "getch" 911 if args.getch 912 else ("tim" if args.tim else ("color" if args.color else None)) 913 ) 914 915 if args.app or len(sys.argv) == 1: 916 run_environment(args) 917 return 918 919 with ptg.terminal.record() as recording: 920 if args.size: 921 ptg.tim.print(f"{ptg.terminal.width}x{ptg.terminal.height}") 922 923 elif args.version: 924 _print_version() 925 926 elif args.inspect: 927 _run_inspect(args) 928 929 elif args.exec: 930 args.exec = sys.stdin.read() if args.exec == "-" else args.exec 931 932 for name in dir(ptg): 933 obj = getattr(ptg, name, None) 934 globals()[name] = obj 935 936 globals()["print"] = ptg.terminal.print 937 938 exec(args.exec, locals(), globals()) # pylint: disable=exec-used 939 940 elif args.highlight: 941 text = sys.stdin.read() if args.highlight == "-" else args.highlight 942 943 ptg.tim.print(ptg.highlight_python(text)) 944 945 elif args.file: 946 _interpret_file(args) 947 948 if args.export_svg: 949 recording.save_svg(args.export_svg) 950 951 elif args.export_html: 952 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.