Lagrange 2

A new major version means major changes. Lagrange 2 supports user data syncing, paginated view mode, and Tinylog feeds. Reading Gempub books is greatly improved, taking cues from e-readers. The UI appearance has been refreshed and fits better into modern OS design.

This post is also available as a Gempub book: "Lagrange 2" (530 KB)

Download Lagrange v2.0

Turning a New Page

Lagrange turned six years old in September 2026. Looking back, the app evolved the most during the first couple of years. With the COVID-19 lockdowns in 2020/2021, work schedules were pretty quiet with people grappling with the unusual circumstances, so I had plenty of time for hobby programming. I had to slow things down in 2022 after my second child was born and I got rather preoccupied with baby duties. Development did continue, though, with a few larger updates per year. It wasn't that much of a problem for the project since the core functionality of the app was pretty solid by 2022. The 2023–2026 period could be described as refining the experience and adding a few nice-to-haves, like SOCKS proxy support and the directory structure sidebar.

Work on version 2 began in March 2026, roughly seven months ago, and after the release of version 1.21 in August 2026 it has been the main focus for me. Save for bug fixes, I don't expect version 1 to receive much development attention going forward.

Having used Lagrange daily for several years now, I've had plenty of time to think about the areas of the app that need improvement. Topping the list are the lack of user data syncing, inflexible and inefficient document layout, rudimentary UI appearance, and a suboptimal reading experience for longer content. I am addressing all of these in version 2.0.

⚠ Upgrading (and downgrading)

Before upgrading to version 2.0, I recommend making a backup copy of your user data (User Data > Export). This allows running version 1 from an alternative user directory, using the exported data.

Migration from v1 to v2 is safe: your existing user data is preserved as is. However, due to many changes in the internal config/state files, downgrading back to v1 is not recommended. v2 uses different names for many of the files, so v1 won't see them. You'll effectively have two sets of intermingled data, some of which is shared by v1 and v2, some not. For example, both versions write their preferences to "prefs.cfg", so v1 will omit all v2-specific settings, causing them to be reset to defaults. Another case is "bookmarks.ini" that is used by both v1 and v2: changes to bookmarks made in v2 will carry over to v1, with the caveat that trashed bookmarks will appear as regular bookmarks in v1. To avoid confusion, it is therefore best to use separate user directories for v1 and v2, if v1 is still in use.

Look and Feel

Version 2 pushes Lagrange's custom GUI toolkit forward. The appearance and behavior of the UI widgets has been fine-tuned on desktop (GUI and TUI) and on mobile. I would describe the changes as maturing and evolving the old design, following the direction the app has had for many years, without introducing anything radically new.

Rounded all-around

As the Macintosh showed us in the 80s, having UI elements with rounded corners is actually quite nice. It looks pleasing, even friendly, compared to the harsh, sharp corners for which pixel-based computer screens are most naturally suited. This particular UI trend has evolved toward ever larger corner radii, as if worn down by time. Modern browsers and operating systems seem to be full of large lozenges and other pill shapes. Personally, I find that level of extreme rounding off-putting. Instead, if the rounding is kept tasteful and subtle, it makes for a delightful appearance.

Version 2 softens many UI elements with rounded corners. As a side effect, this helps the custom GUI blend more seamlessly with modern operating systems — especially the mobile ones. Technically, this isn't a large change. Instead of drawing the UI elements with lines and rectangles, a few variants of round corners (of varying radius and thickness) are pre-rendered and then reused everywhere. The end result is a coherent appearance across buttons, input fields, dialogs, and menus.

Context menu with rounded corners (PNG, 32 KB)

System fonts

A key design goal of Lagrange has always been platform-independence. The app has provided its own text rendering routines, and therefore also its own font management. This approach provides a lot of freedom: you can use the same text rendering in the terminal and the GUI just by changing the font metrics and what a "glyph" means. You also get the same set of fonts everywhere since you're always manually installing the files on the device. The downside is that system fonts are off-limits, because that would mean implementing font and text routines suitable for each platform.

Version 2 keeps the existing text rendering as-is (basic stb_truetype, HarfBuzz, and TUI variants), including the fontpack system, but adds two new backends:

The Windows version has been using Segoe UI as the UI font by default, and now on Apple operating systems the app uses the system font (SF Pro) as well. The system icon fonts on macOS, iOS, Android, and Windows are used for the app's UI icons. This makes it feel more like it belongs on the platform.

And yes, color Emoji are now supported. I still recommend using the monochrome Emoji for a more consistent visual appearance. Regardless of whether color Emoji are enabled, any missing Emoji glyphs now fall back to the system fonts.

The cost of these changes is maintaining alternative implementations of the same functionality, but fortunately Core Text and FreeType are not moving targets so the maintenance burden is manageable. Crucially, these are only backends, so the rest of the app can remain oblivious to how the text rendering is actually done.

Sliding with precision

The Lagrange GUI toolkit has always had a bit of Minimum Viable Product feel to it. This has kept the code nimbler and more manageable at the cost of a bare-bones look. The set of widgets has essentially been limited to simple labels, buttons (which are also labels), lists, and input fields. Choosing a value from a range has been done either using a series of radio buttons (e.g., scrolling speed), or an input field (UI scaling factor, cache sizes). This has been adequate but clunky.

Version 2 adds a new widget class for sliders. These are used particularly in Preferences/Settings for entering numeric values.

Sliders in the Preferences dialog (PNG, 11 KB)

Document-themed tab bar

In version 1, the tab bar would use document theme colors in some cases. Most of the time, it would use UI colors, and the UI accent color usually clashes with any colored document themes. Version 2 improves on this by making the tab bar always match the document theme. The selected tab button uses a bold font, while the text in the other buttons is dimmed. These changes make it easier to see at a glance which tab is active, and the tab bar looks like a natural extension of the document area.

Preferences

I feel like the Preferences dialog gets reorganized in every release. Version 2 is no exception. The changes are quite big this time: many options have been moved onto different pages, there are protocol-specific theme pages, and the key binding list's usability has been improved.

SDL3 goodies

SDL3 provides quite a number of new APIs that abstract platform-dependent but commonly available functionality. The new ones that most directly affect Lagrange are:

New document color themes

It has been a while since new document themes were added. There are three new ones in version 2:

Terminal UI

The TUI variant has received its own round of UI polishing in version 2. The primary elements have been rearranged a bit: the menu bar is always visible at the top, and the keyboard shortcuts guide is at the bottom, below the navbar. Large list items have been spaced out to leave room for the key focus frame around them. The TUI has also gained mouse support, bringing it closer to the GUI when it comes to feature parity. This makes the TUI even less like a traditional text interface and more like a "textified GUI". Some aspects of the UI are only reachable through the mouse — dragging list items to reorder them, for example — so this makes a certain kind of sense.

My SDL-emulating SEALCurses library was updated to emulate the SDL3 API instead, so it remains as a translator between the app's GUI widgets and Curses.

Perhaps in the future, the TUI will evolve more toward a true terminal-first experience. An app as feature-packed as Lagrange should look to something complex like Emacs as the role model, I suppose: exhaustively mapping key bindings to every single action, while providing conveniently navigable menus and self-documenting help features.

The app's basic UI structure (a document with sidebars) is probably not the optimal one for the terminal, where horizontal space is at a premium. Due to the fixed-width font that produces relatively wide lines of text, you need to use most of the horizontal space to show content. Traditionally, terminal apps seem to favor switching between screens, perhaps doing a split between a top list and a detail area. It might be interesting to look into similarities between the mobile UI layouts and the TUI, since both deal with limited canvas sizes in one way or another.

The Reading Experience

Reading text is the heart and soul of Gemini. At least when I think of the quintessential reading experience, it certainly isn't scrolling a block of text on a little screen, but instead holding an actual book, settling in and focusing on every page — getting lost in a story, paced by the purposeful turning of pages.

Back in version 1.4, I added Gempub support intending to develop it later into a more full-featured book reading experience, but the changes required to achieve that were too broad for the available time budget. Version 2 takes a huge step forward in this area, adding both a true paginated view mode, and a bookshelf to manage your Gempub books.

Paginated view mode

Any page can be toggled between vertical scrolling mode and paginated mode. Scrolling mode works like before, while paginated mode behaves like an e-reader: the text is laid out in one or two columns per page, and if the window is wide enough, on two pages side-by-side. This means you can actually fill a large window with content (a total of four columns visible), rather than having a narrow bar of text in the middle. The page header shows the current top-level heading, similar to what is shown in the left margin in vertical mode. The footer shows the page numbers, and at the bottom edge, there is a draggable horizontal progress bar, indicating overall position inside the document. The mouse cursor, navbar, and tab bar are automatically hidden, to reserve the full window for focused reading. Clicking or tapping the middle of the page brings the bars back, while tapping on the left and right sides of the window triggers a page turn. This should be quite familiar behavior from e-readers.

Solderpunk's "Orphans of Netscape" as a two-page spread (PNG, 158 KB)

There are several layout tweaks in paginated mode: the headings at the beginning of the document are centered, and so are image captions. The images themselves are each fitted inside a page. Paragraphs are justified by default in paginated mode, for a cleaner look.

Page turning is animated: the pages curl by default, emulating the appearance of an actual paper book. The other options are horizontal scrolling or no animation at all. The curling animation is a simple 2D trick with shadowing to sell the illusion.

Hyphenation

You can enable hyphenation for English text (Preferences > Page Style > Hyphenate). Hyphenation slows down page layout a little but you'll get more evenly wrapped lines. This is good for narrow columns and for evening out word spacing in justified paragraphs.

The "lang" parameter in the document's media type is checked for "en" or some variant of English. If the parameter is missing, text documents that are autodetected to be English will also be hyphenated (based on frequency of specific words).

View toolbar

There is a new floating toolbar with view-specific options. This "view toolbar" lets you toggle paginated mode, change the auto-reload interval, and pin an identity in the tab. (Identity pinning was previously doable only via bookmarks.) The main reason for adding this was to have a quick way to toggle the paginated mode when you encounter a long article where a focused reading experience is preferred. I realized the bar is also a good place to surface tab-specific settings; hence the other actions.

The view toolbar is visible in the top right corner (PNG, 59 KB)

On a phone, the view toolbar is at the bottom of the screen for easy access. It can be hidden if you feel it's getting in the way (Preferences > General > Hide view tools; Settings > Toolbars > Hide View Tools).

Books

If you are not familiar with Gempub, here is a short introduction: it is a Gemtext-based ebook format where each chapter is a Gemtext file. Everything is put inside a ZIP archive along with some metadata and a Gemtext index page linking to all the chapters.

Lagrange version 1 had rudimentary support for Gempub, meaning it opened the archive and showed the index page and a chapter page in split view. Basic chapter navigation was provided via document footer actions. (The buttons that sometimes appear at the bottom of a page, for instance to install a fontpack.) This is a far cry from an actual e-reader, though. The split view was always an awkward half-measure, and was cumbersome to get out of when closing the book. Each chapter was just a long vertically scrolling page, where the reading position was preserved if you were lucky. Closing the tab reset the position because the tab was responsible for storing that information.

In version 1, the user had to save a Gempub book locally before it could be opened, because internally Gempub chapters are treated like normal Gemini pages: you fetch each chapter by requesting a specific URI. Given a ZIP archive, its contents can be mapped to virtual paths rooted at the local file. This was trivial to implement as a small extension to the pre-existing "file://" scheme. The manual saving was cumbersome for the user, though.

Version 2 treats Gempub books as a first-class document type. The intent here is to mirror how e-readers generally work: you just download a book and read it. The book is expected to stay in your library on the device. That is why when you download a .gpub file, it now gets automatically saved, and the book opens in paginated view mode in its own tab. The collection of Gempub books that you have saved (locally or on other synced devices) is called the "bookshelf".

Gempub tabs behave differently than normal tabs. When turning pages, the tab switches between chapters automatically. The entire book flows together naturally, like a real (e)book would. The progress bar in the bottom of the page reflects the total position inside the book, all chapters accounted for. However, despite the differences in tab behavior, book tabs still allow performing all the usual actions like text selection and language translation; the page context menu remains the same.

Gempub has its own set of document theme and layout options, so you can customize the appearance of books separately from other pages.

Books sidebar and the bookshelf

Your books can be managed in the new Books sidebar where you can find the bookshelf, listing all the books you have in your collection. When a book is open, the sidebar also shows the Cover and Contents (Gempub index) pages, with the corresponding Gemtext documents rendered in full inside the sidebar. The Contents tab is useful for jumping quickly to a particular chapter, or getting an overview of the current position in the book. (This is what the split view tried to approximate in v1, since there was no support for showing documents in the sidebar.)

Each book's metadata, source URL, and reading position are saved in a new "books.ini" in your user directory, which is now part of the exported user data.

Saving pages to the bookshelf

As an alternative to saving pages to the Downloads folder, any page you encounter can also be saved to the bookshelf. This produces a single-chapter Gempub out of the page, and the resulting book appears on the bookshelf as any other book would. This is a nice way to read longer articles because the reading position is preserved, so you don't have to finish it in one sitting (or using only one device, as we'll see later). It also provides one solution to the issue of Gempub availability. Modern ebooks sold on stores like Amazon and Kobo certainly won't be available as Gempubs any time soon.

Per-site theme settings

In addition to per-protocol (and Gempub) theme customization, site-specific settings have also expanded to support theming. This means that for any given capsule, you can override the document fonts, color themes, page style, and layout options, fully changing how the capsule appears. As the Gemini specification intends, you are in full control of how content gets presented on clientside; the server and content author cannot control this.

These settings are part of the exported user data together with other site-specific settings.

Site-specific settings for cosmos.skyjake.fi (PNG, 52 KB)

Incremental parsing and layout

In version 1, parsing the document source and laying it out was not particularly efficient. Whenever new data arrived over the network, the entire source was processed from scratch. This meant that a long or slowly progressing (i.e., streaming) download would be re-parsed and laid out multiple times for no actual benefit. When viewing a long page like the Project Gemini F.A.Q., you could really feel the layout routine leisurely chugging along when resizing the sidebars or the window.

Version 2 makes a couple of fundamental changes here. First of all, parsing the document is a separate, incremental phase. This means that the layout-independent document structure, like links and headings, can be processed only once. If the data arrives in pieces, the parts already processed are kept as is, with the parser only going over the new lines. Obviously, this is the right way to do it, but the cost is some extra complexity since the parser needs to maintain its state in a resumable way. The second change is progressive layout. Whenever the layout needs updating (when the document is first loaded, when the view size changes, or the theme changes), the content inside the visible viewport is laid out first. The main thread is then allowed to run normally immediately afterwards, to process input events, keeping the app responsive. At this point, the rest of the document's layout is still undefined. Then, over the next few frames, the rest of the layout is processed in time slices, which allows the UI to remain responsive. The end result is that processing large documents feels much faster.

I've been going over each of the sidebar tabs, finding rough edges and other pain points. Most of the tabs have received significant improvements or at least a facelift.

Improved lists

The sidebars are all basically lists of various kind, so several improvements were made in the underlying ListWidget:

Feeds

Out of all the sidebar tabs, Feeds has been improved the most.

For starters, the list of entries in the Feeds sidebar is now capped at 1000 entries, up from 100. Depending on how many subscriptions you have, 100 entries would not extend very far into the past. All the known entries are still listed on the "about:feeds" page, but obviously it is nicer to have them listed in the actual UI.

Reader tab

Feed subscriptions are a very old feature in the app, but they have not been materially improved in many years. The subscriptions have been quite a simple system: periodic fetching of source pages, and a sidebar list showing the most recent entries with the unread ones highlighted. That has really been the extent of the feature.

Compared to RSS readers, what has been missing is a dedicated view for showing the currently selected entry. Opening entries as normal tabs — like Lagrange v1 does — is adequate but also problematic: there is no connection to the entry list, nor can you tell an entry page from a regular page. To make up for this lack of context, in v2, feed entries open in a special-purpose "reader" tab that is marked with the ★ icon (both in the tab bar and in the Tabs sidebar). All feed entries open in the same tab. Opening a link in the entry page does so in a new tab, so the reader tab is effectively always pinned to the current entry.

The reader tab has a banner box at the top detailing the source feed and the date of the entry. The page is kept top-aligned so when you switch entries, the positioning of the entry metadata does not bounce around. The entry page's footer actions provide a way to navigate to the next and previous entries in the Feeds sidebar; the same can be done with the Space / Shift+Space keys, when scrolling has reached the bottom/top of the page.

The dedicated reader tab changes the interaction model to the one familiar from RSS readers, and keeps feed reading separate from regular browsing. This change is perhaps even more important on phones, where the "sidebar" slides up as a sheet, covering most of the page. You can only view either the feeds list or a page at a time, so you end up flipping back and forth. Convenient navigation between entries is therefore needed, and for that purpose, the page pull-down gesture (and in v2, also the pull-up gesture) have been reassigned to entry navigation instead. That is, instead of pull-to-refresh, you'll move to another entry in the feeds list.

Tinylog subscriptions

I've always thought Tinylog is a neat idea. It is an exceedingly simple decentralized microblogging mechanism, like twtxt but more flexible. Publishing Tinylogs is relatively simple: you can put a tinylog.gmi file on your capsule, or have bbs.geminispace.org or Station generate a Tinylog feed for you.

gemini://tinylog.geminispace.org/skyjake

gemini://station.martinrue.com/skyjake/tinylog

The bigger challenge has been on clientside. There are a few aggregators that act as Tinylog "clients", and these provide a great platform for discovering other people's feeds. However, having to view the aggregated entries as unadorned Gemtext is not ideal. Therefore, I decided it would be valuable to support Tinylogs as their own feed type in Lagrange, applying the full functionality of the subscription system.

From a technical point of view, Tinylogs differ from Gemini feeds in a couple of important ways. The source page is cached locally for complete and instant (and offline) access to the entry contents. (There is a configurable maximum file size for these cached pages; old entries exceeding the size are dropped from the local copy.) Each entry also has an exact timestamp, which is shown in the Feeds list. The variable-height items allow showing longer excerpts with up to three lines of text in the list.

Having all the entries locally cached means that if a response link refers to a known entry, its contents can be inserted as a quote under the link. Having the full chain of responses quoted on the Tinylog entry page is naturally quite useful for seeing the full context of the conversation. Mind you, this does require an accurate timestamp in the response links, which may be complicated by the timezone component.

Reader tab with a Tinylog entry that has a quoted response (PNG, 130 KB)

Subscribe to All

For convenience, there is a new utility: in the page context menu, "Tools > Subscribe to All..." is meant for aggregator source feed lists, where each line on the page is a feed URL. This works on Tinylog directories as well as Gemini aggregators like CAPCOM. Subscribing to all is a useful way to get started if you don't know which feeds to subscribe to.

gemini://tla.stilvo.id/directory

gemini://gemini.circumlunar.space/capcom/feeds.txt

Bookmarks

The Bookmarks sidebar has allowed deleting bookmarks without any confirmation, which is a little dangerous. Instead of adding another cumbersome popup, deleted bookmarks are now moved to a Trash folder. Clicking the "🗑 Trash" button, found under the bookmarks filter field, shows the contents of the trash folder where the deleted bookmarks can be restored later if needed.

You can empty the trash via the Bookmarks menu or the sidebar context menu.

Outline

The Outline sidebar has improved legibility: long headings are wrapped to multiple lines, there are separator lines above level 1 headings, and spacing is added between items.

Keeping things in order

The Identities and Subscriptions sidebars now have a configurable sort order. In the former, the options are manual order (drag to rearrange the identities), or by name. The latter has more choices: most/least recently updated, number of entries, and name. Sorting the subscriptions by recent updates is a good way to locate any feeds that have been discontinued or are unreachable.

The sidebar tabs themselves can now be reordered by dragging. Document tabs have supported drag-to-reorder for quite some time, so now the sidebars do, too. The tab order is preserved in the state.lgr file and the Ctrl/Cmd+1...9 shortcuts get automatically reassigned by tab order (from left to right).

(You can't drag the tabs between the left and right sidebars, though. The left/right assignment is still done in Preferences > Sidebars.)

Sync and Internals

If you're anything like me, you have Lagrange running on more than one device. In my case, that means a couple of Mac laptops, iPhone, iPad, a few Android phones, a dual-booting Windows/Debian PC, a Linux server, a sprinkling of build VMs, and of course clagrange in the terminal on some of these, too. In practice, all of these have varying collections of bookmarks and identities, exported across devices at some point in the past.

The fragmentation of the user data has real downsides. If you happen to find, say, a new Gemini game while browsing on your phone, you'll have to remember to somehow bookmark it on your laptop as well. It's unavoidable that the local data sets drift apart over time. Whenever installing the app on a new device, you have to dig up an exported copy of the user data and wonder if it is even moderately up-to-date.

Syncing up

Of course, there is an obvious solution: a sync server. For a long time, I've been resistant to the idea of making a sync server because it means implementing a high-performance Gemini service with the expectation of fairly frequent traffic coming from hundreds of clients. Running that on my Python-based GmCapsule has not been appealing, knowing Python's performance characteristics. (skyjake.fi and bbs.geminispace.org already do already serve hundreds of clients every day, but sync push/pull requests are more frequent.) What changed my mind was implementing a Gopher server in C with the same utility library that powers Lagrange itself. This let me iron out some bugs in the library's networking code when it comes to services, and eventually allowed constructing an OpenSSL TLS layer on top of the listening TCP socket class. The end result is a highly performant, fully native sync server called "lgrsyncd" (Lagrange Sync Daemon).

Lagrange version 2 has built-in support for this new sync service. The server's source code lives in the Lagrange repository (under the "sync" directory) and can be compiled separately from the app. I am running a public sync server on sync.skyjake.fi:1966, which is the default configuration in the app. Self-hosting is also supported, and the server can be configured to permit access via only specific client certificates.

Settings for the sync server are configured on the new "Sync" page (PNG, 49 KB)

Given how common user data syncing is in web browsers, it came as no surprise that syncing has a positive effect on the user experience. There is effectively a single set of user data that exists "in the cloud", shared by all the devices. Without sync, in a multi-device situation, feed subscriptions alone were cumbersome to manage due to having to constantly mark entries as read and needing to subscribe to feeds on each device separately. Manually exporting and importing back and forth between devices could help but was hardly convenient.

Syncing is particularly important for the new Gempub bookshelf feature. When you open a book on one device, its metadata gets automatically synced to all the other devices. If you then want to continue reading a book on another device, you can download a local copy based on the source URL and continue from the synced reading position.

Security

Perhaps the most important question when it comes to hosting other people's data is security. How certain can you be that your data is going to remain safe and private?

The security of the sync service relies on a Sync Identity. This is a self-signed Ed25519 client certificate that you must have on all the devices that sync the same set of user data. The sync server uses TLS 1.3, which means that the client certificate is encrypted when transmitted over the wire, for additional privacy.

The private key of the Sync Identity is used for generating a symmetric encryption key that encrypts all data locally before it is uploaded anywhere (AES-256-GCM). This means the server is unable to decrypt your data. It only stores the binary blobs uploaded by clients. It also means that if you lose the sync identity's private key, you cannot decrypt any of your data on the server, either. Fortunately, since all the synced devices have a full copy of the user data, it is fairly straightforward to generate a new sync identity and switch all devices over to it. The server will eventually delete stale accounts that haven't been accessed in a long time.

Frequency

The synced data is quite coarse. There are two types of encrypted blobs that get uploaded to the server: a full export of the user data, and a short list of the recently visited URLs from the past couple of days. The latter is important for keeping track of the unread status of feed entries. Whenever you locally edit your bookmarks, identities, or other synced data, the full export is triggered soon after. The visited URLs are also uploaded every few minutes. On the receiving end, the other devices have a configurable interval for checking the sync server to see if there are new updates. The data blobs are then downloaded, decrypted locally, and the remote data is merged with any local changes. The server, having no way to decrypt the data, cannot do any merges or consolidation of the data. That all happens locally on the devices that perform a sync.

Internal changes

The code base has evolved a lot during the development of version 2. A new library has been split off, and many new source files and subdirectories have been added. SDL3 is now required for building the app, which alone touches a large subset of the code. SDL3's audio API in particular has a different design, and most of the API enums and many of the functions have been renamed, too. The changes were largely systematic, fortunately.

Core library and gmget

The very first change done in version 2 was splitting off the core parts of the app into a separate "Core" library. This includes preferences (config files, site-settings, etc.), TOFU/certificate store, network requests (Gemini, Titan, and other supported protocols), managing history of visited URLs, and other low-level utilities.

The library comes with a curl-like `gmget` utility for making network requests on the command line.

You can probably see that `gmget` is a very thin CLI on top of the Core library. It can be useful in scripts since it has direct access to Lagrange's identities and user data.

Client certificates

I mentioned Ed25519 earlier in the Sync section. Both Ed25519 and ECDSA (P-256) are now available for regular client certificates, too. These signatures are considered more secure than the previous choice of 2048-bit RSA. The new default is ECDSA.

Config files

Lagrange's user directory contains a set of text and binary files where the user data is stored. They use basic formats and are easy to read and write; the data is intended to live fully in memory while the app is running.

In the beginning, the text files used ad-hoc formats basically containing a mere dump of text lines. Over time, TOML (.ini) files were added to provide bookmarks and site-specific settings a more structured representation. The rest have remained .txt files. Version 2 changes this by introducing a slightly more formal .lst format that is still just a list of text strings, but now it has a well-defined header line for metadata. This should make potential future format changes less painful.

Other notable changes:

MIME hooks become "tools"

The awkward mimehooks.txt is no longer used. There is a new tools.ini TOML file that serves the same purpose. It comes with a few additional perks, too: the filters can be specified to skip automatic runs, so they can be triggered via the Tools context menu and/or via keyboard shortcuts, on any page.

There is no automatic migration for mimehooks.txt. You will need to reconfigure your content filters if you happen to use this feature.

In addition to the old automatic Atom feed converter, there is also a new manual built-in tool: Unwarp Paragraphs. This is useful if you encounter a page where the author has used (unnecessary) hard line breaks. The unwrapping removes the extra line breaks, joining paragraphs into single lines that wrap correctly to the page width.

Summary

Befitting a major version number bump, Lagrange 2 changes the core experience in several ways:

The 2.x series

I won't try to predict exactly how the v2.x series is going to play out, but based on past experience, after a larger release there always comes a period of bug fixing and polishing of rough edges. I would expect that to be a major theme for the next 12 months.

Splitting off the Core library serves a long-term strategic goal. With the SDL-based implementation locked into certain assumptions and tradeoffs — sharing as much code as possible between platforms — solving fundamental issues like accessibility support requires a new approach. A common Core library facilitates building other app implementations with different assumptions. Relying on a given platform's own frameworks, a new app implementation would get built-in support for accessibility and other native features. The Core library is still too bare-bones in its current form, but there is a viable path forward here where more of the SDL app migrates downward into shared components. In any case, the SDL app will remain the flagship version; any alternative future implementations will take time to become fully formed.

Thank you

Many thanks to all the UI translators and to all who have sent bug reports over the years. Given the multitude of platforms the app runs on, it is impossible to test every configuration. Your reports help keep the ship on an even keel.

Version 2.0 begins a new chapter for Lagrange. I hope you will enjoy using it as much as I do.

📅 2026-11-06

🏷 Lagrange

skyjake's Gemlog

CC-BY-SA 4.0

Proxied content from gemini://skyjake.fi/gemlog/2026/11/lagrange-2.0.gmi (external content)

Gemini request details:

Original URL
gemini://skyjake.fi/gemlog/2026/11/lagrange-2.0.gmi
Status code
Success
Meta
text/gemini;charset=utf-8;lang=en
Proxied by
kineto

Be advised that no attempt was made to verify the remote SSL certificate.