Skip to content

A gentle guide to Haskell's Brick

Brick is a library for creating beautiful terminal user interfaces (TUIs). Resources to learn it include the User Guide, code examples, and this rather outdated but still helpful video. I found the video the most approachable, and I think Brick is still in need of a friendly guide - so here is my attempt.

Concepts

Brick is composed of a few key concepts, which are composed together under its App type: State, Widgets, and Events.

State is a data type which your UI depends upon and transforms to produce widgets. It can be whatever you want it to be. For example, a file browsing application would have the current directory inside its state.

Widgets are the things which get rendered to screen. They are instructions that tell Brick what to draw on screen. For example, the str function produces a widget which draws a piece of text when rendered. vBox produces a widget from a list of widgets, rendering them in a vertical column. You can create your own custom widgets too.

Events are caused by the user doing something (interacting with your UI), and your app processes these events in order to do something and change what is rendered. There can be mouse events, keyboard events, and custom events of your own devising.

The App Type

Brick requires an instance of its App type before we can do anything with it. It takes three arguments - a state, a custom event type (you can leave it as a type variable if you don't have one), and a Resource Name type (see #Resource Names).

It contains five functions:

  1. appDraw - the drawing function - that is, a function which takes your application state and produces a list of widgets to render. State in, user interface out. The widget list is in order, from topmost to bottommost. This function is called when the application first starts, and also after every event that is processed by appHandleEvent. However, only the parts of the screen that have changed are actually updated for efficiency.

  2. appChooseCursor - a function for choosing which cursor position to render. The rendering process for a Widget may return information about where that widget would like to place the cursor. For example, a text editor will need to report a cursor position. However, since a Widget may be a composite of many such cursor-placing widgets, we have to have a way of choosing which of the reported cursor positions, if any, is the one we actually want to honour.

    Brick.Main provides various convenience functions to make cursor selection easy in common cases: - neverShowCursor: never show any cursor. - showFirstCursor: always show the first cursor request given; good for applications with only one cursor-placing widget. - showCursorNamed: show the cursor with the specified resource name or show no cursor if the name was not associated with any requested cursor position.

  3. appHandleEvent - the function which processes (handles) any events that come in. It decides how to modify the application state in response to an event. It returns an EventM (event monad) computation. The EventM monad has two parameters: the resource name and the application state.

    Once the event handler has performed any relevant state updates, it can also indicate what should happen once the event handler has finished executing. By default, after an event handler has completed, Brick will redraw the screen with the application state (by calling appDraw) and wait for the next input event. However, there are two other options:

    • Brick.Main.halt - stop the application. The application stops processing any more events and control returns to the function that started it.
    • Brick.Main.continueWithoutRedraw - continue the application, but do not redraw the screen using the new state before waiting for another input event. This is mainly useful for performance tuning.

    The EventM monad is a transformer around IO so I/O is possible in this monad by using liftIO. Keep in mind, however, that event handlers should execute as quickly as possible to avoid introducing screen redraw latency. Consider using background threads to work asynchronously when handling an event would otherwise cause redraw latency.

  4. appStartEvent - the function which runs once on application start. It receives the initial application state, and is for any setup logic.

  5. appAttrMap - the function which returns an attribute map. The attribute map assigns attributes to elements of the interface (widgets), for example colour. Attributes are the styles to the content of the widgets. Rather than specifying specific attributes when drawing a widget (e.g. red-on-black text) we specify an attribute name that is an abstract name for the kind of thing we are drawing, e.g. "keyword" or "e-mail address." We then provide an attribute map which maps those attribute names to actual attributes.

    We run the application described by the instance of the app type by passing it to Brick.Main.defaultMain or Brick.Main.customMain (along with an initial state).

Resource Names

Resource Names are names used to differentiate between various entities: cursors, viewports, rendering extents, and mouse events. Assigning unique (!) names to these resource types allows us to distinguish between events based on the part of the interface to which an event is related.

Your application must provide some type of name. For simple applications that don't make use of resource names, you may use (). But if your application has more than one named resource, you must provide a type capable of assigning a unique name to every resource that needs one.