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 useText
.
The App
will need a few things:
picoSmosApp :: App TextCursor e Text
=
picoSmosApp App
= draw
{ appDraw = showFirstCursor
, appChooseCursor = handleEvent
, appHandleEvent = pure
, appStartEvent = const $ attrMap Vty.defAttr []
, appAttrMap }
appDraw
: A way to draw the state onto the screen. We do this by turning it into abrick
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 1 $
padAll "cursor" (Location (textCursorIndex tc, 0)) $
showCursor
txtWrap (rebuildTextCursor tc) ]
Piece by piece, backward:
rebuildTextCursor :: TextCursor -> Text
turns aTextCursor
into aText
to display.txtWrap :: Text -> Widget n
turns aText
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 textpadAll 1
adds some nice paddingborder
adds a nice bordercenterLayer
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 letterKLeft -> 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 keyKEsc -> 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.