pytermgui

A simple yet powerful TUI framework for your Python (3.7+) applications.

There is a couple of parts that make up this module, all building on top of eachother to achieve the final result. Your usage depends on which part of the library you use. I will provide an example of usage for each.

Getting started

Getting started with pytermgui is quite easy. The library allows you to define layouts inline, thus letting you get by without ever having to subclass Widget or WindowManager. However, if you do want that functionality the later sections of this guide will tell you how to.

How to: Create your own app

Most primitive usecase

To create your own app, it is recommended to use the pytermgui.window_manager.WindowManager context. Technically, all that is needed for the app to run correctly is to call pytermgui.window_manager.WindowManager.run with no arguments.

import pytermgui as ptg

with ptg.WindowManager() as manager:
    manager.run()

This will give you an empty screen you can quit using CTRL_C. That is doesn't really get us anywhere though, does it?

Adding windows as variables

In order to actually have something going on, you can call pytermgui.widgets.WindowManager.add with a window of your choice. Adding a window to the manager will bring it to focus.

Here is a quick example of the most basic usecase:

import pytermgui as ptg

with ptg.WindowManager() as manager:
    window = ptg.Window(
        "[wm-title]My first window!",
        "",
        ["Exit", lambda *_: manager.exit()],
    )

    manager.add(window)
    manager.run()

Adding windows during definition

You don't need to assign your window to a variable if you aren't going to reference it later. Since all widgets implement __add__ and __iadd__, you can define them as a sum of other widgets.

For example:

import pytermgui as ptg

with ptg.WindowManager() as manager:
    manager.add(
        ptg.Window()
        + "[wm-title]My first window!"
        + ""
        + ["Exit", lambda *_: manager.exit()]
    )

    manager.run()

Useful methods to know about

Note: For markup functions, it is recommended to use the markup name that is exported by the module.

import pytermgui as ptg

ptg.markup.alias("my-tag", "blue @141")

with ptg.markup as mprint:
    mprint("This is [my-tag]my-tag!")

How to: Configure your Application using YAML

Why you should bother

While the programmatic interface for the style system is alright, it's a bit clunky to use for serious customization. In order to improve your experience, you can use a YAML file with your configuration in its config section. Additionally, you can define markup in a nicer way as well.

For the purposes of this guide, we will use YAML defined inside our Python code. pytermgui.file_loaders.FileLoader.load can take etiher strings or files, so the PTG_CONFIG name below can be trivially modified to refernce a file.

Note: You will need to install PyYAML in order to use the YAML loader class.

Base application

For the purposes of this section, this is the application we will use:

import sys
import pytermgui as ptg

# Define empty config as the base
# 
# Note that we define markup tags as empty.
# markup tags **need** to be defined for program
# execution.
PTG_CONFIG = """\
config: {}

markup:
    title: ""
    body: ""
"""

with ptg.WindowManager() as manager:
    # Initialize a loader, and load our config
    loader = ptg.YamlLoader()
    loader.load(PTG_CONFIG)

    # Create a test window
    manager.add(
        ptg.Window()
        + "[title]This is our title"
        + ""
        + {"[body]First key": ["First button"]}
        + {"[body]Second key": ["Second button"]}
        + {"[body]Third key": ["Third button"]}
        + ""
        + ["Exit", lambda *_: manager.exit()]
    )

    manager.run()

This gives us the following, very bland interface.

Adding markup

As you could see above, defining markup aliases is quite easy. You cannot and will possibly never be able to define macros in a markup file for two main reasons:

  • It would be a nightmare to implement in terms of technicalities
  • It would be a hotbed for RCE security risks

There is some leeway on both of those however, and that may be revisited in the future.

Anyways, markup aliases. The markup section of your file should be the following format:

markup:
    alias-name: alias-value

...which would stand for

ptg.markup.alias("alias-name", "alias-value")

You should not include [ and ] delimiters around either arguments.

As an example, let's expand our PTG_CONFIG from above:

config: {}

markup:
    title: 210 bold
    body: 245 italic

Getting there.

Customizing widget styles & chars

The markup configuration allows for quite a bit more customization, using the thus-far untouched config section.

The basic syntax goes as follows:

config:
    WidgetName:
        styles:
            style_key: markup_str

        chars:
            char_key: char_str

As you can see, styles take markup strings. This is because they are converted right into pytermgui.styles.MarkupFormatter objects, which means you can incorporate both {depth} and {item} into them.

Note: While it is permitted, not including depth nor item into your markup will turn it static, likely causing some headaches.

So for a simple example, let's define some styles & chars:

config:
    Window:
        styles:
            border: &border-style "[60]{item}"
            corner: *border-style

    Button:
        styles:
            label: "[@235 108 bold]{item}"

    Splitter:
        chars:
            separator: "   "

markup:
    title: 210 bold
    body: 245 italic

Note: We used the & and * symbols above. In YAML, & creates a named anchor point, and * allows you to reference it. Here is a cool article on the subject.

How to: Define widgets in YAML

Setup

The file loader system also has support for defining entire widgets in files, as can be seen by the hello world example.

These files can be interpreted with a CLI flag and require no actual code to "run".

This is how the widgets section of the file should look like:

widgets:
    WidgetName:
        type: WidgetTypeKnownToPTG
        arbitrary-attribute: arbitrary-value

    # Alternatively, if you don't need your widget to be named,
    # this syntax will automatically assign `type` to the key provided.
    WidgetTypeKnownToPTG:
        arbitrary-attribute: arbitrary-value

Where WidgetTypeKnownToPTG must be a widget subclass either included in the library by default, or registered using the loader's serializer. The second is only possible when you are interpreting it from Python yourself, and not when you use ptg -f.

Registering a widget goes as follows:

import ptg as pytermgui

class YourCustomWidget(ptg.Widget):
    ...

serializer = ptg.Serializer()
serializer.register(YourCustomWidget)

loader = ptg.YamlLoader(serializer)

with open("your-file.yaml", "r") as ptg_file:
    namespace = loader.load(serializer)

All of the FileLoader subclasses take a pytermgui.serializer.Serializer instance as their first argument, setting up their own if one was not given. This object is then used to instantiate all of the widgets it encounters, so registering a widget to it will allow the loader to use it.

Note: Serializers can dump any, even unknown widget types, but can only load known ones.

Once all your widgets are registered and load correctly, you can access them by the WidgetName attribute of your namespace.

Loading a namespace using ptg -f

The simplest way to use these files is running ptg -f "filename", where filename points to your namespace file. As mentioned above, you can only use PTG native widget types with this method.

What that CLI tool does is essentially this:

import pytermgui as ptg

with open(args.file, "r") as file:
    namespace = ptg.YamlLoader().load(args.file)

with ptg.WindowManager() as manager:
    for widget in namespace.widgets:
        manager.add(widget)

    manager.run()

Loading a namespace programatically

Loading namespaces manually is more or less defined above, with one extra detail. You can either loop through all defined widgets as show previously, or reference them as the key they were defined as in your namespace file.

For example:

widgets:
    MyWidget:
        ...

...where ... would be filled with your data, would allow you to use

my_widget = namespace.MyWidget

...to reference the instance that was created.

Note: This is the specific instance the loader created. If you plan on using this as a sort of template, be sure to run copy() before modifying your widget.

Defining widget attributes

So, now that you know how to get your namespace file loaded, let's get some information into it.

The syntax goes like this:

widgets:
    WidgetName:
        type: WidgetType
        widgets:
            - WidgetType:
                arbitrary-attribute: arbitrary-value

        arbitrary-attribute: arbitrary-value

As you can see, any arbitrary attribute can be set in this method. You can also define a list of widgets, using the widgets key. The loader only looks for keys starting with widgets, so you can separate your widgets into categories or groups if that tickles your fancy.

For example:

widgets:
    MyContainer:
        type: Container
        widgets_header:
            - Label:
                value: This is my header
            - Label:
                value: ""

        widgets_body:
            - Button:
                label: Press me

This syntax is the equivalent of putting all three widget definitions under one widgets key.

Defining custom boxes

The boxes section allows you to define your own namespace file local pytermgui.widgets.boxes.Box classes. You may only use these within the file they were defined in.

Note: The preferred naming convention for boxes is WHATEVER_THIS_IS. Not sure why, but that's how all the native boxes were defined and it stands out enough for clarity.

A basic box definition would go like this:

boxes:
    MY_BOX: [
        "0bbb1",
        "a x c",
        "3ddd2"
    ]

In this example, we would get a box with the corner characters

["0", "1", "2", "3"]

...and border characters

["a", "b", "c", "d"]

For more information on the semantics of boxes, check out pytermgui.widgets.boxes.Box. The characters defined here are directly passed to the Box constructor.

Combining our knowledge to create an application

Using everything we learnt above, we can define a simple application:

namespace.yaml

config:
    Window:
        styles:
            border: &w-border-style "[#5A6572]{item}"
            corner: *w-border-style

    Container:
        styles:
            border: &c-border-style "[#7D98A1]{item}"
            corner: *c-border-style

    Button:
        styles:
            label: "[@#1C2220 #A9B4C2]{item}"
            highlight: "[#1C2220 @#A9B4C2]{item}"

    Splitter:
        chars:
            separator: "   "

boxes:
    OUTER: [
        "█▀▀▀█",
        "█ x █",
        "█▄▄▄█",
    ]

markup:
    title: "#A9B4C2 bold"
    subtitle: "italic dim"
    body: "245"

widgets:
    MainWindow:
      type: Window
      box: OUTER
      width: 70
      widgets:
        - Label:
            value: "[title]YAML is cool!"
        - Label: {}

        - Label:
            value: "[subtitle]Here are some facts about it:"
        - Label: {}

        - Splitter:
            widgets:
                - Container:
                    box: OUTER
                    widgets:
                        - Label:
                              value: >-
                                -[body] YAML originally stood for "Yet Another Markup
                                Language", but was later modified to mean "YAML Ain't 
                                Markup".

                              parent_align: 0

                - Container:
                    box: OUTER
                    widgets:
                        - Label:
                              value: >-
                                -[body] The language was designed by Clark Evans, Ingy
                                döt Net and Oren Ben-Kiki.

                              parent_align: 0

        - Label: {}
        - Button:
            id: button-definition
            label: Get definition

    PopupWindow:
        type: Window
        is_modal: true
        width: 50

        box: OUTER
        widgets:
            - Label:
                value: "[title]YAML is defined as..."
            - Label: {}

            - Label:
                value: "[body]...a human-readable data-serialization language."
            - Label: {}

            - Label:
                parent_align: 0
                value: "[body italic]Press CTRL_W to close this window."

runner.py

import pytermgui as ptg

namespace = ptg.YamlLoader().load(PTG_NAMESPACE)

with ptg.WindowManager() as manager:
    manager.add(namespace.MainWindow.center())

    popup = namespace.PopupWindow.center()
    popup.bind(ptg.keys.CTRL_W, lambda window, _: window.close())

    button = ptg.get_widget("button-definition")
    button.onclick = lambda *_: manager.add(popup)

    manager.run()

How to: Define your own Widget

As mentioned above, you usually don't have to do this. pytermgui tries to make everything definable inline, but for some functionalities you might need more than what is exposed.

Important things to know

Firstly, the most important aspect of how the library works is the pytermgui.window_manager threading system. As listening for any IO, let it be keyboard or mouse, is always blocking, we cannot print while that is happening. This obviously will not do, so we separate the input and output to different threads.

The main thread is input. This thread is completely handled by the library, and the only place the user can influence it is with the pytermgui.widgets.base.Widget.bind system.

The secondary thread handles the output. This is where the users (you) are able to have a say, as most widget code is run here.

Everything a girl widget needs

You should always subclass pytermgui.widgets.base.Widget when creating your own widget. There is a lot of abstractions, implementations and all sort of useful junk that the base class does, and reimplementing it is almost never a good idea. (Though if you manage to reimplement a feature-complete Widget class in a better manner than the library does, PRs are always welcome!)

The things you should define

For any output, use the get_lines method. This returns a list[str], where each item of the list is exactly as long as the widget is wide, and the length of the list is equal to the height of your widget. What goes in this list is completely up to you, pytermgui will render it as long as it fits those criteria. If you need to do any blocking or long actions for your UI's contents, it is best to do that in a thread and have the thread update some instance attribute, which can then be displayed here. This method is called a lot (at least once a frame, usually more), so keep it performant.

For mouse interaction, use handle_mouse. This gets a MouseEvent and MouseTarget as its arguments, and should return True if widget.select should be run by the caller, False otherwise. The return value generally stands for whether the given widget could handle the input, but sometimes the above distinction is useful to know about.

For handling keyboard input, use handle_key. This simply gets a str key as its argument, and should return True if input was handled, False otherwise. It should also call self.execute_binding(key) before it does any of its own handling, as bindings enjoy priority over normal keyboard actions.

For having a number of suboptions within the widget, define the selectables_length property. This is defined by Container already, so if you subclass any of its children you won't have to worry about it. selectables_length defines how many options can be selected within the widget, and is checked by its parent. Be sure to make it a @property, otherwise you will get errors.

For custom styles & chars, define those attributes in the widget body. There is plenty of examples in the pytermgui.widgets submodule on how to do this.

Layers of the API

The API has a strict layered structure. Modules can only use names defined below them in the structure, as such a Widget has no clue about a WindowManager, unless it has a strictly defined reference to it.

Low level

At the base, there is pytermgui.ansi_interface and pytermgui.input, handling terminal APIs for output and input respectively. This is the lowest-level layer of the library.

import pytermgui as ptg

ptg.set_alt_buffer()

print('This terminal will be discarded on r"\n" input.')
while ptg.getch() != r"\n":
    print("Wrong key.")

ptg.unset_alt_buffer()

Helper level

On top of that, there is the helper layer, including things like pytermgui.helpers, pytermgui.context_managers and the kind. These provide no extra functionality, only combine functions defined below them in order to make them more usable.

import pytermgui as ptg

text = r"This is some \033[1mlong\033[0m and \033[38;5;141mstyled\033[0m text."

for line in ptg.break_line(text, limit=10):
    print(line)

High level

Building on all that is the relatively high level pytermgui.widgets module. This part uses things from everything defined before it to create a visually appealing interface system. It introduces a lot of its own APIs and abstractions, and leaves all the parts below it free of cross-layer dependencies.

The highest layer of the library is for pytermgui.window_manager. This layer combines parts from everything below. It introduces abstractions on top of the pytermgui.widget system, and creates its own featureset.

import pytermgui as ptg
with ptg.WindowManager() as manager:
    manager.add(
        ptg.Window()
        + "[141 bold]Title"
        + "[grey italic]body text"
        + ""
        + ["Button"]
    )
    manager.run()

View Source
"""
A simple yet powerful TUI framework for your Python (3.7+) applications.

There is a couple of parts that make up this module, all building on top
of eachother to achieve the final result. Your usage depends on which part
of the library you use. I will provide an example of usage for each.

.. include:: ../docs/getting_started.md
"""

# https://github.com/python/mypy/issues/4930
# mypy: ignore-errors

from __future__ import annotations

from typing import Union, Any, Optional
from random import shuffle
import sys

from .enums import *
from .parser import *
from .widgets import *
from .helpers import *
from .inspector import *
from .animations import *
from .serializer import *
from .exceptions import *
from .prettifiers import *
from .file_loaders import *
from .ansi_interface import *
from .window_manager import *
from .input import getch, keys
from .context_managers import alt_buffer, cursor_at, mouse_handler

# Silence warning if running as standalone module
if "-m" in sys.argv:
    import warnings

    warnings.filterwarnings("ignore")

__version__ = "4.1.0"


def auto(data: Any, **widget_args: Any) -> Optional[Widget | list[Splitter]]:
    """Creates a widget from specific data structures.

    This conversion includes various widget classes, as well as some shorthands for
    more complex objects.  This method is called implicitly whenever a non-widget is
    attempted to be added to a Widget.


    Args:
        data: The structure to convert. See below for formats.
        **widget_args: Arguments passed straight to the widget constructor.

    Returns:
        The widget or list of widgets created, or None if the passed structure could
        not be converted.

    <br>
    <details style="text-align: left">
        <summary style="all: revert; cursor: pointer">Data structures:</summary>

    `pytermgui.widgets.base.Label`:

    * Created from `str`
    * Syntax example: `"Label value"`

    `pytermgui.widgets.extra.Splitter`:

    * Created from `tuple[Any]`
    * Syntax example: `(YourWidget(), "auto_syntax", ...)`

    `pytermgui.widgets.extra.Splitter` prompt:

    * Created from `dict[Any, Any]`
    * Syntax example: `{YourWidget(): "auto_syntax"}`

    `pytermgui.widgets.buttons.Button`:

    * Created from `list[str, pytermgui.widgets.buttons.MouseCallback]`
    * Syntax example: `["Button label", lambda target, caller: ...]`

    `pytermgui.widgets.buttons.Checkbox`:

    * Created from `list[bool, Callable[[bool], Any]]`
    * Syntax example: `[True, lambda checked: ...]`

    `pytermgui.widgets.buttons.Toggle`:

    * Created from `list[tuple[str, str], Callable[[str], Any]]`
    * Syntax example: `[("On", "Off"), lambda new_value: ...]`
    </details>

    Example:

    ```python3
    from pytermgui import Container
    form = (
        Container(id="form")
        + "[157 bold]This is a title"
        + ""
        + {"[72 italic]Label1": "[210]Button1"}
        + {"[72 italic]Label2": "[210]Button2"}
        + {"[72 italic]Label3": "[210]Button3"}
        + ""
        + ["Submit", lambda _, button, your_submit_handler(button.parent)]
    )
    ```
    """
    # In my opinion, returning immediately after construction is much more readable.
    # pylint: disable=too-many-return-statements

    # Nothing to do.
    if isinstance(data, Widget):
        # Set all **widget_args
        for key, value in widget_args.items():
            setattr(data, key, value)

        return data

    # Label
    if isinstance(data, str):
        return Label(data, **widget_args)

    # Splitter
    if isinstance(data, tuple):
        return Splitter(*data, **widget_args)

    # buttons
    if isinstance(data, list):
        label = data[0]
        onclick = None
        if len(data) > 1:
            onclick = data[1]

        # Checkbox
        if isinstance(label, bool):
            return Checkbox(onclick, checked=label, **widget_args)

        # Toggle
        if isinstance(label, tuple):
            assert len(label) == 2
            return Toggle(label, onclick, **widget_args)

        return Button(label, onclick, **widget_args)

    # prompt splitter
    if isinstance(data, dict):
        rows: list[Splitter] = []

        for key, value in data.items():
            left = auto(key, parent_align=HorizontalAlignment.LEFT)
            right = auto(value, parent_align=HorizontalAlignment.RIGHT)

            rows.append(Splitter(left, right, **widget_args))

        if len(rows) == 1:
            return rows[0]

        return rows

    return None


# Alternative binding for the `auto` method
Widget.from_data = staticmethod(auto)
#   def auto( data: Any, **widget_args: Any ) -> Union[pytermgui.widgets.base.Widget, list[pytermgui.widgets.layouts.Splitter], NoneType]:
View Source
def auto(data: Any, **widget_args: Any) -> Optional[Widget | list[Splitter]]:
    """Creates a widget from specific data structures.

    This conversion includes various widget classes, as well as some shorthands for
    more complex objects.  This method is called implicitly whenever a non-widget is
    attempted to be added to a Widget.


    Args:
        data: The structure to convert. See below for formats.
        **widget_args: Arguments passed straight to the widget constructor.

    Returns:
        The widget or list of widgets created, or None if the passed structure could
        not be converted.

    <br>
    <details style="text-align: left">
        <summary style="all: revert; cursor: pointer">Data structures:</summary>

    `pytermgui.widgets.base.Label`:

    * Created from `str`
    * Syntax example: `"Label value"`

    `pytermgui.widgets.extra.Splitter`:

    * Created from `tuple[Any]`
    * Syntax example: `(YourWidget(), "auto_syntax", ...)`

    `pytermgui.widgets.extra.Splitter` prompt:

    * Created from `dict[Any, Any]`
    * Syntax example: `{YourWidget(): "auto_syntax"}`

    `pytermgui.widgets.buttons.Button`:

    * Created from `list[str, pytermgui.widgets.buttons.MouseCallback]`
    * Syntax example: `["Button label", lambda target, caller: ...]`

    `pytermgui.widgets.buttons.Checkbox`:

    * Created from `list[bool, Callable[[bool], Any]]`
    * Syntax example: `[True, lambda checked: ...]`

    `pytermgui.widgets.buttons.Toggle`:

    * Created from `list[tuple[str, str], Callable[[str], Any]]`
    * Syntax example: `[("On", "Off"), lambda new_value: ...]`
    </details>

    Example:

    ```python3
    from pytermgui import Container
    form = (
        Container(id="form")
        + "[157 bold]This is a title"
        + ""
        + {"[72 italic]Label1": "[210]Button1"}
        + {"[72 italic]Label2": "[210]Button2"}
        + {"[72 italic]Label3": "[210]Button3"}
        + ""
        + ["Submit", lambda _, button, your_submit_handler(button.parent)]
    )
    ```
    """
    # In my opinion, returning immediately after construction is much more readable.
    # pylint: disable=too-many-return-statements

    # Nothing to do.
    if isinstance(data, Widget):
        # Set all **widget_args
        for key, value in widget_args.items():
            setattr(data, key, value)

        return data

    # Label
    if isinstance(data, str):
        return Label(data, **widget_args)

    # Splitter
    if isinstance(data, tuple):
        return Splitter(*data, **widget_args)

    # buttons
    if isinstance(data, list):
        label = data[0]
        onclick = None
        if len(data) > 1:
            onclick = data[1]

        # Checkbox
        if isinstance(label, bool):
            return Checkbox(onclick, checked=label, **widget_args)

        # Toggle
        if isinstance(label, tuple):
            assert len(label) == 2
            return Toggle(label, onclick, **widget_args)

        return Button(label, onclick, **widget_args)

    # prompt splitter
    if isinstance(data, dict):
        rows: list[Splitter] = []

        for key, value in data.items():
            left = auto(key, parent_align=HorizontalAlignment.LEFT)
            right = auto(value, parent_align=HorizontalAlignment.RIGHT)

            rows.append(Splitter(left, right, **widget_args))

        if len(rows) == 1:
            return rows[0]

        return rows

    return None

Creates a widget from specific data structures.

This conversion includes various widget classes, as well as some shorthands for more complex objects. This method is called implicitly whenever a non-widget is attempted to be added to a Widget.

Args
  • data: The structure to convert. See below for formats.
  • **widget_args: Arguments passed straight to the widget constructor.
Returns

The widget or list of widgets created, or None if the passed structure could not be converted.


Data structures:

pytermgui.widgets.base.Label:

  • Created from str
  • Syntax example: "Label value"

pytermgui.widgets.extra.Splitter:

  • Created from tuple[Any]
  • Syntax example: (YourWidget(), "auto_syntax", ...)

pytermgui.widgets.extra.Splitter prompt:

  • Created from dict[Any, Any]
  • Syntax example: {YourWidget(): "auto_syntax"}

pytermgui.widgets.buttons.Button:

  • Created from list[str, pytermgui.widgets.buttons.MouseCallback]
  • Syntax example: ["Button label", lambda target, caller: ...]

pytermgui.widgets.buttons.Checkbox:

  • Created from list[bool, Callable[[bool], Any]]
  • Syntax example: [True, lambda checked: ...]

pytermgui.widgets.buttons.Toggle:

  • Created from list[tuple[str, str], Callable[[str], Any]]
  • Syntax example: [("On", "Off"), lambda new_value: ...]

Example:

from pytermgui import Container
form = (
    Container(id="form")
    + "[157 bold]This is a title"
    + ""
    + {"[72 italic]Label1": "[210]Button1"}
    + {"[72 italic]Label2": "[210]Button2"}
    + {"[72 italic]Label3": "[210]Button3"}
    + ""
    + ["Submit", lambda _, button, your_submit_handler(button.parent)]
)