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.

: 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.

: 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 .

: A way to draw the state onto the screen. We do this by turning it into a . appChooseCursor : Not relevant for us now.

: Not relevant for us now. appHandleEvent : A way to change the state, given an event.

: A way to change the state, given an event. appStartEvent : Not relevant for us now.

: 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.

turns a into a 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.

turns a 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

adds the colored rectangle onto the display, where the user is looking within the text padAll 1 adds some nice padding

adds some nice padding border adds a nice 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

: 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.

: 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.

: 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.

: 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.

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

: Remove a character if the user presses the backspace key. KDel -> mDo textCursorDelete : Delete the next character if the user presses the delete key

: Delete the next character if the user presses the delete key KEsc -> halt tc : Stop the application if the user presses the escape key.

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

: 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