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.
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¶
The three I/O objects are:
EventLoopinstance. 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.
Inputinstance. This is an abstraction on the input stream (stdin).
Outputinstance. 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
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')
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
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:
In order to react to user actions, we need to create a registry of keyboard
shortcuts to pass to our
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
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¶
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 (
Containerinstances), which arrange the layout
- user controls
UIControlinstances), which generate the actual content
An important difference:
|Abstract base class||Examples|
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
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
layout= keyword argument.
app = Application(..., layout=layout, ...)
The rendering flow¶
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))
The visualisation happens in several steps:
Windowobject then requests the
UIControlto create a
UIContentinstance (by calling
create_content()). The user control receives the dimensions of the window, but can still decide to create more or less content.
- 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
- The token list is passed through a list of
Processorobjects. Each processor can do a transformation for each line. (For instance, they can insert or replace some text.)
UIContentinstance which generates such a token lists for each lines.
- First, the buffer’s text is passed to the
- It calculates the horizontal and vertical scrolling, if applicable (if the content would take more space than what is available).
- The content is copied to the correct absolute position
Screen, as requested by the
Renderer. While doing this, the
Windowcan 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
UIContent. And from there,
the line is not passed through the processors or even asked from the
Some build-in processors:
||Highlight the current search results.|
||Highlight the selection.|
||Display input as asterisks. (
||Highlight open/close mismatches for brackets.|
||Insert some text before.|
||Insert some text after.|
||Append auto suggestion text.|
||Visualise leading whitespace.|
||Visualise trailing whitespace.|
||Visualise tabs as n spaces, or some symbols.|
Custom user controls¶
The focus stack¶
Application instance is where all the
components for a prompt_toolkit application come together.
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.
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
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
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:
SimpleFilter, which wraps an expression that takes no input, and evaluates to a boolean.
CLIFilter, which takes a
Most code in prompt_toolkit that expects a boolean will also accept a
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:
Further, these filters can be chained by the
| operators or
negated by the
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