This article was written in collaboration with Alexander Brassel.
Recently, we open sourced Superconsole, a Text-based User Interface (TUI) library written in Rust. The reason for creating Superconsole was to power the future iteration of the Buck build system, giving user-friendly information about what a build is doing. Perhaps the best way to understand what Superconsole can do is to give a sneak preview of the upcoming next version of Buck:
Our design sweet spot was for programs that need some sort of graphic console rendering to the terminal, but don’t require the rendering to be interactive. It needs to be easy to control color, font and spatial location. Buck, Bazel, Top and Cargo are all fantastic examples of tools that could use Superconsole for their TUI.
Our experiences with the original version of Buck gave us a checklist of what features were important:
With those features in mind, let’s delve into how Superconsole works.
A standard terminal produces lines one after another that scroll off as they exceed the number of lines in the terminal. More advanced TUIs like Top take control of the whole terminal area. For Superconsole, we wanted programs to have both features —lines scrolling at the top, and a fixed area at the bottom.
Superconsole takes the scratch space of the terminal and divides it in two. At the bottom, there is a canvas, which is drawn over at each render. Above it, there is an emitted space. Ad-hoc messages may be printed to the emitted space, where they are queued until the next render. This design allows TUI implementers to create a space that renders in place relative to the bottom of the terminal (as seen above), as well as scrolling messages above it. This approach makes it easy to emit one-time, in progress information, while also displaying on-going data.
The canvas area is constructed with a single top-level component. A component is a struct which implements the Component trait, as shown below.
Given information about the current render tick, a component draws an output which is essentially a list of strings. This output is rendered onto the terminal in the canvas section.
Components are easily composable, with most components themselves being composed of smaller components. To that end, we provide standard components for things like splitting, bordering and alignment. The included starter components and the ease at which components can be composed and developed makes it easy to develop robust rendering systems.
It is fairly common for rendering and state management logic to become intertwined. This often leads to programs that are difficult to maintain and debug. In an effort to avoid this, Superconsole deliberately uses Rust’s concept of immutable references to incentivize users to write components that are immutable over renders.
The standard pattern of Superconsole is to have a mutable State object that is updated by the program, and then the rendering of a component reads information from that State. The updates and rendering are separated.
Graphical programs are notoriously difficult to test. TUIs are no exception, and have been known to present difficulties when it comes to verifying correct behavior. Superconsole offers strong properties to make this just a bit easier.
To begin with, all output from a Component is in the form of Vec<Line >, where Line is a Vec<Span >. Each span represents a single region of text with homogenous coloration and emphasis (bold, italic, etc.). Therefore, unit tests can be written for components that simply verify that the list of emitted lines are the same. There’s no need to try and figure out what was written afterwards.
Equality for lines is defined based on appearance whenever possible. This means that, for example, coloration on blank spaces is ignored when comparing equality. This helps significantly with testing, as only the visual aspects must be compared, rather than the exact structure of the output.
Furthermore, due to the separation of state and logic (as discussed in the previous section), state injection into tests is incredibly easy—simply provide a different state when invoking a Component’s render method.
There is no rendering loop in the Superconsole library. Instead, render and emit methods are exposed to the consumer. This allows users to exercise fine-grain control over frame rate and render-worthy events. It also avoids the need for multi-threading abstractions to communicate state information.
Emit methods queue text to be rendered in the emitted method, above the canvas. Render calls cause the emitted queue to be flushed and the top-level component to be re-rendered with the provided state information.
It’s easy to build and run a Superconsole! You can try it for yourself.
git clone https://github.com/facebookincubator/superconsole.git cd superconsole cargo build cargo run -example cargo
Superconsole is a powerful library for developers seeking to quickly iterate on non-interactive TUIs. It provides testability, incentivizes separation of rendering and state, and promotes componentization and composability. We hope that others find it as useful as we on the Buck team have.