pytermgui.cmd

The command-line module of the library.

See ptg --help for more information.

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

An application to play around with TIM.

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:
326    def on_exit(self) -> None:
327        super().on_exit()
328        print(ptg.tim.prettify_markup(self._input.value))
329        ptg.tim.print(self._input.value)

Called on application exit.

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

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

A window for the inspect utility.

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

Initializes object.

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

Retrieves an object from any valid import path.

An import path could be something like

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

Called on application exit.

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

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

Processes command line arguments.

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

Opens a modal dialogue & saves a screenshot.

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

Runs the WindowManager environment.

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

Runs the program.

Args
  • argv: A list of arguments, not included the 0th element pointing to the executable path.