Building full screen applications

prompt_toolkit can be used to create complex full screen terminal applications. Typically, an application consists of a layout (to describe the graphical part) and a set of key bindings.

The sections below describe the components required for full screen applications (or custom, non full screen applications), and how to assemble them together.

Warning

This is going to change.

The information below is still up to date, but we are planning to refactor some of the internal architecture of prompt_toolkit, to make it easier to build full screen applications. This will however be backwards-incompatible. The refactoring should probably be complete somewhere around half 2017.

Running the application

To run our final Full screen Application, we need three I/O objects, and an Application instance. These are passed as arguments to CommandLineInterface.

The three I/O objects are:

  • An EventLoop instance. This is basically a while-true loop that waits for user input, and when it receives something (like a key press), it will send that to the application.
  • An Input instance. This is an abstraction on the input stream (stdin).
  • An Output instance. This is an abstraction on the output stream, and is called by the renderer.

The input and output objects are optional. However, the eventloop is always required.

We’ll come back to what the Application instance is later.

So, the only thing we actually need in order to run an application is the following:

from prompt_toolkit.interface import CommandLineInterface
from prompt_toolkit.application import Application
from prompt_toolkit.shortcuts import create_eventloop

loop = create_eventloop()
application = Application()
cli = CommandLineInterface(application=application, eventloop=loop)
# cli.run()
print('Exiting')

Note

In the example above, we don’t run the application yet, as otherwise it will hang indefinitely waiting for a signal to exit the event loop. This is why the cli.run() part is commented.

(Actually, it would accept the Enter key by default. But that’s only because by default, a buffer called DEFAULT_BUFFER has the focus; its AcceptAction is configured to return the result when accepting, and there is a default Enter key binding that calls the AcceptAction of the currently focussed buffer. However, the content of the DEFAULT_BUFFER buffer is not yet visible, so it’s hard to see what’s going on.)

Let’s now bind a keyboard shortcut to exit:

Key bindings

In order to react to user actions, we need to create a registry of keyboard shortcuts to pass to our Application. The easiest way to do so, is to create a KeyBindingManager, and then attach handlers to our desired keys. Keys contains a few predefined keyboards shortcut that can be useful.

To create a registry, we can simply instantiate a KeyBindingManager and take its registry attribute:

from prompt_toolkit.key_binding.manager import KeyBindingManager
manager = KeyBindingManager()
registry = manager.registry

Update the Application constructor, and pass the registry as one of the argument.

application = Application(key_bindings_registry=registry)

To register a new keyboard shortcut, we can use the add_binding() method as a decorator of the key handler:

from prompt_toolkit.keys import Keys

@registry.add_binding(Keys.ControlQ, eager=True)
def exit_(event):
    """
    Pressing Ctrl-Q will exit the user interface.

    Setting a return value means: quit the event loop that drives the user
    interface and return this value from the `CommandLineInterface.run()` call.
    """
    event.cli.set_return_value(None)

In this particular example we use eager=True to trigger the callback as soon as the shortcut Ctrl-Q is pressed. The callback is named exit_ for clarity, but it could have been named _ (underscore) as well, because the we won’t refer to this name.

Creating a layout

A layout is a composition of Container and UIControl that will describe the disposition of various element on the user screen.

Various Layouts can refer to Buffers that have to be created and pass to the application separately. This allow an application to have its layout changed, without having to reconstruct buffers. You can imagine for example switching from an horizontal to a vertical split panel layout and vice versa,

There are two types of classes that have to be combined to construct a layout:

  • containers (Container instances), which arrange the layout
  • user controls (UIControl instances), which generate the actual content

Note

An important difference:

  • containers use absolute coordinates, and paint on a Screen instance.
  • user controls create a UIContent instance. This is a collection of lines that represent the actual content. A UIControl is not aware of the screen.
Abstract base class Examples
Container HSplit VSplit FloatContainer Window
UIControl BufferControl TokenListControl FillControl

The Window class itself is particular: it is a Container that can contain a UIControl. Thus, it’s the adaptor between the two.

The Window class also takes care of scrolling the content if the user control created a Screen that is larger than what was available to the Window.

Here is an example of a layout that displays the content of the default buffer on the left, and displays "Hello world" on the right. In between it shows a vertical line:

from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.layout.containers import VSplit, Window
from prompt_toolkit.layout.controls import BufferControl, FillControl, TokenListControl
from prompt_toolkit.layout.dimension import LayoutDimension as D

from pygments.token import Token

layout = VSplit([
    # One window that holds the BufferControl with the default buffer on the
    # left.
    Window(content=BufferControl(buffer_name=DEFAULT_BUFFER)),

    # A vertical line in the middle. We explicitely specify the width, to make
    # sure that the layout engine will not try to divide the whole width by
    # three for all these windows. The `FillControl` will simply fill the whole
    # window by repeating this character.
    Window(width=D.exact(1),
           content=FillControl('|', token=Token.Line)),

    # Display the text 'Hello world' on the right.
    Window(content=TokenListControl(
        get_tokens=lambda cli: [(Token, 'Hello world')])),
])

The previous section explains how to create an application, you can just pass the currently created layout when you create the Application instance using the layout= keyword argument.

app = Application(..., layout=layout, ...)

The rendering flow

Understanding the rendering flow is important for understanding how Container and UIControl objects interact. We will demonstrate it by explaining the flow around a BufferControl.

Note

A BufferControl is a UIControl for displaying the content of a Buffer. A buffer is the object that holds any editable region of text. Like all controls, it has to be wrapped into a Window.

Let’s take the following code:

from prompt_toolkit.enums import DEFAULT_BUFFER
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import BufferControl

Window(content=BufferControl(buffer_name=DEFAULT_BUFFER))

What happens when a Renderer objects wants a Container to be rendered on a certain Screen?

The visualisation happens in several steps:

  1. The Renderer calls the write_to_screen() method of a Container. This is a request to paint the layout in a rectangle of a certain size.

    The Window object then requests the UIControl to create a UIContent instance (by calling create_content()). The user control receives the dimensions of the window, but can still decide to create more or less content.

    Inside the create_content() method of UIControl, there are several steps:

    1. First, the buffer’s text is passed to the lex_document() method of a Lexer. This returns a function which for a given line number, returns a token list for that line (that’s a list of (Token, text) tuples).
    2. The token list is passed through a list of Processor objects. Each processor can do a transformation for each line. (For instance, they can insert or replace some text.)
    3. The UIControl returns a UIContent instance which generates such a token lists for each lines.

The Window receives the UIContent and then:

  1. It calculates the horizontal and vertical scrolling, if applicable (if the content would take more space than what is available).
  2. The content is copied to the correct absolute position Screen, as requested by the Renderer. While doing this, the Window can possible wrap the lines, if line wrapping was configured.

Note that this process is lazy: if a certain line is not displayed in the Window, then it is not requested from the UIContent. And from there, the line is not passed through the processors or even asked from the Lexer.

Input processors

An Processor is an object that processes the tokens of a line in a BufferControl before it’s passed to a UIContent instance.

Some build-in processors:

Processor Usage:
HighlightSearchProcessor Highlight the current search results.
HighlightSelectionProcessor Highlight the selection.
PasswordProcessor Display input as asterisks. (* characters).
BracketsMismatchProcessor Highlight open/close mismatches for brackets.
BeforeInput Insert some text before.
AfterInput Insert some text after.
AppendAutoSuggestion Append auto suggestion text.
ShowLeadingWhiteSpaceProcessor Visualise leading whitespace.
ShowTrailingWhiteSpaceProcessor Visualise trailing whitespace.
TabsProcessor Visualise tabs as n spaces, or some symbols.

The TokenListControl

Custom user controls

The Window class

The Window class exposes many interesting functionality that influences the behaviour of user controls.

Buffers

The focus stack

The Application instance

The Application instance is where all the components for a prompt_toolkit application come together.

Note

Actually, not all the components; just everything that is not dependent on I/O (i.e. all components except for the eventloop and the input/output objects).

This way, it’s possible to create an Application instance and later decide to run it on an asyncio eventloop or in a telnet server.

from prompt_toolkit.application import Application

application = Application(
    layout=layout,
    key_bindings_registry=registry,

    # Let's add mouse support as well.
    mouse_support=True,

    # For fullscreen:
    use_alternate_screen=True)

We are talking about full screen applications, so it’s important to pass use_alternate_screen=True. This switches to the alternate terminal buffer.

Filters (reactivity)

Many places in prompt_toolkit expect a boolean. For instance, for determining the visibility of some part of the layout (it can be either hidden or visible), or a key binding filter (the binding can be active on not) or the wrap_lines option of BufferControl, etc.

These booleans however are often dynamic and can change at runtime. For instance, the search toolbar should only be visible when the user is actually searching (when the search buffer has the focus). The wrap_lines option could be changed with a certain key binding. And that key binding could only work when the default buffer got the focus.

In prompt_toolkit, we decided to reduce the amount of state in the whole framework, and apply a simple kind of reactive programming to describe the flow of these booleans as expressions. (It’s one-way only: if a key binding needs to know whether it’s active or not, it can follow this flow by evaluating an expression.)

There are two kind of expressions:

Most code in prompt_toolkit that expects a boolean will also accept a CLIFilter.

One way to create a CLIFilter instance is by creating a Condition. For instance, the following condition will evaluate to True when the user is searching:

from prompt_toolkit.filters import Condition
from prompt_toolkit.enums import DEFAULT_BUFFER

is_searching = Condition(lambda cli: cli.is_searching)

This filter can then be used in a key binding, like in the following snippet:

from prompt_toolkit.key_binding.manager import KeyBindingManager

manager = KeyBindingManager.for_prompt()

@manager.registry.add_binding(Keys.ControlT, filter=is_searching)
def _(event):
    # Do, something, but only when searching.
    pass

There are many built-in filters, ready to use:

  • HasArg
  • HasCompletions
  • HasFocus
  • InFocusStack
  • HasSearch
  • HasSelection
  • HasValidationError
  • IsAborting
  • IsDone
  • IsMultiline
  • IsReadOnly
  • IsReturning
  • RendererHeightIsKnown

Further, these filters can be chained by the & and | operators or negated by the ~ operator.

Some examples:

from prompt_toolkit.key_binding.manager import KeyBindingManager
from prompt_toolkit.filters import HasSearch, HasSelection

manager = KeyBindingManager()

@manager.registry.add_binding(Keys.ControlT, filter=~is_searching)
def _(event):
    # Do, something, but not when when searching.
    pass

@manager.registry.add_binding(Keys.ControlT, filter=HasSearch() | HasSelection())
def _(event):
    # Do, something, but not when when searching.
    pass

Input hooks

Running on the asyncio event loop