Lagrange v1.3

Version 1.3 has wide-ranging improvements that touch on all facets of the application. Now I regret calling v1.2 a "Big One" because this release eclipses it quite easily. 😄

In this post I'll try to keep the discussion on a high level instead of listing all the minute details. You'll find the complete release notes via the link above, or here:

UI localization

It is an important side effect of maximizing portability that the potential user base is also maximally large and diverse. This includes different nationalities and people with accessibility requirements. While neither of these is exactly trivial to tackle in software that tries to minimize dependencies, the former one at least can be achieved without having to rely on platform-specific APIs.

Broadly speaking, there are four major challenges in achieving full support for multiple languages:

This release addresses the first two: the language of the user interface can be changed, and there is a new "Translate..." menu item in the page context menu.

With the exception of the Finnish translation that I did myself, the current set of UI translations have been kindly contributed by fellow Geminauts.

If you are interested in working on a translation for your language, please do register on the site and let me know. While working on the Finnish one I found there's a curious attraction to translating these (mostly) short text snippets — maybe you'll find it fun as well?

LibreTranslate integration

Most people speak only one or two languages, so to make content more broadly available it is important to translate it as well. Given worldwide scale and continual production, really the only feasible solution here is good old machine learning. Nowadays automated translations are pretty okay — at least you'll get the gist of what's being said even if much of the nuance is lost.

I'm running a LibreTranslate instance on a home server, at the same IP address as It's behind an Apache reverse proxy, and the access logs do not record IP addresses or any content about the (HTTPS) translation requests. While the quality of the translation may not be state-of-the-art, I hope this arrangement is more palatable than relying on the web giants for this service.

This is a bit experimental, though! I have no idea if the server will implode when more requests start coming in. It is also dreadfully slow for longer content. I'll keep an eye on it and see how it goes...

Gemtext lends itself to machine translation pretty well. The markup is simple and newlines generally seem to be preserved in the translation. When it comes to LibreTranslate in particular, I do have to strip the markup before making a request, because it seems some language models interpret markup characters in unexpected ways or just omit them entirely. A bit of improvement is still needed for preformatted blocks because they are translated, too, sometimes with amusing results.

Collapsing preformatted blocks, showing alt text

Prior to this release, Lagrange has been ignoring an important aspect of Gemtext: "alt text" from preformatted blocks has not been viewable by any means inside the app. While interpretation of the alt text has been up for some debate, arguably its most valuable use is speech-friendly description of the contents of the preformatted text for accessibility purposes.

Of course, Lagrange's custom UI widgets have zero integration with screen readers, so the app cannot currently use alt text for that. On the whole, though, it helps that authors are aware that alt text is important and should be used consistently, so accessibility-oriented clients have more useful content available.

In v1.3, if a preformatted block has alt text, it gets displayed while hovering the mouse over a block. Clicking on the block collapses it and the alt text is displayed permanently. There is also an option to collapse all blocks on page load.

|  \ /     Here's a demo!      /\   |
|   X    Click on this text   (  )  |
|  / \    or hover over it.    \/   |

Trust via CAs

The only part of the certificate that matters is the pubkey field.

I tend to agree that putting too much emphasis on the self-signed certificate fields (such as CN/SAN) is a bit pointless since the information is not guaranteed to be valid in any way. Lagrange will warn you of discrepancies in these fields, but will show page contents regardless. I am leaning towards adding a setting to disable TOFU domain verification entirely, but haven't done so yet. It should be noted that v1.3 has relaxed the domain checks a little: a certificate for "domain.tld" will be considered valid for any subdomains, too (i.e., an automatic wildcard is applied).

Certificate Authorities provide some kind of validity assurances, although the CA system has its own issues. Still, it is an established mechanism that is widely in use and support for it is built into TLS libraries.

Lagrange can now be told about trusted CA root certificates, so those can be used to verify server certificates and automatically mark them as trusted, much like how web browsers behave when it comes to HTTPS. This only applies if the server sends a full certificate chain, though. The Page Information dialog now includes the CA check status as a new item.

Theme adjustments

A procedural theme system is challenging to make 100% perfect in all instances. I've been applying some tweaks to the color selection algorithm to avoid the worst purple-on-green offences of Colorful Dark.

Some of the themes have also been less than optimal in light/dark UI modes. Most notably, Gray now has light and dark variants depending on the UI mode.

Bold link appearance with custom icons

Links are supposed to stand out as interactive elements of the page, and prior to v1.3 this has been indicated only with color and an icon in the page margin.

There are issues with relying on color, though. In Colorful Dark, where body text is usually colored, I've found it harder to read the bright-white link lines compared to the rest of the text. Also, on bright backgrounds, links and body text have been drawn in the same color, leaving the icon as the only differentiator.

To improve on both of these problems, links are now displayed using a bold font. This allows them to be distinct on light backgrounds, too, and they can be clearly tinted with the theme color for consistency. I think overall this improves readability nicely.

A common convention on some capsules is to put an Emoji or other Unicode character at the start of a link label. In selected cases these are now promoted to the page margin to be used instead of the usual arrowhead icon. The purpose of the link icon is to indicate the type of the link, though, so these custom icons only apply to Gemini links that lead to the same domain. Web/Gopher/Finger links and links to other domains will use the 🌐/👉 icons like before.

Touch screen support

SDL is great when it comes to dealing with low-level input, and on many devices this also includes touch screen support. Touch screen input can now be used to interact with the app UI, to scroll, tap, drag, and long-press. Under the hood this emulates mouse and trackpad events, so UI widgets don't actually need any additional input event handling that would complicate things.

Most of this was done for the mobile port, but it works everywhere else, too.

Runtime UI rescaling

Lagrange has had a setting for changing the UI scale factor since v0.1, but until now it has required restarting the app to redo widget layout and update all the metrics within. Now this can be done on the fly.

This is great especially if your system has displays with different DPIs, e.g., a laptop connected to an external monitor. Once the window detects that display DPI has changed, it gets rescaled automatically.

Implementing this also required a few improvements in multi-monitor support. Previously, there have been a few places in the code where the app has assumed to exist only on the primary display.

Word/paragraph selection

A small but important tweak to selecting text: one can double click to start selecting entire words, or triple click to select full paragraphs (i.e., Gemtext source lines). I expect this to be quite useful on mobile where selecting by individual characters can get fiddly.

Not a massive change internally, but it did lead me to discover a few bugs in double click event processing. The code is now cleaner, and you can do amazing things like click on buttons repeatedly without triggering double clicks.

Unread feed entry counter

In the absence of sidebar view filtering to see only unread entries, for example, I've added a little counter to the Feeds tab button for showing the number of unread entries in the list. I've also been considering making the unread count visible elsewhere if the sidebar is hidden. After all, if you care enough to subscribe to a page, it would be appropriate for the UI to make you aware of new entries as they are discovered.

I have noticed a few miscounts that are likely due to "headings" subscriptions, but didn't get a chance to track those down. The unread status of an entry depends on the browsing history: if the URL is found in the history, the entry is considered read.

Basic command line usage

So far I've been very focused on GUI usage, but of course the command line is useful and important, too. This release adds some basic (and common) command line arguments such as `--version` and `--help`.

There is a very significant change/fix here as well: Lagrange will now refuse to run if it detects that another instance is already running using the same runtime directory. All data therein is stored as plain text files (and application state as binary serializations) that are inherently not protected against simultaneous reads and writes by multiple instances. So exclusive access is enforced to prevent data loss.

One way to solve this would be to switch to a proper database for storing the state, but that would increase complexity and bring in additional dependencies. An instance would also have to be ready to react to externally applied database changes. Perhaps it will make sense to go this far at some point, but it seems overkill for now.

With this detection in place, a newly started instance is aware if the app is already running, thus opening the door to communicate with it via the command line. I needed to implement an IPC mechanism for this. On POSIX platforms this is pretty straightforward, as one can use the USR1/USR2 signals to ping a running instance. I went with a very simple method: writing commands to a text file and sending USR1, which will cause the recipient to check the contents of the command file. Unfortunately, these signals are not available on Windows, so there I had to use one of the Win32 APIs that essentially implement the same behavior, but in less elegant Microsoftian style.

There is plenty of room for expansion here, but for now command line options allow one to open/close tabs and list all the open tabs in the running instance.

Other UI improvements

UI enhancements is my favorite topic 🤩, but I'll try to contain myself to keep this post from ballooning any further. The majority of UI changes in v1.3 were driven by the mobile port, but each one serves a purpose on the desktop as well: menu icons make it easier to find items at a glance; soft shadows behind popups provide a cleaner separation from the background, which is an issue with some UI color combinations; macOS/iOS scrollbars fade as per platform conventions; and some UI elements were moved or restyled to better convey their relationship with other elements, or just to make things more consistent.

Next up

It's a safe bet that adding this many new features introduced quite a few new bugs as well. In April it may be wise to reserve some time for general polishing and bug hunting.

I also plan to try out a few UI changes in the mobile version, now that I have some experience with the current design and have a better grasp of its shortcomings.

📅 2021-04-02

🏷 Lagrange

CC-BY-SA 4.0