From e106bf94eff07d9a59771d9ccc4406421e18ab64 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 19:35:20 +0200 Subject: Adding upstream version 3.0.36. Signed-off-by: Daniel Baumann --- docs/pages/advanced_topics/architecture.rst | 97 ++++++ docs/pages/advanced_topics/asyncio.rst | 30 ++ docs/pages/advanced_topics/filters.rst | 169 ++++++++++ docs/pages/advanced_topics/index.rst | 18 + docs/pages/advanced_topics/input_hooks.rst | 41 +++ docs/pages/advanced_topics/key_bindings.rst | 388 ++++++++++++++++++++++ docs/pages/advanced_topics/rendering_flow.rst | 86 +++++ docs/pages/advanced_topics/rendering_pipeline.rst | 157 +++++++++ docs/pages/advanced_topics/styling.rst | 320 ++++++++++++++++++ docs/pages/advanced_topics/unit_testing.rst | 125 +++++++ 10 files changed, 1431 insertions(+) create mode 100644 docs/pages/advanced_topics/architecture.rst create mode 100644 docs/pages/advanced_topics/asyncio.rst create mode 100644 docs/pages/advanced_topics/filters.rst create mode 100644 docs/pages/advanced_topics/index.rst create mode 100644 docs/pages/advanced_topics/input_hooks.rst create mode 100644 docs/pages/advanced_topics/key_bindings.rst create mode 100644 docs/pages/advanced_topics/rendering_flow.rst create mode 100644 docs/pages/advanced_topics/rendering_pipeline.rst create mode 100644 docs/pages/advanced_topics/styling.rst create mode 100644 docs/pages/advanced_topics/unit_testing.rst (limited to 'docs/pages/advanced_topics') diff --git a/docs/pages/advanced_topics/architecture.rst b/docs/pages/advanced_topics/architecture.rst new file mode 100644 index 0000000..c690104 --- /dev/null +++ b/docs/pages/advanced_topics/architecture.rst @@ -0,0 +1,97 @@ +.. _architecture: + + +Architecture +============ + +TODO: this is a little outdated. + +:: + + +---------------------------------------------------------------+ + | InputStream | + | =========== | + | - Parses the input stream coming from a VT100 | + | compatible terminal. Translates it into data input | + | and control characters. Calls the corresponding | + | handlers of the `InputStreamHandler` instance. | + | | + | e.g. Translate '\x1b[6~' into "Keys.PageDown", call | + | the `feed_key` method of `InputProcessor`. | + +---------------------------------------------------------------+ + | + v + +---------------------------------------------------------------+ + | InputStreamHandler | + | ================== | + | - Has a `Registry` of key bindings, it calls the | + | bindings according to the received keys and the | + | input mode. | + | | + | We have Vi and Emacs bindings. + +---------------------------------------------------------------+ + | + v + +---------------------------------------------------------------+ + | Key bindings | + | ============ | + | - Every key binding consists of a function that | + | receives an `Event` and usually it operates on | + | the `Buffer` object. (It could insert data or | + | move the cursor for example.) | + +---------------------------------------------------------------+ + | + | Most of the key bindings operate on a `Buffer` object, but + | they don't have to. They could also change the visibility + | of a menu for instance, or change the color scheme. + | + v + +---------------------------------------------------------------+ + | Buffer | + | ====== | + | - Contains a data structure to hold the current | + | input (text and cursor position). This class | + | implements all text manipulations and cursor | + | movements (Like e.g. cursor_forward, insert_char | + | or delete_word.) | + | | + | +-----------------------------------------------+ | + | | Document (text, cursor_position) | | + | | ================================ | | + | | Accessed as the `document` property of the | | + | | `Buffer` class. This is a wrapper around the | | + | | text and cursor position, and contains | | + | | methods for querying this data , e.g. to give | | + | | the text before the cursor. | | + | +-----------------------------------------------+ | + +---------------------------------------------------------------+ + | + | Normally after every key press, the output will be + | rendered again. This happens in the event loop of + | the `Application` where `Renderer.render` is called. + v + +---------------------------------------------------------------+ + | Layout | + | ====== | + | - When the renderer should redraw, the renderer | + | asks the layout what the output should look like. | + | - The layout operates on a `Screen` object that he | + | received from the `Renderer` and will put the | + | toolbars, menus, highlighted content and prompt | + | in place. | + | | + | +-----------------------------------------------+ | + | | Menus, toolbars, prompt | | + | | ======================= | | + | | | | + | +-----------------------------------------------+ | + +---------------------------------------------------------------+ + | + v + +---------------------------------------------------------------+ + | Renderer | + | ======== | + | - Calculates the difference between the last output | + | and the new one and writes it to the terminal | + | output. | + +---------------------------------------------------------------+ diff --git a/docs/pages/advanced_topics/asyncio.rst b/docs/pages/advanced_topics/asyncio.rst new file mode 100644 index 0000000..a692630 --- /dev/null +++ b/docs/pages/advanced_topics/asyncio.rst @@ -0,0 +1,30 @@ +.. _asyncio: + +Running on top of the `asyncio` event loop +========================================== + +.. note:: + + New in prompt_toolkit 3.0. (In prompt_toolkit 2.0 this was possible using a + work-around). + +Prompt_toolkit 3.0 uses asyncio natively. Calling ``Application.run()`` will +automatically run the asyncio event loop. + +If however you want to run a prompt_toolkit ``Application`` within an asyncio +environment, you have to call the ``run_async`` method, like this: + +.. code:: python + + from prompt_toolkit.application import Application + + async def main(): + # Define application. + application = Application( + ... + ) + + result = await application.run_async() + print(result) + + asyncio.get_event_loop().run_until_complete(main()) diff --git a/docs/pages/advanced_topics/filters.rst b/docs/pages/advanced_topics/filters.rst new file mode 100644 index 0000000..4788769 --- /dev/null +++ b/docs/pages/advanced_topics/filters.rst @@ -0,0 +1,169 @@ +.. _filters: + +Filters +======= + +Many places in `prompt_toolkit` require a boolean value that can change over +time. For instance: + +- to specify whether a part of the layout needs to be visible or not; +- or to decide whether a certain key binding needs to be active or not; +- or the ``wrap_lines`` option of + :class:`~prompt_toolkit.layout.BufferControl`; +- etcetera. + +These booleans 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.) + +The (abstract) base class is :class:`~prompt_toolkit.filters.Filter`, which +wraps an expression that takes no input and evaluates to a boolean. Getting the +state of a filter is done by simply calling it. + + +An example +---------- + +The most obvious way to create such a :class:`~prompt_toolkit.filters.Filter` +instance is by creating a :class:`~prompt_toolkit.filters.Condition` instance +from a function. For instance, the following condition will evaluate to +``True`` when the user is searching: + +.. code:: python + + from prompt_toolkit.application.current import get_app + from prompt_toolkit.filters import Condition + + is_searching = Condition(lambda: get_app().is_searching) + +A different way of writing this, is by using the decorator syntax: + +.. code:: python + + from prompt_toolkit.application.current import get_app + from prompt_toolkit.filters import Condition + + @Condition + def is_searching(): + return get_app().is_searching + +This filter can then be used in a key binding, like in the following snippet: + +.. code:: python + + from prompt_toolkit.key_binding import KeyBindings + + kb = KeyBindings() + + @kb.add('c-t', filter=is_searching) + def _(event): + # Do, something, but only when searching. + pass + +If we want to know the boolean value of this filter, we have to call it like a +function: + +.. code:: python + + print(is_searching()) + + +Built-in filters +---------------- + +There are many built-in filters, ready to use. All of them have a lowercase +name, because they represent the wrapped function underneath, and can be called +as a function. + +- :class:`~prompt_toolkit.filters.app.has_arg` +- :class:`~prompt_toolkit.filters.app.has_completions` +- :class:`~prompt_toolkit.filters.app.has_focus` +- :class:`~prompt_toolkit.filters.app.buffer_has_focus` +- :class:`~prompt_toolkit.filters.app.has_selection` +- :class:`~prompt_toolkit.filters.app.has_validation_error` +- :class:`~prompt_toolkit.filters.app.is_aborting` +- :class:`~prompt_toolkit.filters.app.is_done` +- :class:`~prompt_toolkit.filters.app.is_read_only` +- :class:`~prompt_toolkit.filters.app.is_multiline` +- :class:`~prompt_toolkit.filters.app.renderer_height_is_known` +- :class:`~prompt_toolkit.filters.app.in_editing_mode` +- :class:`~prompt_toolkit.filters.app.in_paste_mode` + +- :class:`~prompt_toolkit.filters.app.vi_mode` +- :class:`~prompt_toolkit.filters.app.vi_navigation_mode` +- :class:`~prompt_toolkit.filters.app.vi_insert_mode` +- :class:`~prompt_toolkit.filters.app.vi_insert_multiple_mode` +- :class:`~prompt_toolkit.filters.app.vi_replace_mode` +- :class:`~prompt_toolkit.filters.app.vi_selection_mode` +- :class:`~prompt_toolkit.filters.app.vi_waiting_for_text_object_mode` +- :class:`~prompt_toolkit.filters.app.vi_digraph_mode` + +- :class:`~prompt_toolkit.filters.app.emacs_mode` +- :class:`~prompt_toolkit.filters.app.emacs_insert_mode` +- :class:`~prompt_toolkit.filters.app.emacs_selection_mode` + +- :class:`~prompt_toolkit.filters.app.is_searching` +- :class:`~prompt_toolkit.filters.app.control_is_searchable` +- :class:`~prompt_toolkit.filters.app.vi_search_direction_reversed` + + +Combining filters +----------------- + +Filters can be chained with the ``&`` (AND) and ``|`` (OR) operators and +negated with the ``~`` (negation) operator. + +Some examples: + +.. code:: python + + from prompt_toolkit.key_binding import KeyBindings + from prompt_toolkit.filters import has_selection, has_selection + + kb = KeyBindings() + + @kb.add('c-t', filter=~is_searching) + def _(event): + " Do something, but not while searching. " + pass + + @kb.add('c-t', filter=has_search | has_selection) + def _(event): + " Do something, but only when searching or when there is a selection. " + pass + + +to_filter +--------- + +Finally, in many situations you want your code to expose an API that is able to +deal with both booleans as well as filters. For instance, when for most users a +boolean works fine because they don't need to change the value over time, while +some advanced users want to be able this value to a certain setting or event +that does changes over time. + +In order to handle both use cases, there is a utility called +:func:`~prompt_toolkit.filters.utils.to_filter`. + +This is a function that takes +either a boolean or an actual :class:`~prompt_toolkit.filters.Filter` +instance, and always returns a :class:`~prompt_toolkit.filters.Filter`. + +.. code:: python + + from prompt_toolkit.filters.utils import to_filter + + # In each of the following three examples, 'f' will be a `Filter` + # instance. + f = to_filter(True) + f = to_filter(False) + f = to_filter(Condition(lambda: True)) + f = to_filter(has_search | has_selection) diff --git a/docs/pages/advanced_topics/index.rst b/docs/pages/advanced_topics/index.rst new file mode 100644 index 0000000..4c4fcc9 --- /dev/null +++ b/docs/pages/advanced_topics/index.rst @@ -0,0 +1,18 @@ +.. _advanced_topics: + +Advanced topics +=============== + +.. toctree:: + :caption: Contents: + :maxdepth: 1 + + key_bindings + styling + filters + rendering_flow + asyncio + unit_testing + input_hooks + architecture + rendering_pipeline diff --git a/docs/pages/advanced_topics/input_hooks.rst b/docs/pages/advanced_topics/input_hooks.rst new file mode 100644 index 0000000..16c1bf5 --- /dev/null +++ b/docs/pages/advanced_topics/input_hooks.rst @@ -0,0 +1,41 @@ +.. _input_hooks: + + +Input hooks +=========== + +Input hooks are a tool for inserting an external event loop into the +prompt_toolkit event loop, so that the other loop can run as long as +prompt_toolkit (actually asyncio) is idle. This is used in applications like +`IPython `_, so that GUI toolkits can display their +windows while we wait at the prompt for user input. + +As a consequence, we will "trampoline" back and forth between two event loops. + +.. note:: + + This will use a :class:`~asyncio.SelectorEventLoop`, not the :class: + :class:`~asyncio.ProactorEventLoop` (on Windows) due to the way the + implementation works (contributions are welcome to make that work). + + +.. code:: python + + from prompt_toolkit.eventloop.inputhook import set_eventloop_with_inputhook + + def inputhook(inputhook_context): + # At this point, we run the other loop. This loop is supposed to run + # until either `inputhook_context.fileno` becomes ready for reading or + # `inputhook_context.input_is_ready()` returns True. + + # A good way is to register this file descriptor in this other event + # loop with a callback that stops this loop when this FD becomes ready. + # There is no need to actually read anything from the FD. + + while True: + ... + + set_eventloop_with_inputhook(inputhook) + + # Any asyncio code at this point will now use this new loop, with input + # hook installed. diff --git a/docs/pages/advanced_topics/key_bindings.rst b/docs/pages/advanced_topics/key_bindings.rst new file mode 100644 index 0000000..8b334fc --- /dev/null +++ b/docs/pages/advanced_topics/key_bindings.rst @@ -0,0 +1,388 @@ +.. _key_bindings: + +More about key bindings +======================= + +This page contains a few additional notes about key bindings. + + +Key bindings can be defined as follows by creating a +:class:`~prompt_toolkit.key_binding.KeyBindings` instance: + + +.. code:: python + + from prompt_toolkit.key_binding import KeyBindings + + bindings = KeyBindings() + + @bindings.add('a') + def _(event): + " Do something if 'a' has been pressed. " + ... + + + @bindings.add('c-t') + def _(event): + " Do something if Control-T has been pressed. " + ... + +.. note:: + + :kbd:`c-q` (control-q) and :kbd:`c-s` (control-s) are often captured by the + terminal, because they were used traditionally for software flow control. + When this is enabled, the application will automatically freeze when + :kbd:`c-s` is pressed, until :kbd:`c-q` is pressed. It won't be possible to + bind these keys. + + In order to disable this, execute the following command in your shell, or even + add it to your `.bashrc`. + + .. code:: + + stty -ixon + +Key bindings can even consist of a sequence of multiple keys. The binding is +only triggered when all the keys in this sequence are pressed. + +.. code:: python + + @bindings.add('a', 'b') + def _(event): + " Do something if 'a' is pressed and then 'b' is pressed. " + ... + +If the user presses only `a`, then nothing will happen until either a second +key (like `b`) has been pressed or until the timeout expires (see later). + + +List of special keys +-------------------- + +Besides literal characters, any of the following keys can be used in a key +binding: + ++-------------------+-----------------------------------------+ +| Name + Possible keys | ++===================+=========================================+ +| Escape | :kbd:`escape` | +| Shift + escape | :kbd:`s-escape` | ++-------------------+-----------------------------------------+ +| Arrows | :kbd:`left`, | +| | :kbd:`right`, | +| | :kbd:`up`, | +| | :kbd:`down` | ++-------------------+-----------------------------------------+ +| Navigation | :kbd:`home`, | +| | :kbd:`end`, | +| | :kbd:`delete`, | +| | :kbd:`pageup`, | +| | :kbd:`pagedown`, | +| | :kbd:`insert` | ++-------------------+-----------------------------------------+ +| Control+letter | :kbd:`c-a`, :kbd:`c-b`, :kbd:`c-c`, | +| | :kbd:`c-d`, :kbd:`c-e`, :kbd:`c-f`, | +| | :kbd:`c-g`, :kbd:`c-h`, :kbd:`c-i`, | +| | :kbd:`c-j`, :kbd:`c-k`, :kbd:`c-l`, | +| | | +| | :kbd:`c-m`, :kbd:`c-n`, :kbd:`c-o`, | +| | :kbd:`c-p`, :kbd:`c-q`, :kbd:`c-r`, | +| | :kbd:`c-s`, :kbd:`c-t`, :kbd:`c-u`, | +| | :kbd:`c-v`, :kbd:`c-w`, :kbd:`c-x`, | +| | | +| | :kbd:`c-y`, :kbd:`c-z` | ++-------------------+-----------------------------------------+ +| Control + number | :kbd:`c-1`, :kbd:`c-2`, :kbd:`c-3`, | +| | :kbd:`c-4`, :kbd:`c-5`, :kbd:`c-6`, | +| | :kbd:`c-7`, :kbd:`c-8`, :kbd:`c-9`, | +| | :kbd:`c-0` | ++-------------------+-----------------------------------------+ +| Control + arrow | :kbd:`c-left`, | +| | :kbd:`c-right`, | +| | :kbd:`c-up`, | +| | :kbd:`c-down` | ++-------------------+-----------------------------------------+ +| Other control | :kbd:`c-@`, | +| keys | :kbd:`c-\\`, | +| | :kbd:`c-]`, | +| | :kbd:`c-^`, | +| | :kbd:`c-_`, | +| | :kbd:`c-delete` | ++-------------------+-----------------------------------------+ +| Shift + arrow | :kbd:`s-left`, | +| | :kbd:`s-right`, | +| | :kbd:`s-up`, | +| | :kbd:`s-down` | ++-------------------+-----------------------------------------+ +| Control + Shift + | :kbd:`c-s-left`, | +| arrow | :kbd:`c-s-right`, | +| | :kbd:`c-s-up`, | +| | :kbd:`c-s-down` | ++-------------------+-----------------------------------------+ +| Other shift | :kbd:`s-delete`, | +| keys | :kbd:`s-tab` | ++-------------------+-----------------------------------------+ +| F-keys | :kbd:`f1`, :kbd:`f2`, :kbd:`f3`, | +| | :kbd:`f4`, :kbd:`f5`, :kbd:`f6`, | +| | :kbd:`f7`, :kbd:`f8`, :kbd:`f9`, | +| | :kbd:`f10`, :kbd:`f11`, :kbd:`f12`, | +| | | +| | :kbd:`f13`, :kbd:`f14`, :kbd:`f15`, | +| | :kbd:`f16`, :kbd:`f17`, :kbd:`f18`, | +| | :kbd:`f19`, :kbd:`f20`, :kbd:`f21`, | +| | :kbd:`f22`, :kbd:`f23`, :kbd:`f24` | ++-------------------+-----------------------------------------+ + +There are a couple of useful aliases as well: + ++-------------------+-------------------+ +| :kbd:`c-h` | :kbd:`backspace` | ++-------------------+-------------------+ +| :kbd:`c-@` | :kbd:`c-space` | ++-------------------+-------------------+ +| :kbd:`c-m` | :kbd:`enter` | ++-------------------+-------------------+ +| :kbd:`c-i` | :kbd:`tab` | ++-------------------+-------------------+ + +.. note:: + + Note that the supported keys are limited to what typical VT100 terminals + offer. Binding :kbd:`c-7` (control + number 7) for instance is not + supported. + + +Binding alt+something, option+something or meta+something +--------------------------------------------------------- + +Vt100 terminals translate the alt key into a leading :kbd:`escape` key. +For instance, in order to handle :kbd:`alt-f`, we have to handle +:kbd:`escape` + :kbd:`f`. Notice that we receive this as two individual keys. +This means that it's exactly the same as first typing :kbd:`escape` and then +typing :kbd:`f`. Something this alt-key is also known as option or meta. + +In code that looks as follows: + +.. code:: python + + @bindings.add('escape', 'f') + def _(event): + " Do something if alt-f or meta-f have been pressed. " + + +Wildcards +--------- + +Sometimes you want to catch any key that follows after a certain key stroke. +This is possible by binding the '' key: + +.. code:: python + + @bindings.add('a', '') + def _(event): + ... + +This will handle `aa`, `ab`, `ac`, etcetera. The key binding can check the +`event` object for which keys exactly have been pressed. + + +Attaching a filter (condition) +------------------------------ + +In order to enable a key binding according to a certain condition, we have to +pass it a :class:`~prompt_toolkit.filters.Filter`, usually a +:class:`~prompt_toolkit.filters.Condition` instance. (:ref:`Read more about +filters `.) + +.. code:: python + + from prompt_toolkit.filters import Condition + + @Condition + def is_active(): + " Only activate key binding on the second half of each minute. " + return datetime.datetime.now().second > 30 + + @bindings.add('c-t', filter=is_active) + def _(event): + # ... + pass + +The key binding will be ignored when this condition is not satisfied. + + +ConditionalKeyBindings: Disabling a set of key bindings +------------------------------------------------------- + +Sometimes you want to enable or disable a whole set of key bindings according +to a certain condition. This is possible by wrapping it in a +:class:`~prompt_toolkit.key_binding.ConditionalKeyBindings` object. + +.. code:: python + + from prompt_toolkit.key_binding import ConditionalKeyBindings + + @Condition + def is_active(): + " Only activate key binding on the second half of each minute. " + return datetime.datetime.now().second > 30 + + bindings = ConditionalKeyBindings( + key_bindings=my_bindings, + filter=is_active) + +If the condition is not satisfied, all the key bindings in `my_bindings` above +will be ignored. + + +Merging key bindings +-------------------- + +Sometimes you have different parts of your application generate a collection of +key bindings. It is possible to merge them together through the +:func:`~prompt_toolkit.key_binding.merge_key_bindings` function. This is +preferred above passing a :class:`~prompt_toolkit.key_binding.KeyBindings` +object around and having everyone populate it. + +.. code:: python + + from prompt_toolkit.key_binding import merge_key_bindings + + bindings = merge_key_bindings([ + bindings1, + bindings2, + ]) + + +Eager +----- + +Usually not required, but if ever you have to override an existing key binding, +the `eager` flag can be useful. + +Suppose that there is already an active binding for `ab` and you'd like to add +a second binding that only handles `a`. When the user presses only `a`, +prompt_toolkit has to wait for the next key press in order to know which +handler to call. + +By passing the `eager` flag to this second binding, we are actually saying that +prompt_toolkit shouldn't wait for longer matches when all the keys in this key +binding are matched. So, if `a` has been pressed, this second binding will be +called, even if there's an active `ab` binding. + +.. code:: python + + @bindings.add('a', 'b') + def binding_1(event): + ... + + @bindings.add('a', eager=True) + def binding_2(event): + ... + +This is mainly useful in order to conditionally override another binding. + +Asyncio coroutines +------------------ + +Key binding handlers can be asyncio coroutines. + +.. code:: python + + from prompt_toolkit.application import in_terminal + + @bindings.add('x') + async def print_hello(event): + """ + Pressing 'x' will print 5 times "hello" in the background above the + prompt. + """ + for i in range(5): + # Print hello above the current prompt. + async with in_terminal(): + print('hello') + + # Sleep, but allow further input editing in the meantime. + await asyncio.sleep(1) + +If the user accepts the input on the prompt, while this coroutine is not yet +finished , an `asyncio.CancelledError` exception will be thrown in this +coroutine. + + +Timeouts +-------- + +There are two timeout settings that effect the handling of keys. + +- ``Application.ttimeoutlen``: Like Vim's `ttimeoutlen` option. + When to flush the input (For flushing escape keys.) This is important on + terminals that use vt100 input. We can't distinguish the escape key from for + instance the left-arrow key, if we don't know what follows after "\x1b". This + little timer will consider "\x1b" to be escape if nothing did follow in this + time span. This seems to work like the `ttimeoutlen` option in Vim. + +- ``KeyProcessor.timeoutlen``: like Vim's `timeoutlen` option. + This can be `None` or a float. For instance, suppose that we have a key + binding AB and a second key binding A. If the uses presses A and then waits, + we don't handle this binding yet (unless it was marked 'eager'), because we + don't know what will follow. This timeout is the maximum amount of time that + we wait until we call the handlers anyway. Pass `None` to disable this + timeout. + + +Recording macros +---------------- + +Both Emacs and Vi mode allow macro recording. By default, all key presses are +recorded during a macro, but it is possible to exclude certain keys by setting +the `record_in_macro` parameter to `False`: + +.. code:: python + + @bindings.add('c-t', record_in_macro=False) + def _(event): + # ... + pass + + +Creating new Vi text objects and operators +------------------------------------------ + +We tried very hard to ship prompt_toolkit with as many as possible Vi text +objects and operators, so that text editing feels as natural as possible to Vi +users. + +If you wish to create a new text object or key binding, that is actually +possible. Check the `custom-vi-operator-and-text-object.py` example for more +information. + + +Handling SIGINT +--------------- + +The SIGINT Unix signal can be handled by binding ````. For instance: + +.. code:: python + + @bindings.add('') + def _(event): + # ... + pass + +This will handle a SIGINT that was sent by an external application into the +process. Handling control-c should be done by binding ``c-c``. (The terminal +input is set to raw mode, which means that a ``c-c`` won't be translated into a +SIGINT.) + +For a ``PromptSession``, there is a default binding for ```` that +corresponds to ``c-c``: it will exit the prompt, raising a +``KeyboardInterrupt`` exception. + + +Processing `.inputrc` +--------------------- + +GNU readline can be configured using an `.inputrc` configuration file. This file +contains key bindings as well as certain settings. Right now, prompt_toolkit +doesn't support `.inputrc`, but it should be possible in the future. diff --git a/docs/pages/advanced_topics/rendering_flow.rst b/docs/pages/advanced_topics/rendering_flow.rst new file mode 100644 index 0000000..52acfbd --- /dev/null +++ b/docs/pages/advanced_topics/rendering_flow.rst @@ -0,0 +1,86 @@ +.. _rendering_flow: + +The rendering flow +================== + +Understanding the rendering flow is important for understanding how +:class:`~prompt_toolkit.layout.Container` and +:class:`~prompt_toolkit.layout.UIControl` objects interact. We will demonstrate +it by explaining the flow around a +:class:`~prompt_toolkit.layout.BufferControl`. + +.. note:: + + A :class:`~prompt_toolkit.layout.BufferControl` is a + :class:`~prompt_toolkit.layout.UIControl` for displaying the content of a + :class:`~prompt_toolkit.buffer.Buffer`. A buffer is the object that holds + any editable region of text. Like all controls, it has to be wrapped into a + :class:`~prompt_toolkit.layout.Window`. + +Let's take the following code: + +.. code:: python + + from prompt_toolkit.enums import DEFAULT_BUFFER + from prompt_toolkit.layout.containers import Window + from prompt_toolkit.layout.controls import BufferControl + from prompt_toolkit.buffer import Buffer + + b = Buffer(name=DEFAULT_BUFFER) + Window(content=BufferControl(buffer=b)) + +What happens when a :class:`~prompt_toolkit.renderer.Renderer` objects wants a +:class:`~prompt_toolkit.layout.Container` to be rendered on a certain +:class:`~prompt_toolkit.layout.screen.Screen`? + +The visualisation happens in several steps: + +1. The :class:`~prompt_toolkit.renderer.Renderer` calls the + :meth:`~prompt_toolkit.layout.Container.write_to_screen` method + of a :class:`~prompt_toolkit.layout.Container`. + This is a request to paint the layout in a rectangle of a certain size. + + The :class:`~prompt_toolkit.layout.Window` object then requests + the :class:`~prompt_toolkit.layout.UIControl` to create a + :class:`~prompt_toolkit.layout.UIContent` instance (by calling + :meth:`~prompt_toolkit.layout.UIControl.create_content`). + The user control receives the dimensions of the window, but can still + decide to create more or less content. + + Inside the :meth:`~prompt_toolkit.layout.UIControl.create_content` + method of :class:`~prompt_toolkit.layout.UIControl`, there are several + steps: + + 2. First, the buffer's text is passed to the + :meth:`~prompt_toolkit.lexers.Lexer.lex_document` method of a + :class:`~prompt_toolkit.lexers.Lexer`. This returns a function which + for a given line number, returns a "formatted text list" for that line + (that's a list of ``(style_string, text)`` tuples). + + 3. This list is passed through a list of + :class:`~prompt_toolkit.layout.processors.Processor` objects. + Each processor can do a transformation for each line. + (For instance, they can insert or replace some text, highlight the + selection or search string, etc...) + + 4. The :class:`~prompt_toolkit.layout.UIControl` returns a + :class:`~prompt_toolkit.layout.UIContent` instance which + generates such a token lists for each lines. + +The :class:`~prompt_toolkit.layout.Window` receives the +:class:`~prompt_toolkit.layout.UIContent` and then: + +5. It calculates the horizontal and vertical scrolling, if applicable + (if the content would take more space than what is available). + +6. The content is copied to the correct absolute position + :class:`~prompt_toolkit.layout.screen.Screen`, as requested by the + :class:`~prompt_toolkit.renderer.Renderer`. While doing this, the + :class:`~prompt_toolkit.layout.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 +:class:`~prompt_toolkit.layout.Window`, then it is not requested +from the :class:`~prompt_toolkit.layout.UIContent`. And from there, the line is +not passed through the processors or even asked from the +:class:`~prompt_toolkit.lexers.Lexer`. diff --git a/docs/pages/advanced_topics/rendering_pipeline.rst b/docs/pages/advanced_topics/rendering_pipeline.rst new file mode 100644 index 0000000..6b38ba5 --- /dev/null +++ b/docs/pages/advanced_topics/rendering_pipeline.rst @@ -0,0 +1,157 @@ +The rendering pipeline +====================== + +This document is an attempt to describe how prompt_toolkit applications are +rendered. It's a complex but logical process that happens more or less after +every key stroke. We'll go through all the steps from the point where the user +hits a key, until the character appears on the screen. + + +Waiting for user input +---------------------- + +Most of the time when a prompt_toolkit application is running, it is idle. It's +sitting in the event loop, waiting for some I/O to happen. The most important +kind of I/O we're waiting for is user input. So, within the event loop, we have +one file descriptor that represents the input device from where we receive key +presses. The details are a little different between operating systems, but it +comes down to a selector (like select or epoll) which waits for one or more +file descriptor. The event loop is then responsible for calling the appropriate +feedback when one of the file descriptors becomes ready. + +It is like that when the user presses a key: the input device becomes ready for +reading, and the appropriate callback is called. This is the `read_from_input` +function somewhere in `application.py`. It will read the input from the +:class:`~prompt_toolkit.input.Input` object, by calling +:meth:`~prompt_toolkit.input.Input.read_keys`. + + +Reading the user input +---------------------- + +The actual reading is also operating system dependent. For instance, on a Linux +machine with a vt100 terminal, we read the input from the pseudo terminal +device, by calling `os.read`. This however returns a sequence of bytes. There +are two difficulties: + +- The input could be UTF-8 encoded, and there is always the possibility that we + receive only a portion of a multi-byte character. +- vt100 key presses consist of multiple characters. For instance the "left + arrow" would generate something like ``\x1b[D``. It could be that when we + read this input stream, that at some point we only get the first part of such + a key press, and we have to wait for the rest to arrive. + +Both problems are implemented using state machines. + +- The UTF-8 problem is solved using `codecs.getincrementaldecoder`, which is an + object in which we can feed the incoming bytes, and it will only return the + complete UTF-8 characters that we have so far. The rest is buffered for the + next read operation. +- Vt100 parsing is solved by the + :class:`~prompt_toolkit.input.vt100_parser.Vt100Parser` state machine. The + state machine itself is implemented using a generator. We feed the incoming + characters to the generator, and it will call the appropriate callback for + key presses once they arrive. One thing here to keep in mind is that the + characters for some key presses are a prefix of other key presses, like for + instance, escape (``\x1b``) is a prefix of the left arrow key (``\x1b[D``). + So for those, we don't know what key is pressed until more data arrives or + when the input is flushed because of a timeout. + +For Windows systems, it's a little different. Here we use Win32 syscalls for +reading the console input. + + +Processing the key presses +-------------------------- + +The ``Key`` objects that we receive are then passed to the +:class:`~prompt_toolkit.key_binding.key_processor.KeyProcessor` for matching +against the currently registered and active key bindings. + +This is another state machine, because key bindings are linked to a sequence of +key presses. We cannot call the handler until all of these key presses arrive +and until we're sure that this combination is not a prefix of another +combination. For instance, sometimes people bind ``jj`` (a double ``j`` key +press) to ``esc`` in Vi mode. This is convenient, but we want to make sure that +pressing ``j`` once only, followed by a different key will still insert the +``j`` character as usual. + +Now, there are hundreds of key bindings in prompt_toolkit (in ptpython, right +now we have 585 bindings). This is mainly caused by the way that Vi key +bindings are generated. In order to make this efficient, we keep a cache of +handlers which match certain sequences of keys. + +Of course, key bindings also have filters attached for enabling/disabling them. +So, if at some point, we get a list of handlers from that cache, we still have +to discard the inactive bindings. Luckily, many bindings share exactly the same +filter, and we have to check every filter only once. + +:ref:`Read more about key bindings ...` + + +The key handlers +---------------- + +Once a key sequence is matched, the handler is called. This can do things like +text manipulation, changing the focus or anything else. + +After the handler is called, the user interface is invalidated and rendered +again. + + +Rendering the user interface +---------------------------- + +The rendering is pretty complex for several reasons: + +- We have to compute the dimensions of all user interface elements. Sometimes + they are given, but sometimes this requires calculating the size of + :class:`~prompt_toolkit.layout.UIControl` objects. +- It needs to be very efficient, because it's something that happens on every + single key stroke. +- We should output as little as possible on stdout in order to reduce latency + on slow network connections and older terminals. + + +Calculating the total UI height +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Unless the application is a full screen application, we have to know how much +vertical space is going to be consumed. The total available width is given, but +the vertical space is more dynamic. We do this by asking the root +:class:`~prompt_toolkit.layout.Container` object to calculate its preferred +height. If this is a :class:`~prompt_toolkit.layout.VSplit` or +:class:`~prompt_toolkit.layout.HSplit` then this involves recursively querying +the child objects for their preferred widths and heights and either summing it +up, or taking maximum values depending on the actual layout. +In the end, we get the preferred height, for which we make sure it's at least +the distance from the cursor position to the bottom of the screen. + + +Painting to the screen +^^^^^^^^^^^^^^^^^^^^^^ + +Then we create a :class:`~prompt_toolkit.layout.screen.Screen` object. This is +like a canvas on which user controls can paint their content. The +:meth:`~prompt_toolkit.layout.Container.write_to_screen` method of the root +`Container` is called with the screen dimensions. This will call recursively +:meth:`~prompt_toolkit.layout.Container.write_to_screen` methods of nested +child containers, each time passing smaller dimensions while we traverse what +is a tree of `Container` objects. + +The most inner containers are :class:`~prompt_toolkit.layout.Window` objects, +they will do the actual painting of the +:class:`~prompt_toolkit.layout.UIControl` to the screen. This involves line +wrapping the `UIControl`'s text and maybe scrolling the content horizontally or +vertically. + + +Rendering to stdout +^^^^^^^^^^^^^^^^^^^ + +Finally, when we have painted the screen, this needs to be rendered to stdout. +This is done by taking the difference of the previously rendered screen and the +new one. The algorithm that we have is heavily optimized to compute this +difference as quickly as possible, and call the appropriate output functions of +the :class:`~prompt_toolkit.output.Output` back-end. At the end, it will +position the cursor in the right place. diff --git a/docs/pages/advanced_topics/styling.rst b/docs/pages/advanced_topics/styling.rst new file mode 100644 index 0000000..1169547 --- /dev/null +++ b/docs/pages/advanced_topics/styling.rst @@ -0,0 +1,320 @@ +.. _styling: + +More about styling +================== + +This page will attempt to explain in more detail how to use styling in +prompt_toolkit. + +To some extent, it is very similar to how `Pygments `_ +styling works. + + +Style strings +------------- + +Many user interface controls, like :class:`~prompt_toolkit.layout.Window` +accept a ``style`` argument which can be used to pass the formatting as a +string. For instance, we can select a foreground color: + +- ``"fg:ansired"`` (ANSI color palette) +- ``"fg:ansiblue"`` (ANSI color palette) +- ``"fg:#ffaa33"`` (hexadecimal notation) +- ``"fg:darkred"`` (named color) + +Or a background color: + +- ``"bg:ansired"`` (ANSI color palette) +- ``"bg:#ffaa33"`` (hexadecimal notation) + +Or we can add one of the following flags: + +- ``"bold"`` +- ``"italic"`` +- ``"underline"`` +- ``"blink"`` +- ``"reverse"`` (reverse foreground and background on the terminal.) +- ``"hidden"`` + +Or their negative variants: + +- ``"nobold"`` +- ``"noitalic"`` +- ``"nounderline"`` +- ``"noblink"`` +- ``"noreverse"`` +- ``"nohidden"`` + +All of these formatting options can be combined as well: + +- ``"fg:ansiyellow bg:black bold underline"`` + +The style string can be given to any user control directly, or to a +:class:`~prompt_toolkit.layout.Container` object from where it will propagate +to all its children. A style defined by a parent user control can be overridden +by any of its children. The parent can for instance say ``style="bold +underline"`` where a child overrides this style partly by specifying +``style="nobold bg:ansired"``. + +.. note:: + + These styles are actually compatible with + `Pygments `_ styles, with additional support for + `reverse` and `blink`. Further, we ignore flags like `roman`, `sans`, + `mono` and `border`. + +The following ANSI colors are available (both for foreground and background): + +.. code:: + + # Low intensity, dark. (One or two components 0x80, the other 0x00.) + ansiblack, ansired, ansigreen, ansiyellow, ansiblue + ansimagenta, 'ansicyan, ansigray + + # High intensity, bright. + ansibrightblack, ansibrightred, ansibrightgreen, ansibrightyellow + ansibrightblue, ansibrightmagenta, ansibrightcyan, ansiwhite + +In order to know which styles are actually used in an application, it is +possible to call :meth:`~Application.get_used_style_strings`, when the +application is done. + + +Class names +----------- + +Like we do for web design, it is not a good habit to specify all styling +inline. Instead, we can attach class names to UI controls and have a style +sheet that refers to these class names. The +:class:`~prompt_toolkit.styles.Style` can be passed as an argument to the +:class:`~prompt_toolkit.application.Application`. + +.. code:: python + + from prompt_toolkit.layout import VSplit, Window + from prompt_toolkit.styles import Style + + layout = VSplit([ + Window(BufferControl(...), style='class:left'), + HSplit([ + Window(BufferControl(...), style='class:top'), + Window(BufferControl(...), style='class:bottom'), + ], style='class:right') + ]) + + style = Style([ + ('left', 'bg:ansired'), + ('top', 'fg:#00aaaa'), + ('bottom', 'underline bold'), + ]) + +It is possible to add multiple class names to an element. That way we'll +combine the styling for these class names. Multiple classes can be passed by +using a comma separated list, or by using the ``class:`` prefix twice. + +.. code:: python + + Window(BufferControl(...), style='class:left,bottom'), + Window(BufferControl(...), style='class:left class:bottom'), + +It is possible to combine class names and inline styling. The order in which +the class names and inline styling is specified determines the order of +priority. In the following example for instance, we'll take first the style of +the "header" class, and then override that with a red background color. + +.. code:: python + + Window(BufferControl(...), style='class:header bg:red'), + + +Dot notation in class names +--------------------------- + +The dot operator has a special meaning in a class name. If we write: +``style="class:a.b.c"``, then this will actually expand to the following: +``style="class:a class:a.b class:a.b.c"``. + +This is mainly added for `Pygments `_ lexers, which +specify "Tokens" like this, but it's useful in other situations as well. + + +Multiple classes in a style sheet +--------------------------------- + +A style sheet can be more complex as well. We can for instance specify two +class names. The following will underline the left part within the header, or +whatever has both the class "left" and the class "header" (the order doesn't +matter). + +.. code:: python + + style = Style([ + ('header left', 'underline'), + ]) + + +If you have a dotted class, then it's required to specify the whole path in the +style sheet (just typing ``c`` or ``b.c`` doesn't work if the class is +``a.b.c``): + +.. code:: python + + style = Style([ + ('a.b.c', 'underline'), + ]) + +It is possible to combine this: + +.. code:: python + + style = Style([ + ('header body left.text', 'underline'), + ]) + + +Evaluation order of rules in a style sheet +------------------------------------------ + +The style is determined as follows: + +- First, we concatenate all the style strings from the root control through all + the parents to the child in one big string. (Things at the right take + precedence anyway.) + + E.g: ``class:body bg:#aaaaaa #000000 class:header.focused class:left.text.highlighted underline`` + +- Then we go through this style from left to right, starting from the default + style. Inline styling is applied directly. + + If we come across a class name, then we generate all combinations of the + class names that we collected so far (this one and all class names to the + left), and for each combination which includes the new class name, we look + for matching rules in our style sheet. All these rules are then applied + (later rules have higher priority). + + If we find a dotted class name, this will be expanded in the individual names + (like ``class:left class:left.text class:left.text.highlighted``), and all + these are applied like any class names. + +- Then this final style is applied to this user interface element. + + +Using a dictionary as a style sheet +----------------------------------- + +The order of the rules in a style sheet is meaningful, so typically, we use a +list of tuples to specify the style. But is also possible to use a dictionary +as a style sheet. This makes sense for Python 3.6, where dictionaries remember +their ordering. An ``OrderedDict`` works as well. + +.. code:: python + + from prompt_toolkit.styles import Style + + style = Style.from_dict({ + 'header body left.text': 'underline', + }) + + +Loading a style from Pygments +----------------------------- + +`Pygments `_ has a slightly different notation for +specifying styles, because it maps styling to Pygments "Tokens". A Pygments +style can however be loaded and used as follows: + +.. code:: python + + from prompt_toolkit.styles.pygments import style_from_pygments_cls + from pygments.styles import get_style_by_name + + style = style_from_pygments_cls(get_style_by_name('monokai')) + + +Merging styles together +----------------------- + +Multiple :class:`~prompt_toolkit.styles.Style` objects can be merged together as +follows: + +.. code:: python + + from prompt_toolkit.styles import merge_styles + + style = merge_styles([ + style1, + style2, + style3 + ]) + + +Color depths +------------ + +There are four different levels of color depths available: + ++--------+-----------------+-----------------------------+---------------------------------+ +| 1 bit | Black and white | ``ColorDepth.DEPTH_1_BIT`` | ``ColorDepth.MONOCHROME`` | ++--------+-----------------+-----------------------------+---------------------------------+ +| 4 bit | ANSI colors | ``ColorDepth.DEPTH_4_BIT`` | ``ColorDepth.ANSI_COLORS_ONLY`` | ++--------+-----------------+-----------------------------+---------------------------------+ +| 8 bit | 256 colors | ``ColorDepth.DEPTH_8_BIT`` | ``ColorDepth.DEFAULT`` | ++--------+-----------------+-----------------------------+---------------------------------+ +| 24 bit | True colors | ``ColorDepth.DEPTH_24_BIT`` | ``ColorDepth.TRUE_COLOR`` | ++--------+-----------------+-----------------------------+---------------------------------+ + +By default, 256 colors are used, because this is what most terminals support +these days. If the ``TERM`` enviroment variable is set to ``linux`` or +``eterm-color``, then only ANSI colors are used, because of these terminals. The 24 +bit true color output needs to be enabled explicitely. When 4 bit color output +is chosen, all colors will be mapped to the closest ANSI color. + +Setting the default color depth for any prompt_toolkit application can be done +by setting the ``PROMPT_TOOLKIT_COLOR_DEPTH`` environment variable. You could +for instance copy the following into your `.bashrc` file. + +.. code:: shell + + # export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_1_BIT + export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_4_BIT + # export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_8_BIT + # export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_24_BIT + +An application can also decide to set the color depth manually by passing a +:class:`~prompt_toolkit.output.ColorDepth` value to the +:class:`~prompt_toolkit.application.Application` object: + +.. code:: python + + from prompt_toolkit.output.color_depth import ColorDepth + + app = Application( + color_depth=ColorDepth.ANSI_COLORS_ONLY, + # ... + ) + + +Style transformations +--------------------- + +Prompt_toolkit supports a way to apply certain transformations to the styles +near the end of the rendering pipeline. This can be used for instance to change +certain colors to improve the rendering in some terminals. + +One useful example is the +:class:`~prompt_toolkit.styles.AdjustBrightnessStyleTransformation` class, +which takes `min_brightness` and `max_brightness` as arguments which by default +have 0.0 and 1.0 as values. In the following code snippet, we increase the +minimum brightness to improve rendering on terminals with a dark background. + +.. code:: python + + from prompt_toolkit.styles import AdjustBrightnessStyleTransformation + + app = Application( + style_transformation=AdjustBrightnessStyleTransformation( + min_brightness=0.5, # Increase the minimum brightness. + max_brightness=1.0, + ) + # ... + ) diff --git a/docs/pages/advanced_topics/unit_testing.rst b/docs/pages/advanced_topics/unit_testing.rst new file mode 100644 index 0000000..01498c9 --- /dev/null +++ b/docs/pages/advanced_topics/unit_testing.rst @@ -0,0 +1,125 @@ +.. _unit_testing: + +Unit testing +============ + +Testing user interfaces is not always obvious. Here are a few tricks for +testing prompt_toolkit applications. + + +`PosixPipeInput` and `DummyOutput` +---------------------------------- + +During the creation of a prompt_toolkit +:class:`~prompt_toolkit.application.Application`, we can specify what input and +output device to be used. By default, these are output objects that correspond +with `sys.stdin` and `sys.stdout`. In unit tests however, we want to replace +these. + +- For the input, we want a "pipe input". This is an input device, in which we + can programatically send some input. It can be created with + :func:`~prompt_toolkit.input.create_pipe_input`, and that return either a + :class:`~prompt_toolkit.input.posix_pipe.PosixPipeInput` or a + :class:`~prompt_toolkit.input.win32_pipe.Win32PipeInput` depending on the + platform. +- For the output, we want a :class:`~prompt_toolkit.output.DummyOutput`. This is + an output device that doesn't render anything. We don't want to render + anything to `sys.stdout` in the unit tests. + +.. note:: + + Typically, we don't want to test the bytes that are written to + `sys.stdout`, because these can change any time when the rendering + algorithm changes, and are not so meaningful anyway. Instead, we want to + test the return value from the + :class:`~prompt_toolkit.application.Application` or test how data + structures (like text buffers) change over time. + +So we programmatically feed some input to the input pipe, have the key +bindings process the input and then test what comes out of it. + +In the following example we use a +:class:`~prompt_toolkit.shortcuts.PromptSession`, but the same works for any +:class:`~prompt_toolkit.application.Application`. + +.. code:: python + + from prompt_toolkit.shortcuts import PromptSession + from prompt_toolkit.input import create_pipe_input + from prompt_toolkit.output import DummyOutput + + def test_prompt_session(): + with create_pipe_input() as inp: + inp.send_text("hello\n") + session = PromptSession( + input=inp, + output=DummyOutput(), + ) + + result = session.prompt() + + assert result == "hello" + +In the above example, don't forget to send the `\\n` character to accept the +prompt, otherwise the :class:`~prompt_toolkit.application.Application` will +wait forever for some more input to receive. + +Using an :class:`~prompt_toolkit.application.current.AppSession` +---------------------------------------------------------------- + +Sometimes it's not convenient to pass input or output objects to the +:class:`~prompt_toolkit.application.Application`, and in some situations it's +not even possible at all. +This happens when these parameters are not passed down the call stack, through +all function calls. + +An easy way to specify which input/output to use for all applications, is by +creating an :class:`~prompt_toolkit.application.current.AppSession` with this +input/output and running all code in that +:class:`~prompt_toolkit.application.current.AppSession`. This way, we don't +need to inject it into every :class:`~prompt_toolkit.application.Application` +or :func:`~prompt_toolkit.shortcuts.print_formatted_text` call. + +Here is an example where we use +:func:`~prompt_toolkit.application.create_app_session`: + +.. code:: python + + from prompt_toolkit.application import create_app_session + from prompt_toolkit.shortcuts import print_formatted_text + from prompt_toolkit.output import DummyOutput + + def test_something(): + with create_app_session(output=DummyOutput()): + ... + print_formatted_text('Hello world') + ... + +Pytest fixtures +--------------- + +In order to get rid of the boilerplate of creating the input, the +:class:`~prompt_toolkit.output.DummyOutput`, and the +:class:`~prompt_toolkit.application.current.AppSession`, we create a +single fixture that does it for every test. Something like this: + +.. code:: python + + import pytest + from prompt_toolkit.application import create_app_session + from prompt_toolkit.input import create_pipe_input + from prompt_toolkit.output import DummyOutput + + @pytest.fixture(autouse=True, scope="function") + def mock_input(): + with create_pipe_input() as pipe_input: + with create_app_session(input=pipe_input, output=DummyOutput()): + yield pipe_input + + +Type checking +------------- + +Prompt_toolkit 3.0 is fully type annotated. This means that if a +prompt_toolkit application is typed too, it can be verified with mypy. This is +complementary to unit tests, but also great for testing for correctness. -- cgit v1.2.3