Table of Contents
- Introduction
- Conventions
- Compiling Brick Applications
- The
App
Type - How Widgets and Rendering Work
- How Attributes Work
- Attribute Themes
- Wide Character Support and the
TextWidth
class - Extents
- Paste Support
- Mouse Support
- Viewports
- Input Forms
- Customizable Keybindings
- Joining Borders
- The Rendering Cache
- Implementing Custom Widgets
brick
is a Haskell library for programming terminal user interfaces.
Its main goal is to make terminal user interface development as painless
and as direct as possible. brick
builds on vty; vty provides
the terminal input and output interface and drawing primitives,
while brick
builds on those to provide a high-level application
abstraction, combinators for expressing user interface layouts, and
infrastructure for handling events.
This documentation is intended to provide a high-level overview of the library's design along with guidance for using it, but details on specific functions can be found in the Haddock documentation.
The process of writing an application using brick
entails writing
two important functions:
- A drawing function that turns your application state into a specification of how your interface should be drawn, and
- An event handler that takes your application state and an input event and decides whether to change the state or quit the program.
We write drawing functions in brick
using an extensive set of
primitives and combinators to place text on the screen, set its
attributes (e.g. foreground color), and express layout constraints (e.g.
padding, centering, box layouts, scrolling viewports, etc.).
These functions get packaged into an App
structure that we hand off
to the brick
library's main event loop. We'll cover that in detail
in The App Type.
brick
can be installed in the "usual way," either by installing
the latest Hackage release or by cloning the GitHub repository and
building locally.
To install from Hackage:
$ cabal update $ cabal install brick
To clone and build locally:
$ git clone https://github.com/jtdaugherty/brick.git $ cd brick $ cabal new-build
Your package will need some dependencies:
brick
,vty >= 6.0
, andvty-crossplatform
orvty-unix
orvty-windows
, depending on which platform(s) your application supports.
brick
includes a large collection of feature-specific demonstration
programs. These programs are not built by default but can be built by
passing the demos
flag to cabal install
, e.g.:
$ cabal install brick -f demos
brick
has some API conventions worth knowing about as you read this
documentation and as you explore the library source and write your own
programs.
- Use of microlens packages:
brick
uses themicrolens
family of packages internally and also exposes lenses for many types in the library. However, if you prefer not to use the lens interface in your program, all lens interfaces have non-lens equivalents exported by the same module. In general, the "L
" suffix on something tells you it is a lens; the name without the "L
" suffix is the non-lens version. You can get by without usingbrick
's lens interface but your life will probably be much more pleasant if you use lenses to modify your application state once it becomes sufficiently complex (see appHandleEvent: Handling Events and Event Handlers for Component State). - Attribute names: some modules export attribute names (see How
Attributes Work) associated with user interface elements. These tend
to end in an "
Attr
" suffix (e.g.borderAttr
). In addition, hierarchical relationships between attributes are documented in Haddock documentation. - Use of qualified Haskell identifiers: in this document, where
sensible, I will use fully-qualified identifiers whenever I mention
something for the first time or whenever I use something that is
not part of
brick
. Use of qualified names is not intended to produce executable examples, but rather to guide you in writing yourimport
statements.
Brick applications must be compiled with the threaded RTS using the GHC
-threaded
option.
To use the library we must provide it with a value of type
Brick.Main.App
. This type is a record type whose fields perform
various functions:
data App s e n =
App { appDraw :: s -> [Widget n]
, appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)
, appHandleEvent :: BrickEvent n e -> EventM n s ()
, appStartEvent :: EventM n s ()
, appAttrMap :: s -> AttrMap
}
The App
type is parameterized over three types. These type variables
will appear in the signatures of many library functions and types. They
are:
- The application state type
s
: the type of data that will evolve over the course of the application's execution. Your application will provide the library with its starting value and event handling will transform it as the program executes. When abrick
application exits, the final application state will be returned. - The event type
e
: the type of custom application events that your application will need to produce and handle inappHandleEvent
. All applications will be provided with events from the underlyingvty
library, such as keyboard events or resize events; this type variable indicates the type of additional events the application will need. For more details, see Using Your Own Event Type. - The resource name type
n
: during application execution we sometimes need a way to refer to rendering state, such as the space taken up by a given widget, the state for a scrollable viewport, a mouse click, or a cursor position. For these situations we need a unique handle called a resource name. The typen
specifies the name type the application will use to identify these bits of state produced and managed by the renderer. The resource name type must be provided by your application; for more details, see Resource Names.
The various fields of App
will be described in the sections below.
To run an App
, we pass it to Brick.Main.defaultMain
or
Brick.Main.customMain
along with an initial application state value:
main :: IO ()
main = do
let app = App { ... }
initialState = ...
finalState <- defaultMain app initialState
-- Use finalState and exit
The customMain
function is for more advanced uses; for details see
Using Your Own Event Type.
The value of appDraw
is a function that turns the current
application state into a list of layers of type Widget
, listed
topmost first, that will make up the interface. Each Widget
gets
turned into a vty
layer and the resulting layers are drawn to the
terminal.
The Widget
type is the type of drawing instructions. The body of
your drawing function will use one or more drawing functions to build or
transform Widget
values to describe your interface. These
instructions will then be executed with respect to three things:
- The size of the terminal: the size of the terminal determines how many
Widget
values behave. For example, fixed-sizeWidget
values such as text strings behave the same under all conditions (and get cropped if the terminal is too small) but layout combinators such asBrick.Widgets.Core.vBox
orBrick.Widgets.Center.center
use the size of the terminal to determine how to lay other widgets out. See How Widgets and Rendering Work. - The application's attribute map (
appAttrMap
): drawing functions requesting the use of attributes cause the attribute map to be consulted. See How Attributes Work. - The state of scrollable viewports: the state of any scrollable viewports on the previous drawing will be considered. For more details, see Viewports.
The appDraw
function is called when the event loop begins to draw
the application as it initially appears. It is also called right after
an event is processed by appHandleEvent
. Even though the function
returns a specification of how to draw the entire screen, the underlying
vty
library goes to some trouble to efficiently update only the
parts of the screen that have changed so you don't need to worry about
this.
The most important module providing drawing functions is
Brick.Widgets.Core
. Beyond that, any module in the Brick.Widgets
namespace provides specific kinds of functionality.
The value of appHandleEvent
is a function that decides how to modify
the application state as a result of an event:
appHandleEvent :: BrickEvent n e -> EventM n s ()
appHandleEvent
is responsible for deciding how to change the state
based on incoming events. The single parameter to the event handler is
the event to be handled. Its type variables n
and e
correspond
to the resource name type and event type of your application,
respectively, and must match the corresponding types in App
and
EventM
.
The EventM
monad is parameterized on the resource name type
n
and your application's state type s
. The EventM
monad
is a state monad over s
, so one way to access and modify your
application's state in an event handler is to use the MonadState
type class and associated operations from the mtl
package. The
recommended approach, however, is to use the lens operations from the
microlens-mtl
package with lenses to perform concise state updates.
We'll cover this topic in more detail in Event Handlers for Component
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
: halt the event loop. The application state as it exists after the event handler completes is returned to the caller ofdefaultMain
orcustomMain
.Brick.Main.continueWithoutRedraw
: continue executing the event loop, but do not redraw the screen using the new state before waiting for another input event. This is faster than the default continue behavior since it doesn't redraw the screen; it just leaves up the previous screen contents. This function is only useful when you know that your event handler's state change(s) won't cause anything on the screen to change. Use this only when you are certain that no redraw of the screen is needed and when you are trying to address a performance problem. (See also The Rendering Cache for details on how to deal with rendering performance issues.)
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.
EventM
is also used to make scrolling requests to the renderer (see
Viewports), obtain named extents (see Extents), and other duties.
The top-level appHandleEvent
handler is responsible for managing
the application state, but it also needs to be able to update the state
associated with UI components such as those that come with Brick.
For example, consider an application that uses Brick's built-in text
editor from Brick.Widgets.Edit
. The built-in editor is similar to
the main application in that it has three important elements:
- The editor state of type
Editor t n
: this stores the editor's contents, cursor position, etc. - The editor's drawing function,
renderEditor
: this is responsible for drawing the editor in the UI. - The editor's event handler,
handleEditorEvent
: this is responsible for updating the editor's contents and cursor position in response to key events.
To use the built-in editor, the application must:
- Embed an
Editor t n
somewhere in the application states
, - Render the editor's state at the appropriate place in
appDraw
withrenderEditor
, and - Dispatch events to the editor in the
appHandleEvent
withhandleEditorEvent
.
An example application state using an editor might look like this:
data MyState n = MyState { _editor :: Editor Text n }
makeLenses ''MyState
This declares the MyState
type with an Editor
contained within
it and uses Template Haskell to generate a lens, editor
, to allow us
to easily update the editor state in our event handler.
To dispatch events to the editor
we'd start by writing the
application event handler:
handleEvent :: BrickEvent n e -> EventM n MyState ()
handleEvent e = do
...
But there's a problem: handleEditorEvent
's type indicates that it
can only run over a state of type Editor t n
, but our handler runs
on MyState
. Specifically, handleEditorEvent
has this type:
handleEditorEvent :: BrickEvent n e -> EventM n (Editor t n) ()
This means that to use handleEditorEvent
, it must be composed
into the application's event handler, but since the state types s
and Editor t n
do not match, we need a way to compose these event
handlers. There are two ways to do this:
- Use
Lens.Micro.Mtl.zoom
from themicrolens-mtl
package (re-exported byBrick.Types
for convenience). This function is required when you want to change the state type to a field embedded in your application state using a lens. For example:
handleEvent :: BrickEvent n e -> EventM n MyState ()
handleEvent e = do
zoom editor $ handleEditorEvent e
- Use
Brick.Types.nestEventM
: this function lets you provide a state value and runEventM
using that state. The followingnestEventM
example is equivalent to thezoom
example above:
import Lens.Micro (_1)
import Lens.Micro.Mtl (use, (.=))
handleEvent :: BrickEvent n e -> EventM n MyState ()
handleEvent e = do
editorState <- use editor
(newEditorState, ()) <- nestEventM editorState $ do
handleEditorEvent e
editor .= newEditorState
The zoom
function, together with lenses for your application state's
fields, is by far the best way to manage your state in EventM
. As
you can see from the examples above, the zoom
approach avoids a lot
of boilerplate. The nestEventM
approach is provided in cases where
the state that you need to mutate is not easily accessed by zoom
.
Finally, if you prefer to avoid the use of lenses, you can always use
the MonadState
API to get, put, and modify your state. Keep in
mind that the MonadState
approach will still require the use of
nestEventM
when events scoped to widget states such as Editor
need to be handled.
Since we often need to communicate application-specific events beyond
Vty input events to the event handler, brick supports embedding your
application's custom events in the stream of BrickEvent
-s that
your handler will receive. The type of these events is the type e
mentioned in BrickEvent n e
and App s e n
.
Note: ordinarily your application will not have its own custom event
type, so you can leave this type unused (e.g. App MyState e MyName
)
or just set it to unit (App MyState () MyName
).
Here's an example of using a custom event type. Suppose that you'd like to be able to handle counter events in your event handler. First we define the counter event type:
data CounterEvent = Counter Int
With this type declaration we can now use counter events in our app by
using the application type App s CounterEvent n
. To handle these
events we'll just need to check for AppEvent
values in the event
handler:
myEvent :: BrickEvent n CounterEvent -> EventM n s ()
myEvent (AppEvent (Counter i)) = ...
The next step is to actually generate our custom events and
inject them into the brick
event stream so they make it to the
event handler. To do that we need to create a BChan
for our
custom events, provide that BChan
to brick
, and then send
our events over that channel. Once we've created the channel with
Brick.BChan.newBChan
, we provide it to brick
with
customMain
instead of defaultMain
:
main :: IO ()
main = do
eventChan <- Brick.BChan.newBChan 10
let buildVty = Graphics.Vty.CrossPlatform.mkVty Graphics.Vty.Config.defaultConfig
initialVty <- buildVty
finalState <- customMain initialVty buildVty
(Just eventChan) app initialState
-- Use finalState and exit
The customMain
function lets us have control over how the vty
library is initialized and how brick
gets custom events to give to
our event handler. customMain
is the entry point into brick
when
you need to use your own event type as shown here. In this example we're
using mkVty
provided by the vty-crossplatform
package, which
provides build-time support for both Unix and Windows. If you prefer,
you can use either the vty-unix
package or the vty-windows
package directly instead if you only want to support one platform.
With all of this in place, sending our custom events to the event handler is straightforward:
counterThread :: Brick.BChan.BChan CounterEvent -> IO ()
counterThread chan = do
Brick.BChan.writeBChan chan $ Counter 1
A BChan
, or bounded channel, can hold a limited number of
items before attempts to write new items will block. In the call to
newBChan
above, the created channel has a capacity of 10 items.
Use of a bounded channel ensures that if the program cannot process
events quickly enough then there is a limit to how much memory will
be used to store unprocessed events. Thus the chosen capacity should
be large enough to buffer occasional spikes in event handling latency
without inadvertently blocking custom event producers. Each application
will have its own performance characteristics that determine the best
bound for the event channel. In general, consider the performance of
your event handler when choosing the channel capacity and design event
producers so that they can block if the channel is full.
When an application starts, it may be desirable to perform some of
the duties typically only possible when an event has arrived, such as
setting up initial scrolling viewport state. Since such actions can only
be performed in EventM
and since we do not want to wait until the
first event arrives to do this work in appHandleEvent
, the App
type provides appStartEvent
function for this purpose:
appStartEvent :: EventM n s ()
This function is a handler action to run on the initial application
state. This function is invoked once and only once, at application
startup. This might be a place to make initial viewport scroll requests
or make changes to the Vty environment. You will probably just want
to use return ()
as the implementation of this function for most
applications.
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 honor.
To decide which cursor placement to use, or to decide not to show one at
all, we set the App
type's appChooseCursor
function:
appChooseCursor :: s -> [CursorLocation n] -> Maybe (CursorLocation n)
The event loop renders the interface and collects the
Brick.Types.CursorLocation
values produced by the rendering process
and passes those, along with the current application state, to this
function. Using your application state (to track which text input box
is "focused," say) you can decide which of the locations to return or
return Nothing
if you do not want to show a cursor.
Many widgets in the rendering process can request cursor placements, but
it is up to our application to determine which one (if any) should be
used. Since we can only show at most a single cursor in the terminal,
we need to decide which location to show. One way is by looking at the
resource name contained in the cursorLocationName
field. The name
value associated with a cursor location will be the name used to request
the cursor position with Brick.Widgets.Core.showCursor
.
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.
For example, this widget requests a cursor placement on the first
"o
" in "foo
" associated with the cursor name CustomName
:
data MyName = CustomName
let w = showCursor CustomName (Brick.Types.Location (1, 0))
(Brick.Widgets.Core.str "foobar")
The event handler for this application would use MyName
as its
resource name type n
and would be able to pattern-match on
CustomName
to match cursor requests when this widget is rendered:
myApp =
App { ...
, appChooseCursor = \_ -> showCursorNamed CustomName
}
See the next section for more information on using names.
We saw above in appChooseCursor: Placing the Cursor that resource names are used to describe cursor locations. Resource names are also used to name other kinds of resources:
- viewports (see Viewports)
- rendering extents (see Extents)
- mouse events (see Mouse Support)
Assigning 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.
Resource names can be assigned to any of the resource types mentioned
above, but some resource types--viewports, extents, the render cache,
and cursor locations--form separate resource namespaces. So, for
example, the same name can be assigned to both a viewport and an extent,
since the brick
API provides access to viewports and extents using
separate APIs and data structures. However, if the same name is used for
two resources of the same kind, it is undefined which of those you'll
be getting access to when you go to use one of those resources in your
event handler.
For example, if the same name is assigned to two viewports:
data Name = Viewport1
ui :: Widget Name
ui = (viewport Viewport1 Vertical $ str "Foo") <+>
(viewport Viewport1 Vertical $ str "Bar") <+>
then in EventM
when we attempt to scroll the viewport Viewport1
we don't know which of the two uses of Viewport1
will be affected:
let vp = viewportScroll Viewport1
vScrollBy vp 1
The solution is to ensure that for a given resource type (in this case viewport), a unique name is assigned in each use.
data Name = Viewport1 | Viewport2
ui :: Widget Name
ui = (viewport Viewport1 Vertical $ str "Foo") <+>
(viewport Viewport2 Vertical $ str "Bar") <+>
In brick
we use an attribute map to assign attributes to elements
of the interface. 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. This approach lets us:
- Change the attributes at runtime, letting the user change the attributes of any element of the application arbitrarily without forcing anyone to build special machinery to make this configurable;
- Write routines to load saved attribute maps from disk;
- Provide modular attribute behavior for third-party components, where we would not want to have to recompile third-party code just to change attributes, and where we would not want to have to pass in attribute arguments to third-party drawing functions.
This lets us put the attribute mapping for an entire app, regardless of use of third-party widgets, in one place.
To create a map we use Brick.AttrMap.attrMap
, e.g.,
App { ...
, appAttrMap = const $ attrMap Graphics.Vty.defAttr [(someAttrName, fg blue)]
}
To use an attribute map, we specify the App
field appAttrMap
as
the function to return the current attribute map each time rendering
occurs. This function takes the current application state, so you may
choose to store the attribute map in your application state. You may
also choose not to bother with that and to just set appAttrMap = const
someMap
.
To draw a widget using an attribute name in the map, use
Brick.Widgets.Core.withAttr
. For example, this draws a string with a
blue
background:
let w = withAttr blueBg $ str "foobar"
blueBg = attrName "blueBg"
myMap = attrMap defAttr [ (blueBg, Brick.Util.bg Graphics.Vty.blue)
]
For complete details on how attribute maps and attribute names work, see
the Haddock documentation for the Brick.AttrMap
module. See also
How Attributes Work.
When brick
renders a Widget
, the widget's rendering routine is
evaluated to produce a vty
Image
of the widget. The widget's
rendering routine runs with some information called the rendering
context that contains:
- The size of the area in which to draw things
- The name of the current attribute to use to draw things
- The map of attributes to use to look up attribute names
- The active border style to use when drawing borders
The most important element in the rendering context is the rendering
area: This part of the context tells the widget being drawn how many
rows and columns are available for it to consume. When rendering begins,
the widget being rendered (i.e. a layer returned by an appDraw
function) gets a rendering context whose rendering area is the size of
the terminal. This size information is used to let widgets take up that
space if they so choose. For example, a string "Hello, world!" will
always take up one row and 13 columns, but the string "Hello, world!"
centered will always take up one row and all available columns.
How widgets use space when rendered is described in two pieces of
information in each Widget
: the widget's horizontal and vertical
growth policies. These fields have type Brick.Types.Size
and can
have the values Fixed
and Greedy
. Note that these values are
merely descriptive hints about the behavior of the rendering function,
so it's important that they accurately describe the widget's use of
space.
A widget advertising a Fixed
size in a given dimension is a widget
that will always consume the same number of rows or columns no
matter how many it is given. Widgets can advertise different
vertical and horizontal growth policies for example, the
Brick.Widgets.Center.hCenter
function centers a widget and is
Greedy
horizontally and defers to the widget it centers for vertical
growth behavior.
These size policies govern the box layout algorithm that is at
the heart of every non-trivial drawing specification. When we use
Brick.Widgets.Core.vBox
and Brick.Widgets.Core.hBox
to
lay things out (or use their binary synonyms <=>
and <+>
,
respectively), the box layout algorithm looks at the growth policies of
the widgets it receives to determine how to allocate the available space
to them.
For example, imagine that the terminal window is currently 10 rows high and 50 columns wide. We wish to render the following widget:
let w = (str "Hello," <=> str "World!")
Rendering this to the terminal will result in "Hello," and "World!" underneath it, with 8 rows unoccupied by anything. But if we wished to render a vertical border underneath those strings, we would write:
let w = (str "Hello," <=> str "World!" <=> vBorder)
Rendering this to the terminal will result in "Hello," and "World!"
underneath it, with 8 rows remaining occupied by vertical border
characters ("|
") one column wide. The vertical border widget is
designed to take up however many rows it was given, but rendering the
box layout algorithm has to be careful about rendering such Greedy
widgets because they won't leave room for anything else. Since the box
widget cannot know the sizes of its sub-widgets until they are rendered,
the Fixed
widgets get rendered and their sizes are used to determine
how much space is left for Greedy
widgets.
When using widgets it is important to understand their horizontal and
vertical space behavior by knowing their Size
values. Those should
be made clear in the Haddock documentation.
The rendering context's specification of available space will also govern how widgets get cropped, since all widgets are required to render to an image no larger than the rendering context specifies. If they do, they will be forcibly cropped.
If you'd like to use a Greedy
widget but want to limit how much
space it consumes, you can turn it into a Fixed
widget by using
one of the limiting combinators, Brick.Widgets.Core.hLimit
and
Brick.Widgets.Core.vLimit
. These combinators take widgets and turn
them into widgets with a Fixed
size (in the relevant dimension) and
run their rendering functions in a modified rendering context with a
restricted rendering area.
For example, the following will center a string in 30 columns, leaving room for something to be placed next to it as the terminal width changes:
let w = hLimit 30 $ hCenter $ str "Hello, world!"
The rendering context contains an attribute map (see How Attributes
Work and appAttrMap: Managing Attributes) which is used to look up
attribute names from the drawing specification. The map originates from
Brick.Main.appAttrMap
and can be manipulated on a per-widget basis
using Brick.Widgets.Core.updateAttrMap
.
Widgets in the Brick.Widgets.Border
module draw border characters
(horizontal, vertical, and boxes) between and around other widgets. To
ensure that widgets across your application share a consistent visual
style, border widgets consult the rendering context's active border
style, a value of type Brick.Widgets.Border.Style
, to get the
characters used to draw borders.
The default border style is Brick.Widgets.Border.Style.unicode
. To
change border styles, use the Brick.Widgets.Core.withBorderStyle
combinator to wrap a widget and change the border style it uses when
rendering. For example, this will use the ascii
border style instead
of unicode
:
let w = withBorderStyle Brick.Widgets.Border.Style.ascii $
Brick.Widgets.Border.border $ str "Hello, world!"
By default, borders in adjacent widgets do not connect to each other. This can lead to visual oddities, for example, when horizontal borders are drawn next to vertical borders by leaving a small gap like this:
│─
You can request that adjacent borders connect to each other with
Brick.Widgets.Core.joinBorders
. Two borders drawn with the
same attribute and border style, and both under the influence of
joinBorders
, will produce a border like this instead:
├─
See Joining Borders for further details.
In addition to letting us map names to attributes, attribute maps provide hierarchical attribute inheritance: a more specific attribute derives any properties (e.g. background color) that it does not specify from more general attributes in hierarchical relationship to it, letting us customize only the parts of attributes that we want to change without having to repeat ourselves.
For example, this draws a string with a foreground color of white
on
a background color of blue
:
let w = withAttr specificAttr $ str "foobar"
generalAttr = attrName "general"
specificAttr = attrName "general" <> attrName "specific"
myMap = attrMap defAttr [ (generalAttr, bg blue)
, (specificAttr, fg white)
]
When drawing a widget, Brick keeps track of the current attribute it
is using to draw to the screen. The attribute it tracks is specified
by its attribute name, which is a hierarchical name referring to the
attribute in the attribute map. In the example above, the map contains
two attribute names: generalAttr
and specificAttr
. Both names
are made up of segments: general
is the first segment for both
names, and specific
is the second segment for specificAttr
.
This tells Brick that specificAttr
is a more specialized version
of generalAttr
. We'll see below how that affects the resulting
attributes that Brick uses.
When it comes to drawing something on the screen with either of these
attributes, Brick looks up the desired attribute name in the map
and uses the result to draw to the screen. In the example above,
withAttr
is used to tell Brick that when drawing str "foobar"
,
the attribute specificAttr
should be used. Brick looks that name
up in the attribute map and finds a match: an attribute with a white
foreground color. However, what happens next is important: Brick then
looks up the more general attribute name derived from specificAttr
,
which it gets by removing the last segment in the name, specific
.
The resulting name, general
, is then looked up. The new result is
then merged with the initial lookup, yielding an attribute with a
white foreground color and a blue background color. This happens because
the specificAttr
entry did not specify a background color. If it
had, that would have been used instead. In this way, we can create
inheritance relationships between attributes, much the same way CSS
supports inheritance of styles based on rule specificity.
Brick uses Vty's attribute type, Attr
, which has three components:
foreground color, background color, and style. These three components
can be independently specified to have an explicit value, and any
component not explicitly specified can default to whatever the terminal
is currently using. Vty styles can be combined together, e.g. underline
and bold, so styles are cumulative.
What if a widget attempts to draw with an attribute name that is not
specified in the map at all? In that case, the attribute map's "default
attribute" is used. In the example above, the default attribute for the
map is Vty's defAttr
value, which means that the terminal's default
colors and style should be used. But that attribute can be customized
as well, and any attribute map lookup results will get merged with the
default attribute for the map. So, for example, if you'd like your
entire application background to be blue unless otherwise specified, you
could create an attribute map as follows:
let myMap = attrMap (bg blue) [ ... ]
This way, we can avoid repeating the desired background color and all of the other map entries can just set foreground colors and styles where needed.
In addition to using the attribute map provided by appAttrMap
, the
map and attribute lookup behavior can be customized on a per-widget
basis by using various functions from Brick.Widgets.Core
:
updateAttrMap
-- allows transformations of the attribute map,forceAttr
-- forces all attribute lookups to map to the value of the specified attribute name,withDefAttr
-- changes the default attribute for the attribute map to the one with the specified name, andoverrideAttr
-- creates attribute map lookup synonyms between attribute names.
Brick provides support for customizable attribute themes. This works as follows:
- The application provides a default theme built in to the program.
- The application customizes the theme by loading theme customizations from a user-specified customization file.
- The application can save new customizations to files for later re-loading.
Customizations are written in an INI-style file. Here's an example:
[default]
default.fg = blue
default.bg = black
[other]
someAttribute.fg = red
someAttribute.style = underline
otherAttribute.style = [underline, bold]
otherAttribute.inner.fg = white
In the above example, the theme's default attribute -- the one that is
used when no other attributes are used -- is customized. Its foreground
and background colors are set. Then, other attributes specified by
the theme -- someAttribute
and otherAttribute
-- are also
customized. This example shows that styles can be customized, too, and
that a custom style can either be a single style (in this example,
underline
) or a collection of styles to be applied simultaneously
(in this example, underline
and bold
). Lastly, the hierarchical
attribute name otherAttribute.inner
refers to an attribute name
with two components, otherAttribute <> inner
, similar to the
specificAttr
attribute described in How Attributes Work. Full
documentation for the format of theme customization files can be found
in the module documentation for Brick.Themes
.
The above example can be used in a brick
application as follows.
First, the application provides a default theme:
import Brick.Themes (Theme, newTheme)
import Brick (attrName)
import Brick.Util (fg, on)
import Graphics.Vty (defAttr, white, blue, yellow, magenta)
defaultTheme :: Theme
defaultTheme =
newTheme (white `on` blue)
[ (attrName "someAttribute", fg yellow)
, (attrName "otherAttribute", fg magenta)
]
Notice that the attributes in the theme have defaults: someAttribute
will default to a yellow foreground color if it is not customized. (And
its background will default to the theme's default background color,
blue, if it not customized either.) Then, the application can customize
the theme with the user's customization file:
import Brick.Themes (loadCustomizations)
main :: IO ()
main = do
customizedTheme <- loadCustomizations "custom.ini" defaultTheme
Now we have a customized theme based on defaultTheme
. The next step
is to build an AttrMap
from the theme:
import Brick.Themes (themeToAttrMap)
main :: IO ()
main = do
customizedTheme <- loadCustomizations "custom.ini" defaultTheme
let mapping = themeToAttrMap customizedTheme
The resulting AttrMap
can then be returned by appAttrMap
as described in How Attributes Work and appAttrMap: Managing
Attributes.
If the theme is further customized at runtime, any changes can be saved
with Brick.Themes.saveCustomizations
.
Brick attempts to support rendering wide characters in all widgets, and the brick editor supports entering and editing wide characters. Wide characters are those such as many Asian characters and emoji that need more than a single terminal column to be displayed.
Unfortunately, there is not a fully correct solution to determining the character width that the user's terminal will use for a given character. The current recommendation is to avoid use of wide characters due to these issues. If you still must use them, you can read vty's documentation for options that will affect character width calculations.
As a result of supporting wide characters, it is important to know that computing the length of a string to determine its screen width will only work for single-column characters. So, for example, if you want to support wide characters in your application, this will not work:
let width = Data.Text.length t
If the string contains any wide characters, their widths will not be
counted properly. In order to get this right, use the TextWidth
type
class to compute the width:
let width = Brick.Widgets.Core.textWidth t
The TextWidth
type class uses Vty's character width routine to
compute the width by looking up the string's characters in a Unicode
width table. If you need to compute the width of a single character, use
Graphics.Text.wcwidth
.
When an application needs to know where a particular widget was drawn
by the renderer, the application can request that the renderer record
the extent of the widget--its upper-left corner and size--and provide
access to it in an event handler. Extents are represented using Brick's
Brick.Types.Extent
type. In the following example, the application
needs to know where the bordered box containing "Foo" is rendered:
ui = center $ border $ str "Foo"
We don't want to have to care about the particulars of the layout to find out where the bordered box got placed during rendering. To get this information we request that the extent of the box be reported to us by the renderer using a resource name:
data Name = FooBox
ui = center $
reportExtent FooBox $
border $ str "Foo"
Now, whenever the ui
is rendered, the extent of the bordered box
containing "Foo" will be recorded. We can then look it up in event
handlers in EventM
:
mExtent <- Brick.Main.lookupExtent FooBox
case mExtent of
Nothing -> ...
Just (Extent _ upperLeft (width, height)) -> ...
Some terminal emulators support "bracketed paste" mode. This feature enables OS-level paste operations to send the pasted content as a single chunk of data and bypass the usual input processing that the application does. This enables more secure handling of pasted data since the application can detect that a paste occurred and avoid processing the pasted data as ordinary keyboard input. For more information, see bracketed paste mode.
The Vty library used by brick provides support for bracketed pastes, but
this mode must be enabled. To enable paste mode, we need to get access
to the Vty library handle in EventM
(in e.g. appHandleEvent
):
import Control.Monad (when)
import qualified Graphics.Vty as V
do
vty <- Brick.Main.getVtyHandle
let output = V.outputIface vty
when (V.supportsMode output V.BracketedPaste) $
liftIO $ V.setMode output V.BracketedPaste True
Once enabled, paste mode will generate Vty EvPaste
events. These
events will give you the entire pasted content as a ByteString
which
you must decode yourself if, for example, you expect it to contain UTF-8
text data.
Some terminal emulators support mouse interaction. The Vty library used
by brick provides these low-level events if mouse mode has been enabled.
To enable mouse mode, we need to get access to the Vty library handle in
EventM
:
do
vty <- Brick.Main.getVtyHandle
let output = outputIface vty
when (supportsMode output Mouse) $
liftIO $ setMode output Mouse True
Bear in mind that some terminals do not support mouse interaction, so
use Vty's getModeStatus
to find out whether your terminal will
provide mouse events.
Also bear in mind that terminal users will usually expect to be able to interact with your application entirely without a mouse, so if you do choose to enable mouse interaction, consider using it to improve existing interactions rather than provide new functionality that cannot already be managed with a keyboard.
Once mouse events have been enabled, Vty will generate EvMouseDown
and EvMouseUp
events containing the mouse button clicked, the
location in the terminal, and any modifier keys pressed.
handleEvent (VtyEvent (EvMouseDown col row button mods) = ...
Although these events may be adequate for your needs, brick
provides
a higher-level mouse event interface that ties into the drawing
language. The disadvantage to the low-level interface described above is
that you still need to determine what was clicked, i.e., the part of
the interface that was under the mouse cursor. There are two ways to do
this with brick
: with click reporting and extent checking.
The click reporting approach is the most high-level approach offered
by brick
and the one that we recommend you use. In this approach,
we use Brick.Widgets.Core.clickable
when drawing the interface to
request that a given widget generate MouseDown
and MouseUp
events when it is clicked.
data Name = MyButton
ui :: Widget Name
ui = center $
clickable MyButton $
border $
str "Click me"
handleEvent (MouseDown MyButton button modifiers coords) = ...
handleEvent (MouseUp MyButton button coords) = ...
This approach enables event handlers to use pattern matching to check
for mouse clicks on specific regions; this uses Extent checking
under the hood but makes it possible to denote which widgets are
clickable in the interface description. The event's click coordinates
are local to the widget being clicked. In the above example, a click
on the upper-left corner of the border would result in coordinates of
(0,0)
.
The extent checking approach entails requesting extents (see Extents) for parts of your interface, then checking the Vty mouse click event's coordinates against one or more extents. This approach is slightly lower-level than the direct mouse click reporting approach above but is provided in case you need more control over how mouse clicks are dealt with.
The most direct way to do this is to check a specific extent:
handleEvent (VtyEvent (EvMouseDown col row _ _)) = do
mExtent <- lookupExtent SomeExtent
case mExtent of
Nothing -> return ()
Just e -> do
if Brick.Main.clickedExtent (col, row) e
then ...
else ...
This approach works well enough if you know which extent you're interested in checking, but what if there are many extents and you want to know which one was clicked? And what if those extents are in different layers? The next approach is to find all clicked extents:
handleEvent (VtyEvent (EvMouseDown col row _ _)) = do
extents <- Brick.Main.findClickedExtents (col, row)
-- Then check to see if a specific extent is in the list, or just
-- take the first one in the list.
This approach finds all clicked extents and returns them in a list with the following properties:
- For extents
A
andB
, ifA
's layer is higher thanB
's layer,A
comes beforeB
in the list. - For extents
A
andB
, ifA
andB
are in the same layer andA
is contained withinB
,A
comes beforeB
in the list.
As a result, the extents are ordered in a natural way, starting with the most specific extents and proceeding to the most general.
A viewport is a scrollable window onto a widget. Viewports have a
scrolling direction of type Brick.Types.ViewportType
which can be
one of:
Horizontal
: the viewport can only scroll horizontally.Vertical
: the viewport can only scroll vertically.Both
: the viewport can scroll both horizontally and vertically.
The Brick.Widgets.Core.viewport
combinator takes another widget
and embeds it in a named viewport. We name the viewport so that we can
keep track of its scrolling state in the renderer, and so that you can
make scrolling requests. The viewport's name is its handle for these
operations (see Scrolling Viewports in Event Handlers and Resource
Names). The viewport name must be unique across your application.
For example, the following puts a string in a horizontally-scrollable viewport:
-- Assuming that App uses 'Name' for its resource names:
data Name = Viewport1
let w = viewport Viewport1 Horizontal $ str "Hello, world!"
A viewport
specification means that the widget in the viewport will
be placed in a viewport window that is Greedy
in both directions
(see Available Rendering Area). This is suitable if we want the
viewport size to be the size of the entire terminal window, but if
we want to limit the size of the viewport, we might use limiting
combinators (see Limiting Rendering Area):
let w = hLimit 5 $
vLimit 1 $
viewport Viewport1 Horizontal $ str "Hello, world!"
Now the example produces a scrollable window one row high and five columns wide initially showing "Hello". The next two sections discuss the two ways in which this viewport can be scrolled.
The most direct way to scroll a viewport is to make scrolling requests
in the EventM
event-handling monad. Scrolling requests ask the
renderer to update the state of a viewport the next time the user
interface is rendered. Those state updates will be made with respect
to the previous viewport state, i.e., the state of the viewports as
of the end of the most recent rendering. This approach is the best
approach to use to scroll widgets that have no notion of a cursor.
For cursor-based scrolling, see Scrolling Viewports With Visibility
Requests.
To make scrolling requests, we first create a
Brick.Main.ViewportScroll
from a viewport name with
Brick.Main.viewportScroll
:
-- Assuming that App uses 'Name' for its resource names:
data Name = Viewport1
let vp = viewportScroll Viewport1
The ViewportScroll
record type contains a number of scrolling
functions for making scrolling requests:
hScrollPage :: Direction -> EventM n s ()
hScrollBy :: Int -> EventM n s ()
hScrollToBeginning :: EventM n s ()
hScrollToEnd :: EventM n s ()
vScrollPage :: Direction -> EventM n s ()
vScrollBy :: Int -> EventM n s ()
vScrollToBeginning :: EventM n s ()
vScrollToEnd :: EventM n s ()
In each case the scrolling function scrolls the viewport by the
specified amount in the specified direction; functions prefixed with
h
scroll horizontally and functions prefixed with v
scroll
vertically.
Scrolling operations do nothing when they don't make sense for the
specified viewport; scrolling a Vertical
viewport horizontally is a
no-op, for example.
Using viewportScroll
we can write an event handler that scrolls the
Viewport1
viewport one column to the right:
myHandler :: e -> EventM n s ()
myHandler e = do
let vp = viewportScroll Viewport1
hScrollBy vp 1
When we need to scroll widgets only when a cursor in the viewport leaves the viewport's bounds, we need to use visibility requests. A visibility request is a hint to the renderer that some element of a widget inside a viewport should be made visible, i.e., that the viewport should be scrolled to bring the requested element into view.
To use a visibility request to make a widget in a viewport visible, we
simply wrap it with visible
:
-- Assuming that App uses 'Name' for its resource names:
data Name = Viewport1
let w = viewport Viewport1 Horizontal $
(visible $ str "Hello,") <+> (str " world!")
This example requests that the Viewport1
viewport be scrolled so
that "Hello," is visible. We could extend this example with a value
in the application state indicating which word in our string should
be visible and then use that to change which string gets wrapped with
visible
; this is the basis of cursor-based scrolling.
Note that a visibility request does not change the state of a viewport if the requested widget is already visible! This important detail is what makes visibility requests so powerful, because they can be used to capture various cursor-based scenarios:
- The
Brick.Widgets.Edit
widget uses a visibility request to make its 1x1 cursor position visible, thus making the text editing widget fully scrollable while being entirely scrolling-unaware. - The
Brick.Widgets.List
widget uses a visibility request to make its selected item visible regardless of its size, which makes the list widget scrolling-unaware.
Brick supports drawing both vertical and horizontal scroll bars on
viewports. To enable scroll bars, wrap your call to viewport
with
a call to withVScrollBars
and/or withHScrollBars
. If you don't
like the appearance of the resulting scroll bars, you can customize
how they are drawn by making your own ScrollbarRenderer
and using
withVScrollBarRenderer
and/or withHScrollBarRenderer
. Note that
when you enable scrollbars, the content of your viewport will lose one
column of available space if vertical scroll bars are enabled and one
row of available space if horizontal scroll bars are enabled.
Scroll bars can also be configured to draw "handles" with
withHScrollBarHandles
and withVScrollBarHandles
.
Lastly, scroll bars can be configured to report mouse events on
each scroll bar element. To enable mouse click reporting, use
withClickableHScrollBars
and withClickableVScrollBars
.
For a demonstration of the scroll bar API in action, see the
ViewportScrollbarsDemo.hs
demonstration program.
Viewports impose one restriction: a viewport that is scrollable in
some direction can only embed a widget that has a Fixed
size in
that direction. This extends to Both
type viewports: they can only
embed widgets that are Fixed
in both directions. This restriction
is because when viewports embed a widget, they relax the rendering area
constraint in the rendering context, but doing so to a large enough
number for Greedy
widgets would result in a widget that is too big
and not scrollable in a useful way.
Violating this restriction will result in a runtime exception.
While it's possible to construct interfaces with editors and other interactive inputs manually, this process is somewhat tedious: all of the event dispatching has to be written by hand, a focus ring or other construct needs to be managed, and most of the rendering code needs to be written. Furthermore, this process makes it difficult to follow some common patterns:
- We typically want to validate the user's input, and only collect it once it has been validated.
- We typically want to notify the user when a particular field's contents are invalid.
- It is often helpful to be able to create a new data type to represent the fields in an input interface, and use it to initialize the input elements and later collect the (validated) results.
- A lot of the rendering and event-handling work to be done is repetitive.
The Brick.Forms
module provides a high-level API to automate all of
the above work in a type-safe manner.
Let's consider an example data type that we'd want to use as the
basis for an input interface. This example comes directly from the
FormDemo.hs
demonstration program.
data UserInfo =
FormState { _name :: T.Text
, _age :: Int
, _address :: T.Text
, _ridesBike :: Bool
, _handed :: Handedness
, _password :: T.Text
} deriving (Show)
data Handedness = LeftHanded
| RightHanded
| Ambidextrous
deriving (Show, Eq)
Suppose we want to build an input form for the above data. We might want
to use an editor to allow the user to enter a name and an age. We'll
need to ensure that the user's input for age is a valid integer. For
_ridesBike
we might want a checkbox-style input, and for _handed
we might want a radio button input. For _password
, we'd definitely
like a password input box that conceals the input.
If we were to build an interface for this data manually, we'd need to
deal with converting the data above to the right types for inputs. For
example, for _age
we'd need to convert an initial age value to
Text
, put it in an editor with Brick.Widgets.Edit.editor
, and
then at a later time, parse the value and reconstruct an age from the
editor's contents. We'd also need to tell the user if the age value was
invalid.
Brick's Forms
API provides input field types for all of the above
use cases. Here's the form that we can use to allow the user to edit a
UserInfo
value:
mkForm :: UserInfo -> Form UserInfo e Name
mkForm =
newForm [ editTextField name NameField (Just 1)
, editTextField address AddressField (Just 3)
, editShowableField age AgeField
, editPasswordField password PasswordField
, radioField handed [ (LeftHanded, LeftHandField, "Left")
, (RightHanded, RightHandField, "Right")
, (Ambidextrous, AmbiField, "Both")
]
, checkboxField ridesBike BikeField "Do you ride a bicycle?"
]
A form is represented using a Form s e n
value and is parameterized
with some types:
s
- the type of form state managed by the form (in this caseUserInfo
)e
- the event type of the application (must match the event type used withApp
)n
- the resource name type of the application (must match the resource name type used withApp
)
First of all, the above code assumes we've derived lenses for
UserInfo
using Lens.Micro.TH.makeLenses
. Once we've done
that, each field that we specify in the form must provide a lens into
UserInfo
so that we can declare the particular field of UserInfo
that will be edited by the field. For example, to edit the _name
field we use the name
lens to create a text field editor with
editTextField
. All of the field constructors above are provided by
Brick.Forms
.
Each form field also needs a resource name (see Resource Names). The resource names are assigned to the individual form inputs so the form can automatically track input focus and handle mouse click events.
The form carries with it the value of UserInfo
that reflects the
contents of the form. Whenever an input field in the form handles an
event, its contents are validated and rewritten to the form state (in
this case, a UserInfo
record).
The mkForm
function takes a UserInfo
value, which is really
just an argument to newForm
. This UserInfo
value will be used
to initialize all of the form fields. Each form field will use the lens
provided to extract the initial value from the UserInfo
record,
convert it into an appropriate state type for the field in question, and
later validate that state and convert it back into the appropriate type
for storage in UserInfo
.
The form value itself -- of type Form
-- must be stored in your
application state. You should only ever call newForm
when you need
to initialize a totally new form. Once initialized, the form needs to be
kept around and updated by event handlers in order to work.
For example, if the initial UserInfo
value's _age
field has the
value 0
, the editShowableField
will call show
on 0
,
convert that to Text
, and initialize the editor for _age
with
the text string "0"
. Later, if the user enters more text -- changing
the editor contents to "10"
, say -- the Read
instance for
Int
(the type of _age
) will be used to parse "10"
. The
successfully-parsed value 10
will then be written to the _age
field of the form's UserInfo
state using the age
lens. The use
of Show
and Read
here is a feature of the field type we have
chosen for _age
, editShowableField
.
For other field types we may have other needs. For instance,
Handedness
is a data type representing all the possible choices
we want to provide for a user's handedness. We wouldn't want the user
to have to type in a text string for this option. A more appropriate
input interface is a list of radio buttons to choose from amongst
the available options. For that we have radioField
. This field
constructor takes a list of all of the available options, and updates
the form state with the value of the currently-selected option.
Rendering forms is done easily using the Brick.Forms.renderForm
function. However, as written above, the form will not look especially
nice. We'll see a few text editors followed by some radio buttons and a
check box. But we'll need to customize the output a bit to make the form
easier to use. For that, we have the Brick.Forms.@@=
operator. This
operator lets us provide a function to augment the Widget
generated
by the field's rendering function so we can do things like add labels,
control layout, or change attributes:
(str "Name: " <+>) @@=
editTextField name NameField (Just 1)
Now when we invoke renderForm
on a form using the above example,
we'll see a "Name:"
label to the left of the editor field for
the _name
field of UserInfo
.
Brick provides this interface to controlling per-field rendering because many form fields either won't have labels or will have different layout requirements, so an alternative API such as building the label into the field API doesn't always make sense.
Brick defaults to rendering individual fields' inputs, and the entire
form, in a vertical box using vBox
. Use setFormConcat
and
setFieldConcat
to change this behavior to, e.g., hBox
.
The Brick.Forms
module uses and exports two attribute names (see
How Attributes Work):
focusedFormInputAttr
- this attribute is used to render the form field that has the focus.invalidFormInputAttr
- this attribute is used to render any form field that has user input that has invalid validation.
Your application should set both of these. Some good mappings in the attribute map are:
focusedFormInputAttr
-black `on` yellow
invalidFormInputAttr
-white `on` red
Handling form events is easy: we just use zoom
to call
Brick.Forms.handleFormEvent
with the BrickEvent
and a lens
to access the Form
in the application state. This automatically
dispatches input events to the currently-focused input field, and it
also manages focus changes with Tab
and Shift-Tab
keybindings.
(For details on all of its behaviors, see the Haddock documentation for
handleFormEvent
.) It's still up to the application to decide when
events should go to the form in the first place.
Since the form field handlers take BrickEvent
values, that means
that custom fields could even handle application-specific events (of the
type e
above).
Once the application has decided that the user should be done with the
form editing session, the current state of the form can be obtained
with Brick.Forms.formState
. In the example above, this would
return a UserInfo
record containing the values for each field in
the form as of the last time it was valid input. This means that
the user might have provided invalid input to a form field that is
not reflected in the form state due to failing validation.
Since the formState
is always a valid set of values, it might
be surprising to the user if the values used do not match the last
values they saw on the screen; the Brick.Forms.allFieldsValid
can be used to determine if the last visual state of the form had
any invalid entries and doesn't match the value of formState
. A
list of any fields which had invalid values can be retrieved with the
Brick.Forms.invalidFields
function.
While each form field type provides a validator function to validate
its current user input value, that function is pure. As a result it's
not suitable for doing validation that requires I/O such as searching
a database or making network requests. If your application requires
that kind of validation, you can use the Brick.Forms.setFieldValid
function to set the validation state of any form field as you see
fit. The validation state set by that function will be considered by
allFieldsValid
and invalidFields
. See FormDemo.hs
for an
example of this API.
Note that if mouse events are enabled in your application (see Mouse Support), all built-in form fields will respond to mouse interaction. Radio buttons and check boxes change selection on mouse clicks and editors change cursor position on mouse clicks.
If the built-in form field types don't meet your needs, Brick.Forms
exposes all of the data types needed to implement your own field types.
For more details on how to do this, see the Haddock documentation for
the FormFieldState
and FormField
data types along with the
implementations of the built-in form field types.
Brick applications typically start out by explicitly checking incoming
events for specific keys in appHandleEvent
. While this works well
enough, it results in tight coupling between the input key events and
the event handlers that get run. As applications evolve, it becomes
important to decouple the input key events and their handlers to allow
the input keys to be customized by the user. That's where Brick's
customizable keybindings API comes in.
The customizable keybindings API provides:
Brick.Keybindings.Parse
: parsing and loading user-provided keybinding configuration files,Brick.Keybindings.Pretty
: pretty-printing keybindings and generating keybinding help text inWidget
, plain text, and Markdown formats so you can provide help to users both within the program and outside of it,Brick.Keybindings.KeyEvents
: specifying the application's abstract key events and their configuration names,Brick.Keybindings.KeyConfig
: bundling default and customized keybindings for each abstract event into a structure for use by the dispatcher, andBrick.Keybindings.KeyDispatcher
: specifying handlers and dispatching incoming key events to them.
This section of the User Guide describes the API at a high level,
but Brick also provides a complete working example of the custom
keybinding API in programs/CustomKeybindingDemo.hs
and
provides detailed documentation on how to use the API, including a
step-by-step process for using it, in the module documentation for
Brick.Keybindings.KeyDispatcher
.
The following table compares Brick application design decisions and runtime behaviors in a typical application to those of an application that uses the customizable keybindings API:
Approach | Before runtime | At runtime |
Typical application (no custom keybindings) | The application author decides which keys will trigger application behaviors. The event handler is written to pattern-match on specific keys. |
|
Application with custom keybindings API integrated | The application author specifies the possible abstract events that correspond to the application's behaviors. The events are given default keybindings. The application provides event handlers for the abstract events, not specific keys. If desired, the application can load user-defined custom keybindings from an INI file at startup to override the application's defaults. |
|
An important issue to consider in using the custom keybinding API is that it is possible for the user to map the same key to more than one event. We refer to this situation as a keybinding collision. Whether the collision represents a problem depends on how the events in question are going to be handled by the application. This section provides an example scenario and describes a way to deal with this situation.
Suppose an application has two key events:
data KeyEvent = QuitEvent
| CloseWindowEvent
allKeyEvents :: KeyEvents KeyEvent
allKeyEvents =
K.keyEvents [ ("quit", QuitEvent)
, ("close-window", CloseWindowEvent)
]
defaultBindings :: [(KeyEvent, [Binding])]
defaultBindings =
[ (QuitEvent, [ctrl 'q'])
, (CloseWindowEvent, [bind KEsc])
]
Suppose also that the application using the above key events has a
feature that opens a window, and that CloseWindowEvent
is used to
close the window, while QuitEvent
is used to quit the application.
A user might then provide a custom INI file to rebind keys as follows:
[keybindings] quit = Esc close-window = Esc
While this is a valid configuration for the user to provide, it would
result in a keybinding collision for Esc
since it is now bound
to two events. Whether that's a problem depends entirely on how
QuitEvent
and CloseWindowEvent
are handled:
- If the application handles both events in the same event handler,
the
KeyDispatcher
for those events would fail to construct sinceEsc
maps to more than one event. Building aKeyDispatcher
from aKeyConfig
with such a collision would fail and return information about the collisions. - If the application handles the two events in different dispatchers
then the collision has no effect and is not a problem since different
KeyDispatcher
values would be constructed to handle the events separately. This could happen, for instance, if the application only ever handledCloseWindowEvent
when the window in question was open and only handledQuitEvent
when the window had been closed. This kind of "modal" approach to handling events means that we only consider a key to have a collision if it is bound to two or more events that are handled in the same event handling context.
There's also another situation that would be problematic, which is
when an abstract event like QuitEvent
has a key mapping that
collides with a key handler that is bound to a specific key using
Brick.Keybindings.KeyDispatcher.onKey
rather than an abstract event:
K.onKey (K.bind '\t') "Increment the counter by 10" $
counter %= (+ 10)
If onKey
is used, the handler it creates is only triggered by the
specified key (Tab
in the example above). But the handler may be
included alongside handlers in the same dispatcher that are also
triggered by Tab
, so if those event handlers were provided together
when creating a KeyDispatcher
then it would fail to construct due to
the collision.
Brick provides Brick.Keybindings.KeyConfig.keyEventMappings
to help
finding collisions at the key configuration level. Finding out about
collisions at the dispatcher level is possible by handling the failure
case when calling Brick.Keybindings.KeyDispatcher.keyDispatcher
.
Brick supports a feature called "joinable borders" which means that borders drawn in adjacent widgets can be configured to automatically "join" with each other using the appropriate intersection characters. This feature is helpful for creating seamless connected borders without the need for manual calculations to determine where to draw intersection characters.
Under normal circumstances, widgets are self-contained in that their
renderings do not interact with the appearance of adjacent widgets. This
is unfortunate for borders: one often wants to draw a T-shaped character
at the intersection of a vertical and horizontal border, for example.
To facilitate automatically adding such characters, brick
offers
some border-specific capabilities for widgets to re-render themselves
as information about neighboring widgets becomes available during the
rendering process.
Border-joining works by iteratively redrawing the edges of widgets as those edges come into contact with other widgets during rendering. If the adjacent edge locations of two widgets both use joinable borders, the Brick will re-draw one of the characters so that it connects seamlessly with the adjacent border.
When a widget is rendered, it can report supplementary information about each position on its edges. Each position has four notional line segments extending from its center, arranged like this:
top
|
|
left ----+---- right
|
|
bottom
These segments can independently be drawn, accepting, and
offering, as captured in the Brick.Types.BorderSegment
type:
data BorderSegment = BorderSegment
{ bsAccept :: Bool
, bsOffer :: Bool
, bsDraw :: Bool
}
If no information is reported for a position, it assumed that it is not drawn, not accepting, and not offering -- and so it will never be rewritten. This situation is the ordinary situation where an edge location is not a border at all, or is a border that we don't want to join to other borders.
Line segments that are drawn are used for deciding which part of the
BorderStyle
to use if this position needs to be updated. (See also
The Active Border Style.) For example, suppose a position needs to
be redrawn, and already has the left and bottom segments drawn; then it
will replace the current character with the upper-right corner drawing
character bsCornerTR
from its border style.
The accepting and offering properties are used to perform a small handshake between neighboring widgets; when the handshake is successful, one segment will transition to being drawn. For example, suppose a horizontal and vertical border widget are drawn next to each other:
top
(offering) top
|
|
left + right left ----+---- right
| (offering) (offering)
|
bottom bottom
(offering)
These borders are accepting in all directions, drawn in the directions
signified by visible lines, and offering in the directions written.
Since the horizontal border on the right is offering towards the
vertical border, and the vertical border is accepting from the direction
towards the horizontal border, the right segment of the vertical
border will transition to being drawn. This will trigger an update of
the Image
associated with the left widget, overwriting whatever
character is there currently with a bsIntersectL
character instead.
The state of the segments afterwards will be the same, but the fact that
there is one more segment drawn will be recorded:
top
(offering) top
|
|
left +---- right left ----+---- right
| (offering) (offering)
|
bottom bottom
(offering)
It is important that this be recorded: we may later place this combined
widget to the right of another horizontal border, in which case we
would want to transition again from a bsIntersectL
character to a
bsIntersectFull
character that represents all four segments being
drawn.
Because this involves an interaction between multiple widgets, we
may find that the two widgets involved were rendered under different
rendering contexts. To avoid mixing and matching border styles and
drawing attributes, each location records not just the state of its
four segments but also the border style and attribute that were active
at the time the border was drawn. This information is stored in
Brick.Types.DynBorder
.
data DynBorder = DynBorder
{ dbStyle :: BorderStyle
, dbAttr :: Attr
, dbSegments :: Edges BorderSegment
}
The Brick.Types.Edges
type has one field for each direction:
data Edges a = Edges { eTop, eBottom, eLeft, eRight :: a }
In addition to the offer/accept handshake described above, segments also
check that their neighbor's BorderStyle
and Attr
match their own
before transitioning from undrawn to drawn to avoid visual glitches from
trying to connect e.g. unicode
borders to ascii
ones or green
borders to red ones.
The above description applies to a single location; any given widget's
result may report information about any location on its border using the
Brick.BorderMap.BorderMap
type. A BorderMap a
is close kin to a
Data.Map.Map Location a
except that each BorderMap
has a fixed
rectangle on which keys are retained. Values inserted at other keys are
silently discarded.
For backwards compatibility, all the widgets that ship with brick
avoid reporting any border information by default, but brick
offers
three ways of modifying the border-joining behavior of a widget.
Brick.Widgets.Core.joinBorders
instructs any borders drawn in its child widget to report their edge information. It does this by setting a flag in the rendering context that tells theBrick.Widgets.Border
widgets to report the information described above. Consequently, widgets drawn in this context will join their borders with neighbors.Brick.Widgets.Core.separateBorders
does the opposite ofjoinBorders
by unsetting the same context flag, preventing border widgets from attempting to connect.Brick.Widgets.Core.freezeBorders
lets its child widget connect its borders internally but prevents it from connecting with anything outside thefreezeBorders
call. It does this by deleting the edge metadata about its child widget. This means that any connections already made within the child widget will stay as they are but no new connections will be made to adjacent widgets. For example, one might use this to create a box with internal but no external connections:joinBorders . freezeBorders . border . hBox $ [str "left", vBorder, str "right"]
Or to create a box that allows external connections but not internal ones:
joinBorders . border . freezeBorders . hBox $ [str "left", vBorder, str "right"]
When creating new widgets, if you would like joinBorders
and
separateBorders
to affect the behavior of your widget, you may do
so by consulting the ctxDynBorders
field of the rendering context
before writing to your Result
's borders
field.
When widgets become expensive to render, brick
provides a rendering
cache that automatically caches and re-uses stored Vty images from
previous renderings to avoid expensive renderings. To cache the
rendering of a widget, just wrap it in the Brick.Widgets.Core.cached
function:
data Name = ExpensiveThing
ui :: Widget Name
ui = center $
cached ExpensiveThing $
border $
str "This will be cached"
In the example above, the first time the border $ str "This will be
cached"
widget is rendered, the resulting Vty image will be stored
in the rendering cache under the key ExpensiveThing
. On subsequent
renderings the cached Vty image will be used instead of re-rendering the
widget. This example doesn't need caching to improve performance, but
more sophisticated widgets might.
Once cached
has been used to store something in the rendering cache,
periodic cache invalidation may be required. For example, if the cached
widget is built from application state, the cache will need to be
invalidated when the relevant state changes. The cache may also need to
be invalidated when the terminal is resized. To invalidate the cache, we
use the cache invalidation functions in EventM
:
handleEvent ... = do
-- Invalidate just a single cache entry:
Brick.Main.invalidateCacheEntry ExpensiveThing
-- Invalidate the entire cache (useful on a resize):
Brick.Main.invalidateCache
brick
exposes all of the internals you need to implement your
own widgets. Those internals, together with Graphics.Vty
, can be
used to create widgets from the ground up. You'll need to implement
your own widget if you can't write what you need in terms of existing
combinators. For example, an ordinary widget like
myWidget :: Widget n
myWidget = str "Above" <=> str "Below"
can be expressed with <=>
and str
and needs no custom behavior.
But suppose we want to write a widget that renders some string followed
by the number of columns in the space available to the widget. We can't
do this without writing a custom widget because we need access to the
rendering context. We can write such a widget as follows:
customWidget :: String -> Widget n
customWidget s =
Widget Fixed Fixed $ do
ctx <- getContext
render $ str (s <> " " <> show (ctx^.availWidthL))
The Widget
constructor takes the horizontal and vertical growth
policies as described in How Widgets and Rendering Work. Here we just
provide Fixed
for both because the widget will not change behavior
if we give it more space. We then get the rendering context and append
the context's available columns to the provided string. Lastly we call
render
to render the widget we made with str
. The render
function returns a Brick.Types.Result
value:
data Result n =
Result { image :: Graphics.Vty.Image
, cursors :: [Brick.Types.CursorLocation n]
, visibilityRequests :: [Brick.Types.VisibilityRequest]
, extents :: [Extent n]
, borders :: BorderMap DynBorder
}
The rendering function runs in the RenderM
monad, which gives us
access to the rendering context (see How Widgets and Rendering Work)
via the Brick.Types.getContext
function as shown above. The context
tells us about the dimensions of the rendering area and the current
attribute state of the renderer, among other things:
data Context =
Context { ctxAttrName :: AttrName
, availWidth :: Int
, availHeight :: Int
, ctxBorderStyle :: BorderStyle
, ctxAttrMap :: AttrMap
, ctxDynBorders :: Bool
}
and has lens fields exported as described in Conventions.
As shown here, the job of the rendering function is to return a
rendering result which means producing a vty
Image
. In addition,
if you so choose, you can also return one or more cursor positions in
the cursors
field of the Result
as well as visibility requests
(see Viewports) in the visibilityRequests
field. Returned
visibility requests and cursor positions should be relative to the
upper-left corner of your widget, Location (0, 0)
. When your widget
is placed in others, such as boxes, the Result
data you returned
will be offset (as described in Rendering Sub-Widgets) to result in
correct coordinates once the entire interface has been rendered.
The most important fields of the context are the rendering area fields
availWidth
and availHeight
. These fields must be used to
determine how much space your widget has to render.
To perform an attribute lookup in the attribute map for the context's
current attribute, use Brick.Types.attrL
.
For example, to build a widget that always fills the available width and height with a fill character using the current attribute, we could write:
myFill :: Char -> Widget n
myFill ch =
Widget Greedy Greedy $ do
ctx <- getContext
let a = ctx^.attrL
return $ Result (Graphics.Vty.charFill a ch (ctx^.availWidthL) (ctx^.availHeightL))
[] [] [] Brick.BorderMap.empty
If your custom widget wraps another, then in addition to rendering
the wrapped widget and augmenting its returned Result
it must
also translate the resulting cursor locations, visibility requests,
and extents. This is vital to maintaining the correctness of
rendering metadata as widget layout proceeds. To do so, use the
Brick.Widgets.Core.addResultOffset
function to offset the elements
of a Result
by a specified amount. The amount depends on the nature
of the offset introduced by your wrapper widget's logic.
Widgets are not required to respect the rendering context's width and
height restrictions. Widgets may be embedded in viewports or translated
so they must render without cropping to work in those scenarios.
However, widgets rendering other widgets should enforce the rendering
context's constraints to avoid using more space than is available. The
Brick.Widgets.Core.cropToContext
function is provided to make this
easy:
let w = cropToContext someWidget
Widgets wrapped with cropToContext
can be safely embedded in other
widgets. If you don't want to crop in this way, you can use any of
vty
's cropping functions to operate on the Result
image as
desired.
Sub-widgets may specify specific attribute name values influencing
that sub-widget. If the custom widget utilizes its own attribute
names but needs to render the sub-widget, it can use overrideAttr
or mapAttrNames
to convert its custom names to the names that the
sub-widget uses for rendering its output.