Announcing cursor-brick

Date 2019-08-14

This post announces the new cursor-brick library. It is a small library to help you define brick widgets for cursors.

While making picosmos, nanosmos, microsmos, millismos and incidentally also smos, I noticed that I was duplicating a lot of functionality to draw cursors to the screen. Now, cursors in the cursor library are defined independent of the machinery that will manipulate or draw them, but I'm still using them mostly only together with brick.

Folds and widgets

As of the latest release, all cursors in the cursor library have fold helper functions. For example, for nonempty list cursors, there is now a foldNonEmptyCursor helper function:

foldNonEmptyCursor :: ([b] -> a -> [b] -> c) -> NonEmptyCursor a b -> c
foldNonEmptyCursor func NonEmptyCursor {..} =
  func (reverse nonEmptyCursorPrev) nonEmptyCursorCurrent nonEmptyCursorNext

This allowed certain convenience functions like the following specialisation to Widget n from brick in cursor-brick.

nonEmptyCursorWidget :: ([b] -> a -> [b] -> Widget n) -> NonEmptyCursor a b -> Widget n
nonEmptyCursorWidget = foldNonEmptyCursor

Easy enough, but cursor-brick also offers more practical combinators like this:

horizontalNonEmptyCursorWidget
  :: (b -> Widget n) -- | How to render items before the focus
  -> (a -> Widget n) -- | How to render the focus
  -> (b -> Widget n) -- | How to render items after the focus
  -> NonEmptyCursor a b -> Widget n

There is a similar verticalNonEmptyCursorWidget and there are also effectful versions of these combinators:

horizontalNonEmptyCursorWidgetM ::
     Applicative f
  => (b -> f (Widget n)) -- | How to render items before the focus
  -> (a -> f (Widget n)) -- | How to render the focus
  -> (b -> f (Widget n)) -- | How to render items after the focus
  -> NonEmptyCursor a b
  -> f (Widget n)

Text cursor and TextField cursor widgets

Drawing text to a screen is suprisingly complex. Brick takes care of most of the complexity, but there are some pieces that you still have to take care of.

Brick has two (modulo Text -> String conversion) functions to draw text to the screen: txt and txtWrap. One wraps text that goes off-screen and the other one does not. We need to use the appropriate version for our use-case.

The next bit of complexity is that txt and txtWrap both require their input to be sanitised. Indeed, the documentation says:

The input string must not contain tab characters.

The cursor-brick library has some helper functions to take care of this:

-- | Draw an arbitrary Text, it will be sanitised.
textWidget :: Text -> Widget n

-- | Draw an arbitrary Text (with wrapping), it will be sanitised.
textWidgetWrap :: Text -> Widget n

-- | Replace tabs by spaces so that brick doesn't render nonsense.
sanitiseText :: Text -> Text

Next, to draw the blink-y box to the screen that we call a cursor, brick has a showCursor function. This function can be used to draw a TextCursor in such a way that the blink-y box ends up in the right place. The cursor-brick library also takes care of that:

-- | Make a text cursor widget with a blink-y box.
selectedTextCursorWidget :: n -> TextCursor -> Widget n

-- | Make a text cursor widget without a blink-y box.
textCursorWidget :: TextCursor -> Widget n

Note that text will not be wrapped, because otherwise the blink-y box will not be in the right place on the screen.

There are similar functions for textfield cursors:

-- | Make a textfield cursor widget with a blink-y box.
selectedTextFieldCursorWidget :: n -> TextFieldCursor -> Widget n

-- | Make a textfield cursor widget without a blink-y box.
textFieldCursorWidget :: TextFieldCursor -> Widget n

Tree cursor fold and widgets

The tree cursor folds and widgets have already been alluded to in the previous blogpost about microsmos. A tree cursor is a strange beast, and conceptualising how to render it took a few iterations. The currently selected node needs to be rendered possibly differently from the other nodes. (Otherwise there would be no difference between rendering a tree and rendering a tree cursor.)

The way this happens is that you supply a function to draw the currently selected tree, and another function that can wrap whatever you made with that. This wrapping function will then add onto what you just made with the surrounding trees.

treeCursorWidget ::
     forall a b n.
     ([CTree b] -> b -> [CTree b] -> Widget n -> Widget n)
  -> (a -> CForest b -> Widget n)
  -> TreeCursor a b -> Widget n
treeCursorWidget = foldTreeCursor

There is also an effectful version of this combinator.

References

The cursor-brick library is available on Hackage. Cursors originated in the work on Smos. This post is part of an effort to encourage contributions to Smos. The simplest contribution could be to just try out smos and provide feedback on the experience. Smos is a purely functional semantic forest editor of a subset of YAML that is intended to replace Emacs' Org-mode for Getting Things Done.

Previous
Millismos: Writing a simple forest-editor with brick.

Start your Haskell project from a template

Haskell templates
Next
Cursors, Part 6: The Forest Cursor