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.

#   AppWindow(args: argparse.Namespace | None = None, **attrs: Any)
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.
#   app_title: str

The display title of the application.

#   app_id: str

The short identifier used by ArgumentParser.

#   standalone: bool

Whether this app was launched directly from the CLI.

#   overflow = <Overflow.SCROLL: 1>
#   vertical_align = <VerticalAlignment.TOP: 0>
#   def setup(self) -> None:
View Source
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)

Centers window, sets its width & height.

#   def on_exit(self) -> None:
View Source
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()

Called on application exit.

Should be used to print current application state to the user's shell.

#   class GetchWindow(AppWindow):
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.

#   GetchWindow(args: argparse.Namespace | None = None, **attrs: Any)
View Source
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()

Initializes object.

Args
  • widgets: Widgets to add to this window after initilization.
  • attrs: Attributes that are passed to the constructor.
#   app_title: str = 'Getch'

The display title of the application.

#   app_id: str = 'getch'

The short identifier used by ArgumentParser.

#   def on_exit(self) -> None:
View Source
127    def on_exit(self) -> None:
128        super().on_exit()
129
130        for line in self._content.get_lines():
131            print(line)

Called on application exit.

Should be used to print current application state to the user's shell.

#   class ColorPickerWindow(AppWindow):
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.

#   ColorPickerWindow(args: argparse.Namespace | None = None, **attrs: Any)
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.
#   app_title: str = 'ColorPicker'

The display title of the application.

#   app_id: str = 'color'

The short identifier used by ArgumentParser.

#   def on_exit(self) -> None:
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.

#   class TIMWindow(AppWindow):
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.

#   TIMWindow(args: argparse.Namespace | None = None, **attrs: Any)
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.
#   app_title: str = 'TIM Playground'

The display title of the application.

#   app_id: str = 'tim'

The short identifier used by ArgumentParser.

#   def on_exit(self) -> None:
View Source
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)

Called on application exit.

Should be used to print current application state to the user's shell.

#   class InspectorWindow(AppWindow):
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.

#   InspectorWindow(args: argparse.Namespace | None = None, **attrs: Any)
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.
#   app_title: str = 'Inspector'

The display title of the application.

#   app_id: str = 'inspect'

The short identifier used by ArgumentParser.

#  
@staticmethod
def obj_from_path(path: str) -> object | None:
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

pytermgui.window_manager.compositor.Compositor

...or if the library in question imports its parts within __init__.py-s: pytermgui.Compositor

#   def on_exit(self) -> None:
View Source
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)

Called on application exit.

Should be used to print current application state to the user's shell.

#   def process_args(argv: list[str] | None = None) -> argparse.Namespace:
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.

#   def run_environment(args: argparse.Namespace) -> None:
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.
#   def main(argv: list[str] | None = None) -> None:
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.