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
dataclassdecorator 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.