I’m going to give a short introduction to brick,
a Haskell library for building terminal user interfaces. So far I’ve used
implement Conway’s Game of Life and a
Tetris clone. I’ll explain the basics,
walk through an example snake application, and then explain some more complicated scenarios.
The first thing I’ll say is that this package has some of the most impressive documentation and resources, which makes it easy to figure out pretty much anything you need to do. I’ll try to make this useful, but I imagine if you’re reading this then it is mostly being used as a reference in addition to the existing resources:
- Demo programs (clone down to explore the code and run them locally)
- User guide
- Haddock docs
- Google group
The basic idea
brick is very declarative. Once your base application logic is in place, the interface
is generally built by two functions: drawing and handling events. The drawing function
takes your app state
s and produces the visuals
takes your app state, an event (e.g. user presses the
'm' key), and produces the resulting
app state. That’s pretty much it.
Structure of the app
The library makes it easy to separate the concerns of your application and the interface;
I like to have a module with all of the core business logic that exports the core state of the app
and functions for modifying it, and then have an interface module that just handles the setup,
drawing, and handling events. So let’s just use the
simple stack template and add two
and our dependencies to
Since this tutorial is about
brick, I’ll elide most of the implementation details
of the actual game, but here are some of the key types and scaffolding:
All of this is pretty self-explanatory, with the possible exception of lenses if you
haven’t seen them. At first glance they may seem complicated (and the underlying
theory arguably is), but using them as getters and setters is very straightforward.
So, if you are following along because you are writing a terminal app like this, I’d
recommend using them, but they are not required to use
Here are the core functions for playing the game:
To start, we need to determine what our
App s e n type parameters are.
This will completely describe the interface application and be passed to one of the library’s
main style functions for execution. Note that
s is the app state,
e is an event type,
n is a resource name. The
e is abstracted so that we can provide custom events.
n is usually a custom sum type called
Name which allows us to name particular viewports.
This is important so that we can keep track of where the user currently has focus, such as
typing in one of two textboxes; however, for this simple snake game we don’t need
to worry about that.
In simpler cases, the state
s can directly coincide with a core datatype such as our
In many cases however, it will be necessary to
wrap the core state within the ui state
s to keep track of things
that are interface specific (more on this later).
Let’s write out our app definition and leave some undefined functions:
So far I’ve only used
brick to make games which need to be redrawn as time passes,
with or without user input. This requires using
Brick.customMain with that
event type, and opening a forked process to
forever feed that event type into
the channel. Since this is a common scenario, there is a
Brick.BChan module that makes
this pretty quick:
We do need to import
customMain allows us to specify
IO Vty.Graphics.Vty handle, but we’re only customizing the existence of the
BChan Tick. The app is now bootstrapped, and all we need to do is
theMap (handles styling).
Handling events is largely straightforward, and can be very clean when your underlying application logic is taken care of in a core module. All we do is essentially map events to the proper state modifiers.
It’s probably obvious, but
continue will continue execution with the supplied state value,
which is then drawn. We can also
halt to stop execution, which will essentially
finish the evaluation of our
customMain and result in
IO Game, where the resulting
game is the last value that we supplied to
Drawing is fairly simple as well but can require a good amount of code to position things how you want them. I like to break up the visual space into regions with drawing functions for each one.
This will center the overall interface (
put the stats and grid widgets horizontally side by side (
and separate them by a 2-character width (
padRight (Pad 2)).
Let’s move forward with the stats column:
I’m throwing in that
hLimit 11 to prevent the widget greediness caused by the outer
I’m also using
vBox to show some other options of aligning widgets;
a list of widgets vertically and horizontally, respectfully. They can be thought of as folds
over the binary
The score is straightforward, but it is the first border in this tutorial. Borders are well documented in the border demo and the Haddocks for that matter.
We also only show the “game over” widget if the game is actually over.
In that case, we are rendering the string widget with the
gameOverAttr attribute name.
Attribute names are basically type safe names that we can assign to widgets to apply
predetermined styles, similar to assigning a class name to a div in HTML and defining the
CSS styles for that class elsewhere.
Attribute names implement
IsString, so they are easy to construct with
Now for the main event:
There’s actually nothing new here! We’ve already covered all the
brick functions necessary
to draw the grid. My approach to grids is to render a square cell widget
cw with different
colors depending on the cell state. The easiest way to draw a colored square is to stick
two characters side by side. If we assign an attribute with a matching foreground and background,
then it doesn’t matter what the two characters are (provided that they aren’t some crazy
Unicode characters that might render to an unexpected size). However, if we want empty cells
to render with the same color as the user’s default background color, then spaces are a good choice.
Finally, we’ll define the attribute map:
Another thing to mention is that the attributes form a hierarchy and
can be combined in a parent-child relationship via
mappend. I haven’t
actually used this feature, but it does sound quite handy. For a more
detailed discussion see the
One difficult problem I encountered was implementing a variable speed in the GoL.
I could have just used the same approach above with the minimum thread delay
(corresponding to the maximum speed) and counted
Tick events, only issuing an actual
step in the game when the modular count of
Ticks reached an amount corresponding to the
current game speed, but that’s kind of an ugly approach.
Instead, I reached out to the author and he advised me to use a
TVar within the app state.
I had never used
TVar, but it’s pretty easy!
tv <- atomically $ newTVar (value :: a) creates a new mutable reference to a value
TVar a, and returns it in
IO. In this case
value is an
which represents the delay between game steps.
Then in the forked process, we read the delay from the
TVar reference and use that
to space out the calls to
writeBChan chan Tick.
I store that same
tv :: TVar Int in the brick app state, so that the user can change
brick let’s you build TUIs very quickly. I was able to write
snake along with this
tutorial within a few hours. More complicated interfaces can be tougher, but if you can
successfully separate the interface and core functionality, you’ll have an easier time
tacking on the frontend.
Lastly, let me remind you to look in the demo programs to figure stuff out, as many scenarios are covered throughout them.