pytermgui.cmd

The command-line module of the library.

See ptg --help for more information.

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

A generic application window.

It contains a header with the app's title, as well as some global settings.

AppWindow(args: argparse.Namespace | None = None, **attrs: Any)
47    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
48        super().__init__(**attrs)
49
50        self.standalone = bool(getattr(args, self.app_id, None))
51
52        bottom = ptg.Container.chars["border"][-1]
53        header_box = ptg.boxes.Box(
54            [
55                "",
56                " x ",
57                bottom * 3,
58            ]
59        )
60
61        self._add_widget(ptg.Container(f"[ptg.title]{self.app_title}", box=header_box))
62        self._add_widget("")

Initializes object.

Args
  • widgets: Widgets to add to this window after initilization.
  • attrs: Attributes that are passed to the constructor.
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:
64    def setup(self) -> None:
65        """Centers window, sets its width & height."""
66
67        self.width = int(self.terminal.width * 2 / 3)
68        self.height = int(self.terminal.height * 2 / 3)
69        self.center(store=False)

Centers window, sets its width & height.

def on_exit(self) -> None:
71    def on_exit(self) -> None:
72        """Called on application exit.
73
74        Should be used to print current application state to the user's shell.
75        """
76
77        ptg.tim.print(f"{_title()} - [dim]{self.app_title}")
78        print()

Called on application exit.

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

class GetchWindow(AppWindow):
 81class GetchWindow(AppWindow):
 82    """A window for the Getch utility."""
 83
 84    app_title = "Getch"
 85    app_id = "getch"
 86
 87    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
 88        super().__init__(args, **attrs)
 89
 90        self.bind(ptg.keys.ANY_KEY, self._update)
 91
 92        self._content = ptg.Container("Press any key...", static_width=50)
 93        self._add_widget(self._content)
 94
 95        self.setup()
 96
 97    def _update(self, _: ptg.Widget, key: str) -> None:
 98        """Updates window contents on keypress."""
 99
100        self._content.set_widgets([])
101        name = _get_key_name(key)
102
103        if name != ascii(key):
104            name = f"keys.{name}"
105
106        style = ptg.HighlighterStyle(ptg.highlight_python)
107
108        items = [
109            "[ptg.title]Your output",
110            "",
111            {"[ptg.detail]key": ptg.Label(name, style=style)},
112            {"[ptg.detail]value:": ptg.Label(ascii(key), style=style)},
113            {"[ptg.detail]len()": ptg.Label(str(len(key)), style=style)},
114            {
115                "[ptg.detail]real_length()": ptg.Label(
116                    str(ptg.real_length(key)), style=style
117                )
118            },
119        ]
120
121        for item in items:
122            self._content += item
123
124        if self.standalone:
125            assert self.manager is not None
126            self.manager.stop()
127
128    def on_exit(self) -> None:
129        super().on_exit()
130
131        for line in self._content.get_lines():
132            print(line)

A window for the Getch utility.

GetchWindow(args: argparse.Namespace | None = None, **attrs: Any)
87    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
88        super().__init__(args, **attrs)
89
90        self.bind(ptg.keys.ANY_KEY, self._update)
91
92        self._content = ptg.Container("Press any key...", static_width=50)
93        self._add_widget(self._content)
94
95        self.setup()

Initializes object.

Args
  • widgets: Widgets to add to this window after initilization.
  • attrs: Attributes that are passed to the constructor.
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:
128    def on_exit(self) -> None:
129        super().on_exit()
130
131        for line in self._content.get_lines():
132            print(line)

Called on application exit.

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

class ColorPickerWindow(AppWindow):
135class ColorPickerWindow(AppWindow):
136    """A window to pick colors from the xterm-256 palette."""
137
138    app_title = "ColorPicker"
139    app_id = "color"
140
141    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
142        super().__init__(args, **attrs)
143
144        self._chosen_rgb = ptg.str_to_color("black")
145
146        self._colorpicker = ptg.ColorPicker()
147        self._add_widget(ptg.Collapsible("xterm-256", "", self._colorpicker).expand())
148        self._add_widget("")
149        self._add_widget(
150            ptg.Collapsible(
151                "RGB & HEX", "", self._create_rgb_picker(), static_width=81
152            ).expand(),
153        )
154
155        self.setup()
156
157    def _create_rgb_picker(self) -> ptg.Container:
158        """Creates the RGB picker 'widget'."""
159
160        root = ptg.Container(static_width=72)
161
162        matrix = ptg.DensePixelMatrix(68, 20)
163        hexdisplay = ptg.Label()
164        rgbdisplay = ptg.Label()
165
166        sliders = [ptg.Slider() for _ in range(3)]
167
168        def _get_rgb() -> tuple[int, int, int]:
169            """Computes the RGB value from the 3 sliders."""
170
171            values = [int(255 * slider.value) for slider in sliders]
172
173            return values[0], values[1], values[2]
174
175        def _update(*_) -> None:
176            """Updates the matrix & displays with the current color."""
177
178            color = self._chosen_rgb = ptg.RGBColor.from_rgb(_get_rgb())
179            for row in range(matrix.rows):
180                for col in range(matrix.columns):
181                    matrix[row, col] = color.hex
182
183            hexdisplay.value = f"[ptg.body]{color.hex}"
184            rgbdisplay.value = f"[ptg.body]rgb({', '.join(map(str, color.rgb))})"
185            matrix.build()
186
187        red, green, blue = sliders
188
189        # red.styles.filled_selected__cursor = "red"
190        # green.styles.filled_selected__cursor = "green"
191        # blue.styles.filled_selected__cursor = "blue"
192
193        for slider in sliders:
194            slider.onchange = _update
195
196        root += hexdisplay
197        root += rgbdisplay
198        root += ""
199
200        root += matrix
201        root += ""
202
203        root += red
204        root += green
205        root += blue
206
207        _update()
208        return root
209
210    def on_exit(self) -> None:
211        super().on_exit()
212
213        color = self._chosen_rgb
214        eightbit = " ".join(
215            button.get_lines()[0] for button in self._colorpicker.chosen
216        )
217
218        ptg.tim.print("[ptg.title]Your colors:")
219        ptg.tim.print(f"    [{color.hex}]{color.rgb}[/] // [{color.hex}]{color.hex}")
220        ptg.tim.print()
221        ptg.tim.print(f"    {eightbit}")

A window to pick colors from the xterm-256 palette.

ColorPickerWindow(args: argparse.Namespace | None = None, **attrs: Any)
141    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
142        super().__init__(args, **attrs)
143
144        self._chosen_rgb = ptg.str_to_color("black")
145
146        self._colorpicker = ptg.ColorPicker()
147        self._add_widget(ptg.Collapsible("xterm-256", "", self._colorpicker).expand())
148        self._add_widget("")
149        self._add_widget(
150            ptg.Collapsible(
151                "RGB & HEX", "", self._create_rgb_picker(), static_width=81
152            ).expand(),
153        )
154
155        self.setup()

Initializes object.

Args
  • widgets: Widgets to add to this window after initilization.
  • attrs: Attributes that are passed to the constructor.
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:
210    def on_exit(self) -> None:
211        super().on_exit()
212
213        color = self._chosen_rgb
214        eightbit = " ".join(
215            button.get_lines()[0] for button in self._colorpicker.chosen
216        )
217
218        ptg.tim.print("[ptg.title]Your colors:")
219        ptg.tim.print(f"    [{color.hex}]{color.rgb}[/] // [{color.hex}]{color.hex}")
220        ptg.tim.print()
221        ptg.tim.print(f"    {eightbit}")

Called on application exit.

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

class TIMWindow(AppWindow):
224class TIMWindow(AppWindow):
225    """An application to play around with TIM."""
226
227    app_title = "TIM Playground"
228    app_id = "tim"
229
230    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
231        super().__init__(args, **attrs)
232
233        if self.standalone:
234            self.bind(
235                ptg.keys.RETURN,
236                lambda *_: self.manager.stop() if self.manager is not None else None,
237            )
238
239        self._generate_colors()
240
241        self._output = ptg.Label(parent_align=0)
242
243        self._input = ptg.InputField()
244        self._input.styles.value__fill = lambda _, item: item
245
246        self._showcase = self._create_showcase()
247
248        self._input.bind(ptg.keys.ANY_KEY, lambda *_: self._update_output())
249
250        self._add_widget(
251            ptg.Container(
252                ptg.Container(self._output),
253                self._showcase,
254                ptg.Container(self._input),
255                box="EMPTY",
256                static_width=60,
257            )
258        )
259
260        self.bind(ptg.keys.CTRL_R, self._generate_colors)
261
262        self.setup()
263
264        self.select(0)
265
266    @staticmethod
267    def _random_rgb() -> ptg.Color:
268        """Returns a random Color."""
269
270        rgb = tuple(random.randint(0, 255) for _ in range(3))
271
272        return ptg.RGBColor.from_rgb(rgb)  # type: ignore
273
274    def _update_output(self) -> None:
275        """Updates the output field."""
276
277        self._output.value = self._input.value
278
279    def _generate_colors(self, *_) -> None:
280        """Generates self._example_{255,rgb,hex}."""
281
282        ptg.tim.alias("ptg.timwindow.255", str(random.randint(16, 233)))
283        ptg.tim.alias("ptg.timwindow.rgb", ";".join(map(str, self._random_rgb().rgb)))
284        ptg.tim.alias("ptg.timwindow.hex", self._random_rgb().hex)
285
286    @staticmethod
287    def _create_showcase() -> ptg.Container:
288        """Creates the showcase container."""
289
290        def _show_style(name: str) -> str:
291            return f"[{name}]{name}"  # .replace("'", "")
292
293        def _create_table(source: Iterable[tuple[str, str]]) -> ptg.Container:
294            root = ptg.Container()
295
296            for left, right in source:
297                row = ptg.Splitter(
298                    ptg.Label(left, parent_align=0), ptg.Label(right, parent_align=2)
299                ).styles(separator="ptg.border")
300
301                row.set_char("separator", f" {ptg.Container.chars['border'][0]}")
302
303                root += row
304
305            return root
306
307        prefix = "ptg.timwindow"
308        tags = [_show_style(style) for style in ptg.markup.style_maps.STYLES]
309        colors = [
310            f"[[{prefix}.255]0-255[/]]",
311            f"[[{prefix}.hex]#RRGGBB[/]]",
312            f"[[{prefix}.rgb]RRR;GGG;BBB[/]]",
313            "",
314            f"[[inverse {prefix}.255]@0-255[/]]",
315            f"[[inverse {prefix}.hex]@#RRGGBB[/]]",
316            f"[[inverse {prefix}.rgb]@RRR;GGG;BBB[/]]",
317        ]
318
319        tag_container = _create_table(zip_longest(tags, colors, fillvalue=""))
320        user_container = _create_table(
321            (_show_style(tag), f"[{tag}]{value}")
322            for tag, value in ptg.tim.aliases.items()
323            if not tag.startswith("/")
324        )
325
326        return ptg.Container(tag_container, user_container, box="EMPTY")
327
328    def on_exit(self) -> None:
329        super().on_exit()
330        ptg.tim.print(ptg.highlight_tim(self._input.value))
331        ptg.tim.print(self._input.value)

An application to play around with TIM.

TIMWindow(args: argparse.Namespace | None = None, **attrs: Any)
230    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
231        super().__init__(args, **attrs)
232
233        if self.standalone:
234            self.bind(
235                ptg.keys.RETURN,
236                lambda *_: self.manager.stop() if self.manager is not None else None,
237            )
238
239        self._generate_colors()
240
241        self._output = ptg.Label(parent_align=0)
242
243        self._input = ptg.InputField()
244        self._input.styles.value__fill = lambda _, item: item
245
246        self._showcase = self._create_showcase()
247
248        self._input.bind(ptg.keys.ANY_KEY, lambda *_: self._update_output())
249
250        self._add_widget(
251            ptg.Container(
252                ptg.Container(self._output),
253                self._showcase,
254                ptg.Container(self._input),
255                box="EMPTY",
256                static_width=60,
257            )
258        )
259
260        self.bind(ptg.keys.CTRL_R, self._generate_colors)
261
262        self.setup()
263
264        self.select(0)

Initializes object.

Args
  • widgets: Widgets to add to this window after initilization.
  • attrs: Attributes that are passed to the constructor.
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:
328    def on_exit(self) -> None:
329        super().on_exit()
330        ptg.tim.print(ptg.highlight_tim(self._input.value))
331        ptg.tim.print(self._input.value)

Called on application exit.

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

class InspectorWindow(AppWindow):
334class InspectorWindow(AppWindow):
335    """A window for the `inspect` utility."""
336
337    app_title = "Inspector"
338    app_id = "inspect"
339
340    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
341        super().__init__(args, **attrs)
342
343        self._input = ptg.InputField(value="boxes.Box")
344
345        self._output = ptg.Container(box="EMPTY")
346        self._update()
347
348        self._input.bind(ptg.keys.ENTER, self._update)
349
350        self._add_widget(
351            ptg.Container(
352                self._output,
353                "",
354                ptg.Container(self._input),
355                box="EMPTY",
356            )
357        )
358
359        self.setup()
360
361        self.select(0)
362
363    @staticmethod
364    def obj_from_path(path: str) -> object | None:
365        """Retrieves an object from any valid import path.
366
367        An import path could be something like:
368            pytermgui.window_manager.compositor.Compositor
369
370        ...or if the library in question imports its parts within `__init__.py`-s:
371            pytermgui.Compositor
372        """
373
374        parts = path.split(".")
375
376        if parts[0] in dir(builtins):
377            obj = getattr(builtins, parts[0])
378
379        elif parts[0] in dir(ptg):
380            obj = getattr(ptg, parts[0])
381
382        else:
383            try:
384                obj = importlib.import_module(".".join(parts[:-1]))
385            except (ValueError, ModuleNotFoundError) as error:
386                return (
387                    f"Could not import object at path {path!r}: {error}."
388                    + " Maybe try using the --eval flag?"
389                )
390
391        try:
392            obj = getattr(obj, parts[-1])
393        except AttributeError:
394            return obj
395
396        return obj
397
398    def _update(self, *_) -> None:
399        """Updates output with new inspection result."""
400
401        obj = self.obj_from_path(self._input.value)
402
403        self._output.vertical_align = ptg.VerticalAlignment.CENTER
404        self._output.set_widgets([ptg.inspect(obj)])
405
406    def on_exit(self) -> None:
407        super().on_exit()
408
409        self._output.vertical_align = ptg.VerticalAlignment.TOP
410        for line in self._output.get_lines():
411            print(line)

A window for the inspect utility.

InspectorWindow(args: argparse.Namespace | None = None, **attrs: Any)
340    def __init__(self, args: Namespace | None = None, **attrs: Any) -> None:
341        super().__init__(args, **attrs)
342
343        self._input = ptg.InputField(value="boxes.Box")
344
345        self._output = ptg.Container(box="EMPTY")
346        self._update()
347
348        self._input.bind(ptg.keys.ENTER, self._update)
349
350        self._add_widget(
351            ptg.Container(
352                self._output,
353                "",
354                ptg.Container(self._input),
355                box="EMPTY",
356            )
357        )
358
359        self.setup()
360
361        self.select(0)

Initializes object.

Args
  • widgets: Widgets to add to this window after initilization.
  • attrs: Attributes that are passed to the constructor.
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:
363    @staticmethod
364    def obj_from_path(path: str) -> object | None:
365        """Retrieves an object from any valid import path.
366
367        An import path could be something like:
368            pytermgui.window_manager.compositor.Compositor
369
370        ...or if the library in question imports its parts within `__init__.py`-s:
371            pytermgui.Compositor
372        """
373
374        parts = path.split(".")
375
376        if parts[0] in dir(builtins):
377            obj = getattr(builtins, parts[0])
378
379        elif parts[0] in dir(ptg):
380            obj = getattr(ptg, parts[0])
381
382        else:
383            try:
384                obj = importlib.import_module(".".join(parts[:-1]))
385            except (ValueError, ModuleNotFoundError) as error:
386                return (
387                    f"Could not import object at path {path!r}: {error}."
388                    + " Maybe try using the --eval flag?"
389                )
390
391        try:
392            obj = getattr(obj, parts[-1])
393        except AttributeError:
394            return obj
395
396        return obj

Retrieves an object from any valid import path.

An import path could be something like

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:
406    def on_exit(self) -> None:
407        super().on_exit()
408
409        self._output.vertical_align = ptg.VerticalAlignment.TOP
410        for line in self._output.get_lines():
411            print(line)

Called on application exit.

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

def process_args(argv: list[str] | None = None) -> argparse.Namespace:
432def process_args(argv: list[str] | None = None) -> Namespace:
433    """Processes command line arguments."""
434
435    parser = ArgumentParser(
436        description=f"{ptg.tim.parse(_title())}'s command line environment."
437    )
438
439    apps = [short for (_, short), _ in APPLICATION_MAP.items()]
440
441    app_group = parser.add_argument_group("Applications")
442    app_group.add_argument(
443        "--app",
444        type=str.lower,
445        help="Launch an app.",
446        metavar=f"{', '.join(app.capitalize() for app in apps)}",
447        choices=apps,
448    )
449
450    app_group.add_argument(
451        "-g", "--getch", help="Launch the Getch app.", action="store_true"
452    )
453
454    app_group.add_argument(
455        "-t", "--tim", help="Launch the TIM Playground app.", action="store_true"
456    )
457
458    app_group.add_argument(
459        "-c",
460        "--color",
461        help="Launch the ColorPicker app.",
462        action="store_true",
463    )
464
465    inspect_group = parser.add_argument_group("Inspection")
466    inspect_group.add_argument(
467        "-i", "--inspect", help="Inspect an object.", metavar="PATH_OR_CODE"
468    )
469    inspect_group.add_argument(
470        "-e",
471        "--eval",
472        help="Evaluate the expression given to `--inspect` instead of treating it as a path.",
473        action="store_true",
474    )
475
476    inspect_group.add_argument(
477        "--methods", help="Always show methods when inspecting.", action="store_true"
478    )
479    inspect_group.add_argument(
480        "--dunder",
481        help="Always show __dunder__ methods when inspecting.",
482        action="store_true",
483    )
484    inspect_group.add_argument(
485        "--private",
486        help="Always show _private methods when inspecting.",
487        action="store_true",
488    )
489
490    util_group = parser.add_argument_group("Utilities")
491    util_group.add_argument(
492        "-s",
493        "--size",
494        help="Output the current terminal size in WxH format.",
495        action="store_true",
496    )
497
498    util_group.add_argument(
499        "-v",
500        "--version",
501        help="Print version & system information.",
502        action="store_true",
503    )
504
505    util_group.add_argument(
506        "--highlight",
507        help=(
508            "Highlight some python-like code syntax."
509            + " No argument or '-' will read STDIN."
510        ),
511        metavar="SYNTAX",
512        const="-",
513        nargs="?",
514    )
515
516    util_group.add_argument(
517        "--exec",
518        help="Execute some Python code. No argument or '-' will read STDIN.",
519        const="-",
520        nargs="?",
521    )
522
523    util_group.add_argument("-f", "--file", help="Interpret a PTG-YAML file.")
524    util_group.add_argument(
525        "--print-only",
526        help="When interpreting YAML, print the environment without running it interactively.",
527        action="store_true",
528    )
529
530    export_group = parser.add_argument_group("Exporters")
531
532    export_group.add_argument(
533        "--export-svg",
534        help="Export the result of any non-interactive argument as an SVG file.",
535        metavar="FILE",
536    )
537    export_group.add_argument(
538        "--export-html",
539        help="Export the result of any non-interactive argument as an HTML file.",
540        metavar="FILE",
541    )
542
543    argv = argv or sys.argv[1:]
544    args = parser.parse_args(args=argv)
545
546    return args

Processes command line arguments.

def screenshot(man: pytermgui.window_manager.manager.WindowManager) -> None:
549def screenshot(man: ptg.WindowManager) -> None:
550    """Opens a modal dialogue & saves a screenshot."""
551
552    tempname = ".screenshot_temp.svg"
553
554    modal: ptg.Window
555
556    def _finish(*_: Any) -> None:
557        """Closes the modal and renames the window."""
558
559        man.remove(modal)
560        filename = field.value or "screenshot"
561
562        if not filename.endswith(".svg"):
563            filename += ".svg"
564
565        os.rename(tempname, filename)
566
567        man.toast("[ptg.title]Screenshot saved!", "", f"[ptg.detail]{filename}")
568
569    title = sys.argv[0]
570    field = ptg.InputField(prompt="Save as: ")
571
572    man.screenshot(title=title, filename=tempname)
573
574    modal = man.alert(
575        "[ptg.title]Screenshot taken!", "", ptg.Container(field), "", ["Save!", _finish]
576    )

Opens a modal dialogue & saves a screenshot.

def run_environment(args: argparse.Namespace) -> None:
726def run_environment(args: Namespace) -> None:
727    """Runs the WindowManager environment.
728
729    Args:
730        args: An argparse namespace containing relevant arguments.
731    """
732
733    def _find_focused(manager: ptg.WindowManager) -> ptg.Window | None:
734        if manager.focused is None:
735            return None
736
737        # Find foremost non-persistent window
738        for window in manager:
739            if window.is_persistent:
740                continue
741
742            return window
743
744        return None
745
746    def _toggle_attachment(manager: ptg.WindowManager) -> None:
747        focused = _find_focused(manager)
748
749        if focused is None:
750            return
751
752        slot = manager.layout.body
753        if slot.content is None:
754            slot.content = focused
755        else:
756            slot.detach_content()
757
758        manager.layout.apply()
759
760    def _close_focused(manager: ptg.WindowManager) -> None:
761        focused = _find_focused(manager)
762
763        if focused is None:
764            return
765
766        focused.close()
767
768    _configure_widgets()
769
770    window: AppWindow | None = None
771    with ptg.WindowManager() as manager:
772        app_picker = _create_app_picker(manager)
773
774        manager.bind(
775            ptg.keys.CTRL_W,
776            lambda *_: _close_focused(manager),
777            "Close window",
778        )
779        manager.bind(
780            ptg.keys.F12,
781            lambda *_: screenshot(manager),
782            "Screenshot",
783        )
784        manager.bind(
785            ptg.keys.CTRL_F,
786            lambda *_: _toggle_attachment(manager),
787            "Toggle layout",
788        )
789
790        manager.bind(
791            ptg.keys.CTRL_A,
792            lambda *_: {
793                manager.focus(app_picker),  # type: ignore
794                app_picker.execute_binding(ptg.keys.CTRL_A),
795            },
796        )
797        manager.bind(
798            ptg.keys.ALT + ptg.keys.TAB,
799            lambda *_: manager.focus_next(),
800        )
801
802        if not args.app:
803            manager.layout = _create_layout()
804
805            manager.add(_create_header(), assign="header")
806            manager.add(app_picker, assign="applications")
807            manager.add(_create_footer(manager), assign="footer")
808
809            manager.toast(
810                "[ptg.title]Welcome to the [ptg.brand_title]"
811                + "PyTermGUI[/ptg.brand_title ptg.title] CLI!",
812                offset=ptg.terminal.height // 2 - 3,
813                delay=700,
814            )
815
816        else:
817            manager.layout.add_slot("Body")
818
819            app = _app_from_short(args.app)
820            window = app(args)
821            manager.add(window, assign="body")
822
823    window = window or manager.focused  # type: ignore
824    if window is None or not isinstance(window, AppWindow):
825        return
826
827    window.on_exit()

Runs the WindowManager environment.

Args
  • args: An argparse namespace containing relevant arguments.
def main(argv: list[str] | None = None) -> None:
895def main(argv: list[str] | None = None) -> None:
896    """Runs the program.
897
898    Args:
899        argv: A list of arguments, not included the 0th element pointing to the
900            executable path.
901    """
902
903    _create_aliases()
904
905    args = process_args(argv)
906
907    args.app = args.app or (
908        "getch"
909        if args.getch
910        else ("tim" if args.tim else ("color" if args.color else None))
911    )
912
913    if args.app or len(sys.argv) == 1:
914        run_environment(args)
915        return
916
917    with ptg.terminal.record() as recording:
918        if args.size:
919            ptg.tim.print(f"{ptg.terminal.width}x{ptg.terminal.height}")
920
921        elif args.version:
922            _print_version()
923
924        elif args.inspect:
925            _run_inspect(args)
926
927        elif args.exec:
928            args.exec = sys.stdin.read() if args.exec == "-" else args.exec
929
930            for name in dir(ptg):
931                obj = getattr(ptg, name, None)
932                globals()[name] = obj
933
934            globals()["print"] = ptg.terminal.print
935
936            exec(args.exec, locals(), globals())  # pylint: disable=exec-used
937
938        elif args.highlight:
939            text = sys.stdin.read() if args.highlight == "-" else args.highlight
940
941            ptg.tim.print(ptg.highlight_python(text))
942
943        elif args.file:
944            _interpret_file(args)
945
946        if args.export_svg:
947            recording.save_svg(args.export_svg)
948
949        elif args.export_html:
950            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.