on 2020-11-28 in tui
For a long time I wanted to write some terminal TUI applications. But for various reason no terminal library really clicked with me.
Coming from classical event loop driven graphical user interface programming, I was mostly looking for something similar just adapted for terminal use. I was first looking for something high level with one of the usual event loops and a nice widget abstraction. But as I said nothing really clicked.
So maybe I had to write that high level library. So next I was looking for some low level library to just do the raw terminal interfacing.
There is of course ncurses, which is the gold standard in support for output to a wide range of terminals. But for input processing it too heavily depends on correct terminfo entries, which sadly in the modern world of frozen in the past enterprise deployments is just not working out too well. Worse these kind of "don't touch anything" of deployments have forced terminals to completely undermine the system with setting $TERM to outright nonsensical values, usually claiming to be xterm, while clearly not being a complete xterm at all. Also a generic event loop was not a common thing back when the standard for curses was written.
Some other libraries did not offer the low level interfaces I wanted -- instead combining the low level parts (a cell matrix and input events) directly with higher level parts I didn't intend to use. Some even used interesting hacks that could never work over ssh for terminal emulator detection. Also I was looking for something with permissive licensing and usable from C or C++ code.
For input handling there is libtermkey which seemed nice, but also was deprecated already. Still this library seemed to have some good ideas and certainly was a good inspiration for how to go about handling terminal input. Specially the idea, that interpreting terminal input with something closely matching a ECMA-48 sequence parser is the way forward instead of just matching against a small list of known terminal inputs.
So I started with experimenting with alternative ways to handle keyboard input. One important part in handling input from terminals is to handle input with graceful degradation when a sequence is not known. While sadly the terminal to application communication is not prefix-free, it follows a certain structure for most of the sequences that allows ignoring unknown sequences. That's something I really wanted to have. It's just ugly to end up with junk characters when trying a key combination that is not supported.
Another important point was that I wanted input processing that nicely fits into a event loop based environment, like it is common in many modern applications. Thus having the input processing mixed with terminal I/O was something that I wanted to avoid.
But following the rabbit hole even deeper now I had to understand how terminals really work.
Documentation of terminals of course varies. The best documented terminal is xterm. It has a full list of almost1 all supported sequences here. It's documentation is terse but complete. Many other terminals applications don't seem to have any up to date documentation of the sequences they support. Documentation of the terminal to application communication isn't very common among terminals either.
So I started looking into the source of various terminals and related standards like ECMA-48. It soon was evident that I needed much more organized notes, or even better some real documentation covering more than just one terminal.
So the rabbit hole now started to bottom out at writing such documentation and learning how the terminals actually work. After reading lots of terminal code (as code speaks more authoritatively than standards from 20 years ago) I ended up with something of an start of understanding how things seem to work. Of course after putting in that amount of work it should be accessible to others so I got that documentation a nice home at https://terminalguide.namepad.de.
Now with a better understanding of how terminals work, I continued experimenting with terminal input processing. Could I avoid using timeouts to disambiguate input? Instead combine input and output together to get the terminal to output further bytes to make deciding easier. That seems to work at least when the terminal connection is reasonably low latency. But that needed to have terminal input and output processing some by one integrated library it seemed.
So that drove me to look more into the output side as well. Output needs to know which terminal implementation is at the other side of the connection a lot more than input2. And looking at $TERM does not really work that well nowadays. So do the terminals differ in responses enough to detect the implementation by finger printing responses? It seems that many of the modern terminals do3. For some even the version is available in that way. Of course finger printing is really not nice (and the code a lot worse) so really we need something better. Maybe we get some feature self reporting by terminals in the future.
So in the end the experiment for terminal input processing morphed into a full low level terminal interface library. Which now is finally available on GitHub.
Termpaint only targets modern VT1xx like terminals. Thus it requires a terminal that supports utf-8 and primarily targets terminals released in the last couple of years. But that is a bit fuzzy with support for xterm versions going back at least to 264 (from 2010, already solid back then and version reporting allows for selectively enabling of features) but other terminals might only cover versions a few years back to some newer terminals like alacritty that are only supported in fairly recent versions.
Termpaint has a core that does not know anything about how bytes go from or to the terminal thus can fit into any event loop or be used with synchronous terminal I/O.
It has a output model with surfaces that are simply two dimensional arrays of cells with styles, colors and chars (with support to wide characters as used with some languages and emoji).
It has terminal fingerprinting instead of using $TERM because too many terminals lie and an application should really work without having to fiddle with "unbreak it" settings. I also try to test it with a lot of different terminals and terminal versions4.
All functions work with the usual SSH setups.
It has text measuring functions heavily inspired by libtickit.
It has an input parsing sub library that is bundled but usable stand alone (could be extracted if there is demand).
Check out termpaint: https://github.com/termpaint/termpaint
Of course, the higher level library is still on my list of things to do. I've got a good start on that code done already, but it will still take some time to be in a state to put it on GitHub.
Nevertheless Termpaint can be useful without having an explicit higher level library as well. Typical terminal interfaces use the rich but constrained nature of text and semi-graphics glyphs with colors and attributes to create interfaces that are quite possible to draw without complicated support libraries. And sometimes doing things by hand can be quite satisfying too.
It's still early, but a friend already came up with a useful application to monitor YouTube channels for new videos and display them. See the screenshot above. It's called yttui and is available at: https://git.schroedingers-bit.net/trilader/yttui
The editor in the screenshot at the top also exists but depends on my unfinished higher level TUI library, so it will be released some time in the future.
xterm has some experimental sequences that are not documented↩
For most part there are not may sequences used for different keys by different terminals. And the most important sequence is backspace which has it's own setting in the terminal interface layer of *nix like kernels that the application can just use. Of course some details still differ and termpaint now uses terminal detection information for input processing as well.↩
But of course many simpler terminals are not really distinguishable by fingerprinting.↩