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