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
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.An
Inputinstance. This is an abstraction on the input stream (stdin).An
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 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 (
Containerinstances), which arrange the layoutuser controls (
UIControlinstances), which generate the actual content
Note
An important difference:
containers use absolute coordinates, and paint on a
Screeninstance.user controls create a
UIContentinstance. This is a collection of lines that represent the actual content. AUIControlis not aware of the screen.
Abstract base class |
Examples |
|---|---|
|
|
|
|
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:
The
Renderercalls thewrite_to_screen()method of aContainer. This is a request to paint the layout in a rectangle of a certain size.The
Windowobject then requests theUIControlto create aUIContentinstance (by callingcreate_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 ofUIControl, there are several steps:First, the buffer’s text is passed to the
lex_document()method of aLexer. 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).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.)The
UIControlreturns aUIContentinstance which generates such a token lists for each lines.
The Window receives the
UIContent and then:
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 theRenderer. While doing this, theWindowcan 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: |
|---|---|
|
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. |
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:
SimpleFilter, which wraps an expression that takes no input, and evaluates to a boolean.CLIFilter, which takes aCommandLineInterfaceas input.
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:
HasArgHasCompletionsHasFocusInFocusStackHasSearchHasSelectionHasValidationErrorIsAbortingIsDoneIsMultilineIsReadOnlyIsReturningRendererHeightIsKnown
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