In the Land of Columns and Characters

Previously, I've written about pros and cons of cross-platform applications:

As discussed in those posts, while there are definite drawbacks and challenges in basing an app on a fully custom UI toolkit, there are also unbeatable advantages. One of those is the freedom to adapt and transform the UI without limitations, like I've already done to have versions of Lagrange built specifically for the Mac, Windows, Linux, *BSD GUI desktops, and the iOS, iPadOS, and Android mobile operating systems. (iPadOS is worth mentioning separately as there are some unique UI variations for the tablet form factor.)

GUIs are nowadays quite the common denominator, but in the grand scheme of computing they are relative newcomers. For decades before GUIs, various text-based terminal interfaces were the norm. I'm sure most people reading this are already very familiar with the terminal, as it remains crucial to this day for many use cases, especially when one is connecting to a server over the network. Just like Gemtext, a text-based UI is fast and easy to stream over a network connection, be it a simple command line or something more complex.

One thing that really interests me in the Lagrange project is exploring the boundaries of native cross-platform programming, because I fundamentally dislike the notion that modern cross-platform apps need to be based on a web browser for things to make sense. With the major desktop and mobile platforms now supported by the app — at least in Beta form — I'm setting sights on more exotic frontiers, and the terminal is a logical next step.

Porting a GUI toolkit to a text-based terminal isn't as outlandish as it may initially seem, because the terminal is feature-rich enough to support "pseudo-graphical" rendering: one can hide the blinking cursor, use traditional box-drawing and Unicode characters, and change the foreground and background colors to create the illusion of complicated visual elements. Some terminals can even support mouse pointer input. If the GUI design is not overly complex, rendering it with text characters yields a perfectly usable result with only minor tweaks here and there.

Lagrange is in a fortunate position because it's already based on a cross-platform abstraction: SDL hides many of the ugly details of window management, video and input devices, and some commonly needed utilities like timers. This abstraction gives the necessary flexibility to swap the layers underneath for something else, without the app really having to worry about it at all.

I've written a small library that can be used instead of SDL. It uses Curses to do text-mode rendering and terminal-based input events.

This is very much tailored for Lagrange's needs and omits everything that isn't crucial for the terminal, such as audio output. I chose Curses because it has a long history and wide availability; in the future, newer and more feature-rich TUI libraries could be used instead in similar fashion.

I discovered an unexpected beneficial side effect when it comes to text rendering. You may recall that Lagrange had a simple text renderer that was used before HarfBuzz was taken into use. Turns out, if I define the fonts to be one units high and mostly monospaced, this old simple text renderer can be used for typesetting and rendering pages and the UI for the terminal just as well as it did for the GUI. Now this old rendering routine isn't languishing behind a build option, but can serve as the primary text renderer in the TUI version, helping to keep it compatible with the rest of the code base.

The biggest challenge in the transformation has been Unicode characters and Emoji in particular. Apparently after Unicode version 9, many characters were defined to be two columns wide, but this may or may not be supported by one's terminal software, or perhaps the terminal or the font it uses disagrees about the widths of certain characters. It is easy to end up with one-character offsets on lines where Emoji are appearing, for example. Switching the terminal font sometimes alleviates the problems, and some terminals have compatibility settings that may help. In any case, the end result is that Lagrange will need a suite of compatibility settings. Such settings are useful for other purposes, too, because not all terminals support extended color palettes, and that is required for Lagrange's themes to appear correctly. I'll likely end up with a "video mode" setting to control how fancy the UI presentation will be, ranging from full Unicode and RGB colors to basic two-color ASCII.

In practice, I will be distributing a separate `clagrange` TUI executable in the future. It will not link against SDL or any of its dependencies, making it quite a bit slimmer. While it can use the same user data as the GUI version, some things like the persistent UI state and preferences are saved to separate files so they don't conflict with the GUI version. Otherwise, weird situations may occur: once I accidentally restored a bunch of windows from the GUI version and suddenly the TUI app's behavior became wildly erratic and broken, as the overlapping windows fought over the same Curses output surface. I've opted to keep the TUI version single-window for now, but multiple fullscreen windows would be possible in the future.

I find it quite exciting that a single app, with only minimal per-platform code, can scale all the way from the desktop to a mobile GUI and to the other direction, down to a text-based console, with virtually all of its features available regardless of platform.

It's interesting to note that when it comes to complexity, mobile devices require the most sophistication with details like animated UI transitions and multi-touch input gestures being effectively mandatory. The TUI is the simplest variant, with its system-provided "fonts" and keyboard-focused interaction, and the desktop falls somewhere in the middle. One way to look at this is "breadth-first" portability — while there are still a few ways to expand like this, one shouldn't forget about "depth" as well, with more platform-specific integrations for iOS, Android, and various accessibility features on commonly-used platforms. Nevertheless, all of this is possible thanks to the narrow focus and scope of a Gemini client. If I was building a general-purpose UI framework instead of a bespoke one, the result would be much heavier and more complex to account for the needs of multiple applications and their use cases. I'm not really interested in pursuing that direction, because the simplicity of Gemini is so nice and refreshing.

📅 2022-04-15

🏷 Programming, Lagrange

CC-BY-SA 4.0