Interactivity

This page will teach you how to get user input and make your application react to it.

Picking an I/O Method

To enable interactivity, you need some way to get input from the user. Functui has built in support for multiple ways of getting input, but if you don’t have any strong preferences it is strongly recommended to use the functui.io.raw module which is the in-house solution for cross-platform input and output. The rest of this page will assume that you are using that module, but even if you are not, there are still much to take away from this page.

See also

You can get an overview over all I/O methods in the I/O Overview document.

Using functui.io.raw for Input

Before you start polling for input, it is a good idea to do some terminal configuration. For example, hiding the mouse cursor, enabling mouse events, disabling line wrapping, enabling xterm escape codes on windows, etc. This can be done with the terminal function that returns a context manager that can be used with a with statement.

from functui.io.raw import terminal

with terminal() as term:
    # once inside the `with` block, we may wait for input.
    event = term.block_until_input()

# once we leave the `with` block, all terminal configuration gets reset.

A context manager is used here so that when your application exits, all of the terminal configuration is reverted automatically.

As demonstrated in the above example, the block_until_input() method is used to get input. That method returns an InputEvent object, which is best explained with the API documentation below.

class functui.classes.InputEvent(key_event, mouse_position_event)[source]
key_event: str | None

Represents both key and mouse button events. Is set to None if no key or mouse button event was emitted.

mouse_position_event: Coordinate | None

New mouse position. Is set to None if mouse position was not changed.

The mouse_position_event is just a coordinate representing the position of the mouse. key_event returns a string that represents user’s input, for example "ctrl+c", "a" or "left mouse"`. The notable thing here is that many modifier key combinations are not supported due to terminal weirdness. For example ctrl+i is missing because it emits the same control code as when pressing tab. Or even worse alt+ combinations are not supported at all due to needing to rely on timings to distinguish them from pressing esc followed by some other key. All of those input quirks are documented in the String Key Codes Specification document.

Immediate Mode Rendering

When it comes to rendering an interactive application, functui takes the most stupidly simple approach, namely redrawing the whole application every frame (also known as “immediate mode” ui).

This approach may seem wasteful, which it may be to some extent, but the pros of this approach vastly outweigh the cons. Firstly a lot of ui’s require redrawing everything every frame anyway, think of the system profiler htop. Secondly, even if you don’t redraw every frame, functui’s node’s rendering functions are cached, meaning given the same layout structure, the rendering function won’t need to calculate everything again.

But most importantly, immediate mode ui simplifies your code, by a lot! Since your layout gets redrawn every frame, it becomes a direct representation of your program’s state. No need to worry about updating the ui when you change some variable, your ui will reflect the variable’s state automatically! And with immediate mode ui, there is no need to use callbacks or implement reactive programming patterns since there just isn’t anything to react to.

Applying the Immediate Mode Approach

Now when we know that we will be redrawing the ui every frame, structuring the application becomes easy. We can put the application rendering code into a while loop and just re-render every time we get some input. Also we can use get_terminal_size() method to make the layout respond to terminal size and use display_to_result() to render the layout.

with terminal() as term:
    while True:
        layout = ...

        # render
        res = layout_to_result(layout, term.get_terminal_size())
        term.display_result(res)

        # wait for input
        event = term.block_until_input()

        # update (do something with input)
        if event.key_event == "ctrl+c":
            break # exit program

Tip

As you may have noticed, we used layout_to_result() instead of layout_to_str() which was taught in the introduction. The former function returns a Result object which contains all the draw commands required to render the layout. This intermediate step (converting to result and then rendering instead of just rendering) is very useful because the Result object can store additional data apart from draw commands. For example, the size and position of nodes, which is needed to implement mouse hover and such.

The Elm Architecture

The above example turns out to be very similar to “the elm architecture” which is a common software architecture pattern for immediate mode ui’s, and is the recommended way to structure your functui applications. The elm architectures separates your program into three parts which all have a unique responsibility.

  • The Model A class that contains all of your programs mutable state. It is recommended to implements it with the dataclass decorator to avoid writing boilerplate code.

from dataclasses import dataclass
from functui.nav import NavState

@dataclass
class Model:
    nav: NavState =  NavState() # for keyboard and mouse navigation (more on that later)
    foo: int
    bar: str
    ... # and other variables you need in your program
  • The View Function - A function that renders the layout based on the model’s state. Here it is important that it does not change anything about the model. This way, your rendering code is separated from your logic which makes your program much simpler.

def view(m: Model):
    layout = text(str(m.foo)) | border
    return layout
  • The Update Function - A function that does all the logic and changes the model.

def update(m: Model, event: InputEvent):
    if event.key_event == "ctrl+k"
        ... # do something

Then the update and view functions are called every frame to run your application. Putting it all together looks something like this:

from functui.io.raw import terminal

m = Model()

def view(m: Model):
    return text("hello world")

def update(m: Model, event: InputEvent):
    ...


with terminal() as term:
    while True:
        # render and fit terminal
        res = layout_to_result(view(m), term.get_terminal_size())
        term.display_result(res)

        # wait for input
        event = term.block_until_input()

        # update
        if event.key_event == "ctrl+c":
            break # exit program
        update(event, res, m)

Tip

The elm architecture is really similar to the example under the Applying the Immediate Mode Approach section. It is encouraged to look for the differences between those two examples.

Mouse and Keyboard Navigation?

Now that you know how to get user input and how to structure you’re code, the time has come to work with actual widgets like buttons, scrollable lists etc. Those are enabled by the functui.nav module discussed in the next document.