Picosmos: Writing a simple single-line text-editor with brick.

Date 2018-12-14

Using the cursors as defined in the previous posts about list cursors and text cursors we can now take the first step toward writing a Purely Functional Semantic Editor. In this post we will write a purely functional text editor for a single line of text using brick.

Picosmos: editing a line.

In the previous two posts: cursors: list and cursors: text we explored the cursors that we will need to be able to write an editor for a single line of text. Now it is time to put all the pices together, and write a simple editor for a single line of text. We will be using brick to interface with the terminal interface, but you can use these concepts for a GUI as well, as long as you can somehow fit in the ELM architecture.

A brick app

The central piece of the editor is a brick App. It has three type variables:

  • s: The state of the application. In our case, that will be a text cursor.

  • e: The type for custom events. We will not use these, so we will leave it a paramter.

  • n: The type for names of pieces of the display. For this toy example, we will use Text.

The App will need a few things:

picoSmosApp :: App TextCursor e Text
picoSmosApp =
    App
    { appDraw = draw
    , appChooseCursor = showFirstCursor
    , appHandleEvent = handleEvent
    , appStartEvent = pure
    , appAttrMap = const $ attrMap Vty.defAttr []
    }
  • appDraw: A way to draw the state onto the screen. We do this by turning it into a brick Widget.

  • appChooseCursor: Not relevant for us now.

  • appHandleEvent: A way to change the state, given an event.

  • appStartEvent: Not relevant for us now.

  • appAttrMap: Not relevant for us now.

Drawing a text cursor

To implement the draw :: TextCursor -> [Widget Text] function, we have to explain to brick how to render a TextCursor to the terminal screen. We do this by turning a TextCursor into a Widget Text. The reason why the result is a list, is so that you could draw multiple layers, but we will only use one layer.

draw :: TextCursor -> [Widget Text]
draw tc =
    [ centerLayer $
      border $
      padAll 1 $
      showCursor "cursor" (Location (textCursorIndex tc, 0)) $
      txtWrap (rebuildTextCursor tc)
    ]

Piece by piece, backward:

  • rebuildTextCursor :: TextCursor -> Text turns a TextCursor into a Text to display.

  • txtWrap :: Text -> Widget n turns a Text into a widget that displas the text, and wraps around if the display is too small.

  • showCursor "cursor" (Location (textCursorIndex tc, 0)) adds the colored rectangle onto the display, where the user is looking within the text

  • padAll 1 adds some nice padding

  • border adds a nice border

  • centerLayer centers the result in the middle of the terminal screen.

Not so hard, right?

Dealing with user input

Whenever a user presses a button, we want to possibly change the current text cursor state. This is why we implement the appHandleEvent function.

This code is very simple. It looks at the event that brick received, and optionally changes the TextCursor state.

handleEvent :: TextCursor -> BrickEvent Text e -> EventM Text (Next TextCursor)
handleEvent tc e =
    case e of
        VtyEvent ve ->
            case ve of
                EvKey key mods ->
                    let mDo func = continue . fromMaybe tc $ func tc
                    in case key of
                           KChar c -> mDo $ textCursorInsert c
                           KLeft -> mDo textCursorSelectPrev
                           KRight -> mDo textCursorSelectNext
                           KHome -> continue $ textCursorSelectStart tc
                           KEnd -> continue $ textCursorSelectEnd tc
                           KBS -> mDo textCursorRemove
                           KDel -> mDo textCursorDelete
                           KEsc -> halt tc
                           KEnter -> halt tc
                           _ -> continue tc
                _ -> continue tc
        _ -> continue tc
  • KChar c -> mDo $ textCursorInsert c: Insert a character in front of the cursor if the user pressed a key with a letter

  • KLeft -> mDo textCursorSelectPrev: Go left in the text cursor if the user presses the left arrow key.

  • KRight -> mDo textCursorSelectNext: Go rigth in the text cursor if the user presses the left arrow key.

  • KHome -> continue $ textCursorSelectStart tc: Jump to the start if the user presses the home key.

  • KEnd -> continue $ textCursorSelectEnd tc: Jump to the end if the user presses the end key.

  • KBS -> mDo textCursorRemove: Remove a character if the user presses the backspace key.

  • KDel -> mDo textCursorDelete: Delete the next character if the user presses the delete key

  • KEsc -> halt tc: Stop the application if the user presses the escape key.

  • KEnter -> halt tc: Stop the application if the user presses the enter key.

  • _ -> continue tc: Do nothing if anything else happens, but do not stop the application.

References

That is it. Now we have a a functional line-of-text editor. The full code can be found on github. Text cursors are available in the cursor package on Hackage. 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 editor of a subset of YAML that is intended to replace Emacs' Org-mode for Getting Things Done.

Previous
2018; Year in review

Start your Haskell project from a template

Haskell templates
Next
Cursors, Part 2: The text cursor