Table of contents
History of development
The idea of spiral was formed after the initial release of lcalc (ver. 1.0.0) — my greatest command line application ever designed at that moment. Unfortunately, that release wasn’t too popular, so lcalc was going to be reworked once more. Versions 1.0.0–1.0.1 had rich line editing built in the executable itself, I wasn’t using other system libraries due to their lack of modern features. Unfortunately, my implementation of rich editing in lcalc left much to be desired:
While it did support UTF-8, lcalc only supported entering the characters valid in lcalc expressions. That was a deliberate design choice, because all of those characters occupy only one cell on a terminal, making it easier for me.
Overall, lcalc implementation was tailored only with lcalc in mind, but I would like a general purpose implementation that can be reused across programs.
Yes, there are multiple libraries exist for rich editing. I like them, but I didn’t choose them due to two reasons:
some libraries lack syntax highlighting (readline, linenoise);
some libraries had unconvenient keybindings (isocline).
So that’s when I got an idea.
If there is no library that suits me, I should make my own. Besides, I always wanted to practice writing libraries more than programs.
And so, the development of spiral began. I want it to:
- have full UTF-8 support for all characters;
- be portable across programs;
- implement Helix1 editor keybindings;
- have a comprehensible, but at the same time composable API, so it could suit more users.
Lastly, the name “spiral” was chosen as an analogy to the word “helix”: while Helix is a TUI text editor, spiral is going to be its command line sibling.
Warning: Currently, this project isn’t complete, doesn’t have any stable release, and is in active development. Many things could change and be reimplemented in the future, including this article.
Architecture
spiral is a modular library. It consists of the following modules:
spiral-term
: reading the terminal;spiral-core
: manipulating the line;spiral-tui
: writing to the terminal;spiral
: unified API to all of the above.
Every module is designed to be possible to use separately or substitute for another implementation (for example, reading the terminal in alternative way, implementing different keybinding mechanism or having different UI).
Unified API uses all of its submodules in the following fashion:
In the data flow diagram above:
spiral-term
is “Event reader”;spiral-core
is “Editor”;spiral-tui
is “User interface”.
Now that the submodules have been introduced, next sections will describe every submodule in details.
The terminal user interface
spiral-tui
’s task is simple: printing every line to the terminal. However, the execution gets complicated very quickly due to complexities in how terminals work. This is why spiral-tui
is a separate module, dealing with all the complexity while exposing a relatively simple API.
Immediate mode UI
See immediate mode UI on Wikipedia for the definition.
In spiral, difference between immediate and retained mode UI would be as follows:
- In immediate mode UI, on every line modification the whole line gets erased and drawn, again and again.
- In retained mode UI, on every line modification line gets erased and redrawn only from the edited part.
In case of text, retained mode is especially problematic, and here is an example.
If you have a string:
Retained mode is the best!
And then you replace the word “Retained” with “Immediate”:
Retained mode is the best!
Immediate mode is the best!
You’ll not only have to redraw the “Immediate” word, but also “ mode is the best!“ part, because it was visually shifted. So basically, in worst case scenario (modifications at the start of the line), retained mode is not any better than immediate mode.
In the best case scenario (modifications at the end of the line), however, it’s slightly better:
Retained mode is the worst!
Retained mode is the best!
Retained mode will just redraw “worst!” to “best!”, while immediate mode will reiterate and redraw the full line.
However, retained mode has one major drawback: complexity. It is a complex way of implementing UI, because the renderer needs to store a lot of information about every printed character. So the main question is:
Does the retained mode complexity pays off?
Let’s try to answer it by looking at practical applications:
It is important to understand the size of processed data in spiral. Text editors like Helix, of course, are being used for large volumes of data, to process a text of size counting multiple KiBs, to process a multiline text. It makes sense for them to use retained mode UI: when user edits only one line, only one line has to be redrawn; when user adds new line or deletes lines in the text, only lines visible on the screen have to be redrawn.
Command line editors are different. While they should’t have limit on the size of line they can edit, they are a wrong tool for editing large text. Their common usage is scripting languages, REPLs, where you just need to enter a short command to get the result. And so, command line editing libraries usually work with one line of text less than 1KiB. There is not really any benefit from using retained mode UI: you’ll have to redraw the whole line anyway, because it is always visible on screen.
Adding new features is complicated in retained mode. For example, I really want to implement syntax highlighting in the spiral, but tokenization of line can change significantly, from start to end, forcing to redraw the whole line anyway.
For example, let’s imagine a simple syntax highlighter that highlights matching pairs of parentheses
()
:5 + (7 * (8 - 4) + 6)
It also highlights errors when there is no match:
5 + (7 * (8 - 4) + 6
The only change of text was deletion of “)” at the end, which is extremely simple to update in retained mode. However, our syntax highlighter noticed an error with the previous ‘(’ symbol and highlighted it red, which forces us to redraw that symbol as well. An additional complexity is required to implement an interaction with syntax highlighter and retained mode UI.
On the other hand, all highlighting tokens are being iterated over in immediate mode UI anyway, making arbitrary changes of syntax highlighting straightforward.
In conclusion, significant convenience of immediate mode outweights slight gain of performance in retained mode. It allows to make the library less inter–integrated, but more composable and extendable.